@agentsky/sky 2.20.2 → 2.21.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-HB7SPKQ5.js → chunk-6JMFB3CV.js} +1 -1
- package/dist/{chunk-WTFBVTMI.js → chunk-ZZ6UF7QA.js} +1 -1
- package/dist/index.js +24 -13
- package/dist/{trace-HTWXINXI.js → trace-NMBMU4GS.js} +1 -1
- package/dist/{trace-library-QR54Q4Z3.js → trace-library-OWYB75AR.js} +96 -10
- package/package.json +3 -2
- 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,119 @@
|
|
|
1
|
+
// Source registry. Every agent is one self-contained module in this folder; nothing else needs to know about it.
|
|
2
|
+
//
|
|
3
|
+
// A module default-exports:
|
|
4
|
+
// {
|
|
5
|
+
// id: "gemini-cli", // written as trace.harness; also the folder name under ~/.trace-market/sources/
|
|
6
|
+
// name: "Gemini CLI",
|
|
7
|
+
// kind: "native" | "local" | "export",
|
|
8
|
+
// native: the scan reads its files in place (Claude Code, Codex, Cursor agent transcripts, Hermes) — roots only
|
|
9
|
+
// local: found on this computer in its own format; collect() converts it to trace/v1 files
|
|
10
|
+
// export: a file the person downloads from a cloud service (claude.ai, ChatGPT, Muse…); import converts it
|
|
11
|
+
// roots(home) -> string[] // native/local: where it lives on this computer (missing paths are fine)
|
|
12
|
+
// sniff(listing) -> boolean // export: listing = { names: string[], read(name) -> string } over the zip or folder
|
|
13
|
+
// *collect(input, ctx) -> trace/v1 sessions // local: input is one existing root; export: the zip, folder or file
|
|
14
|
+
// }
|
|
15
|
+
//
|
|
16
|
+
// A session is { format: "trace/v1", harness, session_id, model, title, started_at, ended_at, cwd, messages[] } where a
|
|
17
|
+
// message is user {text, attachments?} | assistant {text, thinking?, tool_calls?: [{id, name, input}]} | tool
|
|
18
|
+
// {tool_use_id, tool_name, is_error, text}. Keep everything the source recorded; scrubbing happens later, on upload.
|
|
19
|
+
|
|
20
|
+
import fs from "node:fs";
|
|
21
|
+
import os from "node:os";
|
|
22
|
+
import path from "node:path";
|
|
23
|
+
import { execFileSync } from "node:child_process";
|
|
24
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
25
|
+
|
|
26
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
27
|
+
let cache = null;
|
|
28
|
+
|
|
29
|
+
export async function loadSources() {
|
|
30
|
+
if (cache) return cache;
|
|
31
|
+
const files = fs.readdirSync(HERE).filter((f) => f.endsWith(".mjs") && !f.startsWith("_") && f !== "index.mjs").sort();
|
|
32
|
+
cache = [];
|
|
33
|
+
for (const f of files) {
|
|
34
|
+
const m = (await import(pathToFileURL(path.join(HERE, f)).href)).default;
|
|
35
|
+
if (m?.id && m.kind) cache.push(m);
|
|
36
|
+
}
|
|
37
|
+
return cache;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Zip or folder as { names, read } so an export adapter can sniff without unpacking everything. */
|
|
41
|
+
export function listing(input) {
|
|
42
|
+
const st = fs.statSync(input);
|
|
43
|
+
if (st.isDirectory()) {
|
|
44
|
+
const names = [];
|
|
45
|
+
const walk = (d, rel) => { for (const e of fs.readdirSync(d, { withFileTypes: true })) { const r = rel ? `${rel}/${e.name}` : e.name; if (e.isDirectory()) walk(path.join(d, e.name), r); else names.push(r); } };
|
|
46
|
+
walk(input, "");
|
|
47
|
+
return { names, read: (n) => fs.readFileSync(path.join(input, n), "utf8") };
|
|
48
|
+
}
|
|
49
|
+
if (/\.zip$/i.test(input)) {
|
|
50
|
+
const names = execFileSync("unzip", ["-Z1", input], { encoding: "utf8", maxBuffer: 256 * 1024 * 1024 }).split("\n").filter((n) => n && !n.endsWith("/"));
|
|
51
|
+
return { names, read: (n) => execFileSync("unzip", ["-p", input, n], { encoding: "utf8", maxBuffer: 2 * 1024 ** 3 }) };
|
|
52
|
+
}
|
|
53
|
+
return { names: [path.basename(input)], read: () => fs.readFileSync(input, "utf8") };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Write sessions as <dir>/<id>/<session_id>.json; unchanged sessions are not rewritten. */
|
|
57
|
+
export function writeSessions(sessions, dir) {
|
|
58
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
59
|
+
let written = 0, total = 0;
|
|
60
|
+
for (const s of sessions) {
|
|
61
|
+
if (!s?.messages?.some((m) => m.role === "user")) continue;
|
|
62
|
+
total++;
|
|
63
|
+
const name = String(s.session_id || "").replace(/[^\w.-]+/g, "_").slice(0, 120) || `session-${total}`;
|
|
64
|
+
const p = path.join(dir, `${name}.json`);
|
|
65
|
+
const body = JSON.stringify({ format: "trace/v1", ...s });
|
|
66
|
+
if (fs.existsSync(p) && fs.readFileSync(p, "utf8") === body) continue;
|
|
67
|
+
fs.writeFileSync(p, body, { mode: 0o600 });
|
|
68
|
+
written++;
|
|
69
|
+
}
|
|
70
|
+
return { total, written };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function fingerprint(root) {
|
|
74
|
+
// Cheap change detector: newest mtime and file count under the root, two levels deep is enough for every source we know.
|
|
75
|
+
let newest = 0, count = 0;
|
|
76
|
+
const visit = (p, depth) => { let st; try { st = fs.statSync(p); } catch { return; } newest = Math.max(newest, st.mtimeMs); count++; if (st.isDirectory() && depth < 4) { let ents = []; try { ents = fs.readdirSync(p); } catch {} for (const e of ents) visit(path.join(p, e), depth + 1); } };
|
|
77
|
+
visit(root, 0);
|
|
78
|
+
return `${count}:${Math.round(newest)}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Run every local adapter whose data exists; convert into <sourcesDir>/<id>/. Skips roots unchanged since last time. */
|
|
82
|
+
export async function collectLocal(sourcesDir, { home = os.homedir(), only = null, force = false, log = () => {} } = {}) {
|
|
83
|
+
const statePath = path.join(sourcesDir, ".collect-state.json");
|
|
84
|
+
let state = {}; try { state = JSON.parse(fs.readFileSync(statePath, "utf8")); } catch {}
|
|
85
|
+
const report = [];
|
|
86
|
+
for (const src of await loadSources()) {
|
|
87
|
+
if (src.kind !== "local" || (only && !only.includes(src.id))) continue;
|
|
88
|
+
const roots = (src.roots?.(home) ?? []).filter((r) => fs.existsSync(r));
|
|
89
|
+
for (const root of roots) {
|
|
90
|
+
const fp = fingerprint(root), key = `${src.id}:${root}`;
|
|
91
|
+
if (!force && state[key] === fp) { report.push({ id: src.id, root, skipped: "unchanged" }); continue; }
|
|
92
|
+
try {
|
|
93
|
+
const res = writeSessions(src.collect(root, { home }), path.join(sourcesDir, src.id));
|
|
94
|
+
state[key] = fp;
|
|
95
|
+
report.push({ id: src.id, root, ...res });
|
|
96
|
+
} catch (e) {
|
|
97
|
+
report.push({ id: src.id, root, error: e.message });
|
|
98
|
+
log(`${src.name}: ${e.message}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
fs.mkdirSync(sourcesDir, { recursive: true, mode: 0o700 });
|
|
103
|
+
fs.writeFileSync(statePath, JSON.stringify(state), { mode: 0o600 });
|
|
104
|
+
return report;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Recognise a downloaded export and convert it into <sourcesDir>/<id>/. `as` forces an adapter. */
|
|
108
|
+
export async function importExport(input, sourcesDir, { as = null } = {}) {
|
|
109
|
+
const sources = (await loadSources()).filter((s) => s.kind === "export");
|
|
110
|
+
const l = listing(input);
|
|
111
|
+
const src = as ? sources.find((s) => s.id === as) : sources.find((s) => { try { return s.sniff(l); } catch { return false; } });
|
|
112
|
+
if (!src) return { error: `not a supported export. Supported: ${sources.map((s) => s.id).join(", ")}` };
|
|
113
|
+
return { id: src.id, name: src.name, ...writeSessions(src.collect(input, { listing: l }), path.join(sourcesDir, src.id)) };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** One row per known agent: is it on this computer, and where. */
|
|
117
|
+
export async function describeSources(home = os.homedir()) {
|
|
118
|
+
return (await loadSources()).map((s) => ({ id: s.id, name: s.name, kind: s.kind, found: (s.roots?.(home) ?? []).filter((r) => fs.existsSync(r)) }));
|
|
119
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// Jan (Menlo Research) local AI client.
|
|
2
|
+
// Threads are stored at ~/jan/threads/<thread-id>/
|
|
3
|
+
// thread.json — metadata: id, title, created_at, updated_at, assistants[{model:{id,...}}], usage?
|
|
4
|
+
// messages.jsonl — one message per line:
|
|
5
|
+
// { id, thread_id, role:"user"|"assistant"|"tool", content:[{type:"text",text:{value,annotations}}],
|
|
6
|
+
// created_at (Unix seconds), status, name?, tool_call_id?, tool_calls?[{id,type,function:{name,arguments}}] }
|
|
7
|
+
//
|
|
8
|
+
// Jan reference: https://github.com/janhq/jan – packages/core/src/types/message/messageEntity.ts
|
|
9
|
+
|
|
10
|
+
import fs from "node:fs";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { createHash } from "node:crypto";
|
|
13
|
+
import { FORMAT } from "../normalize.mjs";
|
|
14
|
+
|
|
15
|
+
const THREADS_REL = "jan/threads";
|
|
16
|
+
|
|
17
|
+
function readJson(p) {
|
|
18
|
+
try { return JSON.parse(fs.readFileSync(p, "utf8")); } catch { return null; }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function parseJsonl(raw) {
|
|
22
|
+
const out = [];
|
|
23
|
+
for (const line of raw.split("\n")) {
|
|
24
|
+
const t = line.trim();
|
|
25
|
+
if (!t) continue;
|
|
26
|
+
try { out.push(JSON.parse(t)); } catch { /* skip partial */ }
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Extract text from a Jan content array or string. */
|
|
32
|
+
function contentText(content) {
|
|
33
|
+
if (typeof content === "string") return content;
|
|
34
|
+
if (!Array.isArray(content)) return "";
|
|
35
|
+
return content
|
|
36
|
+
.map((p) => {
|
|
37
|
+
if (typeof p === "string") return p;
|
|
38
|
+
if (p?.type === "text") {
|
|
39
|
+
const t = p.text;
|
|
40
|
+
if (typeof t === "string") return t;
|
|
41
|
+
if (t && typeof t === "object") return t.value || "";
|
|
42
|
+
return "";
|
|
43
|
+
}
|
|
44
|
+
return "";
|
|
45
|
+
})
|
|
46
|
+
.filter(Boolean)
|
|
47
|
+
.join("\n");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export default {
|
|
51
|
+
id: "jan",
|
|
52
|
+
name: "Jan (Menlo Research)",
|
|
53
|
+
kind: "local",
|
|
54
|
+
roots: (home) => [path.join(home, THREADS_REL)],
|
|
55
|
+
|
|
56
|
+
*collect(root) {
|
|
57
|
+
let threadDirs;
|
|
58
|
+
try { threadDirs = fs.readdirSync(root); } catch { return; }
|
|
59
|
+
|
|
60
|
+
for (const td of threadDirs) {
|
|
61
|
+
const threadDir = path.join(root, td);
|
|
62
|
+
try { if (!fs.statSync(threadDir).isDirectory()) continue; } catch { continue; }
|
|
63
|
+
|
|
64
|
+
const meta = readJson(path.join(threadDir, "thread.json"));
|
|
65
|
+
const msgsPath = path.join(threadDir, "messages.jsonl");
|
|
66
|
+
let raw;
|
|
67
|
+
try { raw = fs.readFileSync(msgsPath, "utf8"); } catch { continue; }
|
|
68
|
+
const rows = parseJsonl(raw);
|
|
69
|
+
if (!rows.length) continue;
|
|
70
|
+
|
|
71
|
+
const messages = [];
|
|
72
|
+
const callNames = new Map();
|
|
73
|
+
|
|
74
|
+
for (const row of rows) {
|
|
75
|
+
const role = row.role;
|
|
76
|
+
const ts = row.created_at ? new Date(Number(row.created_at) * 1000).toISOString() : undefined;
|
|
77
|
+
const text = contentText(row.content);
|
|
78
|
+
|
|
79
|
+
if (role === "user") {
|
|
80
|
+
if (text) messages.push({ role: "user", ts, text });
|
|
81
|
+
} else if (role === "assistant") {
|
|
82
|
+
const tool_calls = (row.tool_calls || []).map((tc) => {
|
|
83
|
+
const id = tc.id || `call-${messages.length}`;
|
|
84
|
+
const name = tc.function?.name || tc.name || "tool";
|
|
85
|
+
let input = tc.function?.arguments ?? tc.input ?? {};
|
|
86
|
+
if (typeof input === "string") { try { input = JSON.parse(input); } catch { /* keep */ } }
|
|
87
|
+
callNames.set(id, name);
|
|
88
|
+
return { id, name, input };
|
|
89
|
+
});
|
|
90
|
+
if (text || tool_calls.length) {
|
|
91
|
+
messages.push({ role: "assistant", ts, text, ...(tool_calls.length ? { tool_calls } : {}) });
|
|
92
|
+
}
|
|
93
|
+
} else if (role === "tool") {
|
|
94
|
+
const callId = row.tool_call_id || "";
|
|
95
|
+
messages.push({
|
|
96
|
+
role: "tool", ts,
|
|
97
|
+
tool_use_id: callId,
|
|
98
|
+
tool_name: callNames.get(callId) || row.name || "",
|
|
99
|
+
is_error: false,
|
|
100
|
+
text: text.slice(0, 1_000_000),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!messages.some((m) => m.role === "user")) continue;
|
|
106
|
+
|
|
107
|
+
const threadId = meta?.id || td;
|
|
108
|
+
const model = meta?.assistants?.[0]?.model?.id || meta?.assistants?.[0]?.model?.name || "";
|
|
109
|
+
const started = meta?.created_at ? new Date(Number(meta.created_at) * 1000).toISOString() : null;
|
|
110
|
+
const ended = meta?.updated_at ? new Date(Number(meta.updated_at) * 1000).toISOString() : null;
|
|
111
|
+
const usage = meta?.usage
|
|
112
|
+
? {
|
|
113
|
+
...(meta.usage.prompt_tokens != null ? { input_tokens: meta.usage.prompt_tokens } : {}),
|
|
114
|
+
...(meta.usage.completion_tokens != null ? { output_tokens: meta.usage.completion_tokens } : {}),
|
|
115
|
+
}
|
|
116
|
+
: undefined;
|
|
117
|
+
|
|
118
|
+
const sid = createHash("sha256").update(`jan:${threadId}`).digest("hex").slice(0, 16);
|
|
119
|
+
|
|
120
|
+
yield {
|
|
121
|
+
format: FORMAT,
|
|
122
|
+
harness: "jan",
|
|
123
|
+
session_id: sid,
|
|
124
|
+
model,
|
|
125
|
+
title: meta?.title || "",
|
|
126
|
+
started_at: started,
|
|
127
|
+
ended_at: ended,
|
|
128
|
+
cwd: "",
|
|
129
|
+
...(usage && Object.keys(usage).length ? { usage } : {}),
|
|
130
|
+
messages,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Kilo Code (VS Code extension: kilocode.kilo-code)
|
|
2
|
+
// Tasks: <vscode-globalStorage>/kilocode.kilo-code/tasks/<id>/api_conversation_history.json
|
|
3
|
+
// Same format as Cline. See _cline-family.mjs.
|
|
4
|
+
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { clineRoots, collectClineRoot } from "./_cline-family.mjs";
|
|
7
|
+
|
|
8
|
+
export default {
|
|
9
|
+
id: "kilo-code",
|
|
10
|
+
name: "Kilo Code (VS Code extension)",
|
|
11
|
+
kind: "local",
|
|
12
|
+
roots: (home) => clineRoots(home, "kilocode.kilo-code"),
|
|
13
|
+
*collect(root) { yield* collectClineRoot(root, "kilo-code"); },
|
|
14
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// Kiro stores agent chat sessions as .chat files under
|
|
2
|
+
// ~/Library/Application Support/Kiro/User/globalStorage/kiro.kiroagent/<workspace-hash>/
|
|
3
|
+
//
|
|
4
|
+
// Each .chat file is a JSON object:
|
|
5
|
+
// { executionId, actionId, context, validations,
|
|
6
|
+
// chat: [ {role:"human"|"bot"|"tool", content: string} ],
|
|
7
|
+
// metadata: { modelId, modelProvider, workflow, workflowId, startTime, endTime } }
|
|
8
|
+
//
|
|
9
|
+
// The "chat" value is a plain JS array (but may deserialize as an object with numeric
|
|
10
|
+
// keys in some versions); messages use roles "human" (user), "bot" (assistant), "tool".
|
|
11
|
+
// Tool messages are context injections (workspace layout, etc.) not tool results — they
|
|
12
|
+
// have no structured id. Tool call structure is embedded as XML in bot content strings
|
|
13
|
+
// rather than as separate structured objects, so we cannot extract call/result pairs;
|
|
14
|
+
// we keep the full text of every bot turn.
|
|
15
|
+
|
|
16
|
+
import fs from "node:fs";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
import { FORMAT } from "../normalize.mjs";
|
|
19
|
+
import { folderByHash } from "./_project-root.mjs";
|
|
20
|
+
|
|
21
|
+
const KIRO_GLOBAL = ["Library", "Application Support", "Kiro", "User", "globalStorage", "kiro.kiroagent"];
|
|
22
|
+
|
|
23
|
+
export default {
|
|
24
|
+
id: "kiro",
|
|
25
|
+
name: "Kiro (AWS)",
|
|
26
|
+
kind: "local",
|
|
27
|
+
roots: (home) => [path.join(home, ...KIRO_GLOBAL)],
|
|
28
|
+
*collect(root, ctx = {}) {
|
|
29
|
+
// Each direct child directory is sha256(workspace folder) cut to 32 hex; .chat files are inside. Kiro also keeps
|
|
30
|
+
// workspace-sessions/<base64url(folder)>, which names the folders to try first.
|
|
31
|
+
let entries; try { entries = fs.readdirSync(root, { withFileTypes: true }); } catch { return; }
|
|
32
|
+
let known = []; try { known = fs.readdirSync(path.join(root, "workspace-sessions")).map((n) => Buffer.from(n.replace(/_/g, "="), "base64url").toString("utf8")).filter((p) => p.startsWith("/")); } catch {}
|
|
33
|
+
const folderOf = folderByHash(ctx.home || path.resolve(root, "../../../../../.."), known);
|
|
34
|
+
for (const e of entries) {
|
|
35
|
+
if (!e.isDirectory()) continue;
|
|
36
|
+
const wsDir = path.join(root, e.name);
|
|
37
|
+
let chatFiles; try { chatFiles = fs.readdirSync(wsDir); } catch { continue; }
|
|
38
|
+
for (const cf of chatFiles) {
|
|
39
|
+
if (!cf.endsWith(".chat")) continue;
|
|
40
|
+
const filePath = path.join(wsDir, cf);
|
|
41
|
+
let obj; try { obj = JSON.parse(fs.readFileSync(filePath, "utf8")); } catch { continue; }
|
|
42
|
+
if (!obj) continue;
|
|
43
|
+
|
|
44
|
+
// Normalise the chat field to an array
|
|
45
|
+
const rawChat = obj.chat;
|
|
46
|
+
const chat = Array.isArray(rawChat) ? rawChat : (rawChat && typeof rawChat === "object" ? Object.values(rawChat) : []);
|
|
47
|
+
if (!chat.length) continue;
|
|
48
|
+
|
|
49
|
+
const meta = obj.metadata || {};
|
|
50
|
+
// One execution writes a .chat per step (each its own model call), so the step file names the session.
|
|
51
|
+
const sessionId = obj.executionId ? `${obj.executionId}:${cf.replace(/\.chat$/, "")}` : cf.replace(/\.chat$/, "");
|
|
52
|
+
const started_at = meta.startTime ? new Date(meta.startTime).toISOString() : null;
|
|
53
|
+
const ended_at = meta.endTime ? new Date(meta.endTime).toISOString() : null;
|
|
54
|
+
|
|
55
|
+
const messages = [];
|
|
56
|
+
for (const m of chat) {
|
|
57
|
+
const role = m.role;
|
|
58
|
+
const t = typeof m.content === "string" ? m.content : "";
|
|
59
|
+
if (role === "human") {
|
|
60
|
+
if (t) messages.push({ role: "user", text: t });
|
|
61
|
+
} else if (role === "bot") {
|
|
62
|
+
if (t) messages.push({ role: "assistant", text: t });
|
|
63
|
+
}
|
|
64
|
+
else if (role === "tool" && t) {
|
|
65
|
+
// Workspace context Kiro injected (file tree, open files…): kept as an attachment of the turn it precedes.
|
|
66
|
+
const last = messages.at(-1);
|
|
67
|
+
const doc = { kind: "document", media_type: "text/plain", name: "kiro context", text: t };
|
|
68
|
+
if (last?.role === "user") (last.attachments ||= []).push(doc); else messages.push({ role: "user", text: "", attachments: [doc] });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!messages.some((m) => m.role === "user")) continue;
|
|
73
|
+
yield { format: FORMAT, harness: "kiro", session_id: sessionId, ...(obj.executionId ? { execution_id: obj.executionId } : {}), model: meta.modelId || "", ...(meta.modelProvider ? { model_provider: meta.modelProvider } : {}), title: "", started_at, ended_at, cwd: folderOf(e.name), messages };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// LM Studio (LM Studio, Inc.) local conversation history.
|
|
2
|
+
// Conversations are stored as JSON files in:
|
|
3
|
+
// ~/.lmstudio/conversations/*.json (newer LM Studio ≥ 0.3)
|
|
4
|
+
// ~/.cache/lm-studio/conversations/*.json (older cache path)
|
|
5
|
+
//
|
|
6
|
+
// Each file is a single conversation object:
|
|
7
|
+
// { id, title, createdAt (ms), lastInteraction? (ms), model?,
|
|
8
|
+
// messages: [{ id, role:"user"|"assistant", content, timestamp (ms),
|
|
9
|
+
// modelIdentifier?, tokens?: {prompt, completion} }] }
|
|
10
|
+
//
|
|
11
|
+
// Reference: LM Studio desktop changelog and community-documented export formats.
|
|
12
|
+
|
|
13
|
+
import fs from "node:fs";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import { createHash } from "node:crypto";
|
|
16
|
+
import { FORMAT } from "../normalize.mjs";
|
|
17
|
+
|
|
18
|
+
const ROOTS = [
|
|
19
|
+
".lmstudio/conversations",
|
|
20
|
+
".cache/lm-studio/conversations",
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
function readJson(p) {
|
|
24
|
+
try { return JSON.parse(fs.readFileSync(p, "utf8")); } catch { return null; }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function msgText(content) {
|
|
28
|
+
if (typeof content === "string") return content;
|
|
29
|
+
if (Array.isArray(content)) {
|
|
30
|
+
return content
|
|
31
|
+
.map((p) => (typeof p === "string" ? p : p?.text || ""))
|
|
32
|
+
.filter(Boolean)
|
|
33
|
+
.join("\n");
|
|
34
|
+
}
|
|
35
|
+
return "";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export default {
|
|
39
|
+
id: "lm-studio",
|
|
40
|
+
name: "LM Studio",
|
|
41
|
+
kind: "local",
|
|
42
|
+
roots: (home) => ROOTS.map((r) => path.join(home, r)),
|
|
43
|
+
|
|
44
|
+
*collect(root) {
|
|
45
|
+
let entries;
|
|
46
|
+
try { entries = fs.readdirSync(root); } catch { return; }
|
|
47
|
+
|
|
48
|
+
for (const f of entries) {
|
|
49
|
+
if (!f.endsWith(".json")) continue;
|
|
50
|
+
const filePath = path.join(root, f);
|
|
51
|
+
const data = readJson(filePath);
|
|
52
|
+
if (!data || !Array.isArray(data.messages)) continue;
|
|
53
|
+
|
|
54
|
+
const messages = [];
|
|
55
|
+
for (const msg of data.messages) {
|
|
56
|
+
const role = msg.role;
|
|
57
|
+
if (role !== "user" && role !== "assistant") continue;
|
|
58
|
+
const text = msgText(msg.content);
|
|
59
|
+
const ts = msg.timestamp ? new Date(Number(msg.timestamp)).toISOString() : undefined;
|
|
60
|
+
if (text) messages.push({ role, ts, text });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!messages.some((m) => m.role === "user")) continue;
|
|
64
|
+
|
|
65
|
+
const convId = data.id || f.slice(0, -5);
|
|
66
|
+
const sid = createHash("sha256").update(`lm-studio:${convId}`).digest("hex").slice(0, 16);
|
|
67
|
+
const started = data.createdAt ? new Date(Number(data.createdAt)).toISOString() : null;
|
|
68
|
+
const ended = data.lastInteraction ? new Date(Number(data.lastInteraction)).toISOString() : null;
|
|
69
|
+
const model = data.model || data.messages.find((m) => m.modelIdentifier)?.modelIdentifier || "";
|
|
70
|
+
|
|
71
|
+
yield {
|
|
72
|
+
format: FORMAT,
|
|
73
|
+
harness: "lm-studio",
|
|
74
|
+
session_id: sid,
|
|
75
|
+
model,
|
|
76
|
+
title: data.title || "",
|
|
77
|
+
started_at: started,
|
|
78
|
+
ended_at: ended,
|
|
79
|
+
cwd: "",
|
|
80
|
+
messages,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Muse (Meta AI): "Download your agent data" export. Parsing lives in ../muse.mjs.
|
|
2
|
+
import { readMuseTranscript, museSessions } from "../muse.mjs";
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
id: "muse",
|
|
6
|
+
name: "Muse (Meta AI)",
|
|
7
|
+
kind: "export",
|
|
8
|
+
how: "Muse → Settings → Download your agent data",
|
|
9
|
+
sniff: (l) => l.names.some((n) => /(^|\/)Conversation with Muse.*\.txt$/i.test(n)),
|
|
10
|
+
*collect(input) { yield* museSessions(readMuseTranscript(input)); },
|
|
11
|
+
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// OpenCode stores sessions in a SQLite database at
|
|
2
|
+
// ~/.local/share/opencode/opencode.db
|
|
3
|
+
// with three tables:
|
|
4
|
+
// session – id, directory, title, time_created (ms), parent_id
|
|
5
|
+
// message – id, session_id, time_created (ms), data (JSON blob):
|
|
6
|
+
// { role, time, modelID, providerID, tokens?, ... }
|
|
7
|
+
// part – id, message_id, session_id, time_created (ms), data (JSON blob):
|
|
8
|
+
// { type:"text"|"tool"|"reasoning"|"step-start"|"step-finish"|"patch", ... }
|
|
9
|
+
// text – { type:"text", text }
|
|
10
|
+
// tool – { type:"tool", callID, tool (name), state:{ status, input, output } }
|
|
11
|
+
// reasoning – { type:"reasoning", text }
|
|
12
|
+
//
|
|
13
|
+
// Messages are sorted by time_created; parts are sorted the same way.
|
|
14
|
+
|
|
15
|
+
import fs from "node:fs";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import { execFileSync } from "node:child_process";
|
|
18
|
+
import { FORMAT } from "../normalize.mjs";
|
|
19
|
+
|
|
20
|
+
function query(db, sql, params = []) {
|
|
21
|
+
// sqlite3 -json doesn't support bound params; we inline them safely since all values are strings.
|
|
22
|
+
let q = sql;
|
|
23
|
+
for (const p of params) q = q.replace("?", JSON.stringify(String(p)));
|
|
24
|
+
try {
|
|
25
|
+
const out = execFileSync("sqlite3", ["-json", db, q], { encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
|
|
26
|
+
return JSON.parse(out || "[]");
|
|
27
|
+
} catch { return []; }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function parseData(raw) { try { return JSON.parse(raw || "{}"); } catch { return {}; } }
|
|
31
|
+
|
|
32
|
+
export default {
|
|
33
|
+
id: "opencode",
|
|
34
|
+
name: "OpenCode",
|
|
35
|
+
kind: "local",
|
|
36
|
+
roots: (home) => [path.join(home, ".local", "share", "opencode")],
|
|
37
|
+
*collect(root) {
|
|
38
|
+
const db = path.join(root, "opencode.db");
|
|
39
|
+
if (!fs.existsSync(db)) return;
|
|
40
|
+
|
|
41
|
+
const sessions = query(db, "SELECT id, directory, title, time_created, time_updated, parent_id FROM session WHERE time_archived IS NULL ORDER BY time_created");
|
|
42
|
+
if (!sessions.length) return;
|
|
43
|
+
|
|
44
|
+
for (const sess of sessions) {
|
|
45
|
+
const msgs = query(db, "SELECT id, time_created, data FROM message WHERE session_id = ? ORDER BY time_created, id", [sess.id]);
|
|
46
|
+
if (!msgs.length) continue;
|
|
47
|
+
|
|
48
|
+
const partRows = query(db, "SELECT message_id, data FROM part WHERE session_id = ? ORDER BY time_created, id", [sess.id]);
|
|
49
|
+
const partsByMsg = new Map();
|
|
50
|
+
for (const pr of partRows) {
|
|
51
|
+
const list = partsByMsg.get(pr.message_id) || [];
|
|
52
|
+
list.push(parseData(pr.data));
|
|
53
|
+
partsByMsg.set(pr.message_id, list);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const messages = [];
|
|
57
|
+
let model = "", started_at = null, ended_at = null;
|
|
58
|
+
|
|
59
|
+
for (const row of msgs) {
|
|
60
|
+
const data = parseData(row.data);
|
|
61
|
+
const ts = row.time_created ? new Date(Number(row.time_created)).toISOString() : undefined;
|
|
62
|
+
if (ts) { started_at ||= ts; ended_at = ts; }
|
|
63
|
+
const parts = partsByMsg.get(row.id) || [];
|
|
64
|
+
|
|
65
|
+
if (data.role === "user") {
|
|
66
|
+
const t = parts.filter((p) => p.type === "text" && p.text).map((p) => p.text).join("\n");
|
|
67
|
+
if (t) messages.push({ role: "user", ts, text: t });
|
|
68
|
+
} else if (data.role === "assistant") {
|
|
69
|
+
model ||= data.modelID ? `${data.providerID || ""}/${data.modelID}`.replace(/^\//, "") : "";
|
|
70
|
+
const textBlocks = parts.filter((p) => p.type === "text" && p.text).map((p) => p.text).join("\n");
|
|
71
|
+
const thinking = parts.filter((p) => p.type === "reasoning" && p.text).map((p) => p.text).join("\n");
|
|
72
|
+
const toolParts = parts.filter((p) => p.type === "tool");
|
|
73
|
+
const tool_calls = toolParts.map((p) => ({ id: p.callID, name: typeof p.tool === "string" ? p.tool : (p.tool?.name ?? "tool"), input: p.state?.input ?? {} }));
|
|
74
|
+
if (textBlocks || tool_calls.length || thinking) {
|
|
75
|
+
messages.push({ role: "assistant", ts, text: textBlocks, ...(thinking ? { thinking } : {}), ...(tool_calls.length ? { tool_calls } : {}) });
|
|
76
|
+
}
|
|
77
|
+
for (const p of toolParts) {
|
|
78
|
+
const out = p.state?.output;
|
|
79
|
+
if (out == null) continue;
|
|
80
|
+
messages.push({ role: "tool", ts, tool_use_id: p.callID, tool_name: typeof p.tool === "string" ? p.tool : (p.tool?.name ?? "tool"), is_error: p.state?.status === "error", text: String(out).slice(0, 1_000_000) });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!messages.some((m) => m.role === "user")) continue;
|
|
86
|
+
|
|
87
|
+
const usage = data_to_usage(msgs.map((r) => parseData(r.data)));
|
|
88
|
+
yield {
|
|
89
|
+
format: FORMAT, harness: "opencode", session_id: sess.id, model, title: sess.title || "",
|
|
90
|
+
started_at, ended_at, cwd: sess.directory || "",
|
|
91
|
+
...(sess.parent_id ? { parent_session_id: sess.parent_id } : {}),
|
|
92
|
+
...(Object.keys(usage).length ? { usage } : {}), messages,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
function data_to_usage(msgDataList) {
|
|
99
|
+
let input = 0, output = 0;
|
|
100
|
+
for (const d of msgDataList) {
|
|
101
|
+
const tk = d.tokens;
|
|
102
|
+
if (typeof tk === "object" && tk) {
|
|
103
|
+
input += Number(tk.input || 0);
|
|
104
|
+
output += Number(tk.output || 0);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (!input && !output) return {};
|
|
108
|
+
return { input_tokens: input, output_tokens: output };
|
|
109
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Qwen Code CLI stores sessions under ~/.qwen/tmp/<project-dir>/chats/ in the same JSON/JSONL
|
|
2
|
+
// format as Gemini CLI (they share the same session-file schema).
|
|
3
|
+
// See gemini-cli.mjs for the format details.
|
|
4
|
+
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { FORMAT } from "../normalize.mjs";
|
|
8
|
+
import { projectRootResolver } from "./_project-root.mjs";
|
|
9
|
+
|
|
10
|
+
function text(content) {
|
|
11
|
+
if (typeof content === "string") return content;
|
|
12
|
+
if (Array.isArray(content)) return content.map((p) => p?.text ?? "").filter(Boolean).join("\n");
|
|
13
|
+
return "";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function parseSession(filePath) {
|
|
17
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
18
|
+
const firstChar = raw.trimStart()[0];
|
|
19
|
+
if (firstChar === "{") {
|
|
20
|
+
try {
|
|
21
|
+
const obj = JSON.parse(raw);
|
|
22
|
+
return { sessionId: obj.sessionId, projectHash: obj.projectHash, startTime: obj.startTime, lastUpdated: obj.lastUpdated, msgs: obj.messages || [] };
|
|
23
|
+
} catch { /* fall through */ }
|
|
24
|
+
}
|
|
25
|
+
const lines = raw.trim().split("\n");
|
|
26
|
+
let header = null;
|
|
27
|
+
const msgs = [];
|
|
28
|
+
for (const line of lines) {
|
|
29
|
+
const t = line.trim();
|
|
30
|
+
if (!t) continue;
|
|
31
|
+
let obj; try { obj = JSON.parse(t); } catch { continue; }
|
|
32
|
+
if (obj.$set) continue;
|
|
33
|
+
if (!header) { header = obj; continue; }
|
|
34
|
+
msgs.push(obj);
|
|
35
|
+
}
|
|
36
|
+
return { sessionId: header?.sessionId, projectHash: header?.projectHash, startTime: header?.startTime, lastUpdated: header?.lastUpdated, msgs };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function* sessionsFromChatsDir(chatsDir, cwdOf = () => "") {
|
|
40
|
+
let entries; try { entries = fs.readdirSync(chatsDir); } catch { return; }
|
|
41
|
+
for (const name of entries) {
|
|
42
|
+
if (!/\.(json|jsonl)$/.test(name)) continue;
|
|
43
|
+
const filePath = path.join(chatsDir, name);
|
|
44
|
+
let parsed; try { parsed = parseSession(filePath); } catch { continue; }
|
|
45
|
+
const { sessionId, startTime, lastUpdated, msgs } = parsed;
|
|
46
|
+
if (!sessionId) continue;
|
|
47
|
+
const messages = [];
|
|
48
|
+
let model = "";
|
|
49
|
+
for (const m of msgs) {
|
|
50
|
+
const ts = m.timestamp;
|
|
51
|
+
if (m.type === "user") {
|
|
52
|
+
const t = text(m.content);
|
|
53
|
+
if (t) messages.push({ role: "user", ts, text: t });
|
|
54
|
+
} else if (m.type === "gemini" || m.type === "qwen" || m.type === "assistant") {
|
|
55
|
+
model ||= m.model || "";
|
|
56
|
+
const t = text(m.content);
|
|
57
|
+
const calls = (m.toolCalls || []).map((tc) => ({ id: String(tc.id), name: tc.name, input: tc.args ?? {} }));
|
|
58
|
+
const thinking = m.thoughts && typeof m.thoughts === "string" && m.thoughts.trim() ? m.thoughts.trim() : undefined;
|
|
59
|
+
if (t || calls.length || thinking) {
|
|
60
|
+
messages.push({ role: "assistant", ts, text: t, ...(thinking ? { thinking } : {}), ...(calls.length ? { tool_calls: calls } : {}) });
|
|
61
|
+
}
|
|
62
|
+
for (const tc of m.toolCalls || []) {
|
|
63
|
+
const results = Array.isArray(tc.result) ? tc.result : [];
|
|
64
|
+
for (const r of results) {
|
|
65
|
+
const fr = r?.functionResponse;
|
|
66
|
+
if (!fr) continue;
|
|
67
|
+
const out = fr.response?.output ?? JSON.stringify(fr.response ?? "");
|
|
68
|
+
messages.push({ role: "tool", ts, tool_use_id: String(fr.id ?? tc.id), tool_name: fr.name ?? tc.name, is_error: tc.status === "error", text: String(out).slice(0, 1_000_000) });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
} else if (m.type === "error") {
|
|
72
|
+
const t = text(m.content);
|
|
73
|
+
if (t) messages.push({ role: "assistant", ts, text: t });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (!messages.some((m) => m.role === "user")) continue;
|
|
77
|
+
yield { format: FORMAT, harness: "qwen-code", session_id: sessionId, model, title: "", started_at: startTime, ended_at: lastUpdated, cwd: cwdOf(path.basename(path.dirname(chatsDir)), parsed.projectHash, messages), messages };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export default {
|
|
82
|
+
id: "qwen-code",
|
|
83
|
+
name: "Qwen Code CLI",
|
|
84
|
+
kind: "local",
|
|
85
|
+
roots: (home) => [path.join(home, ".qwen", "tmp")],
|
|
86
|
+
*collect(root) {
|
|
87
|
+
const resolve = projectRootResolver(path.dirname(root), path.dirname(path.dirname(root)));
|
|
88
|
+
let entries; try { entries = fs.readdirSync(root, { withFileTypes: true }); } catch { return; }
|
|
89
|
+
for (const e of entries) {
|
|
90
|
+
if (!e.isDirectory()) continue;
|
|
91
|
+
const chatsDir = path.join(root, e.name, "chats");
|
|
92
|
+
if (fs.existsSync(chatsDir)) yield* sessionsFromChatsDir(chatsDir, resolve);
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Roo Code (VS Code extension: rooveterinaryinc.roo-cline)
|
|
2
|
+
// Tasks: <vscode-globalStorage>/rooveterinaryinc.roo-cline/tasks/<id>/api_conversation_history.json
|
|
3
|
+
// Same format as Cline. See _cline-family.mjs.
|
|
4
|
+
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { clineRoots, collectClineRoot } from "./_cline-family.mjs";
|
|
7
|
+
|
|
8
|
+
export default {
|
|
9
|
+
id: "roo-code",
|
|
10
|
+
name: "Roo Code (VS Code extension)",
|
|
11
|
+
kind: "local",
|
|
12
|
+
roots: (home) => clineRoots(home, "rooveterinaryinc.roo-cline"),
|
|
13
|
+
*collect(root) { yield* collectClineRoot(root, "roo-code"); },
|
|
14
|
+
};
|