hillclimb 0.1.8 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +2 -2
  2. package/dist/cli.js +1176 -216
  3. 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 fs16 from "fs";
5
- import path19 from "path";
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/identity.ts
128
+ // src/gitignore.ts
129
129
  import fs2 from "fs";
130
130
  import path3 from "path";
131
- var IDENTITY_PATH = path3.join(configDir(), "identity.json");
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 fs2.promises.readFile(IDENTITY_PATH, "utf-8");
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 fs2.promises.mkdir(configDir(), { recursive: true, mode: 448 });
201
+ await fs3.promises.mkdir(configDir(), { recursive: true, mode: 448 });
153
202
  const tmp = `${IDENTITY_PATH}.tmp`;
154
- await fs2.promises.writeFile(tmp, JSON.stringify(file, null, 2), {
203
+ await fs3.promises.writeFile(tmp, JSON.stringify(file, null, 2), {
155
204
  mode: 384
156
205
  });
157
- await fs2.promises.rename(tmp, IDENTITY_PATH);
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 fs3 from "fs";
196
- import path4 from "path";
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 path4.join(configDir(), "logs");
296
+ return path5.join(configDir(), "logs");
248
297
  }
249
298
  function todayLogPath() {
250
- return path4.join(logsDir(), `${pacificDateString(/* @__PURE__ */ new Date())}.log`);
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
- fs3.mkdirSync(logsDir(), { recursive: true, mode: 448 });
262
- fs3.appendFileSync(todayLogPath(), line);
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 fs4 from "fs";
599
+ import fs5 from "fs";
551
600
  import os2 from "os";
552
- import path5 from "path";
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(path5.join(os2.homedir(), ".claude"))
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(path5.join(os2.homedir(), ".cursor"))
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(path5.join(os2.homedir(), ".local", "share", "opencode")) || isDir(path5.join(os2.homedir(), ".config", "opencode"))
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(path5.join(os2.homedir(), ".codex"))
689
+ detect: () => isDir(path6.join(os2.homedir(), ".codex"))
641
690
  }
642
691
  ];
643
692
  function isDir(p7) {
644
693
  try {
645
- return fs4.statSync(p7).isDirectory();
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 = path5.join("User", "globalStorage", "github.copilot-chat");
701
+ const suffix = path6.join("User", "globalStorage", "github.copilot-chat");
653
702
  const candidates = [
654
- path5.join(home, "Library", "Application Support", "Code", suffix),
655
- path5.join(home, ".config", "Code", suffix),
656
- process.env.APPDATA ? path5.join(process.env.APPDATA, "Code", suffix) : null
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 path5.join(repoRoot, def.settingsFile);
714
+ return path6.join(repoRoot, def.settingsFile);
666
715
  }
667
716
  async function readJson(file) {
668
717
  try {
669
- const raw = await fs4.promises.readFile(file, "utf-8");
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 fs4.promises.mkdir(path5.dirname(file), { recursive: true });
680
- await fs4.promises.writeFile(file, `${JSON.stringify(obj, null, 2)}
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 = {}) {
@@ -801,7 +850,7 @@ function copilotUninstall(settings, eventName, command) {
801
850
  }
802
851
  return true;
803
852
  }
804
- var OPENCODE_PLUGIN_VERSION = 3;
853
+ var OPENCODE_PLUGIN_VERSION = 4;
805
854
  var OPENCODE_PLUGIN_MARKER = `// HILLCLIMB_OPENCODE_PLUGIN_VERSION=${OPENCODE_PLUGIN_VERSION}`;
806
855
  var OPENCODE_PLUGIN_CONTENT = `${OPENCODE_PLUGIN_MARKER}
807
856
  // Auto-installed by \`npx hillclimb\`. Do not edit manually \u2014 re-running
@@ -815,20 +864,54 @@ import path from "node:path";
815
864
  const TOOL = "opencode";
816
865
  const TRANSCRIPT_DIR = path.join(os.homedir(), ".hillclimb", "opencode-transcripts");
817
866
 
818
- // sessionID -> Map<messageID, messageInfo>. Using a Map-of-Maps so late
819
- // message.updated events (streaming deltas) overwrite earlier versions of
820
- // the same message instead of appending duplicates.
867
+ // sessionID -> Map<messageID, { info, parts }>. Using a Map-of-Maps so late
868
+ // message.updated / message.part.updated events overwrite earlier versions
869
+ // instead of appending duplicates.
821
870
  const sessionMessages = new Map();
822
871
 
823
- function ingestMessage(sessionID, info) {
824
- if (!sessionID || !info) return;
872
+ function ensureRecord(sessionID, messageID, info) {
873
+ if (!sessionID || !messageID) return null;
825
874
  let bucket = sessionMessages.get(sessionID);
826
875
  if (!bucket) {
827
876
  bucket = new Map();
828
877
  sessionMessages.set(sessionID, bucket);
829
878
  }
830
- const id = info.id || ((info.role || "unknown") + "-" + bucket.size);
831
- bucket.set(id, info);
879
+ let record = bucket.get(messageID);
880
+ if (!record) {
881
+ record = {
882
+ info: info || { id: messageID, sessionID },
883
+ parts: new Map(),
884
+ };
885
+ bucket.set(messageID, record);
886
+ } else if (info) {
887
+ record.info = Object.assign({}, record.info || {}, info);
888
+ }
889
+ return record;
890
+ }
891
+
892
+ function ingestMessage(sessionID, info) {
893
+ if (!sessionID || !info) return;
894
+ const id = info.id || ((info.role || "unknown") + "-" + Date.now());
895
+ ensureRecord(sessionID, id, info);
896
+ }
897
+
898
+ function ingestPart(sessionID, part) {
899
+ if (!part) return;
900
+ const sid = sessionID || part.sessionID;
901
+ const messageID = part.messageID;
902
+ if (!sid || !messageID) return;
903
+ const record = ensureRecord(sid, messageID, { id: messageID, sessionID: sid });
904
+ if (!record) return;
905
+ const partID = part.id || ((part.type || "part") + "-" + record.parts.size);
906
+ record.parts.set(partID, part);
907
+ }
908
+
909
+ function serializeRecord(record) {
910
+ const info = record.info || {};
911
+ const parts = Array.from(record.parts.values());
912
+ if (parts.length === 0) return info;
913
+ // Preserve the old top-level message-info shape while adding parts.
914
+ return Object.assign({}, info, { parts });
832
915
  }
833
916
 
834
917
  function writeTranscript(sessionID) {
@@ -838,7 +921,7 @@ function writeTranscript(sessionID) {
838
921
  fs.mkdirSync(TRANSCRIPT_DIR, { recursive: true });
839
922
  const file = path.join(TRANSCRIPT_DIR, sessionID + ".jsonl");
840
923
  const lines = [];
841
- for (const msg of bucket.values()) lines.push(JSON.stringify(msg));
924
+ for (const record of bucket.values()) lines.push(JSON.stringify(serializeRecord(record)));
842
925
  fs.writeFileSync(file, lines.join("\\n") + "\\n");
843
926
  return file;
844
927
  } catch {
@@ -868,17 +951,24 @@ export const HillclimbPlugin = async ({ directory }) => ({
868
951
  event: async ({ event }) => {
869
952
  const type = event && event.type;
870
953
  const props = (event && event.properties) || {};
871
- const sessionID = props.sessionID;
954
+ const info = props.info || {};
955
+ const sessionID = props.sessionID || info.sessionID || info.id;
956
+ const cwd = directory || props.directory || info.directory;
872
957
 
873
958
  if (type === "message.updated") {
874
- ingestMessage(sessionID, props.info);
959
+ ingestMessage(sessionID, info);
960
+ return;
961
+ }
962
+
963
+ if (type === "message.part.updated") {
964
+ ingestPart(sessionID, props.part);
875
965
  return;
876
966
  }
877
967
 
878
968
  if (type === "session.created" && sessionID) {
879
969
  spawnHillclimb("git-traces --tool=opencode", {
880
970
  session_id: sessionID,
881
- cwd: directory,
971
+ cwd,
882
972
  hook_event_name: "session.created",
883
973
  tool: TOOL,
884
974
  });
@@ -891,7 +981,7 @@ export const HillclimbPlugin = async ({ directory }) => ({
891
981
  // so a single session produces one contribution, not one per turn.
892
982
  spawnHillclimb("git-traces --tool=opencode", {
893
983
  session_id: sessionID,
894
- cwd: directory,
984
+ cwd,
895
985
  hook_event_name: "session.idle",
896
986
  tool: TOOL,
897
987
  });
@@ -909,7 +999,7 @@ export const HillclimbPlugin = async ({ directory }) => ({
909
999
  if (transcriptPath) {
910
1000
  spawnHillclimb("upload", {
911
1001
  session_id: sessionID,
912
- cwd: directory,
1002
+ cwd,
913
1003
  transcript_path: transcriptPath,
914
1004
  hook_event_name: "session.deleted",
915
1005
  tool: TOOL,
@@ -929,7 +1019,7 @@ export const HillclimbPlugin = async ({ directory }) => ({
929
1019
  if (transcriptPath) {
930
1020
  spawnHillclimb("upload", {
931
1021
  session_id: sid,
932
- cwd: directory,
1022
+ cwd,
933
1023
  transcript_path: transcriptPath,
934
1024
  hook_event_name: "server.instance.disposed",
935
1025
  tool: TOOL,
@@ -938,7 +1028,7 @@ export const HillclimbPlugin = async ({ directory }) => ({
938
1028
  }
939
1029
  sessionMessages.clear();
940
1030
  spawnHillclimb("git-traces --tool=opencode", {
941
- cwd: directory,
1031
+ cwd,
942
1032
  hook_event_name: "server.instance.disposed",
943
1033
  tool: TOOL,
944
1034
  });
@@ -951,19 +1041,19 @@ export default HillclimbPlugin;
951
1041
  `;
952
1042
  async function opencodeInstall(file) {
953
1043
  try {
954
- const existing = await fs4.promises.readFile(file, "utf-8");
1044
+ const existing = await fs5.promises.readFile(file, "utf-8");
955
1045
  if (existing === OPENCODE_PLUGIN_CONTENT) {
956
1046
  return { installed: 0, alreadyPresent: 1 };
957
1047
  }
958
1048
  } catch {
959
1049
  }
960
- await fs4.promises.mkdir(path5.dirname(file), { recursive: true });
961
- await fs4.promises.writeFile(file, OPENCODE_PLUGIN_CONTENT);
1050
+ await fs5.promises.mkdir(path6.dirname(file), { recursive: true });
1051
+ await fs5.promises.writeFile(file, OPENCODE_PLUGIN_CONTENT);
962
1052
  return { installed: 1, alreadyPresent: 0 };
963
1053
  }
964
1054
  async function opencodeCheck(file) {
965
1055
  try {
966
- const content = await fs4.promises.readFile(file, "utf-8");
1056
+ const content = await fs5.promises.readFile(file, "utf-8");
967
1057
  return content.includes(OPENCODE_PLUGIN_MARKER);
968
1058
  } catch {
969
1059
  return false;
@@ -1021,7 +1111,7 @@ async function installHooksForTool(repoRoot, def) {
1021
1111
  const file = settingsPath(repoRoot, def);
1022
1112
  if (def.format === "opencode") {
1023
1113
  const r = await opencodeInstall(file);
1024
- return { settingsFile: file, ...r };
1114
+ return { settingsFile: file, ...r, changed: r.installed > 0 };
1025
1115
  }
1026
1116
  const settings = await readJson(file);
1027
1117
  let installed = 0;
@@ -1044,7 +1134,7 @@ async function installHooksForTool(repoRoot, def) {
1044
1134
  }
1045
1135
  }
1046
1136
  if (mutated) await writeJson(file, settings);
1047
- return { settingsFile: file, installed, alreadyPresent };
1137
+ return { settingsFile: file, installed, alreadyPresent, changed: mutated };
1048
1138
  }
1049
1139
  async function checkHooksForTool(repoRoot, def) {
1050
1140
  const file = settingsPath(repoRoot, def);
@@ -1084,6 +1174,29 @@ async function installDetectedHooks(repoRoot) {
1084
1174
  }
1085
1175
  return results;
1086
1176
  }
1177
+ async function healHookForTool(repoRoot, tool) {
1178
+ const def = TOOLS.find((candidate) => candidate.tool === tool);
1179
+ if (!def) {
1180
+ return {
1181
+ tool,
1182
+ changed: false,
1183
+ alreadyCurrent: false,
1184
+ installed: 0,
1185
+ alreadyPresent: 0,
1186
+ skipped: "unsupported-tool"
1187
+ };
1188
+ }
1189
+ const r = await installHooksForTool(repoRoot, def);
1190
+ return {
1191
+ tool: def.tool,
1192
+ label: def.label,
1193
+ settingsFile: r.settingsFile,
1194
+ changed: r.changed,
1195
+ alreadyCurrent: !r.changed,
1196
+ installed: r.installed,
1197
+ alreadyPresent: r.alreadyPresent
1198
+ };
1199
+ }
1087
1200
  async function checkAllHooks(repoRoot) {
1088
1201
  const results = [];
1089
1202
  for (const def of TOOLS) {
@@ -1097,17 +1210,17 @@ async function checkAllHooks(repoRoot) {
1097
1210
  }
1098
1211
  return results;
1099
1212
  }
1100
- var CODEX_CONFIG_PATH = path5.join(os2.homedir(), ".codex", "config.toml");
1213
+ var CODEX_CONFIG_PATH = path6.join(os2.homedir(), ".codex", "config.toml");
1101
1214
  async function ensureCodexHooksEnabled() {
1102
1215
  let content;
1103
1216
  try {
1104
- content = await fs4.promises.readFile(CODEX_CONFIG_PATH, "utf-8");
1217
+ content = await fs5.promises.readFile(CODEX_CONFIG_PATH, "utf-8");
1105
1218
  } catch (err) {
1106
1219
  if (err.code === "ENOENT") {
1107
- await fs4.promises.mkdir(path5.dirname(CODEX_CONFIG_PATH), {
1220
+ await fs5.promises.mkdir(path6.dirname(CODEX_CONFIG_PATH), {
1108
1221
  recursive: true
1109
1222
  });
1110
- await fs4.promises.writeFile(
1223
+ await fs5.promises.writeFile(
1111
1224
  CODEX_CONFIG_PATH,
1112
1225
  "[features]\nhooks = true\n"
1113
1226
  );
@@ -1148,7 +1261,7 @@ hooks = true
1148
1261
  `;
1149
1262
  }
1150
1263
  if (content === original) return false;
1151
- await fs4.promises.writeFile(CODEX_CONFIG_PATH, content);
1264
+ await fs5.promises.writeFile(CODEX_CONFIG_PATH, content);
1152
1265
  return true;
1153
1266
  }
1154
1267
  var CLAUDE_DEF = TOOLS[0];
@@ -1571,6 +1684,29 @@ async function runInit(args = []) {
1571
1684
  p3.log.success(`Installed ${r.label} hook`);
1572
1685
  }
1573
1686
  }
1687
+ try {
1688
+ const ignored = await ensureGitignored(
1689
+ repoRoot,
1690
+ hookResults.map((r) => r.settingsFile)
1691
+ );
1692
+ if (ignored.changed) {
1693
+ appendLog(
1694
+ "info",
1695
+ `init: gitignored hook files (${ignored.added.join(",")})`
1696
+ );
1697
+ p3.log.success(
1698
+ `${ignored.created ? "Created" : "Updated"} .gitignore for hook files`
1699
+ );
1700
+ }
1701
+ } catch (err) {
1702
+ appendLog(
1703
+ "warn",
1704
+ `init: could not update .gitignore for hook files: ${formatError(err)}`
1705
+ );
1706
+ p3.log.warn(
1707
+ `Could not update .gitignore for hook files: ${err instanceof Error ? err.message : String(err)}`
1708
+ );
1709
+ }
1574
1710
  if (hookResults.some((r) => r.tool === "codex")) {
1575
1711
  try {
1576
1712
  const changed = await ensureCodexHooksEnabled();
@@ -1821,7 +1957,7 @@ async function runLogout(args = []) {
1821
1957
  }
1822
1958
 
1823
1959
  // src/commands/status.ts
1824
- import path6 from "path";
1960
+ import path7 from "path";
1825
1961
  async function runStatus(args = []) {
1826
1962
  const debug = args.includes("--debug");
1827
1963
  header("status");
@@ -1839,7 +1975,7 @@ async function runStatus(args = []) {
1839
1975
  return;
1840
1976
  }
1841
1977
  const { repoRoot, config } = match;
1842
- const repoName = repo?.name ?? path6.basename(repoRoot);
1978
+ const repoName = repo?.name ?? path7.basename(repoRoot);
1843
1979
  if (IS_TTY) {
1844
1980
  process.stdout.write(` ${dim("Checking login...")}`);
1845
1981
  }
@@ -1933,7 +2069,7 @@ async function runStatus(args = []) {
1933
2069
  try {
1934
2070
  const hookStatuses = await checkAllHooks(repoRoot);
1935
2071
  for (const h of hookStatuses) {
1936
- const rel = path6.relative(repoRoot, h.settingsFile) || h.settingsFile;
2072
+ const rel = path7.relative(repoRoot, h.settingsFile) || h.settingsFile;
1937
2073
  debugRow(
1938
2074
  `${h.label} hook:`,
1939
2075
  `${rel} ${dim(`(${h.installed ? "installed" : "missing"})`)}`
@@ -1950,20 +2086,20 @@ async function runStatus(args = []) {
1950
2086
 
1951
2087
  // src/commands/upload.ts
1952
2088
  import { spawn as spawn2 } from "child_process";
1953
- import fs8 from "fs";
2089
+ import fs9 from "fs";
1954
2090
  import os5 from "os";
1955
- import path11 from "path";
2091
+ import path12 from "path";
1956
2092
 
1957
2093
  // src/middleware/pattern-redact.ts
1958
2094
  import os3 from "os";
1959
2095
  import { Worker } from "worker_threads";
1960
2096
 
1961
2097
  // src/middleware/file-utils.ts
1962
- import fs5 from "fs";
2098
+ import fs6 from "fs";
1963
2099
  async function checkBinary(filePath) {
1964
2100
  let handle = null;
1965
2101
  try {
1966
- handle = await fs5.promises.open(filePath, "r");
2102
+ handle = await fs6.promises.open(filePath, "r");
1967
2103
  const buf = Buffer.alloc(8192);
1968
2104
  const { bytesRead } = await handle.read(buf, 0, 8192, 0);
1969
2105
  for (let i = 0; i < bytesRead; i++) {
@@ -1988,7 +2124,7 @@ async function readFileContent(file) {
1988
2124
  return { kind: "binary" };
1989
2125
  }
1990
2126
  try {
1991
- const content = await fs5.promises.readFile(file.absolutePath, "utf-8");
2127
+ const content = await fs6.promises.readFile(file.absolutePath, "utf-8");
1992
2128
  return { kind: "text", content };
1993
2129
  } catch {
1994
2130
  return { kind: "error" };
@@ -10440,8 +10576,8 @@ var RedactMiddleware = class {
10440
10576
  var middleware = [];
10441
10577
 
10442
10578
  // src/middleware/secrets.ts
10443
- import fs6 from "fs";
10444
- import path7 from "path";
10579
+ import fs7 from "fs";
10580
+ import path8 from "path";
10445
10581
  var KNOWN_NON_SECRETS = /* @__PURE__ */ new Set([
10446
10582
  "true",
10447
10583
  "false",
@@ -10521,7 +10657,7 @@ async function parseEnvFile(filePath) {
10521
10657
  const values = [];
10522
10658
  let content;
10523
10659
  try {
10524
- content = await fs6.promises.readFile(filePath, "utf-8");
10660
+ content = await fs7.promises.readFile(filePath, "utf-8");
10525
10661
  } catch {
10526
10662
  return values;
10527
10663
  }
@@ -10589,16 +10725,16 @@ function addWithVariants(set, value) {
10589
10725
  async function discoverEnvFiles(repoRoot) {
10590
10726
  let entries;
10591
10727
  try {
10592
- entries = await fs6.promises.readdir(repoRoot);
10728
+ entries = await fs7.promises.readdir(repoRoot);
10593
10729
  } catch {
10594
10730
  return [];
10595
10731
  }
10596
10732
  const envFiles = [];
10597
10733
  for (const name of entries) {
10598
10734
  if (!name.startsWith(".env")) continue;
10599
- const filePath = path7.join(repoRoot, name);
10735
+ const filePath = path8.join(repoRoot, name);
10600
10736
  try {
10601
- const stat = await fs6.promises.stat(filePath);
10737
+ const stat = await fs7.promises.stat(filePath);
10602
10738
  if (stat.isFile()) envFiles.push(name);
10603
10739
  } catch {
10604
10740
  }
@@ -10621,7 +10757,7 @@ async function collectSecrets(repoRoot, envFiles, additionalFiles) {
10621
10757
  }
10622
10758
  }
10623
10759
  for (const filePath of additionalFiles) {
10624
- const resolved = path7.resolve(repoRoot, filePath);
10760
+ const resolved = path8.resolve(repoRoot, filePath);
10625
10761
  sourceFiles.push(resolved);
10626
10762
  for (const value of await parseEnvFile(resolved)) {
10627
10763
  if (isUsableValue(value)) {
@@ -10646,7 +10782,7 @@ async function collectSecrets(repoRoot, envFiles, additionalFiles) {
10646
10782
  }
10647
10783
 
10648
10784
  // src/normalizer/index.ts
10649
- import path8 from "path";
10785
+ import path9 from "path";
10650
10786
 
10651
10787
  // src/normalizer/claude.ts
10652
10788
  function stringify(value) {
@@ -11416,6 +11552,288 @@ function convertEventToStep2(event, stepId, defaultModelName) {
11416
11552
  return null;
11417
11553
  }
11418
11554
 
11555
+ // src/normalizer/types.ts
11556
+ var ATIF_VERSION = "ATIF-v1.6";
11557
+ function excludeNone(obj) {
11558
+ const result = {};
11559
+ for (const [key, value] of Object.entries(obj)) {
11560
+ if (value === void 0 || value === null) continue;
11561
+ if (Array.isArray(value)) {
11562
+ result[key] = value.map(
11563
+ (item) => typeof item === "object" && item !== null && !Array.isArray(item) ? excludeNone(item) : item
11564
+ );
11565
+ } else if (typeof value === "object" && !Array.isArray(value)) {
11566
+ result[key] = excludeNone(value);
11567
+ } else {
11568
+ result[key] = value;
11569
+ }
11570
+ }
11571
+ return result;
11572
+ }
11573
+
11574
+ // src/normalizer/copilotChat.ts
11575
+ function asObject(value) {
11576
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
11577
+ }
11578
+ function asNumber(value) {
11579
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
11580
+ }
11581
+ function compactExtra(extra) {
11582
+ const result = {};
11583
+ for (const [key, value] of Object.entries(extra)) {
11584
+ if (value !== void 0 && value !== null) result[key] = value;
11585
+ }
11586
+ return Object.keys(result).length > 0 ? result : void 0;
11587
+ }
11588
+ function parseJsonLines(content) {
11589
+ const entries = [];
11590
+ for (const line of content.split("\n")) {
11591
+ const trimmed = line.trim();
11592
+ if (!trimmed) continue;
11593
+ try {
11594
+ const parsed = asObject(JSON.parse(trimmed));
11595
+ if (!parsed) continue;
11596
+ entries.push({
11597
+ type: typeof parsed.type === "string" ? parsed.type : void 0,
11598
+ data: asObject(parsed.data),
11599
+ id: typeof parsed.id === "string" ? parsed.id : void 0,
11600
+ timestamp: typeof parsed.timestamp === "string" ? parsed.timestamp : void 0
11601
+ });
11602
+ } catch {
11603
+ }
11604
+ }
11605
+ return entries;
11606
+ }
11607
+ function parseArguments(value) {
11608
+ const obj = asObject(value);
11609
+ if (obj) return obj;
11610
+ if (typeof value === "string") {
11611
+ try {
11612
+ const parsed = asObject(JSON.parse(value));
11613
+ if (parsed) return parsed;
11614
+ } catch {
11615
+ return value ? { input: value } : {};
11616
+ }
11617
+ }
11618
+ return value === void 0 || value === null ? {} : { value };
11619
+ }
11620
+ function buildMetrics2(data) {
11621
+ const inputTokens = asNumber(data.inputTokens) ?? 0;
11622
+ const outputTokens = asNumber(data.outputTokens) ?? 0;
11623
+ const cacheReadTokens = asNumber(data.cacheReadTokens) ?? 0;
11624
+ const cacheWriteTokens = asNumber(data.cacheWriteTokens) ?? 0;
11625
+ const cost = asNumber(data.cost);
11626
+ if (!inputTokens && !outputTokens && !cacheReadTokens && !cacheWriteTokens && !cost) {
11627
+ return void 0;
11628
+ }
11629
+ return {
11630
+ prompt_tokens: inputTokens + cacheReadTokens || void 0,
11631
+ completion_tokens: outputTokens || void 0,
11632
+ cached_tokens: cacheReadTokens || void 0,
11633
+ cost_usd: cost || void 0,
11634
+ extra: compactExtra({
11635
+ cache_write_tokens: cacheWriteTokens || void 0,
11636
+ duration_ms: data.duration,
11637
+ initiator: data.initiator,
11638
+ api_call_id: data.apiCallId,
11639
+ provider_call_id: data.providerCallId,
11640
+ parent_tool_call_id: data.parentToolCallId,
11641
+ quota_snapshots: data.quotaSnapshots,
11642
+ copilot_usage: data.copilotUsage
11643
+ })
11644
+ };
11645
+ }
11646
+ function finalMetricsFromSteps(steps) {
11647
+ let prompt = 0;
11648
+ let completion = 0;
11649
+ let cached = 0;
11650
+ let cost = 0;
11651
+ for (const step of steps) {
11652
+ prompt += step.metrics?.prompt_tokens ?? 0;
11653
+ completion += step.metrics?.completion_tokens ?? 0;
11654
+ cached += step.metrics?.cached_tokens ?? 0;
11655
+ cost += step.metrics?.cost_usd ?? 0;
11656
+ }
11657
+ return {
11658
+ total_prompt_tokens: prompt || void 0,
11659
+ total_completion_tokens: completion || void 0,
11660
+ total_cached_tokens: cached || void 0,
11661
+ total_cost_usd: cost || void 0,
11662
+ total_steps: steps.length
11663
+ };
11664
+ }
11665
+ function makeToolCall(request) {
11666
+ const req = asObject(request);
11667
+ if (!req) return null;
11668
+ const callId = typeof req.toolCallId === "string" && req.toolCallId || typeof req.id === "string" && req.id || "";
11669
+ const name = typeof req.name === "string" && req.name || typeof req.toolName === "string" && req.toolName || "tool";
11670
+ return {
11671
+ tool_call_id: callId,
11672
+ function_name: name,
11673
+ arguments: parseArguments(req.arguments)
11674
+ };
11675
+ }
11676
+ function toolCallFromExecutionStart(data) {
11677
+ const callId = typeof data.toolCallId === "string" && data.toolCallId || typeof data.id === "string" && data.id || "";
11678
+ const name = typeof data.toolName === "string" && data.toolName || typeof data.name === "string" && data.name || "tool";
11679
+ return {
11680
+ tool_call_id: callId,
11681
+ function_name: name,
11682
+ arguments: parseArguments(data.arguments)
11683
+ };
11684
+ }
11685
+ function contentFromToolResult(data) {
11686
+ const result = asObject(data.result);
11687
+ if (typeof result?.content === "string") return result.content;
11688
+ if (typeof data.content === "string") return data.content;
11689
+ return void 0;
11690
+ }
11691
+ function convertCopilotChatToTrajectory(jsonlContent, sessionId) {
11692
+ const entries = parseJsonLines(jsonlContent);
11693
+ if (entries.length === 0) return null;
11694
+ let sid = sessionId ?? "unknown";
11695
+ let copilotVersion = "unknown";
11696
+ let vscodeVersion;
11697
+ let cwd;
11698
+ let defaultModelName;
11699
+ const steps = [];
11700
+ const pendingReasoning = [];
11701
+ const pendingToolSteps = /* @__PURE__ */ new Map();
11702
+ let lastAgentStep;
11703
+ for (const entry of entries) {
11704
+ const data = entry.data ?? {};
11705
+ if (entry.type === "session.start") {
11706
+ if (!sessionId && typeof data.sessionId === "string")
11707
+ sid = data.sessionId;
11708
+ if (typeof data.copilotVersion === "string")
11709
+ copilotVersion = data.copilotVersion;
11710
+ if (typeof data.vscodeVersion === "string")
11711
+ vscodeVersion = data.vscodeVersion;
11712
+ const context = asObject(data.context);
11713
+ if (typeof context?.cwd === "string") cwd = context.cwd;
11714
+ continue;
11715
+ }
11716
+ if (entry.type === "user.message" || entry.type === "system.message") {
11717
+ const content = typeof data.content === "string" && data.content || typeof data.transformedContent === "string" && data.transformedContent || "";
11718
+ if (!content.trim()) continue;
11719
+ const source = entry.type === "system.message" ? "system" : "user";
11720
+ const step = {
11721
+ step_id: steps.length + 1,
11722
+ timestamp: entry.timestamp,
11723
+ source,
11724
+ message: content,
11725
+ extra: compactExtra({
11726
+ attachments: data.attachments,
11727
+ source: data.source,
11728
+ agent_mode: data.agentMode,
11729
+ interaction_id: data.interactionId
11730
+ })
11731
+ };
11732
+ steps.push(step);
11733
+ continue;
11734
+ }
11735
+ if (entry.type === "assistant.reasoning") {
11736
+ if (typeof data.content === "string" && data.content.trim()) {
11737
+ pendingReasoning.push(data.content.trim());
11738
+ }
11739
+ continue;
11740
+ }
11741
+ if (entry.type === "assistant.message") {
11742
+ const toolCalls = (Array.isArray(data.toolRequests) ? data.toolRequests : []).flatMap((request) => {
11743
+ const call = makeToolCall(request);
11744
+ return call ? [call] : [];
11745
+ });
11746
+ const content = typeof data.content === "string" && data.content.trim() ? data.content.trim() : "";
11747
+ const reasoning = typeof data.reasoningText === "string" && data.reasoningText.trim() || pendingReasoning.join("\n\n") || void 0;
11748
+ pendingReasoning.length = 0;
11749
+ const step = {
11750
+ step_id: steps.length + 1,
11751
+ timestamp: entry.timestamp,
11752
+ source: "agent",
11753
+ message: content || (toolCalls.length > 0 ? "(tool use)" : ""),
11754
+ model_name: defaultModelName,
11755
+ extra: compactExtra({
11756
+ message_id: data.messageId,
11757
+ phase: data.phase,
11758
+ output_tokens: data.outputTokens
11759
+ })
11760
+ };
11761
+ if (reasoning) step.reasoning_content = reasoning;
11762
+ if (toolCalls.length > 0) step.tool_calls = toolCalls;
11763
+ if (step.message || step.tool_calls?.length || step.reasoning_content) {
11764
+ steps.push(step);
11765
+ lastAgentStep = step;
11766
+ for (const call of toolCalls) {
11767
+ if (call.tool_call_id) pendingToolSteps.set(call.tool_call_id, step);
11768
+ }
11769
+ }
11770
+ continue;
11771
+ }
11772
+ if (entry.type === "tool.execution_start") {
11773
+ const toolCall = toolCallFromExecutionStart(data);
11774
+ const step = {
11775
+ step_id: steps.length + 1,
11776
+ timestamp: entry.timestamp,
11777
+ source: "agent",
11778
+ message: `Executed ${toolCall.function_name} ${toolCall.tool_call_id}`.trim(),
11779
+ model_name: defaultModelName,
11780
+ tool_calls: [toolCall]
11781
+ };
11782
+ steps.push(step);
11783
+ lastAgentStep = step;
11784
+ if (toolCall.tool_call_id)
11785
+ pendingToolSteps.set(toolCall.tool_call_id, step);
11786
+ continue;
11787
+ }
11788
+ if (entry.type === "tool.execution_complete") {
11789
+ const callId = typeof data.toolCallId === "string" && data.toolCallId || typeof data.id === "string" && data.id || "";
11790
+ const target = pendingToolSteps.get(callId);
11791
+ const content = contentFromToolResult(data);
11792
+ if (target && content !== void 0) {
11793
+ const observation = target.observation ?? { results: [] };
11794
+ observation.results.push({
11795
+ source_call_id: callId || void 0,
11796
+ content
11797
+ });
11798
+ target.observation = observation;
11799
+ const extra = { ...target.extra ?? {} };
11800
+ extra.tool_success = data.success;
11801
+ target.extra = compactExtra(extra);
11802
+ }
11803
+ if (callId) pendingToolSteps.delete(callId);
11804
+ continue;
11805
+ }
11806
+ if (entry.type === "assistant.usage") {
11807
+ if (typeof data.model === "string" && !defaultModelName) {
11808
+ defaultModelName = data.model;
11809
+ if (lastAgentStep && !lastAgentStep.model_name) {
11810
+ lastAgentStep.model_name = defaultModelName;
11811
+ }
11812
+ }
11813
+ const metrics = buildMetrics2(data);
11814
+ if (metrics && lastAgentStep && !lastAgentStep.metrics) {
11815
+ lastAgentStep.metrics = metrics;
11816
+ }
11817
+ }
11818
+ }
11819
+ if (steps.length === 0) return null;
11820
+ return {
11821
+ schema_version: ATIF_VERSION,
11822
+ session_id: sid,
11823
+ agent: {
11824
+ name: "github-copilot-chat",
11825
+ version: copilotVersion,
11826
+ model_name: defaultModelName,
11827
+ extra: compactExtra({
11828
+ vscode_version: vscodeVersion,
11829
+ cwd
11830
+ })
11831
+ },
11832
+ steps,
11833
+ final_metrics: finalMetricsFromSteps(steps)
11834
+ };
11835
+ }
11836
+
11419
11837
  // src/normalizer/cursor.ts
11420
11838
  function convertCursorToTrajectory(jsonlContent, sessionId) {
11421
11839
  const lines = [];
@@ -11473,22 +11891,501 @@ function convertCursorToTrajectory(jsonlContent, sessionId) {
11473
11891
  };
11474
11892
  }
11475
11893
 
11476
- // src/normalizer/types.ts
11477
- function excludeNone(obj) {
11894
+ // src/normalizer/opencode.ts
11895
+ function asObject2(value) {
11896
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
11897
+ }
11898
+ function asArray(value) {
11899
+ return Array.isArray(value) ? value : [];
11900
+ }
11901
+ function asNumber2(value) {
11902
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
11903
+ }
11904
+ function compactExtra2(extra) {
11478
11905
  const result = {};
11479
- for (const [key, value] of Object.entries(obj)) {
11480
- if (value === void 0 || value === null) continue;
11481
- if (Array.isArray(value)) {
11482
- result[key] = value.map(
11483
- (item) => typeof item === "object" && item !== null && !Array.isArray(item) ? excludeNone(item) : item
11906
+ for (const [key, value] of Object.entries(extra)) {
11907
+ if (value !== void 0 && value !== null) result[key] = value;
11908
+ }
11909
+ return Object.keys(result).length > 0 ? result : void 0;
11910
+ }
11911
+ function parseJsonLines2(content) {
11912
+ const events = [];
11913
+ for (const line of content.split("\n")) {
11914
+ const trimmed = line.trim();
11915
+ if (!trimmed) continue;
11916
+ try {
11917
+ const parsed = JSON.parse(trimmed);
11918
+ const obj = asObject2(parsed);
11919
+ if (obj) events.push(obj);
11920
+ } catch {
11921
+ }
11922
+ }
11923
+ return events;
11924
+ }
11925
+ function timestampToIso(value) {
11926
+ const numeric = asNumber2(value);
11927
+ if (numeric === void 0) return void 0;
11928
+ const millis = numeric < 1e12 ? numeric * 1e3 : numeric;
11929
+ const date = new Date(millis);
11930
+ return Number.isNaN(date.getTime()) ? void 0 : date.toISOString();
11931
+ }
11932
+ function timeFromObject(value) {
11933
+ const obj = asObject2(value);
11934
+ if (!obj) return void 0;
11935
+ return timestampToIso(obj.start ?? obj.created ?? obj.completed ?? obj.end);
11936
+ }
11937
+ function stringify2(value) {
11938
+ if (typeof value === "string") return value;
11939
+ try {
11940
+ return JSON.stringify(value);
11941
+ } catch {
11942
+ return String(value);
11943
+ }
11944
+ }
11945
+ function argsFromUnknown(value) {
11946
+ const obj = asObject2(value);
11947
+ if (obj) return obj;
11948
+ if (typeof value === "string") {
11949
+ try {
11950
+ const parsed = JSON.parse(value);
11951
+ const parsedObj = asObject2(parsed);
11952
+ if (parsedObj) return parsedObj;
11953
+ } catch {
11954
+ return value ? { input: value } : {};
11955
+ }
11956
+ }
11957
+ return value === void 0 || value === null ? {} : { value };
11958
+ }
11959
+ function modelNameFromInfo(info) {
11960
+ const model = asObject2(info.model);
11961
+ const modelID = typeof info.modelID === "string" && info.modelID || typeof model?.modelID === "string" && model.modelID || void 0;
11962
+ const providerID = typeof info.providerID === "string" && info.providerID || typeof model?.providerID === "string" && model.providerID || void 0;
11963
+ if (providerID && modelID) return `${providerID}/${modelID}`;
11964
+ return modelID;
11965
+ }
11966
+ function metricsFromTokens(tokens, cost) {
11967
+ const t = asObject2(tokens);
11968
+ if (!t) return void 0;
11969
+ const cache = asObject2(t.cache);
11970
+ const input = asNumber2(t.input) ?? 0;
11971
+ const output = asNumber2(t.output) ?? 0;
11972
+ const reasoning = asNumber2(t.reasoning) ?? 0;
11973
+ const cacheRead = asNumber2(cache?.read) ?? 0;
11974
+ const cacheWrite = asNumber2(cache?.write) ?? 0;
11975
+ const costUsd = asNumber2(cost);
11976
+ if (!input && !output && !cacheRead && !cacheWrite && !costUsd) {
11977
+ return void 0;
11978
+ }
11979
+ const extra = compactExtra2({
11980
+ reasoning_tokens: reasoning || void 0,
11981
+ cache_write_tokens: cacheWrite || void 0
11982
+ });
11983
+ return {
11984
+ prompt_tokens: input + cacheRead || void 0,
11985
+ completion_tokens: output || void 0,
11986
+ cached_tokens: cacheRead || void 0,
11987
+ cost_usd: costUsd || void 0,
11988
+ extra
11989
+ };
11990
+ }
11991
+ function metricsFromInfo(info) {
11992
+ return metricsFromTokens(info.tokens, info.cost);
11993
+ }
11994
+ function addFinalMetricTotals(totals, metrics) {
11995
+ if (!metrics) return;
11996
+ totals.total_prompt_tokens += metrics.prompt_tokens ?? 0;
11997
+ totals.total_completion_tokens += metrics.completion_tokens ?? 0;
11998
+ totals.total_cached_tokens += metrics.cached_tokens ?? 0;
11999
+ totals.total_cost_usd += metrics.cost_usd ?? 0;
12000
+ }
12001
+ function finalMetricsFromSteps2(steps) {
12002
+ const totals = {
12003
+ total_prompt_tokens: 0,
12004
+ total_completion_tokens: 0,
12005
+ total_cached_tokens: 0,
12006
+ total_cost_usd: 0,
12007
+ total_steps: steps.length,
12008
+ extra: {}
12009
+ };
12010
+ for (const step of steps) addFinalMetricTotals(totals, step.metrics);
12011
+ return {
12012
+ total_prompt_tokens: totals.total_prompt_tokens || void 0,
12013
+ total_completion_tokens: totals.total_completion_tokens || void 0,
12014
+ total_cached_tokens: totals.total_cached_tokens || void 0,
12015
+ total_cost_usd: totals.total_cost_usd || void 0,
12016
+ total_steps: steps.length
12017
+ };
12018
+ }
12019
+ function entryFromLine(line) {
12020
+ const info = asObject2(line.info);
12021
+ if (info) {
12022
+ return {
12023
+ info,
12024
+ parts: asArray(line.parts).flatMap((part) => {
12025
+ const obj = asObject2(part);
12026
+ return obj ? [obj] : [];
12027
+ })
12028
+ };
12029
+ }
12030
+ if (typeof line.role === "string" || typeof line.sessionID === "string") {
12031
+ return {
12032
+ info: line,
12033
+ parts: asArray(line.parts).flatMap((part) => {
12034
+ const obj = asObject2(part);
12035
+ return obj ? [obj] : [];
12036
+ })
12037
+ };
12038
+ }
12039
+ return null;
12040
+ }
12041
+ function entriesFromEventWrappers(lines) {
12042
+ const byMessage = /* @__PURE__ */ new Map();
12043
+ function getEntry(messageID, sessionID) {
12044
+ let entry = byMessage.get(messageID);
12045
+ if (!entry) {
12046
+ entry = { info: { id: messageID, sessionID }, parts: [] };
12047
+ byMessage.set(messageID, entry);
12048
+ }
12049
+ return entry;
12050
+ }
12051
+ for (const line of lines) {
12052
+ const type = line.type;
12053
+ const props = asObject2(line.properties) ?? line;
12054
+ if (type === "message.updated") {
12055
+ const info = asObject2(props.info);
12056
+ const id = typeof info?.id === "string" ? info.id : void 0;
12057
+ if (!info || !id) continue;
12058
+ const entry = getEntry(id, info.sessionID);
12059
+ entry.info = info;
12060
+ continue;
12061
+ }
12062
+ if (type === "message.part.updated") {
12063
+ const part = asObject2(props.part);
12064
+ const messageID = typeof part?.messageID === "string" && part.messageID || void 0;
12065
+ if (!part || !messageID) continue;
12066
+ const entry = getEntry(messageID, part.sessionID);
12067
+ const partID = typeof part.id === "string" ? part.id : void 0;
12068
+ const existingIndex = partID ? entry.parts.findIndex((p7) => p7.id === partID) : -1;
12069
+ if (existingIndex >= 0) entry.parts[existingIndex] = part;
12070
+ else entry.parts.push(part);
12071
+ }
12072
+ }
12073
+ return [...byMessage.values()];
12074
+ }
12075
+ function getSessionId(sessionId, exportInfo, entries) {
12076
+ if (sessionId) return sessionId;
12077
+ if (typeof exportInfo?.id === "string") return exportInfo.id;
12078
+ for (const entry of entries) {
12079
+ if (typeof entry.info.sessionID === "string") return entry.info.sessionID;
12080
+ }
12081
+ return "unknown";
12082
+ }
12083
+ function getAgentVersion(exportInfo) {
12084
+ return typeof exportInfo?.version === "string" && exportInfo.version || "unknown";
12085
+ }
12086
+ function sortEntries(entries) {
12087
+ return [...entries].sort((a, b) => {
12088
+ const at = asNumber2(asObject2(a.info.time)?.created) ?? 0;
12089
+ const bt = asNumber2(asObject2(b.info.time)?.created) ?? 0;
12090
+ return at - bt;
12091
+ });
12092
+ }
12093
+ function splitAssistantParts(parts) {
12094
+ let sawBoundary = false;
12095
+ const groups = [];
12096
+ let current = [];
12097
+ for (const part of parts) {
12098
+ const type = part.type;
12099
+ if (type === "step-start") {
12100
+ sawBoundary = true;
12101
+ if (current.length > 0) groups.push(current);
12102
+ current = [part];
12103
+ continue;
12104
+ }
12105
+ current.push(part);
12106
+ if (type === "step-finish") {
12107
+ sawBoundary = true;
12108
+ groups.push(current);
12109
+ current = [];
12110
+ }
12111
+ }
12112
+ if (current.length > 0) groups.push(current);
12113
+ return sawBoundary ? groups : [parts];
12114
+ }
12115
+ function textFromFilePart(part) {
12116
+ const filename = typeof part.filename === "string" ? part.filename : void 0;
12117
+ const url = typeof part.url === "string" ? part.url : void 0;
12118
+ const mime = typeof part.mime === "string" ? part.mime : void 0;
12119
+ const label = filename ?? url;
12120
+ if (!label) return void 0;
12121
+ return mime ? `[file:${mime}] ${label}` : `[file] ${label}`;
12122
+ }
12123
+ function buildUserStep(entry, stepId, defaultModelName) {
12124
+ const textParts = [];
12125
+ const extra = {};
12126
+ for (const part of entry.parts) {
12127
+ if (part.type === "text" && typeof part.text === "string") {
12128
+ if (part.text.trim()) textParts.push(part.text.trim());
12129
+ continue;
12130
+ }
12131
+ if (part.type === "file") {
12132
+ const fileText = textFromFilePart(part);
12133
+ if (fileText) textParts.push(fileText);
12134
+ }
12135
+ }
12136
+ const message = textParts.join("\n\n").trim();
12137
+ if (!message) return null;
12138
+ if (entry.info.agent) extra.agent = entry.info.agent;
12139
+ if (entry.info.tools) extra.tools = entry.info.tools;
12140
+ return {
12141
+ step_id: stepId,
12142
+ timestamp: timeFromObject(entry.info.time),
12143
+ source: "user",
12144
+ message,
12145
+ model_name: defaultModelName,
12146
+ extra: compactExtra2(extra)
12147
+ };
12148
+ }
12149
+ function buildAgentStep(parts, info, stepId, defaultModelName, fallbackMetrics) {
12150
+ const textParts = [];
12151
+ const reasoningParts = [];
12152
+ const toolCalls = [];
12153
+ const observationResults = [];
12154
+ const extra = {};
12155
+ let metrics;
12156
+ let timestamp = timeFromObject(info.time);
12157
+ for (const part of parts) {
12158
+ if (!timestamp) timestamp = timeFromObject(part.time);
12159
+ switch (part.type) {
12160
+ case "text":
12161
+ if (typeof part.text === "string" && part.text.trim()) {
12162
+ textParts.push(part.text.trim());
12163
+ }
12164
+ break;
12165
+ case "reasoning":
12166
+ if (typeof part.text === "string" && part.text.trim()) {
12167
+ reasoningParts.push(part.text.trim());
12168
+ }
12169
+ break;
12170
+ case "file": {
12171
+ const fileText = textFromFilePart(part);
12172
+ if (fileText) textParts.push(fileText);
12173
+ break;
12174
+ }
12175
+ case "tool": {
12176
+ const state = asObject2(part.state) ?? {};
12177
+ const callID = typeof part.callID === "string" && part.callID || typeof part.id === "string" && part.id || "";
12178
+ const toolName = typeof part.tool === "string" && part.tool || "tool";
12179
+ const input = argsFromUnknown(state.input);
12180
+ toolCalls.push({
12181
+ tool_call_id: callID,
12182
+ function_name: toolName,
12183
+ arguments: input
12184
+ });
12185
+ const output = state.output ?? state.error;
12186
+ if (output !== void 0 && output !== null) {
12187
+ observationResults.push({
12188
+ source_call_id: callID || void 0,
12189
+ content: stringify2(output)
12190
+ });
12191
+ }
12192
+ if (state.status) extra.status = state.status;
12193
+ if (state.metadata) extra.tool_metadata = state.metadata;
12194
+ if (part.metadata) extra.part_metadata = part.metadata;
12195
+ break;
12196
+ }
12197
+ case "step-finish":
12198
+ metrics = metricsFromTokens(part.tokens, part.cost) ?? metrics;
12199
+ if (part.reason) extra.finish_reason = part.reason;
12200
+ if (part.snapshot) extra.snapshot = part.snapshot;
12201
+ break;
12202
+ case "patch":
12203
+ extra.patches = [...asArray(extra.patches), part];
12204
+ break;
12205
+ case "agent":
12206
+ if (part.name) extra.agent = part.name;
12207
+ break;
12208
+ case "retry":
12209
+ extra.retry = part;
12210
+ break;
12211
+ }
12212
+ }
12213
+ metrics = metrics ?? fallbackMetrics;
12214
+ if (textParts.length === 0 && reasoningParts.length === 0 && toolCalls.length === 0 && !metrics) {
12215
+ return null;
12216
+ }
12217
+ const observation = observationResults.length > 0 ? { results: observationResults } : void 0;
12218
+ const step = {
12219
+ step_id: stepId,
12220
+ timestamp,
12221
+ source: "agent",
12222
+ message: textParts.length > 0 ? textParts.join("\n\n") : "(tool use)",
12223
+ model_name: modelNameFromInfo(info) ?? defaultModelName
12224
+ };
12225
+ if (reasoningParts.length > 0)
12226
+ step.reasoning_content = reasoningParts.join("\n\n");
12227
+ if (toolCalls.length > 0) step.tool_calls = toolCalls;
12228
+ if (observation) step.observation = observation;
12229
+ if (metrics) step.metrics = metrics;
12230
+ const compactedExtra = compactExtra2(extra);
12231
+ if (compactedExtra) step.extra = compactedExtra;
12232
+ return step;
12233
+ }
12234
+ function buildUnavailableAgentStep(info, stepId, defaultModelName) {
12235
+ const metrics = metricsFromInfo(info);
12236
+ if (!metrics && !modelNameFromInfo(info) && !info.error && !info.finish) {
12237
+ return null;
12238
+ }
12239
+ return {
12240
+ step_id: stepId,
12241
+ timestamp: timeFromObject(info.time),
12242
+ source: "agent",
12243
+ message: "(message unavailable)",
12244
+ model_name: modelNameFromInfo(info) ?? defaultModelName,
12245
+ metrics,
12246
+ extra: compactExtra2({
12247
+ content_unavailable: true,
12248
+ finish_reason: info.finish,
12249
+ error: info.error
12250
+ })
12251
+ };
12252
+ }
12253
+ function convertMessageEntriesToTrajectory(entries, sessionId, exportInfo) {
12254
+ if (entries.length === 0) return null;
12255
+ const orderedEntries = sortEntries(entries);
12256
+ const defaultModelName = orderedEntries.map((entry) => modelNameFromInfo(entry.info)).find((model) => typeof model === "string");
12257
+ const steps = [];
12258
+ for (const entry of orderedEntries) {
12259
+ const role = entry.info.role;
12260
+ if (role === "user") {
12261
+ const step = buildUserStep(entry, steps.length + 1, defaultModelName);
12262
+ if (step) steps.push(step);
12263
+ continue;
12264
+ }
12265
+ if (role !== "assistant") continue;
12266
+ if (entry.parts.length === 0) {
12267
+ const step = buildUnavailableAgentStep(
12268
+ entry.info,
12269
+ steps.length + 1,
12270
+ defaultModelName
11484
12271
  );
11485
- } else if (typeof value === "object" && !Array.isArray(value)) {
11486
- result[key] = excludeNone(value);
11487
- } else {
11488
- result[key] = value;
12272
+ if (step) steps.push(step);
12273
+ continue;
12274
+ }
12275
+ const groups = splitAssistantParts(entry.parts);
12276
+ const hasPartMetrics = groups.some(
12277
+ (group) => group.some((part) => part.type === "step-finish")
12278
+ );
12279
+ const fallbackMetrics = hasPartMetrics ? void 0 : metricsFromInfo(entry.info);
12280
+ for (let i = 0; i < groups.length; i++) {
12281
+ const step = buildAgentStep(
12282
+ groups[i],
12283
+ entry.info,
12284
+ steps.length + 1,
12285
+ defaultModelName,
12286
+ i === 0 ? fallbackMetrics : void 0
12287
+ );
12288
+ if (step) steps.push(step);
11489
12289
  }
11490
12290
  }
11491
- return result;
12291
+ if (steps.length === 0) return null;
12292
+ return {
12293
+ schema_version: ATIF_VERSION,
12294
+ session_id: getSessionId(sessionId, exportInfo, orderedEntries),
12295
+ agent: {
12296
+ name: "opencode",
12297
+ version: getAgentVersion(exportInfo),
12298
+ model_name: defaultModelName
12299
+ },
12300
+ steps,
12301
+ final_metrics: finalMetricsFromSteps2(steps)
12302
+ };
12303
+ }
12304
+ function convertRunEventsToTrajectory(events, sessionId) {
12305
+ const session = sessionId ?? (events.map((event) => event.sessionID).find((sid) => typeof sid === "string") || "unknown");
12306
+ const turns = [];
12307
+ let current = null;
12308
+ for (const event of events) {
12309
+ const type = event.type;
12310
+ if (type === "step_start") {
12311
+ current = { parts: [], timestamp: event.timestamp };
12312
+ continue;
12313
+ }
12314
+ if (type === "step_finish") {
12315
+ if (current) {
12316
+ current.finish = asObject2(event.part) ?? {};
12317
+ turns.push(current);
12318
+ current = null;
12319
+ }
12320
+ continue;
12321
+ }
12322
+ if (current && (type === "text" || type === "reasoning" || type === "tool_use")) {
12323
+ const part = asObject2(event.part);
12324
+ if (part) current.parts.push(part);
12325
+ }
12326
+ }
12327
+ const steps = [];
12328
+ for (const turn of turns) {
12329
+ const parts = [...turn.parts];
12330
+ if (turn.finish) {
12331
+ parts.push({ ...turn.finish, type: "step-finish" });
12332
+ }
12333
+ const step = buildAgentStep(
12334
+ parts,
12335
+ { time: { created: turn.timestamp } },
12336
+ steps.length + 1,
12337
+ void 0
12338
+ );
12339
+ if (step) steps.push(step);
12340
+ }
12341
+ if (steps.length === 0) return null;
12342
+ return {
12343
+ schema_version: ATIF_VERSION,
12344
+ session_id: session,
12345
+ agent: {
12346
+ name: "opencode",
12347
+ version: "unknown"
12348
+ },
12349
+ steps,
12350
+ final_metrics: finalMetricsFromSteps2(steps)
12351
+ };
12352
+ }
12353
+ function isRunEvent(lines) {
12354
+ return lines.some(
12355
+ (line) => ["step_start", "step_finish", "text", "reasoning", "tool_use"].includes(
12356
+ String(line.type ?? "")
12357
+ )
12358
+ );
12359
+ }
12360
+ function convertOpenCodeToTrajectory(content, sessionId) {
12361
+ const trimmed = content.trim();
12362
+ if (!trimmed) return null;
12363
+ try {
12364
+ const parsed = JSON.parse(trimmed);
12365
+ const parsedObj = asObject2(parsed);
12366
+ const messages = asArray(parsedObj?.messages);
12367
+ if (parsedObj && messages.length > 0) {
12368
+ const entries2 = messages.flatMap((message) => {
12369
+ const entry = entryFromLine(asObject2(message) ?? {});
12370
+ return entry ? [entry] : [];
12371
+ });
12372
+ return convertMessageEntriesToTrajectory(
12373
+ entries2,
12374
+ sessionId,
12375
+ asObject2(parsedObj.info)
12376
+ );
12377
+ }
12378
+ } catch {
12379
+ }
12380
+ const lines = parseJsonLines2(content);
12381
+ if (lines.length === 0) return null;
12382
+ if (isRunEvent(lines)) return convertRunEventsToTrajectory(lines, sessionId);
12383
+ const wrappedEntries = entriesFromEventWrappers(lines);
12384
+ const entries = wrappedEntries.length > 0 ? wrappedEntries : lines.flatMap((line) => {
12385
+ const entry = entryFromLine(line);
12386
+ return entry ? [entry] : [];
12387
+ });
12388
+ return convertMessageEntriesToTrajectory(entries, sessionId);
11492
12389
  }
11493
12390
 
11494
12391
  // src/normalizer/index.ts
@@ -11500,6 +12397,10 @@ function normalizeContent(sourceName, content, sessionId) {
11500
12397
  return convertCodexToTrajectory(content, sessionId);
11501
12398
  case "cursor":
11502
12399
  return convertCursorToTrajectory(content, sessionId);
12400
+ case "opencode":
12401
+ return convertOpenCodeToTrajectory(content, sessionId);
12402
+ case "copilot-chat":
12403
+ return convertCopilotChatToTrajectory(content, sessionId);
11503
12404
  default:
11504
12405
  return null;
11505
12406
  }
@@ -11511,10 +12412,13 @@ var NormalizeMiddleware = class {
11511
12412
  for (const file of group.files) {
11512
12413
  newFiles.push(file);
11513
12414
  if (!file.absolutePath.endsWith(".jsonl")) continue;
11514
- if (!["claude", "codex", "cursor"].includes(file.sourceName)) continue;
12415
+ if (!["claude", "codex", "cursor", "opencode", "copilot-chat"].includes(
12416
+ file.sourceName
12417
+ ))
12418
+ continue;
11515
12419
  const content = file.content ? file.content.toString("utf-8") : null;
11516
12420
  if (!content) continue;
11517
- const sessionId = file.metadata?.sessionId ?? path8.basename(file.absolutePath, ".jsonl");
12421
+ const sessionId = file.metadata?.sessionId ?? path9.basename(file.absolutePath, ".jsonl");
11518
12422
  try {
11519
12423
  const trajectory = normalizeContent(
11520
12424
  file.sourceName,
@@ -11548,14 +12452,14 @@ import archiver from "archiver";
11548
12452
 
11549
12453
  // src/outputs/archive.ts
11550
12454
  import os4 from "os";
11551
- import path9 from "path";
12455
+ import path10 from "path";
11552
12456
  function getSourceBaseDir(sourceName) {
11553
12457
  const home = os4.homedir();
11554
12458
  switch (sourceName) {
11555
12459
  case "claude":
11556
- return path9.join(home, ".claude", "projects");
12460
+ return path10.join(home, ".claude", "projects");
11557
12461
  case "codex":
11558
- return path9.join(home, ".codex", "sessions");
12462
+ return path10.join(home, ".codex", "sessions");
11559
12463
  default:
11560
12464
  return home;
11561
12465
  }
@@ -11564,8 +12468,8 @@ function addGroupToArchive(archive, group, selectedSources) {
11564
12468
  for (const file of group.files) {
11565
12469
  if (!selectedSources.has(file.sourceName)) continue;
11566
12470
  const baseDir = getSourceBaseDir(file.sourceName);
11567
- const relativePath = file.absolutePath.startsWith(baseDir) ? path9.relative(baseDir, file.absolutePath) : path9.basename(file.absolutePath);
11568
- const archivePath = path9.join(file.sourceName, relativePath);
12471
+ const relativePath = file.absolutePath.startsWith(baseDir) ? path10.relative(baseDir, file.absolutePath) : path10.basename(file.absolutePath);
12472
+ const archivePath = path10.join(file.sourceName, relativePath);
11569
12473
  if (file.content) {
11570
12474
  archive.append(file.content, { name: archivePath });
11571
12475
  } else {
@@ -11639,21 +12543,21 @@ var PlatformUploadOutput = class {
11639
12543
  };
11640
12544
 
11641
12545
  // src/pipeline.ts
11642
- import fs7 from "fs";
11643
- import path10 from "path";
12546
+ import fs8 from "fs";
12547
+ import path11 from "path";
11644
12548
  function canonicalizePath(p7) {
11645
- let resolved = path10.resolve(p7);
11646
- if (resolved.endsWith(path10.sep) && resolved !== path10.sep) {
12549
+ let resolved = path11.resolve(p7);
12550
+ if (resolved.endsWith(path11.sep) && resolved !== path11.sep) {
11647
12551
  resolved = resolved.slice(0, -1);
11648
12552
  }
11649
12553
  return resolved;
11650
12554
  }
11651
12555
  function computeLabel(repoPath, allPaths) {
11652
- const segments = repoPath.split(path10.sep).filter(Boolean);
12556
+ const segments = repoPath.split(path11.sep).filter(Boolean);
11653
12557
  for (let depth = 1; depth <= segments.length; depth++) {
11654
12558
  const label = segments.slice(-depth).join("/");
11655
12559
  const matches = allPaths.filter((p7) => {
11656
- const s = p7.split(path10.sep).filter(Boolean);
12560
+ const s = p7.split(path11.sep).filter(Boolean);
11657
12561
  return s.slice(-depth).join("/") === label;
11658
12562
  });
11659
12563
  if (matches.length === 1) return label;
@@ -11675,7 +12579,7 @@ async function mergeByRepo(files) {
11675
12579
  const groups = [];
11676
12580
  for (const [repoPath, groupFiles] of grouped) {
11677
12581
  const stats = await Promise.all(
11678
- groupFiles.map((f) => fs7.promises.stat(f.absolutePath).catch(() => null))
12582
+ groupFiles.map((f) => fs8.promises.stat(f.absolutePath).catch(() => null))
11679
12583
  );
11680
12584
  let lastModified = /* @__PURE__ */ new Date(0);
11681
12585
  for (const stat of stats) {
@@ -11698,7 +12602,7 @@ async function preloadFiles(group) {
11698
12602
  group.files.map(async (file) => {
11699
12603
  if (file.content) return file;
11700
12604
  try {
11701
- const buf = await fs7.promises.readFile(file.absolutePath);
12605
+ const buf = await fs8.promises.readFile(file.absolutePath);
11702
12606
  const checkLen = Math.min(buf.length, 8192);
11703
12607
  for (let i = 0; i < checkLen; i++) {
11704
12608
  if (buf[i] === 0) {
@@ -11743,7 +12647,7 @@ function lineHasAssistant(line) {
11743
12647
  return line.includes('"type":"assistant"') || line.includes('"role":"assistant"') || line.includes('"type":"assistant.');
11744
12648
  }
11745
12649
  async function hasAssistantMessage(transcriptPath) {
11746
- const stream = fs8.createReadStream(transcriptPath, { encoding: "utf-8" });
12650
+ const stream = fs9.createReadStream(transcriptPath, { encoding: "utf-8" });
11747
12651
  let buffer = "";
11748
12652
  try {
11749
12653
  for await (const chunk of stream) {
@@ -11771,12 +12675,29 @@ function resolveSourceTool(payload) {
11771
12675
  if (payload.hook_event_name === "Stop") return "codex";
11772
12676
  return "claude";
11773
12677
  }
12678
+ async function selfHealHook(repoRoot, tool) {
12679
+ try {
12680
+ const result = await healHookForTool(repoRoot, tool);
12681
+ if (result.skipped) {
12682
+ appendLog("warn", `self-heal: skipped ${tool} hook (${result.skipped})`);
12683
+ return;
12684
+ }
12685
+ if (result.changed) {
12686
+ appendLog("info", `self-heal: updated ${tool} hook`);
12687
+ }
12688
+ } catch (err) {
12689
+ appendLog(
12690
+ "warn",
12691
+ `self-heal: failed to repair ${tool} hook: ${err instanceof Error ? err.message : String(err)}`
12692
+ );
12693
+ }
12694
+ }
11774
12695
  function resolveCursorTranscriptPath(payload) {
11775
12696
  const id = payload.conversation_id ?? payload.session_id;
11776
12697
  const workspace = payload.workspace_roots?.[0];
11777
12698
  if (!id || !workspace) return void 0;
11778
12699
  const encoded = workspace.replace(/^\//, "").replace(/\//g, "-");
11779
- return path11.join(
12700
+ return path12.join(
11780
12701
  os5.homedir(),
11781
12702
  ".cursor",
11782
12703
  "projects",
@@ -11802,9 +12723,19 @@ async function runUploadInner(payload) {
11802
12723
  );
11803
12724
  return;
11804
12725
  }
11805
- const transcriptResolved = path11.resolve(transcriptPath);
12726
+ const match = await findProjectForCwd(cwd);
12727
+ if (!match) {
12728
+ appendLog(
12729
+ "warn",
12730
+ `Skipping session ${sessionId}: no hillclimb config for cwd ${cwd}. Run \`npx hillclimb\` in the repo.`
12731
+ );
12732
+ return;
12733
+ }
12734
+ const { repoRoot, config } = match;
12735
+ await selfHealHook(repoRoot, sourceTool);
12736
+ const transcriptResolved = path12.resolve(transcriptPath);
11806
12737
  try {
11807
- const stat = await fs8.promises.stat(transcriptResolved);
12738
+ const stat = await fs9.promises.stat(transcriptResolved);
11808
12739
  if (!stat.isFile()) {
11809
12740
  appendLog(
11810
12741
  "warn",
@@ -11819,14 +12750,6 @@ async function runUploadInner(payload) {
11819
12750
  );
11820
12751
  return;
11821
12752
  }
11822
- const match = await findProjectForCwd(cwd);
11823
- if (!match) {
11824
- appendLog(
11825
- "warn",
11826
- `Skipping session ${sessionId}: no hillclimb config for cwd ${cwd}. Run \`npx hillclimb\` in the repo.`
11827
- );
11828
- return;
11829
- }
11830
12753
  if (!await hasAssistantMessage(transcriptResolved)) {
11831
12754
  appendLog(
11832
12755
  "info",
@@ -11834,7 +12757,6 @@ async function runUploadInner(payload) {
11834
12757
  );
11835
12758
  return;
11836
12759
  }
11837
- const { repoRoot, config } = match;
11838
12760
  await uploadSession({
11839
12761
  sessionId,
11840
12762
  transcriptPath: transcriptResolved,
@@ -11853,13 +12775,13 @@ async function uploadSession(args) {
11853
12775
  };
11854
12776
  const group = {
11855
12777
  repoPath: repoRoot,
11856
- label: path11.basename(repoRoot),
12778
+ label: path12.basename(repoRoot),
11857
12779
  files: [sourceFile],
11858
12780
  sourceNames: [sourceTool],
11859
12781
  lastModified: now
11860
12782
  };
11861
12783
  const envFileNames = await discoverEnvFiles(repoRoot);
11862
- const envFilePaths = envFileNames.map((n) => path11.join(repoRoot, n));
12784
+ const envFilePaths = envFileNames.map((n) => path12.join(repoRoot, n));
11863
12785
  const secretResult = await collectSecrets(repoRoot, envFilePaths, []);
11864
12786
  const mwChain = [];
11865
12787
  if (secretResult.values.size > 0) {
@@ -12039,9 +12961,9 @@ import { execFileSync as execFileSync2 } from "child_process";
12039
12961
 
12040
12962
  // src/git-traces/git-ops.ts
12041
12963
  import { execFileSync } from "child_process";
12042
- import fs9 from "fs";
12964
+ import fs10 from "fs";
12043
12965
  import os6 from "os";
12044
- import path12 from "path";
12966
+ import path13 from "path";
12045
12967
  import { gzipSync } from "zlib";
12046
12968
  var GIT_COMMAND_TIMEOUT_MS = 12e4;
12047
12969
  var EXEC_OPTS = {
@@ -12165,7 +13087,7 @@ function buildUntrackedTree(repoRoot) {
12165
13087
  for (const relPath of list.split("\0")) {
12166
13088
  if (!relPath) continue;
12167
13089
  try {
12168
- const stat = fs9.lstatSync(path12.join(repoRoot, relPath));
13090
+ const stat = fs10.lstatSync(path13.join(repoRoot, relPath));
12169
13091
  if (stat.size > MAX_UNTRACKED_FILE_BYTES) {
12170
13092
  appendLog(
12171
13093
  "warn",
@@ -12178,7 +13100,7 @@ function buildUntrackedTree(repoRoot) {
12178
13100
  }
12179
13101
  }
12180
13102
  if (kept.length === 0) return null;
12181
- const tmpIndex = path12.join(
13103
+ const tmpIndex = path13.join(
12182
13104
  os6.tmpdir(),
12183
13105
  `hillclimb-untracked-${Date.now()}-${process.pid}`
12184
13106
  );
@@ -12191,7 +13113,7 @@ function buildUntrackedTree(repoRoot) {
12191
13113
  return gitWithEnv(repoRoot, ["write-tree"], env);
12192
13114
  } finally {
12193
13115
  try {
12194
- fs9.unlinkSync(tmpIndex);
13116
+ fs10.unlinkSync(tmpIndex);
12195
13117
  } catch {
12196
13118
  }
12197
13119
  }
@@ -12202,7 +13124,7 @@ function buildSnapshotTree(repoRoot, stashSha) {
12202
13124
  if (!untrackedTree || untrackedTree === EMPTY_TREE_SHA) {
12203
13125
  return trackedTree;
12204
13126
  }
12205
- const tmpIndex = path12.join(
13127
+ const tmpIndex = path13.join(
12206
13128
  os6.tmpdir(),
12207
13129
  `hillclimb-index-${Date.now()}-${process.pid}`
12208
13130
  );
@@ -12230,7 +13152,7 @@ function buildSnapshotTree(repoRoot, stashSha) {
12230
13152
  return gitWithEnv(repoRoot, ["write-tree"], env);
12231
13153
  } finally {
12232
13154
  try {
12233
- fs9.unlinkSync(tmpIndex);
13155
+ fs10.unlinkSync(tmpIndex);
12234
13156
  } catch {
12235
13157
  }
12236
13158
  }
@@ -12244,16 +13166,16 @@ function createBundleFromTree(repoRoot, treeSha, sessionId, label) {
12244
13166
  ]);
12245
13167
  const orphanRef = `refs/hillclimb/bundle/${sessionId}`;
12246
13168
  pinRef(repoRoot, orphanRef, orphanCommit);
12247
- const tmpFile = path12.join(
13169
+ const tmpFile = path13.join(
12248
13170
  os6.tmpdir(),
12249
13171
  `hillclimb-bundle-${Date.now()}.bundle`
12250
13172
  );
12251
13173
  try {
12252
13174
  git(repoRoot, ["bundle", "create", tmpFile, orphanRef]);
12253
- return fs9.readFileSync(tmpFile);
13175
+ return fs10.readFileSync(tmpFile);
12254
13176
  } finally {
12255
13177
  try {
12256
- fs9.unlinkSync(tmpFile);
13178
+ fs10.unlinkSync(tmpFile);
12257
13179
  } catch {
12258
13180
  }
12259
13181
  deleteRef(repoRoot, orphanRef);
@@ -12406,9 +13328,9 @@ function parseCommitFiles(repoRoot, sha) {
12406
13328
  oldPath
12407
13329
  });
12408
13330
  } else {
12409
- const path20 = parts[parts.length - 1];
12410
- indexByPath.set(path20, files.length);
12411
- files.push({ path: path20, status, additions: 0, deletions: 0 });
13331
+ const path21 = parts[parts.length - 1];
13332
+ indexByPath.set(path21, files.length);
13333
+ files.push({ path: path21, status, additions: 0, deletions: 0 });
12412
13334
  }
12413
13335
  }
12414
13336
  for (const line of numstat.split("\n")) {
@@ -12462,21 +13384,21 @@ function cleanupSessionRefs(repoRoot, sessionId) {
12462
13384
 
12463
13385
  // src/git-traces/session-state.ts
12464
13386
  import crypto from "crypto";
12465
- import fs10 from "fs";
13387
+ import fs11 from "fs";
12466
13388
  import os7 from "os";
12467
- import path13 from "path";
13389
+ import path14 from "path";
12468
13390
  var CURRENT_SCHEMA_VERSION = 3;
12469
- var STATE_DIR = path13.join(os7.homedir(), ".hillclimb", "git-traces");
13391
+ var STATE_DIR = path14.join(os7.homedir(), ".hillclimb", "git-traces");
12470
13392
  function stateFileForRepo(repoRoot, tool) {
12471
- const hash = crypto.createHash("sha256").update(`${path13.resolve(repoRoot)}\0${tool}`).digest("hex").slice(0, 16);
12472
- return path13.join(STATE_DIR, `${hash}.json`);
13393
+ const hash = crypto.createHash("sha256").update(`${path14.resolve(repoRoot)}\0${tool}`).digest("hex").slice(0, 16);
13394
+ return path14.join(STATE_DIR, `${hash}.json`);
12473
13395
  }
12474
13396
  function lockFileForRepo(repoRoot, tool) {
12475
13397
  return `${stateFileForRepo(repoRoot, tool)}.lock`;
12476
13398
  }
12477
13399
  async function readSessionState(repoRoot, tool) {
12478
13400
  try {
12479
- const raw = await fs10.promises.readFile(
13401
+ const raw = await fs11.promises.readFile(
12480
13402
  stateFileForRepo(repoRoot, tool),
12481
13403
  "utf-8"
12482
13404
  );
@@ -12491,27 +13413,27 @@ async function readSessionState(repoRoot, tool) {
12491
13413
  }
12492
13414
  async function writeSessionState(state, tool) {
12493
13415
  const file = stateFileForRepo(state.repoRoot, tool);
12494
- await fs10.promises.mkdir(STATE_DIR, { recursive: true, mode: 448 });
13416
+ await fs11.promises.mkdir(STATE_DIR, { recursive: true, mode: 448 });
12495
13417
  const tmp = `${file}.tmp`;
12496
- await fs10.promises.writeFile(tmp, JSON.stringify(state, null, 2), {
13418
+ await fs11.promises.writeFile(tmp, JSON.stringify(state, null, 2), {
12497
13419
  mode: 384
12498
13420
  });
12499
- await fs10.promises.rename(tmp, file);
13421
+ await fs11.promises.rename(tmp, file);
12500
13422
  }
12501
13423
  async function deleteSessionState(repoRoot, tool) {
12502
13424
  try {
12503
- await fs10.promises.unlink(stateFileForRepo(repoRoot, tool));
13425
+ await fs11.promises.unlink(stateFileForRepo(repoRoot, tool));
12504
13426
  } catch {
12505
13427
  }
12506
13428
  }
12507
13429
  async function acquireLock(repoRoot, tool, retries = 3, delayMs = 200) {
12508
13430
  const lockPath = lockFileForRepo(repoRoot, tool);
12509
- await fs10.promises.mkdir(STATE_DIR, { recursive: true, mode: 448 });
13431
+ await fs11.promises.mkdir(STATE_DIR, { recursive: true, mode: 448 });
12510
13432
  for (let i = 0; i < retries; i++) {
12511
13433
  try {
12512
- const fd = await fs10.promises.open(
13434
+ const fd = await fs11.promises.open(
12513
13435
  lockPath,
12514
- fs10.constants.O_CREAT | fs10.constants.O_EXCL | fs10.constants.O_WRONLY
13436
+ fs11.constants.O_CREAT | fs11.constants.O_EXCL | fs11.constants.O_WRONLY
12515
13437
  );
12516
13438
  await fd.write(String(process.pid));
12517
13439
  await fd.close();
@@ -12528,13 +13450,13 @@ async function acquireLock(repoRoot, tool, retries = 3, delayMs = 200) {
12528
13450
  }
12529
13451
  async function releaseLock(repoRoot, tool) {
12530
13452
  try {
12531
- await fs10.promises.unlink(lockFileForRepo(repoRoot, tool));
13453
+ await fs11.promises.unlink(lockFileForRepo(repoRoot, tool));
12532
13454
  } catch {
12533
13455
  }
12534
13456
  }
12535
13457
 
12536
13458
  // src/git-traces/handlers.ts
12537
- var CLI_VERSION = "0.1.0";
13459
+ var CLI_VERSION = "0.2.0";
12538
13460
  var GIT_TRACES_SLUG = "git-traces";
12539
13461
  var MAX_UPLOAD_BYTES = 500 * 1024 * 1024;
12540
13462
  function formatEpochSeconds2(date) {
@@ -12574,14 +13496,16 @@ function execGit(cwd, args) {
12574
13496
  maxBuffer: 50 * 1024 * 1024
12575
13497
  }).toString("utf-8").trim();
12576
13498
  }
13499
+ function canUploadFile(filename, buffer) {
13500
+ if (buffer.byteLength <= MAX_UPLOAD_BYTES) return true;
13501
+ appendLog(
13502
+ "warn",
13503
+ `git-traces: skipping ${filename} (${buffer.byteLength} bytes > ${MAX_UPLOAD_BYTES} limit)`
13504
+ );
13505
+ return false;
13506
+ }
12577
13507
  async function uploadFile(client, contributionId, filename, mimeType, buffer) {
12578
- if (buffer.byteLength > MAX_UPLOAD_BYTES) {
12579
- appendLog(
12580
- "warn",
12581
- `git-traces: skipping ${filename} (${buffer.byteLength} bytes > ${MAX_UPLOAD_BYTES} limit)`
12582
- );
12583
- return;
12584
- }
13508
+ if (!canUploadFile(filename, buffer)) return false;
12585
13509
  const presigned = await client.createUpload(contributionId, {
12586
13510
  originalFilename: filename,
12587
13511
  mimeType,
@@ -12596,6 +13520,11 @@ async function uploadFile(client, contributionId, filename, mimeType, buffer) {
12596
13520
  "info",
12597
13521
  `git-traces: uploaded ${filename} (${buffer.byteLength} bytes)`
12598
13522
  );
13523
+ return true;
13524
+ }
13525
+ function canUploadEpochBaselineArtifacts(epoch, artifacts) {
13526
+ const prefix = epochPrefix(epoch);
13527
+ return canUploadFile(`${prefix}-baseline.bundle`, artifacts.bundleBuffer) && canUploadFile(`${prefix}-baseline.json`, artifacts.metadataBuffer);
12599
13528
  }
12600
13529
  function pinEpochBaseline(repoRoot, sessionId, epoch) {
12601
13530
  const baselineSha = captureBaselineSha(repoRoot);
@@ -12650,30 +13579,34 @@ function buildEpochBaselineArtifacts(params) {
12650
13579
  async function uploadEpochBaselineArtifacts(params) {
12651
13580
  const { client, contributionId, epoch, artifacts } = params;
12652
13581
  const prefix = epochPrefix(epoch);
12653
- await uploadFile(
12654
- client,
12655
- contributionId,
12656
- `${prefix}-baseline.json`,
12657
- "application/json",
12658
- artifacts.metadataBuffer
12659
- );
12660
- await uploadFile(
13582
+ if (!canUploadEpochBaselineArtifacts(epoch, artifacts)) return false;
13583
+ const bundleUploaded = await uploadFile(
12661
13584
  client,
12662
13585
  contributionId,
12663
13586
  `${prefix}-baseline.bundle`,
12664
13587
  "application/x-git-bundle",
12665
13588
  artifacts.bundleBuffer
12666
13589
  );
13590
+ if (!bundleUploaded) return false;
13591
+ return uploadFile(
13592
+ client,
13593
+ contributionId,
13594
+ `${prefix}-baseline.json`,
13595
+ "application/json",
13596
+ artifacts.metadataBuffer
13597
+ );
12667
13598
  }
12668
13599
  async function uploadEpochBaseline(params) {
12669
13600
  const artifacts = buildEpochBaselineArtifacts(params);
12670
13601
  if (!artifacts) return null;
12671
- await uploadEpochBaselineArtifacts({
13602
+ if (!canUploadEpochBaselineArtifacts(params.epoch, artifacts)) return null;
13603
+ const uploaded = await uploadEpochBaselineArtifacts({
12672
13604
  client: params.client,
12673
13605
  contributionId: params.contributionId,
12674
13606
  epoch: params.epoch,
12675
13607
  artifacts
12676
13608
  });
13609
+ if (!uploaded) return null;
12677
13610
  return { baselineTreeSha: artifacts.baselineTreeSha };
12678
13611
  }
12679
13612
  async function openEpoch(params) {
@@ -12783,6 +13716,7 @@ async function handleStop(payload, tool) {
12783
13716
  startedAt: state.startedAt
12784
13717
  });
12785
13718
  if (!artifacts) return;
13719
+ if (!canUploadEpochBaselineArtifacts(1, artifacts)) return;
12786
13720
  const toolLabel = TOOL_LABELS[tool] ?? "Claude";
12787
13721
  const now = /* @__PURE__ */ new Date();
12788
13722
  const epochSeconds = formatEpochSeconds2(now);
@@ -12798,12 +13732,13 @@ Repo: ${cwd}
12798
13732
  Uploaded: ${now.toISOString()}`
12799
13733
  }
12800
13734
  );
12801
- await uploadEpochBaselineArtifacts({
13735
+ const uploaded = await uploadEpochBaselineArtifacts({
12802
13736
  client,
12803
13737
  contributionId: contribution.id,
12804
13738
  epoch: 1,
12805
13739
  artifacts
12806
13740
  });
13741
+ if (!uploaded) return;
12807
13742
  await client.submitContribution(contribution.id);
12808
13743
  state.contributionId = contribution.id;
12809
13744
  state.baselineTreeSha = artifacts.baselineTreeSha;
@@ -12949,6 +13884,30 @@ async function readStdin2() {
12949
13884
  }
12950
13885
  return Buffer.concat(chunks).toString("utf-8");
12951
13886
  }
13887
+ function resolveCwd2(payload) {
13888
+ return payload.cwd ?? payload.workspace_roots?.[0] ?? null;
13889
+ }
13890
+ async function selfHealHook2(payload, tool) {
13891
+ const cwd = resolveCwd2(payload);
13892
+ if (!cwd) return;
13893
+ try {
13894
+ const project = await findProjectForCwd(cwd);
13895
+ if (!project) return;
13896
+ const result = await healHookForTool(project.repoRoot, tool);
13897
+ if (result.skipped) {
13898
+ appendLog("warn", `self-heal: skipped ${tool} hook (${result.skipped})`);
13899
+ return;
13900
+ }
13901
+ if (result.changed) {
13902
+ appendLog("info", `self-heal: updated ${tool} hook`);
13903
+ }
13904
+ } catch (err) {
13905
+ appendLog(
13906
+ "warn",
13907
+ `self-heal: failed to repair ${tool} hook: ${formatError(err)}`
13908
+ );
13909
+ }
13910
+ }
12952
13911
  async function runGitTraces() {
12953
13912
  if (process.env[WORKER_ENV_FLAG2] === "1") {
12954
13913
  await runGitTracesWorker();
@@ -13063,6 +14022,7 @@ async function runGitTracesWorker() {
13063
14022
  );
13064
14023
  return;
13065
14024
  }
14025
+ await selfHealHook2(payload, tool);
13066
14026
  try {
13067
14027
  switch (event) {
13068
14028
  case "SessionStart":
@@ -13100,15 +14060,15 @@ ${stack}` : ""}`
13100
14060
  }
13101
14061
 
13102
14062
  // src/outputs/zip.ts
13103
- import fs12 from "fs";
13104
- import path15 from "path";
14063
+ import fs13 from "fs";
14064
+ import path16 from "path";
13105
14065
  import archiver2 from "archiver";
13106
14066
 
13107
14067
  // src/outputs/downloads.ts
13108
14068
  import { execSync as execSync2 } from "child_process";
13109
- import fs11 from "fs";
14069
+ import fs12 from "fs";
13110
14070
  import os8 from "os";
13111
- import path14 from "path";
14071
+ import path15 from "path";
13112
14072
  function getDownloadsFolder() {
13113
14073
  const home = os8.homedir();
13114
14074
  if (process.platform === "linux") {
@@ -13117,12 +14077,12 @@ function getDownloadsFolder() {
13117
14077
  encoding: "utf-8",
13118
14078
  timeout: 3e3
13119
14079
  }).trim();
13120
- if (xdgDir && fs11.existsSync(xdgDir)) return xdgDir;
14080
+ if (xdgDir && fs12.existsSync(xdgDir)) return xdgDir;
13121
14081
  } catch {
13122
14082
  }
13123
14083
  }
13124
- const downloads = path14.join(home, "Downloads");
13125
- if (fs11.existsSync(downloads)) return downloads;
14084
+ const downloads = path15.join(home, "Downloads");
14085
+ if (fs12.existsSync(downloads)) return downloads;
13126
14086
  return home;
13127
14087
  }
13128
14088
 
@@ -13131,11 +14091,11 @@ function sanitizeFilename(name) {
13131
14091
  return name.replace(/[^a-zA-Z0-9._-]/g, "_");
13132
14092
  }
13133
14093
  function getUniqueFilename(dir, base, ext) {
13134
- let candidate = path15.join(dir, `${base}${ext}`);
13135
- if (!fs12.existsSync(candidate)) return candidate;
14094
+ let candidate = path16.join(dir, `${base}${ext}`);
14095
+ if (!fs13.existsSync(candidate)) return candidate;
13136
14096
  let i = 1;
13137
- while (fs12.existsSync(candidate)) {
13138
- candidate = path15.join(dir, `${base}-${i}${ext}`);
14097
+ while (fs13.existsSync(candidate)) {
14098
+ candidate = path16.join(dir, `${base}-${i}${ext}`);
13139
14099
  i++;
13140
14100
  }
13141
14101
  return candidate;
@@ -13145,13 +14105,13 @@ var ZipOutput = class {
13145
14105
  label = "Save as .zip to Downloads";
13146
14106
  async emit(group, options) {
13147
14107
  const downloadsDir = getDownloadsFolder();
13148
- const repoName = sanitizeFilename(path15.basename(group.repoPath));
14108
+ const repoName = sanitizeFilename(path16.basename(group.repoPath));
13149
14109
  const timeRange = options.timeRange;
13150
14110
  const rangePart = timeRange?.label ?? "all";
13151
14111
  const epochSeconds = Math.floor(Date.now() / 1e3);
13152
14112
  const baseName = `logs-${repoName}-${rangePart}-${epochSeconds}`;
13153
14113
  const outputPath = getUniqueFilename(downloadsDir, baseName, ".zip");
13154
- const output = fs12.createWriteStream(outputPath);
14114
+ const output = fs13.createWriteStream(outputPath);
13155
14115
  const archive = archiver2("zip", { zlib: { level: 6 } });
13156
14116
  const done = new Promise((resolve, reject) => {
13157
14117
  output.on("close", resolve);
@@ -13345,15 +14305,15 @@ async function confirmExport(group, output) {
13345
14305
  }
13346
14306
 
13347
14307
  // src/sources/claude.ts
13348
- import fs13 from "fs";
14308
+ import fs14 from "fs";
13349
14309
  import os9 from "os";
13350
- import path16 from "path";
14310
+ import path17 from "path";
13351
14311
  import readline from "readline";
13352
14312
  var SKIP_DIRS = /* @__PURE__ */ new Set(["tool-results"]);
13353
14313
  async function resolveRepoPath(projectDir) {
13354
- const indexPath = path16.join(projectDir, "sessions-index.json");
14314
+ const indexPath = path17.join(projectDir, "sessions-index.json");
13355
14315
  try {
13356
- const raw = await fs13.promises.readFile(indexPath, "utf-8");
14316
+ const raw = await fs14.promises.readFile(indexPath, "utf-8");
13357
14317
  const data = JSON.parse(raw);
13358
14318
  if (data.originalPath && typeof data.originalPath === "string") {
13359
14319
  return data.originalPath;
@@ -13361,12 +14321,12 @@ async function resolveRepoPath(projectDir) {
13361
14321
  } catch {
13362
14322
  }
13363
14323
  const cwdCounts = /* @__PURE__ */ new Map();
13364
- const entries = await fs13.promises.readdir(projectDir, {
14324
+ const entries = await fs14.promises.readdir(projectDir, {
13365
14325
  withFileTypes: true
13366
14326
  });
13367
14327
  for (const entry of entries) {
13368
14328
  if (!entry.isFile() || !entry.name.endsWith(".jsonl")) continue;
13369
- const cwd = await extractCwdFromJsonl(path16.join(projectDir, entry.name));
14329
+ const cwd = await extractCwdFromJsonl(path17.join(projectDir, entry.name));
13370
14330
  if (cwd) {
13371
14331
  cwdCounts.set(cwd, (cwdCounts.get(cwd) ?? 0) + 1);
13372
14332
  }
@@ -13385,7 +14345,7 @@ async function resolveRepoPath(projectDir) {
13385
14345
  return null;
13386
14346
  }
13387
14347
  async function extractCwdFromJsonl(filePath) {
13388
- const stream = fs13.createReadStream(filePath, { encoding: "utf-8" });
14348
+ const stream = fs14.createReadStream(filePath, { encoding: "utf-8" });
13389
14349
  const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
13390
14350
  try {
13391
14351
  for await (const line of rl) {
@@ -13407,12 +14367,12 @@ async function extractCwdFromJsonl(filePath) {
13407
14367
  async function collectFiles(dir, baseDir, sourceName, repoPath, results) {
13408
14368
  let entries;
13409
14369
  try {
13410
- entries = await fs13.promises.readdir(dir, { withFileTypes: true });
14370
+ entries = await fs14.promises.readdir(dir, { withFileTypes: true });
13411
14371
  } catch {
13412
14372
  return;
13413
14373
  }
13414
14374
  for (const entry of entries) {
13415
- const fullPath = path16.join(dir, entry.name);
14375
+ const fullPath = path17.join(dir, entry.name);
13416
14376
  if (entry.isDirectory()) {
13417
14377
  if (SKIP_DIRS.has(entry.name)) continue;
13418
14378
  await collectFiles(fullPath, baseDir, sourceName, repoPath, results);
@@ -13434,19 +14394,19 @@ function fallbackDecode(encodedName) {
13434
14394
  var ClaudeSource = class {
13435
14395
  name = "claude";
13436
14396
  async scan() {
13437
- const baseDir = path16.join(os9.homedir(), ".claude", "projects");
14397
+ const baseDir = path17.join(os9.homedir(), ".claude", "projects");
13438
14398
  try {
13439
- await fs13.promises.access(baseDir);
14399
+ await fs14.promises.access(baseDir);
13440
14400
  } catch {
13441
14401
  return [];
13442
14402
  }
13443
- const projectDirs = await fs13.promises.readdir(baseDir, {
14403
+ const projectDirs = await fs14.promises.readdir(baseDir, {
13444
14404
  withFileTypes: true
13445
14405
  });
13446
14406
  const dirEntries = projectDirs.filter((d) => d.isDirectory());
13447
14407
  const resultArrays = await Promise.all(
13448
14408
  dirEntries.map(async (dir) => {
13449
- const projectPath = path16.join(baseDir, dir.name);
14409
+ const projectPath = path17.join(baseDir, dir.name);
13450
14410
  const repoPath = await resolveRepoPath(projectPath) ?? fallbackDecode(dir.name);
13451
14411
  const files = [];
13452
14412
  await collectFiles(
@@ -13464,12 +14424,12 @@ var ClaudeSource = class {
13464
14424
  };
13465
14425
 
13466
14426
  // src/sources/codex.ts
13467
- import fs14 from "fs";
14427
+ import fs15 from "fs";
13468
14428
  import os10 from "os";
13469
- import path17 from "path";
14429
+ import path18 from "path";
13470
14430
  import readline2 from "readline";
13471
14431
  async function parseSessionMeta(filePath) {
13472
- const stream = fs14.createReadStream(filePath, { encoding: "utf-8" });
14432
+ const stream = fs15.createReadStream(filePath, { encoding: "utf-8" });
13473
14433
  const rl = readline2.createInterface({ input: stream, crlfDelay: Infinity });
13474
14434
  try {
13475
14435
  for await (const line of rl) {
@@ -13494,12 +14454,12 @@ async function findJsonlFiles(dir) {
13494
14454
  async function walk(d) {
13495
14455
  let entries;
13496
14456
  try {
13497
- entries = await fs14.promises.readdir(d, { withFileTypes: true });
14457
+ entries = await fs15.promises.readdir(d, { withFileTypes: true });
13498
14458
  } catch {
13499
14459
  return;
13500
14460
  }
13501
14461
  for (const entry of entries) {
13502
- const full = path17.join(d, entry.name);
14462
+ const full = path18.join(d, entry.name);
13503
14463
  if (entry.isDirectory()) {
13504
14464
  await walk(full);
13505
14465
  } else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
@@ -13513,11 +14473,11 @@ async function findJsonlFiles(dir) {
13513
14473
  async function loadHistory(historyPath) {
13514
14474
  const map = /* @__PURE__ */ new Map();
13515
14475
  try {
13516
- await fs14.promises.access(historyPath);
14476
+ await fs15.promises.access(historyPath);
13517
14477
  } catch {
13518
14478
  return map;
13519
14479
  }
13520
- const stream = fs14.createReadStream(historyPath, { encoding: "utf-8" });
14480
+ const stream = fs15.createReadStream(historyPath, { encoding: "utf-8" });
13521
14481
  const rl = readline2.createInterface({ input: stream, crlfDelay: Infinity });
13522
14482
  try {
13523
14483
  for await (const line of rl) {
@@ -13544,14 +14504,14 @@ async function loadHistory(historyPath) {
13544
14504
  var CodexSource = class {
13545
14505
  name = "codex";
13546
14506
  async scan() {
13547
- const codexDir = path17.join(os10.homedir(), ".codex");
13548
- const sessionsDir = path17.join(codexDir, "sessions");
14507
+ const codexDir = path18.join(os10.homedir(), ".codex");
14508
+ const sessionsDir = path18.join(codexDir, "sessions");
13549
14509
  try {
13550
- await fs14.promises.access(sessionsDir);
14510
+ await fs15.promises.access(sessionsDir);
13551
14511
  } catch {
13552
14512
  return [];
13553
14513
  }
13554
- const historyPath = path17.join(codexDir, "history.jsonl");
14514
+ const historyPath = path18.join(codexDir, "history.jsonl");
13555
14515
  const [jsonlFiles, historyMap] = await Promise.all([
13556
14516
  findJsonlFiles(sessionsDir),
13557
14517
  loadHistory(historyPath)
@@ -13574,8 +14534,8 @@ var CodexSource = class {
13574
14534
  });
13575
14535
  const historyLines = historyMap.get(meta.sessionId);
13576
14536
  if (historyLines) {
13577
- const sessionDir = path17.relative(sessionsDir, path17.dirname(filePath));
13578
- const historyAbsPath = path17.join(
14537
+ const sessionDir = path18.relative(sessionsDir, path18.dirname(filePath));
14538
+ const historyAbsPath = path18.join(
13579
14539
  sessionsDir,
13580
14540
  sessionDir,
13581
14541
  `history-${meta.sessionId}.jsonl`
@@ -13595,18 +14555,18 @@ var CodexSource = class {
13595
14555
  };
13596
14556
 
13597
14557
  // src/sources/copilotChat.ts
13598
- import fs15 from "fs";
14558
+ import fs16 from "fs";
13599
14559
  import os11 from "os";
13600
- import path18 from "path";
14560
+ import path19 from "path";
13601
14561
  import { fileURLToPath } from "url";
13602
14562
  function vsCodeUserDirs() {
13603
14563
  const home = os11.homedir();
13604
14564
  const dirs = [
13605
- path18.join(home, "Library", "Application Support", "Code", "User"),
13606
- path18.join(home, ".config", "Code", "User")
14565
+ path19.join(home, "Library", "Application Support", "Code", "User"),
14566
+ path19.join(home, ".config", "Code", "User")
13607
14567
  ];
13608
14568
  if (process.env.APPDATA) {
13609
- dirs.push(path18.join(process.env.APPDATA, "Code", "User"));
14569
+ dirs.push(path19.join(process.env.APPDATA, "Code", "User"));
13610
14570
  }
13611
14571
  return dirs;
13612
14572
  }
@@ -13621,7 +14581,7 @@ function uriToFsPath(uri) {
13621
14581
  async function readWorkspaceFolder(workspaceJsonPath) {
13622
14582
  let raw;
13623
14583
  try {
13624
- raw = await fs15.promises.readFile(workspaceJsonPath, "utf-8");
14584
+ raw = await fs16.promises.readFile(workspaceJsonPath, "utf-8");
13625
14585
  } catch {
13626
14586
  return null;
13627
14587
  }
@@ -13643,10 +14603,10 @@ var CopilotChatSource = class {
13643
14603
  async scan() {
13644
14604
  const results = [];
13645
14605
  for (const userDir of vsCodeUserDirs()) {
13646
- const workspaceStorage = path18.join(userDir, "workspaceStorage");
14606
+ const workspaceStorage = path19.join(userDir, "workspaceStorage");
13647
14607
  let hashDirs;
13648
14608
  try {
13649
- hashDirs = await fs15.promises.readdir(workspaceStorage, {
14609
+ hashDirs = await fs16.promises.readdir(workspaceStorage, {
13650
14610
  withFileTypes: true
13651
14611
  });
13652
14612
  } catch {
@@ -13654,22 +14614,22 @@ var CopilotChatSource = class {
13654
14614
  }
13655
14615
  for (const hash of hashDirs) {
13656
14616
  if (!hash.isDirectory()) continue;
13657
- const wsRoot = path18.join(workspaceStorage, hash.name);
13658
- const transcriptsDir = path18.join(
14617
+ const wsRoot = path19.join(workspaceStorage, hash.name);
14618
+ const transcriptsDir = path19.join(
13659
14619
  wsRoot,
13660
14620
  "GitHub.copilot-chat",
13661
14621
  "transcripts"
13662
14622
  );
13663
14623
  let transcriptEntries;
13664
14624
  try {
13665
- transcriptEntries = await fs15.promises.readdir(transcriptsDir, {
14625
+ transcriptEntries = await fs16.promises.readdir(transcriptsDir, {
13666
14626
  withFileTypes: true
13667
14627
  });
13668
14628
  } catch {
13669
14629
  continue;
13670
14630
  }
13671
14631
  const repoPath = await readWorkspaceFolder(
13672
- path18.join(wsRoot, "workspace.json")
14632
+ path19.join(wsRoot, "workspace.json")
13673
14633
  );
13674
14634
  if (!repoPath) continue;
13675
14635
  for (const entry of transcriptEntries) {
@@ -13677,7 +14637,7 @@ var CopilotChatSource = class {
13677
14637
  const sessionId = entry.name.slice(0, -".jsonl".length);
13678
14638
  results.push({
13679
14639
  sourceName: this.name,
13680
- absolutePath: path18.join(transcriptsDir, entry.name),
14640
+ absolutePath: path19.join(transcriptsDir, entry.name),
13681
14641
  repoPath,
13682
14642
  metadata: { sessionId }
13683
14643
  });
@@ -13717,7 +14677,7 @@ function reportRedactionStats(noun, stats) {
13717
14677
  async function filterByTimeRange(group, range) {
13718
14678
  const results = await Promise.all(
13719
14679
  group.files.map(
13720
- (f) => fs16.promises.stat(f.absolutePath).then((s) => ({ file: f, stat: s })).catch(() => null)
14680
+ (f) => fs17.promises.stat(f.absolutePath).then((s) => ({ file: f, stat: s })).catch(() => null)
13721
14681
  )
13722
14682
  );
13723
14683
  const filtered = [];
@@ -13744,10 +14704,10 @@ async function runInteractive() {
13744
14704
  s.start(`Scanning ${source.name} logs...`);
13745
14705
  const allFiles = await source.scan();
13746
14706
  const allGroups = await mergeByRepo(allFiles);
13747
- const repoRoot = path19.resolve(repo.root);
14707
+ const repoRoot = path20.resolve(repo.root);
13748
14708
  const matching = allGroups.filter((g) => {
13749
- const resolved = path19.resolve(g.repoPath);
13750
- return resolved === repoRoot || resolved.startsWith(repoRoot + path19.sep);
14709
+ const resolved = path20.resolve(g.repoPath);
14710
+ return resolved === repoRoot || resolved.startsWith(repoRoot + path20.sep);
13751
14711
  });
13752
14712
  if (matching.length === 0) {
13753
14713
  s.stop(`No ${source.name} logs found for ${repo.name}.`);
@@ -13778,7 +14738,7 @@ async function runInteractive() {
13778
14738
  }
13779
14739
  }
13780
14740
  const envFileNames = await discoverEnvFiles(repoRoot);
13781
- const envFilePaths = envFileNames.map((n) => path19.join(repoRoot, n));
14741
+ const envFilePaths = envFileNames.map((n) => path20.join(repoRoot, n));
13782
14742
  const additionalFiles = await promptSecretFiles(envFileNames);
13783
14743
  const secretResult = await collectSecrets(
13784
14744
  repoRoot,