hillclimb 0.1.9 → 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 +916 -41
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # hillclimb
2
2
 
3
- Extract AI coding tool sessions (Claude Code, Cursor, Codex, opencode) and upload them to a Hillclimb project.
3
+ Extract AI coding tool sessions (Claude Code, Cursor, Codex, opencode, GitHub Copilot Chat) and upload them to a Hillclimb project.
4
4
 
5
5
  ## Quickstart
6
6
 
@@ -18,7 +18,7 @@ Shows login and hook status for the current repo.
18
18
 
19
19
  ## Hooks
20
20
 
21
- `hillclimb` detects which tools you use (`~/.claude`, `~/.cursor`, `~/.codex`, `~/.local/share/opencode`) and installs upload + git-trace hooks for each. You might want to gitignore the respective directories for the tools you use in your repo.
21
+ `hillclimb` detects which tools you use (`~/.claude`, `~/.cursor`, `~/.codex`, `~/.local/share/opencode`, VS Code GitHub Copilot Chat storage) and installs upload + git-trace hooks for each. You might want to gitignore the respective directories for the tools you use in your repo.
22
22
 
23
23
  To manually export historical logs instead of configuring auto-upload, run:
24
24
 
package/dist/cli.js CHANGED
@@ -850,7 +850,7 @@ function copilotUninstall(settings, eventName, command) {
850
850
  }
851
851
  return true;
852
852
  }
853
- var OPENCODE_PLUGIN_VERSION = 3;
853
+ var OPENCODE_PLUGIN_VERSION = 4;
854
854
  var OPENCODE_PLUGIN_MARKER = `// HILLCLIMB_OPENCODE_PLUGIN_VERSION=${OPENCODE_PLUGIN_VERSION}`;
855
855
  var OPENCODE_PLUGIN_CONTENT = `${OPENCODE_PLUGIN_MARKER}
856
856
  // Auto-installed by \`npx hillclimb\`. Do not edit manually \u2014 re-running
@@ -864,20 +864,54 @@ import path from "node:path";
864
864
  const TOOL = "opencode";
865
865
  const TRANSCRIPT_DIR = path.join(os.homedir(), ".hillclimb", "opencode-transcripts");
866
866
 
867
- // sessionID -> Map<messageID, messageInfo>. Using a Map-of-Maps so late
868
- // message.updated events (streaming deltas) overwrite earlier versions of
869
- // 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.
870
870
  const sessionMessages = new Map();
871
871
 
872
- function ingestMessage(sessionID, info) {
873
- if (!sessionID || !info) return;
872
+ function ensureRecord(sessionID, messageID, info) {
873
+ if (!sessionID || !messageID) return null;
874
874
  let bucket = sessionMessages.get(sessionID);
875
875
  if (!bucket) {
876
876
  bucket = new Map();
877
877
  sessionMessages.set(sessionID, bucket);
878
878
  }
879
- const id = info.id || ((info.role || "unknown") + "-" + bucket.size);
880
- 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 });
881
915
  }
882
916
 
883
917
  function writeTranscript(sessionID) {
@@ -887,7 +921,7 @@ function writeTranscript(sessionID) {
887
921
  fs.mkdirSync(TRANSCRIPT_DIR, { recursive: true });
888
922
  const file = path.join(TRANSCRIPT_DIR, sessionID + ".jsonl");
889
923
  const lines = [];
890
- for (const msg of bucket.values()) lines.push(JSON.stringify(msg));
924
+ for (const record of bucket.values()) lines.push(JSON.stringify(serializeRecord(record)));
891
925
  fs.writeFileSync(file, lines.join("\\n") + "\\n");
892
926
  return file;
893
927
  } catch {
@@ -917,17 +951,24 @@ export const HillclimbPlugin = async ({ directory }) => ({
917
951
  event: async ({ event }) => {
918
952
  const type = event && event.type;
919
953
  const props = (event && event.properties) || {};
920
- 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;
921
957
 
922
958
  if (type === "message.updated") {
923
- ingestMessage(sessionID, props.info);
959
+ ingestMessage(sessionID, info);
960
+ return;
961
+ }
962
+
963
+ if (type === "message.part.updated") {
964
+ ingestPart(sessionID, props.part);
924
965
  return;
925
966
  }
926
967
 
927
968
  if (type === "session.created" && sessionID) {
928
969
  spawnHillclimb("git-traces --tool=opencode", {
929
970
  session_id: sessionID,
930
- cwd: directory,
971
+ cwd,
931
972
  hook_event_name: "session.created",
932
973
  tool: TOOL,
933
974
  });
@@ -940,7 +981,7 @@ export const HillclimbPlugin = async ({ directory }) => ({
940
981
  // so a single session produces one contribution, not one per turn.
941
982
  spawnHillclimb("git-traces --tool=opencode", {
942
983
  session_id: sessionID,
943
- cwd: directory,
984
+ cwd,
944
985
  hook_event_name: "session.idle",
945
986
  tool: TOOL,
946
987
  });
@@ -958,7 +999,7 @@ export const HillclimbPlugin = async ({ directory }) => ({
958
999
  if (transcriptPath) {
959
1000
  spawnHillclimb("upload", {
960
1001
  session_id: sessionID,
961
- cwd: directory,
1002
+ cwd,
962
1003
  transcript_path: transcriptPath,
963
1004
  hook_event_name: "session.deleted",
964
1005
  tool: TOOL,
@@ -978,7 +1019,7 @@ export const HillclimbPlugin = async ({ directory }) => ({
978
1019
  if (transcriptPath) {
979
1020
  spawnHillclimb("upload", {
980
1021
  session_id: sid,
981
- cwd: directory,
1022
+ cwd,
982
1023
  transcript_path: transcriptPath,
983
1024
  hook_event_name: "server.instance.disposed",
984
1025
  tool: TOOL,
@@ -987,7 +1028,7 @@ export const HillclimbPlugin = async ({ directory }) => ({
987
1028
  }
988
1029
  sessionMessages.clear();
989
1030
  spawnHillclimb("git-traces --tool=opencode", {
990
- cwd: directory,
1031
+ cwd,
991
1032
  hook_event_name: "server.instance.disposed",
992
1033
  tool: TOOL,
993
1034
  });
@@ -1070,7 +1111,7 @@ async function installHooksForTool(repoRoot, def) {
1070
1111
  const file = settingsPath(repoRoot, def);
1071
1112
  if (def.format === "opencode") {
1072
1113
  const r = await opencodeInstall(file);
1073
- return { settingsFile: file, ...r };
1114
+ return { settingsFile: file, ...r, changed: r.installed > 0 };
1074
1115
  }
1075
1116
  const settings = await readJson(file);
1076
1117
  let installed = 0;
@@ -1093,7 +1134,7 @@ async function installHooksForTool(repoRoot, def) {
1093
1134
  }
1094
1135
  }
1095
1136
  if (mutated) await writeJson(file, settings);
1096
- return { settingsFile: file, installed, alreadyPresent };
1137
+ return { settingsFile: file, installed, alreadyPresent, changed: mutated };
1097
1138
  }
1098
1139
  async function checkHooksForTool(repoRoot, def) {
1099
1140
  const file = settingsPath(repoRoot, def);
@@ -1133,6 +1174,29 @@ async function installDetectedHooks(repoRoot) {
1133
1174
  }
1134
1175
  return results;
1135
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
+ }
1136
1200
  async function checkAllHooks(repoRoot) {
1137
1201
  const results = [];
1138
1202
  for (const def of TOOLS) {
@@ -11488,6 +11552,288 @@ function convertEventToStep2(event, stepId, defaultModelName) {
11488
11552
  return null;
11489
11553
  }
11490
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
+
11491
11837
  // src/normalizer/cursor.ts
11492
11838
  function convertCursorToTrajectory(jsonlContent, sessionId) {
11493
11839
  const lines = [];
@@ -11545,22 +11891,501 @@ function convertCursorToTrajectory(jsonlContent, sessionId) {
11545
11891
  };
11546
11892
  }
11547
11893
 
11548
- // src/normalizer/types.ts
11549
- 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) {
11550
11905
  const result = {};
11551
- for (const [key, value] of Object.entries(obj)) {
11552
- if (value === void 0 || value === null) continue;
11553
- if (Array.isArray(value)) {
11554
- result[key] = value.map(
11555
- (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
11556
12271
  );
11557
- } else if (typeof value === "object" && !Array.isArray(value)) {
11558
- result[key] = excludeNone(value);
11559
- } else {
11560
- 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);
11561
12289
  }
11562
12290
  }
11563
- 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);
11564
12389
  }
11565
12390
 
11566
12391
  // src/normalizer/index.ts
@@ -11572,6 +12397,10 @@ function normalizeContent(sourceName, content, sessionId) {
11572
12397
  return convertCodexToTrajectory(content, sessionId);
11573
12398
  case "cursor":
11574
12399
  return convertCursorToTrajectory(content, sessionId);
12400
+ case "opencode":
12401
+ return convertOpenCodeToTrajectory(content, sessionId);
12402
+ case "copilot-chat":
12403
+ return convertCopilotChatToTrajectory(content, sessionId);
11575
12404
  default:
11576
12405
  return null;
11577
12406
  }
@@ -11583,7 +12412,10 @@ var NormalizeMiddleware = class {
11583
12412
  for (const file of group.files) {
11584
12413
  newFiles.push(file);
11585
12414
  if (!file.absolutePath.endsWith(".jsonl")) continue;
11586
- if (!["claude", "codex", "cursor"].includes(file.sourceName)) continue;
12415
+ if (!["claude", "codex", "cursor", "opencode", "copilot-chat"].includes(
12416
+ file.sourceName
12417
+ ))
12418
+ continue;
11587
12419
  const content = file.content ? file.content.toString("utf-8") : null;
11588
12420
  if (!content) continue;
11589
12421
  const sessionId = file.metadata?.sessionId ?? path9.basename(file.absolutePath, ".jsonl");
@@ -11843,6 +12675,23 @@ function resolveSourceTool(payload) {
11843
12675
  if (payload.hook_event_name === "Stop") return "codex";
11844
12676
  return "claude";
11845
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
+ }
11846
12695
  function resolveCursorTranscriptPath(payload) {
11847
12696
  const id = payload.conversation_id ?? payload.session_id;
11848
12697
  const workspace = payload.workspace_roots?.[0];
@@ -11874,6 +12723,16 @@ async function runUploadInner(payload) {
11874
12723
  );
11875
12724
  return;
11876
12725
  }
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);
11877
12736
  const transcriptResolved = path12.resolve(transcriptPath);
11878
12737
  try {
11879
12738
  const stat = await fs9.promises.stat(transcriptResolved);
@@ -11891,14 +12750,6 @@ async function runUploadInner(payload) {
11891
12750
  );
11892
12751
  return;
11893
12752
  }
11894
- const match = await findProjectForCwd(cwd);
11895
- if (!match) {
11896
- appendLog(
11897
- "warn",
11898
- `Skipping session ${sessionId}: no hillclimb config for cwd ${cwd}. Run \`npx hillclimb\` in the repo.`
11899
- );
11900
- return;
11901
- }
11902
12753
  if (!await hasAssistantMessage(transcriptResolved)) {
11903
12754
  appendLog(
11904
12755
  "info",
@@ -11906,7 +12757,6 @@ async function runUploadInner(payload) {
11906
12757
  );
11907
12758
  return;
11908
12759
  }
11909
- const { repoRoot, config } = match;
11910
12760
  await uploadSession({
11911
12761
  sessionId,
11912
12762
  transcriptPath: transcriptResolved,
@@ -12606,7 +13456,7 @@ async function releaseLock(repoRoot, tool) {
12606
13456
  }
12607
13457
 
12608
13458
  // src/git-traces/handlers.ts
12609
- var CLI_VERSION = "0.1.0";
13459
+ var CLI_VERSION = "0.2.0";
12610
13460
  var GIT_TRACES_SLUG = "git-traces";
12611
13461
  var MAX_UPLOAD_BYTES = 500 * 1024 * 1024;
12612
13462
  function formatEpochSeconds2(date) {
@@ -13034,6 +13884,30 @@ async function readStdin2() {
13034
13884
  }
13035
13885
  return Buffer.concat(chunks).toString("utf-8");
13036
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
+ }
13037
13911
  async function runGitTraces() {
13038
13912
  if (process.env[WORKER_ENV_FLAG2] === "1") {
13039
13913
  await runGitTracesWorker();
@@ -13148,6 +14022,7 @@ async function runGitTracesWorker() {
13148
14022
  );
13149
14023
  return;
13150
14024
  }
14025
+ await selfHealHook2(payload, tool);
13151
14026
  try {
13152
14027
  switch (event) {
13153
14028
  case "SessionStart":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hillclimb",
3
- "version": "0.1.9",
3
+ "version": "0.2.0",
4
4
  "description": "Extract and export AI coding tool logs grouped by repo",
5
5
  "license": "MIT",
6
6
  "type": "module",