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.
- package/README.md +2 -2
- package/dist/cli.js +916 -41
- 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
|
|
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 =
|
|
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,
|
|
868
|
-
// message.updated
|
|
869
|
-
//
|
|
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
|
|
873
|
-
if (!sessionID || !
|
|
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
|
-
|
|
880
|
-
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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/
|
|
11549
|
-
function
|
|
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(
|
|
11552
|
-
if (value
|
|
11553
|
-
|
|
11554
|
-
|
|
11555
|
-
|
|
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
|
-
|
|
11558
|
-
|
|
11559
|
-
}
|
|
11560
|
-
|
|
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
|
|
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(
|
|
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.
|
|
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":
|