@agentsky/sky 2.20.2 → 2.22.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/dist/{chunk-WTFBVTMI.js → chunk-EBH3KXSU.js} +2 -2
- package/dist/{chunk-HB7SPKQ5.js → chunk-XT3W75V3.js} +1 -1
- package/dist/{chunk-GHTAIKQ6.js → chunk-YMYQT6KR.js} +70 -34
- package/dist/index.js +917 -717
- package/dist/{src-QBXMNV2V.js → src-TGRDQ5XR.js} +1 -1
- package/dist/{trace-HTWXINXI.js → trace-PDIOL2TC.js} +2 -2
- package/dist/{trace-library-QR54Q4Z3.js → trace-library-TA3QUBHJ.js} +97 -11
- package/package.json +6 -4
- package/vendor/trace-collect/SOURCE.json +6 -0
- package/vendor/trace-collect/collect.mjs +75 -0
- package/vendor/trace-collect/identity.d.ts +8 -0
- package/vendor/trace-collect/identity.mjs +23 -0
- package/vendor/trace-collect/muse.mjs +87 -0
- package/vendor/trace-collect/normalize.d.ts +6 -0
- package/vendor/trace-collect/normalize.mjs +312 -0
- package/vendor/trace-collect/scrub.d.ts +2 -0
- package/vendor/trace-collect/scrub.mjs +186 -0
- package/vendor/trace-collect/sinks/agentsky.mjs +156 -0
- package/vendor/trace-collect/sources/_cline-family.mjs +182 -0
- package/vendor/trace-collect/sources/_project-root.mjs +40 -0
- package/vendor/trace-collect/sources/amp.mjs +18 -0
- package/vendor/trace-collect/sources/chatgpt.mjs +203 -0
- package/vendor/trace-collect/sources/claude-ai.mjs +94 -0
- package/vendor/trace-collect/sources/claude-code.mjs +4 -0
- package/vendor/trace-collect/sources/claude-desktop.mjs +87 -0
- package/vendor/trace-collect/sources/cline.mjs +14 -0
- package/vendor/trace-collect/sources/codex.mjs +4 -0
- package/vendor/trace-collect/sources/continue.mjs +117 -0
- package/vendor/trace-collect/sources/copilot-chat.mjs +145 -0
- package/vendor/trace-collect/sources/cursor-chat.mjs +274 -0
- package/vendor/trace-collect/sources/cursor.mjs +4 -0
- package/vendor/trace-collect/sources/deepseek.mjs +172 -0
- package/vendor/trace-collect/sources/dsh.mjs +145 -0
- package/vendor/trace-collect/sources/gemini-cli.mjs +113 -0
- package/vendor/trace-collect/sources/gemini.mjs +150 -0
- package/vendor/trace-collect/sources/goose.mjs +93 -0
- package/vendor/trace-collect/sources/grok-cli.mjs +110 -0
- package/vendor/trace-collect/sources/grok.mjs +83 -0
- package/vendor/trace-collect/sources/hermes.mjs +4 -0
- package/vendor/trace-collect/sources/index.mjs +119 -0
- package/vendor/trace-collect/sources/jan.mjs +134 -0
- package/vendor/trace-collect/sources/kilo-code.mjs +14 -0
- package/vendor/trace-collect/sources/kiro.mjs +77 -0
- package/vendor/trace-collect/sources/lm-studio.mjs +84 -0
- package/vendor/trace-collect/sources/muse.mjs +11 -0
- package/vendor/trace-collect/sources/opencode.mjs +109 -0
- package/vendor/trace-collect/sources/qwen-code.mjs +95 -0
- package/vendor/trace-collect/sources/roo-code.mjs +14 -0
- package/vendor/trace-collect/sources/zed.mjs +70 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// claude.ai keeps conversations in the cloud; Settings › Privacy › Export data emails a zip whose
|
|
2
|
+
// conversations.json is an array of { uuid, name, summary, created_at, updated_at, chat_messages[] }.
|
|
3
|
+
// Each message has sender "human" | "assistant", content[] blocks (text, thinking, tool_use, tool_result),
|
|
4
|
+
// attachments[] (pasted or uploaded documents with extracted_content) and files[] (binaries, names only).
|
|
5
|
+
// This turns each conversation into one trace/v1 session so the normal scan → scrub → draft flow can take it.
|
|
6
|
+
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { createHash } from "node:crypto";
|
|
10
|
+
import { execFileSync } from "node:child_process";
|
|
11
|
+
import { FORMAT } from "../normalize.mjs";
|
|
12
|
+
|
|
13
|
+
const IMAGE = /\.(png|jpe?g|gif|webp|heic)$/i;
|
|
14
|
+
|
|
15
|
+
/** conversations.json from an export zip, a folder it was unpacked into, or the file itself. */
|
|
16
|
+
export function readClaudeAiExport(input) {
|
|
17
|
+
const st = fs.statSync(input);
|
|
18
|
+
if (st.isDirectory()) {
|
|
19
|
+
const f = path.join(input, "conversations.json");
|
|
20
|
+
if (!fs.existsSync(f)) throw new Error(`no conversations.json in ${input}`);
|
|
21
|
+
return JSON.parse(fs.readFileSync(f, "utf8"));
|
|
22
|
+
}
|
|
23
|
+
if (input.endsWith(".zip")) {
|
|
24
|
+
const list = execFileSync("unzip", ["-Z1", input], { encoding: "utf8" }).split("\n");
|
|
25
|
+
const f = list.find((n) => /(^|\/)conversations\.json$/.test(n));
|
|
26
|
+
if (!f) throw new Error(`no conversations.json in ${input}`);
|
|
27
|
+
return JSON.parse(execFileSync("unzip", ["-p", input, f], { encoding: "utf8", maxBuffer: 2 * 1024 ** 3 }));
|
|
28
|
+
}
|
|
29
|
+
return JSON.parse(fs.readFileSync(input, "utf8"));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function blockText(c) {
|
|
33
|
+
if (typeof c === "string") return c;
|
|
34
|
+
if (!Array.isArray(c)) return c == null ? "" : JSON.stringify(c);
|
|
35
|
+
return c.map((p) => (typeof p === "string" ? p : p?.type === "text" ? p.text : p?.type === "knowledge" ? [p.title, p.url, p.text].filter(Boolean).join("\n") : p?.type === "image" ? "[image]" : JSON.stringify(p))).filter(Boolean).join("\n\n");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** An edited conversation is a tree; keep the branch that ends at the latest message, the one the person saw last. */
|
|
39
|
+
function lastBranch(msgs) {
|
|
40
|
+
if (!msgs.some((m) => m.parent_message_uuid)) return msgs;
|
|
41
|
+
const byId = new Map(msgs.map((m) => [m.uuid, m]));
|
|
42
|
+
let leaf = msgs.reduce((a, m) => (Date.parse(m.created_at || 0) >= Date.parse(a.created_at || 0) ? m : a), msgs[0]);
|
|
43
|
+
const chain = [], seen = new Set();
|
|
44
|
+
while (leaf && !seen.has(leaf.uuid)) { seen.add(leaf.uuid); chain.unshift(leaf); leaf = byId.get(leaf.parent_message_uuid); }
|
|
45
|
+
return chain.length >= 2 ? chain : msgs;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** One conversation as trace/v1. Attached documents carry their extracted text; binaries are named, never included (the export has no bytes). */
|
|
49
|
+
export function claudeAiSession(conv) {
|
|
50
|
+
const messages = [];
|
|
51
|
+
for (const m of lastBranch(conv.chat_messages || [])) {
|
|
52
|
+
const content = Array.isArray(m.content) && m.content.length ? m.content : [{ type: "text", text: m.text || "" }];
|
|
53
|
+
if (m.sender === "human") {
|
|
54
|
+
const attachments = [
|
|
55
|
+
...(m.attachments || []).filter((a) => a.extracted_content).map((a) => ({ kind: "document", media_type: "text/plain", name: a.file_name || "", text: a.extracted_content })),
|
|
56
|
+
...[...(m.files || []), ...(m.files_v2 || [])].map((f) => ({ kind: IMAGE.test(f.file_name || "") || f.file_kind === "image" ? "image" : "document", media_type: "", name: f.file_name || "" })),
|
|
57
|
+
];
|
|
58
|
+
const text = blockText(content.filter((p) => typeof p === "string" || p.type === "text"));
|
|
59
|
+
if (text || attachments.length) messages.push({ role: "user", ts: m.created_at, text, ...(attachments.length ? { attachments } : {}) });
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
// An assistant message interleaves text, thinking, tool calls and their results in order.
|
|
63
|
+
let cur = null, lastCall = "";
|
|
64
|
+
const open = () => (cur ||= { role: "assistant", ts: m.created_at, text: "" });
|
|
65
|
+
const flush = () => { if (cur && (cur.text || cur.thinking || cur.tool_calls)) messages.push(cur); cur = null; };
|
|
66
|
+
for (const p of content) {
|
|
67
|
+
if (p.type === "text") { const c = open(); c.text += (c.text ? "\n\n" : "") + (p.text || ""); }
|
|
68
|
+
else if (p.type === "thinking" && p.thinking) { const c = open(); c.thinking = [c.thinking, p.thinking].filter(Boolean).join("\n"); }
|
|
69
|
+
else if (p.type === "tool_use") { const c = open(); lastCall = p.id || `${m.uuid}:${(c.tool_calls || []).length}`; (c.tool_calls ||= []).push({ id: lastCall, name: p.name, input: p.input }); }
|
|
70
|
+
else if (p.type === "tool_result") { flush(); messages.push({ role: "tool", ts: m.created_at, tool_use_id: p.tool_use_id || lastCall, tool_name: p.name, is_error: !!p.is_error, text: blockText(p.content).slice(0, 1_000_000) }); }
|
|
71
|
+
}
|
|
72
|
+
flush();
|
|
73
|
+
}
|
|
74
|
+
const models = [...new Set((conv.chat_messages || []).map((m) => m.model).filter(Boolean))];
|
|
75
|
+
return {
|
|
76
|
+
format: FORMAT, harness: "claude-ai", model: conv.model || models[0] || "", ...(models.length > 1 ? { models } : {}),
|
|
77
|
+
session_id: `claude-ai-${createHash("sha256").update(`claude-ai:${conv.uuid}`).digest("hex").slice(0, 16)}`,
|
|
78
|
+
title: conv.name || "", started_at: conv.created_at, ended_at: conv.updated_at, cwd: "", source: "claude-ai-export",
|
|
79
|
+
messages,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function claudeAiSessions(convs) {
|
|
84
|
+
return convs.map(claudeAiSession).filter((s) => s.messages.some((m) => m.role === "user") && s.messages.some((m) => m.role === "assistant"));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export default {
|
|
88
|
+
id: "claude-ai",
|
|
89
|
+
name: "Claude (claude.ai, desktop and mobile chats)",
|
|
90
|
+
kind: "export",
|
|
91
|
+
how: "claude.ai → Settings → Privacy → Export data; download the zip from the email",
|
|
92
|
+
sniff: (l) => l.names.some((n) => /(^|\/)conversations\.json$/.test(n)) && l.names.some((n) => /(^|\/)users\.json$/.test(n)) && /"chat_messages"/.test(l.read(l.names.find((n) => /(^|\/)conversations\.json$/.test(n))).slice(0, 200000)),
|
|
93
|
+
*collect(input) { yield* claudeAiSessions(readClaudeAiExport(input)); },
|
|
94
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// Claude Code: read in place by the scan (lib/normalize.mjs); listed here so every agent shows up in one registry.
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export default { id: "claude-code", name: "Claude Code", kind: "native", roots: (home) => [path.join(home, ".claude/projects")] };
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Claude desktop app agent-mode sessions.
|
|
2
|
+
// Directory: ~/Library/Application Support/Claude/local-agent-mode-sessions
|
|
3
|
+
// Structure: <root>/<workspace-uuid>/<session-uuid>/local_<agent-uuid>/audit.jsonl
|
|
4
|
+
//
|
|
5
|
+
// The audit.jsonl files are in Claude Code JSONL format (type "user" / "assistant" / "system"
|
|
6
|
+
// with message.role and message.content) but written by the desktop app's native agent runner.
|
|
7
|
+
// They live at separate paths from ~/.claude/projects/ so they never overlap with the
|
|
8
|
+
// "claude-code" native source.
|
|
9
|
+
//
|
|
10
|
+
// We reuse normalizeClaudeCode from normalize.mjs and re-label the harness "claude-desktop".
|
|
11
|
+
|
|
12
|
+
import fs from "node:fs";
|
|
13
|
+
import os from "node:os";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import { parseJsonl, normalizeClaudeCode, FORMAT } from "../normalize.mjs";
|
|
16
|
+
|
|
17
|
+
const ROOT_REL = "Library/Application Support/Claude/local-agent-mode-sessions";
|
|
18
|
+
|
|
19
|
+
/** Collect audit.jsonl paths: <root>/<workspace>/<session>/local_<agent>/audit.jsonl */
|
|
20
|
+
function* findAuditFiles(root) {
|
|
21
|
+
let workspaces;
|
|
22
|
+
try { workspaces = fs.readdirSync(root); } catch { return; }
|
|
23
|
+
for (const ws of workspaces) {
|
|
24
|
+
const wsDir = path.join(root, ws);
|
|
25
|
+
let sessions;
|
|
26
|
+
try {
|
|
27
|
+
if (!fs.statSync(wsDir).isDirectory()) continue;
|
|
28
|
+
sessions = fs.readdirSync(wsDir);
|
|
29
|
+
} catch { continue; }
|
|
30
|
+
for (const sess of sessions) {
|
|
31
|
+
const sessDir = path.join(wsDir, sess);
|
|
32
|
+
let agents;
|
|
33
|
+
try {
|
|
34
|
+
if (!fs.statSync(sessDir).isDirectory()) continue;
|
|
35
|
+
agents = fs.readdirSync(sessDir);
|
|
36
|
+
} catch { continue; }
|
|
37
|
+
// The session's own agent is <session>/local_<id>/; helper ("ditto") agents it starts live in <session>/agent/.
|
|
38
|
+
let helpers = []; try { helpers = fs.readdirSync(path.join(sessDir, "agent")).map((h) => path.join("agent", h)); } catch {}
|
|
39
|
+
const main = agents.find((a) => a.startsWith("local_") && !a.startsWith("local_ditto_"));
|
|
40
|
+
for (const ag of [...agents, ...helpers]) {
|
|
41
|
+
const name = path.basename(ag);
|
|
42
|
+
if (!name.startsWith("local_")) continue;
|
|
43
|
+
const auditPath = path.join(sessDir, ag, "audit.jsonl");
|
|
44
|
+
try {
|
|
45
|
+
if (fs.statSync(auditPath).isFile()) yield { auditPath, wsId: ws, sessId: sess, agentId: name, parent: name.startsWith("local_ditto_") && main ? main : null };
|
|
46
|
+
} catch { /* skip */ }
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export default {
|
|
53
|
+
id: "claude-desktop",
|
|
54
|
+
name: "Claude Desktop (agent-mode sessions)",
|
|
55
|
+
kind: "local",
|
|
56
|
+
roots: (home) => [path.join(home, ROOT_REL)],
|
|
57
|
+
|
|
58
|
+
*collect(root) {
|
|
59
|
+
for (const { auditPath, wsId, sessId, agentId, parent } of findAuditFiles(root)) {
|
|
60
|
+
let raw;
|
|
61
|
+
try { raw = fs.readFileSync(auditPath, "utf8"); } catch { continue; }
|
|
62
|
+
let rows;
|
|
63
|
+
try { rows = parseJsonl(raw); } catch { continue; }
|
|
64
|
+
if (!rows.length) continue;
|
|
65
|
+
|
|
66
|
+
// normalizeClaudeCode reads type:user/assistant rows.
|
|
67
|
+
// audit.jsonl rows also have _audit_timestamp / _audit_hmac / client_platform
|
|
68
|
+
// but the core fields match claude-code format.
|
|
69
|
+
let trace;
|
|
70
|
+
try { trace = normalizeClaudeCode(rows, { source: auditPath }); } catch { continue; }
|
|
71
|
+
if (!trace.messages.some((m) => m.role === "user")) continue;
|
|
72
|
+
|
|
73
|
+
// Re-label harness.
|
|
74
|
+
trace.harness = "claude-desktop";
|
|
75
|
+
|
|
76
|
+
// The agent folder name is the one id unique per conversation (rows carry both it and an inner run id).
|
|
77
|
+
trace.session_id = agentId;
|
|
78
|
+
if (parent) trace.parent_session_id = parent;
|
|
79
|
+
trace.cowork = { workspace_id: wsId, session_id: sessId };
|
|
80
|
+
// Rows record the session's working folder (its outputs/ folder) only on some lines.
|
|
81
|
+
trace.cwd = trace.cwd || rows.find((r) => typeof r.cwd === "string")?.cwd || "";
|
|
82
|
+
trace.format = FORMAT;
|
|
83
|
+
|
|
84
|
+
yield trace;
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Cline (VS Code extension: saoudrizwan.claude-dev)
|
|
2
|
+
// Tasks: <vscode-globalStorage>/saoudrizwan.claude-dev/tasks/<id>/api_conversation_history.json
|
|
3
|
+
// See _cline-family.mjs for the shared format and conversion logic.
|
|
4
|
+
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { clineRoots, collectClineRoot } from "./_cline-family.mjs";
|
|
7
|
+
|
|
8
|
+
export default {
|
|
9
|
+
id: "cline",
|
|
10
|
+
name: "Cline (VS Code extension)",
|
|
11
|
+
kind: "local",
|
|
12
|
+
roots: (home) => clineRoots(home, "saoudrizwan.claude-dev"),
|
|
13
|
+
*collect(root) { yield* collectClineRoot(root, "cline"); },
|
|
14
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// Codex (CLI, IDE and desktop app): read in place by the scan (lib/normalize.mjs); listed here so every agent shows up in one registry.
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export default { id: "codex", name: "Codex (CLI, IDE and desktop app)", kind: "native", roots: (home) => [path.join(home, ".codex/sessions"), path.join(home, ".codex/archived_sessions")] };
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// Continue VS Code extension session history.
|
|
2
|
+
// Files: ~/.continue/sessions/<sessionId>.json (set CONTINUE_GLOBAL_DIR to override root)
|
|
3
|
+
// Also ~/.continue-cli/sessions/ (CLI variant)
|
|
4
|
+
//
|
|
5
|
+
// Each file is a Session JSON:
|
|
6
|
+
// { sessionId, title, workspaceDirectory, history: ChatHistoryItem[], mode?, chatModelTitle?, usage? }
|
|
7
|
+
// where each ChatHistoryItem is { message: { role, content, toolCalls?, ... }, ... }
|
|
8
|
+
// roles: "user" | "assistant" | "thinking" | "tool" | "system"
|
|
9
|
+
// content: string | MessagePart[] MessagePart: { type, text?, ... }
|
|
10
|
+
|
|
11
|
+
import fs from "node:fs";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import { createHash } from "node:crypto";
|
|
14
|
+
import { FORMAT } from "../normalize.mjs";
|
|
15
|
+
|
|
16
|
+
const SESSION_DIRS = [
|
|
17
|
+
".continue/sessions",
|
|
18
|
+
".continue-cli/sessions",
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
function readJson(p) {
|
|
22
|
+
try { return JSON.parse(fs.readFileSync(p, "utf8")); } catch { return null; }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function messageText(content) {
|
|
26
|
+
if (typeof content === "string") return content;
|
|
27
|
+
if (!Array.isArray(content)) return content == null ? "" : JSON.stringify(content);
|
|
28
|
+
return content.map((p) => {
|
|
29
|
+
if (typeof p === "string") return p;
|
|
30
|
+
if (p?.type === "text" || p?.type === "imageUrl") return p.text || "";
|
|
31
|
+
if (p?.type === "image_url") return "";
|
|
32
|
+
return "";
|
|
33
|
+
}).filter(Boolean).join("\n");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function buildMessages(history) {
|
|
37
|
+
const messages = [];
|
|
38
|
+
const callNames = new Map();
|
|
39
|
+
for (const item of (history || [])) {
|
|
40
|
+
const m = item?.message;
|
|
41
|
+
if (!m || typeof m !== "object") continue;
|
|
42
|
+
const role = m.role;
|
|
43
|
+
const text = messageText(m.content);
|
|
44
|
+
if (role === "system") continue;
|
|
45
|
+
if (role === "user") {
|
|
46
|
+
if (text) messages.push({ role: "user", text });
|
|
47
|
+
} else if (role === "assistant") {
|
|
48
|
+
const tool_calls = (m.toolCalls || []).map((tc) => {
|
|
49
|
+
const id = tc.id || tc.call_id || `call-${messages.length}`;
|
|
50
|
+
const name = tc.function?.name || tc.name || "tool";
|
|
51
|
+
let input = tc.function?.arguments ?? tc.arguments ?? tc.input ?? {};
|
|
52
|
+
if (typeof input === "string") { try { input = JSON.parse(input); } catch { /* keep */ } }
|
|
53
|
+
callNames.set(id, name);
|
|
54
|
+
return { id, name, input };
|
|
55
|
+
});
|
|
56
|
+
if (text || tool_calls.length) {
|
|
57
|
+
messages.push({ role: "assistant", text, ...(tool_calls.length ? { tool_calls } : {}) });
|
|
58
|
+
}
|
|
59
|
+
} else if (role === "thinking") {
|
|
60
|
+
const thinking = messageText(m.content);
|
|
61
|
+
if (thinking) messages.push({ role: "assistant", text: "", thinking });
|
|
62
|
+
} else if (role === "tool") {
|
|
63
|
+
const toolCallId = m.toolCallId || "";
|
|
64
|
+
messages.push({ role: "tool", tool_use_id: toolCallId, tool_name: callNames.get(toolCallId) || m.name || "", is_error: false, text: text.slice(0, 1_000_000) });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return messages;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export default {
|
|
71
|
+
id: "continue",
|
|
72
|
+
name: "Continue (VS Code extension)",
|
|
73
|
+
kind: "local",
|
|
74
|
+
roots: (home) => SESSION_DIRS.map((d) => path.join(home, d)).filter((p) => fs.existsSync(p)),
|
|
75
|
+
|
|
76
|
+
*collect(root) {
|
|
77
|
+
let entries;
|
|
78
|
+
try { entries = fs.readdirSync(root); } catch { return; }
|
|
79
|
+
for (const f of entries) {
|
|
80
|
+
if (!f.endsWith(".json") || f === "sessions.json") continue;
|
|
81
|
+
const filePath = path.join(root, f);
|
|
82
|
+
const data = readJson(filePath);
|
|
83
|
+
if (!data || !Array.isArray(data.history)) continue;
|
|
84
|
+
|
|
85
|
+
const messages = buildMessages(data.history);
|
|
86
|
+
if (!messages.some((m) => m.role === "user")) continue;
|
|
87
|
+
|
|
88
|
+
const sessionId = data.sessionId || f.slice(0, -5);
|
|
89
|
+
let started = data.dateCreated ? new Date(data.dateCreated).toISOString() : null;
|
|
90
|
+
if (!started) {
|
|
91
|
+
try { started = new Date(fs.statSync(filePath).birthtimeMs).toISOString(); } catch { }
|
|
92
|
+
}
|
|
93
|
+
let ended = null;
|
|
94
|
+
try { ended = new Date(fs.statSync(filePath).mtimeMs).toISOString(); } catch { }
|
|
95
|
+
|
|
96
|
+
const model = data.chatModelTitle || "";
|
|
97
|
+
const usage = data.usage ? {
|
|
98
|
+
...(data.usage.inputTokens != null ? { input_tokens: data.usage.inputTokens } : {}),
|
|
99
|
+
...(data.usage.outputTokens != null ? { output_tokens: data.usage.outputTokens } : {}),
|
|
100
|
+
} : undefined;
|
|
101
|
+
|
|
102
|
+
yield {
|
|
103
|
+
format: FORMAT,
|
|
104
|
+
harness: "continue",
|
|
105
|
+
session_id: createHash("sha256").update(`continue:${sessionId}`).digest("hex").slice(0, 16),
|
|
106
|
+
model,
|
|
107
|
+
title: data.title || "",
|
|
108
|
+
started_at: started,
|
|
109
|
+
ended_at: ended,
|
|
110
|
+
cwd: data.workspaceDirectory || "",
|
|
111
|
+
...(usage && Object.keys(usage).length ? { usage } : {}),
|
|
112
|
+
source: filePath,
|
|
113
|
+
messages,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// GitHub Copilot Chat session files.
|
|
2
|
+
//
|
|
3
|
+
// Locations (macOS):
|
|
4
|
+
// ~/Library/Application Support/<App>/User/workspaceStorage/*/chatSessions/*.json
|
|
5
|
+
// ~/Library/Application Support/<App>/User/globalStorage/github.copilot-chat/emptyWindowChatSessions/*.json
|
|
6
|
+
//
|
|
7
|
+
// Apps: Code, Code - Insiders, Cursor, Windsurf, VSCodium (and Linux ~/.config equivalents)
|
|
8
|
+
//
|
|
9
|
+
// Format: each file is a CopilotChatSession JSON:
|
|
10
|
+
// { requests: [{ message: string|{text?}, response: [{value}]?, result?: {errorDetails?} }]* }
|
|
11
|
+
// The session ID comes from the filename. Workspace path comes from sibling workspace.json.
|
|
12
|
+
//
|
|
13
|
+
// The "cursor" and "claude-code" native sources read agent transcripts from different paths
|
|
14
|
+
// and never read chatSessions files — no deduplication needed.
|
|
15
|
+
|
|
16
|
+
import fs from "node:fs";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
import { createHash } from "node:crypto";
|
|
19
|
+
import { FORMAT } from "../normalize.mjs";
|
|
20
|
+
|
|
21
|
+
const VSCODE_APPS_MAC = [
|
|
22
|
+
"Library/Application Support/Code/User",
|
|
23
|
+
"Library/Application Support/Code - Insiders/User",
|
|
24
|
+
"Library/Application Support/Cursor/User",
|
|
25
|
+
"Library/Application Support/Windsurf/User",
|
|
26
|
+
"Library/Application Support/VSCodium/User",
|
|
27
|
+
];
|
|
28
|
+
const VSCODE_APPS_LINUX = [
|
|
29
|
+
".config/Code/User",
|
|
30
|
+
".config/Code - Insiders/User",
|
|
31
|
+
".config/Cursor/User",
|
|
32
|
+
".config/Windsurf/User",
|
|
33
|
+
".config/VSCodium/User",
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
function appUserDirs(home) {
|
|
37
|
+
return [...VSCODE_APPS_MAC, ...VSCODE_APPS_LINUX].map((p) => path.join(home, p));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function* chatSessionDirs(home) {
|
|
41
|
+
for (const userDir of appUserDirs(home)) {
|
|
42
|
+
// workspaceStorage/*/chatSessions
|
|
43
|
+
const wsRoot = path.join(userDir, "workspaceStorage");
|
|
44
|
+
try {
|
|
45
|
+
for (const ws of fs.readdirSync(wsRoot)) {
|
|
46
|
+
const csDir = path.join(wsRoot, ws, "chatSessions");
|
|
47
|
+
if (fs.existsSync(csDir)) yield { dir: csDir, wsDir: path.join(wsRoot, ws) };
|
|
48
|
+
}
|
|
49
|
+
} catch { /* ok */ }
|
|
50
|
+
// globalStorage/github.copilot-chat/emptyWindowChatSessions
|
|
51
|
+
const ewDir = path.join(userDir, "globalStorage", "github.copilot-chat", "emptyWindowChatSessions");
|
|
52
|
+
if (fs.existsSync(ewDir)) yield { dir: ewDir, wsDir: null };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function readJson(p) {
|
|
57
|
+
try { return JSON.parse(fs.readFileSync(p, "utf8")); } catch { return null; }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function msgText(m) {
|
|
61
|
+
if (!m) return "";
|
|
62
|
+
if (typeof m === "string") return m;
|
|
63
|
+
if (typeof m === "object") return m.text || m.content || JSON.stringify(m);
|
|
64
|
+
return String(m);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function respText(parts) {
|
|
68
|
+
if (!Array.isArray(parts)) return "";
|
|
69
|
+
return parts.map((p) => {
|
|
70
|
+
if (!p) return "";
|
|
71
|
+
if (typeof p.value === "string") return p.value;
|
|
72
|
+
if (p.value && typeof p.value === "object") return p.value.value || p.value.text || JSON.stringify(p.value);
|
|
73
|
+
return "";
|
|
74
|
+
}).filter(Boolean).join("");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function buildMessages(requests) {
|
|
78
|
+
const messages = [];
|
|
79
|
+
for (const req of (requests || [])) {
|
|
80
|
+
const userText = msgText(req.message);
|
|
81
|
+
if (userText) messages.push({ role: "user", text: userText });
|
|
82
|
+
const asst = respText(req.response);
|
|
83
|
+
const isError = !!req.result?.errorDetails;
|
|
84
|
+
if (asst) {
|
|
85
|
+
messages.push({ role: "assistant", text: asst, ...(isError ? { error: req.result.errorDetails.message || "" } : {}) });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return messages;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export default {
|
|
92
|
+
id: "copilot-chat",
|
|
93
|
+
name: "GitHub Copilot Chat",
|
|
94
|
+
kind: "local",
|
|
95
|
+
roots: (home) => {
|
|
96
|
+
const roots = [];
|
|
97
|
+
for (const { dir } of chatSessionDirs(home)) {
|
|
98
|
+
if (!roots.includes(dir)) roots.push(dir);
|
|
99
|
+
}
|
|
100
|
+
return roots;
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
*collect(root, ctx) {
|
|
104
|
+
// root is one chatSessions dir; find workspace path from sibling workspace.json
|
|
105
|
+
const wsDir = path.dirname(root);
|
|
106
|
+
let workspacePath = "";
|
|
107
|
+
const wsJson = readJson(path.join(wsDir, "workspace.json"));
|
|
108
|
+
if (wsJson?.folder) {
|
|
109
|
+
try { workspacePath = new URL(wsJson.folder).pathname; } catch { workspacePath = wsJson.folder; }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let entries;
|
|
113
|
+
try { entries = fs.readdirSync(root); } catch { return; }
|
|
114
|
+
|
|
115
|
+
for (const f of entries) {
|
|
116
|
+
if (!f.endsWith(".json")) continue;
|
|
117
|
+
const sessionId = f.slice(0, -5);
|
|
118
|
+
const data = readJson(path.join(root, f));
|
|
119
|
+
if (!data || !Array.isArray(data.requests)) continue;
|
|
120
|
+
|
|
121
|
+
const messages = buildMessages(data.requests);
|
|
122
|
+
if (!messages.some((m) => m.role === "user")) continue;
|
|
123
|
+
|
|
124
|
+
let started = null, ended = null;
|
|
125
|
+
try {
|
|
126
|
+
const st = fs.statSync(path.join(root, f));
|
|
127
|
+
ended = new Date(st.mtimeMs).toISOString();
|
|
128
|
+
started = new Date(st.birthtimeMs).toISOString();
|
|
129
|
+
} catch { }
|
|
130
|
+
|
|
131
|
+
yield {
|
|
132
|
+
format: FORMAT,
|
|
133
|
+
harness: "copilot-chat",
|
|
134
|
+
session_id: createHash("sha256").update(`copilot-chat:${sessionId}`).digest("hex").slice(0, 16),
|
|
135
|
+
model: "",
|
|
136
|
+
title: "",
|
|
137
|
+
started_at: started,
|
|
138
|
+
ended_at: ended,
|
|
139
|
+
cwd: workspacePath,
|
|
140
|
+
source: path.join(root, f),
|
|
141
|
+
messages,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
};
|