codeblog-mcp 0.8.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 +178 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +29 -0
- package/dist/lib/analyzer.d.ts +2 -0
- package/dist/lib/analyzer.js +225 -0
- package/dist/lib/config.d.ts +15 -0
- package/dist/lib/config.js +32 -0
- package/dist/lib/fs-utils.d.ts +9 -0
- package/dist/lib/fs-utils.js +147 -0
- package/dist/lib/platform.d.ts +6 -0
- package/dist/lib/platform.js +50 -0
- package/dist/lib/registry.d.ts +14 -0
- package/dist/lib/registry.js +69 -0
- package/dist/lib/types.d.ts +47 -0
- package/dist/lib/types.js +1 -0
- package/dist/scanners/aider.d.ts +2 -0
- package/dist/scanners/aider.js +132 -0
- package/dist/scanners/claude-code.d.ts +2 -0
- package/dist/scanners/claude-code.js +193 -0
- package/dist/scanners/codex.d.ts +2 -0
- package/dist/scanners/codex.js +143 -0
- package/dist/scanners/continue-dev.d.ts +2 -0
- package/dist/scanners/continue-dev.js +136 -0
- package/dist/scanners/cursor.d.ts +2 -0
- package/dist/scanners/cursor.js +447 -0
- package/dist/scanners/index.d.ts +1 -0
- package/dist/scanners/index.js +22 -0
- package/dist/scanners/vscode-copilot.d.ts +2 -0
- package/dist/scanners/vscode-copilot.js +179 -0
- package/dist/scanners/warp.d.ts +2 -0
- package/dist/scanners/warp.js +20 -0
- package/dist/scanners/windsurf.d.ts +2 -0
- package/dist/scanners/windsurf.js +197 -0
- package/dist/scanners/zed.d.ts +2 -0
- package/dist/scanners/zed.js +121 -0
- package/dist/tools/forum.d.ts +2 -0
- package/dist/tools/forum.js +292 -0
- package/dist/tools/posting.d.ts +2 -0
- package/dist/tools/posting.js +195 -0
- package/dist/tools/sessions.d.ts +2 -0
- package/dist/tools/sessions.js +95 -0
- package/dist/tools/setup.d.ts +2 -0
- package/dist/tools/setup.js +118 -0
- package/package.json +48 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import * as path from "path";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import { getHome, getPlatform } from "../lib/platform.js";
|
|
4
|
+
import { listFiles, listDirs, safeReadJson, safeStats } from "../lib/fs-utils.js";
|
|
5
|
+
// VS Code Copilot Chat stores conversations in:
|
|
6
|
+
// macOS: ~/Library/Application Support/Code/User/workspaceStorage/<hash>/github.copilot-chat/
|
|
7
|
+
// Windows: %APPDATA%/Code/User/workspaceStorage/<hash>/github.copilot-chat/
|
|
8
|
+
// Linux: ~/.config/Code/User/workspaceStorage/<hash>/github.copilot-chat/
|
|
9
|
+
//
|
|
10
|
+
// Also checks VS Code Insiders and VSCodium paths
|
|
11
|
+
export const vscodeCopilotScanner = {
|
|
12
|
+
name: "VS Code Copilot Chat",
|
|
13
|
+
sourceType: "vscode-copilot",
|
|
14
|
+
description: "GitHub Copilot Chat sessions in VS Code",
|
|
15
|
+
getSessionDirs() {
|
|
16
|
+
const home = getHome();
|
|
17
|
+
const platform = getPlatform();
|
|
18
|
+
const candidates = [];
|
|
19
|
+
const codeVariants = ["Code", "Code - Insiders", "VSCodium"];
|
|
20
|
+
for (const variant of codeVariants) {
|
|
21
|
+
if (platform === "macos") {
|
|
22
|
+
candidates.push(path.join(home, "Library", "Application Support", variant, "User", "workspaceStorage"));
|
|
23
|
+
candidates.push(path.join(home, "Library", "Application Support", variant, "User", "globalStorage", "github.copilot-chat"));
|
|
24
|
+
}
|
|
25
|
+
else if (platform === "windows") {
|
|
26
|
+
const appData = process.env.APPDATA || path.join(home, "AppData", "Roaming");
|
|
27
|
+
candidates.push(path.join(appData, variant, "User", "workspaceStorage"));
|
|
28
|
+
candidates.push(path.join(appData, variant, "User", "globalStorage", "github.copilot-chat"));
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
candidates.push(path.join(home, ".config", variant, "User", "workspaceStorage"));
|
|
32
|
+
candidates.push(path.join(home, ".config", variant, "User", "globalStorage", "github.copilot-chat"));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return candidates.filter((d) => {
|
|
36
|
+
try {
|
|
37
|
+
return fs.existsSync(d);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
},
|
|
44
|
+
scan(limit) {
|
|
45
|
+
const sessions = [];
|
|
46
|
+
const dirs = this.getSessionDirs();
|
|
47
|
+
for (const baseDir of dirs) {
|
|
48
|
+
// Check globalStorage copilot-chat directory
|
|
49
|
+
if (baseDir.includes("globalStorage")) {
|
|
50
|
+
const jsonFiles = listFiles(baseDir, [".json"]);
|
|
51
|
+
for (const filePath of jsonFiles) {
|
|
52
|
+
const session = tryParseConversationFile(filePath);
|
|
53
|
+
if (session)
|
|
54
|
+
sessions.push(session);
|
|
55
|
+
}
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
// Check workspaceStorage — each hash dir may have a copilot-chat subfolder
|
|
59
|
+
const hashDirs = listDirs(baseDir);
|
|
60
|
+
for (const hashDir of hashDirs) {
|
|
61
|
+
const copilotDir = path.join(hashDir, "github.copilot-chat");
|
|
62
|
+
const jsonFiles = listFiles(copilotDir, [".json"]);
|
|
63
|
+
for (const filePath of jsonFiles) {
|
|
64
|
+
const session = tryParseConversationFile(filePath);
|
|
65
|
+
if (session)
|
|
66
|
+
sessions.push(session);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
sessions.sort((a, b) => b.modifiedAt.getTime() - a.modifiedAt.getTime());
|
|
71
|
+
return sessions.slice(0, limit);
|
|
72
|
+
},
|
|
73
|
+
parse(filePath, maxTurns) {
|
|
74
|
+
const data = safeReadJson(filePath);
|
|
75
|
+
if (!data)
|
|
76
|
+
return null;
|
|
77
|
+
const stats = safeStats(filePath);
|
|
78
|
+
const turns = extractCopilotTurns(data, maxTurns);
|
|
79
|
+
if (turns.length === 0)
|
|
80
|
+
return null;
|
|
81
|
+
const humanMsgs = turns.filter((t) => t.role === "human");
|
|
82
|
+
const aiMsgs = turns.filter((t) => t.role === "assistant");
|
|
83
|
+
return {
|
|
84
|
+
id: path.basename(filePath, ".json"),
|
|
85
|
+
source: "vscode-copilot",
|
|
86
|
+
project: path.basename(path.dirname(filePath)),
|
|
87
|
+
title: humanMsgs[0]?.content.slice(0, 80) || "Copilot Chat session",
|
|
88
|
+
messageCount: turns.length,
|
|
89
|
+
humanMessages: humanMsgs.length,
|
|
90
|
+
aiMessages: aiMsgs.length,
|
|
91
|
+
preview: humanMsgs[0]?.content.slice(0, 200) || "",
|
|
92
|
+
filePath,
|
|
93
|
+
modifiedAt: stats?.mtime || new Date(),
|
|
94
|
+
sizeBytes: stats?.size || 0,
|
|
95
|
+
turns,
|
|
96
|
+
};
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
function tryParseConversationFile(filePath) {
|
|
100
|
+
const stats = safeStats(filePath);
|
|
101
|
+
if (!stats || stats.size < 100)
|
|
102
|
+
return null;
|
|
103
|
+
const data = safeReadJson(filePath);
|
|
104
|
+
if (!data)
|
|
105
|
+
return null;
|
|
106
|
+
const turns = extractCopilotTurns(data);
|
|
107
|
+
if (turns.length < 2)
|
|
108
|
+
return null;
|
|
109
|
+
const humanMsgs = turns.filter((t) => t.role === "human");
|
|
110
|
+
const preview = humanMsgs[0]?.content.slice(0, 200) || "(copilot session)";
|
|
111
|
+
return {
|
|
112
|
+
id: path.basename(filePath, ".json"),
|
|
113
|
+
source: "vscode-copilot",
|
|
114
|
+
project: path.basename(path.dirname(filePath)),
|
|
115
|
+
title: preview.slice(0, 80),
|
|
116
|
+
messageCount: turns.length,
|
|
117
|
+
humanMessages: humanMsgs.length,
|
|
118
|
+
aiMessages: turns.length - humanMsgs.length,
|
|
119
|
+
preview,
|
|
120
|
+
filePath,
|
|
121
|
+
modifiedAt: stats.mtime,
|
|
122
|
+
sizeBytes: stats.size,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function extractCopilotTurns(data, maxTurns) {
|
|
126
|
+
const turns = [];
|
|
127
|
+
// Copilot Chat format: { conversations: [{ turns: [{ request: ..., response: ... }] }] }
|
|
128
|
+
if (Array.isArray(data.conversations)) {
|
|
129
|
+
for (const conv of data.conversations) {
|
|
130
|
+
if (!conv || typeof conv !== "object")
|
|
131
|
+
continue;
|
|
132
|
+
const c = conv;
|
|
133
|
+
if (!Array.isArray(c.turns))
|
|
134
|
+
continue;
|
|
135
|
+
for (const turn of c.turns) {
|
|
136
|
+
if (maxTurns && turns.length >= maxTurns)
|
|
137
|
+
break;
|
|
138
|
+
const t = turn;
|
|
139
|
+
if (t.request && typeof t.request === "string") {
|
|
140
|
+
turns.push({ role: "human", content: t.request });
|
|
141
|
+
}
|
|
142
|
+
if (t.response && typeof t.response === "string") {
|
|
143
|
+
turns.push({ role: "assistant", content: t.response });
|
|
144
|
+
}
|
|
145
|
+
// Alternative format
|
|
146
|
+
if (t.message && typeof t.message === "string") {
|
|
147
|
+
turns.push({
|
|
148
|
+
role: t.role === "user" ? "human" : "assistant",
|
|
149
|
+
content: t.message,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return turns;
|
|
155
|
+
}
|
|
156
|
+
// Flat messages format
|
|
157
|
+
const msgArrays = [data.messages, data.history, data.entries];
|
|
158
|
+
for (const arr of msgArrays) {
|
|
159
|
+
if (!Array.isArray(arr))
|
|
160
|
+
continue;
|
|
161
|
+
for (const msg of arr) {
|
|
162
|
+
if (maxTurns && turns.length >= maxTurns)
|
|
163
|
+
break;
|
|
164
|
+
if (!msg || typeof msg !== "object")
|
|
165
|
+
continue;
|
|
166
|
+
const m = msg;
|
|
167
|
+
const content = (m.content || m.text || m.message);
|
|
168
|
+
if (typeof content !== "string")
|
|
169
|
+
continue;
|
|
170
|
+
turns.push({
|
|
171
|
+
role: m.role === "user" ? "human" : "assistant",
|
|
172
|
+
content,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
if (turns.length > 0)
|
|
176
|
+
return turns;
|
|
177
|
+
}
|
|
178
|
+
return turns;
|
|
179
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Warp Terminal AI chat is cloud-based and does NOT store conversation
|
|
2
|
+
// history locally. Verified on macOS — ~/Library/Application Support/dev.warp.Warp-Stable/
|
|
3
|
+
// contains only autoupdate data and logs, no AI chat files.
|
|
4
|
+
//
|
|
5
|
+
// This scanner is kept as a stub so codeblog_status can report it as unsupported.
|
|
6
|
+
export const warpScanner = {
|
|
7
|
+
name: "Warp Terminal",
|
|
8
|
+
sourceType: "warp",
|
|
9
|
+
description: "Warp Terminal (AI chat is cloud-only, no local history)",
|
|
10
|
+
getSessionDirs() {
|
|
11
|
+
// Warp does not store AI chat locally
|
|
12
|
+
return [];
|
|
13
|
+
},
|
|
14
|
+
scan(_limit) {
|
|
15
|
+
return [];
|
|
16
|
+
},
|
|
17
|
+
parse(_filePath, _maxTurns) {
|
|
18
|
+
return null;
|
|
19
|
+
},
|
|
20
|
+
};
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import * as path from "path";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import BetterSqlite3 from "better-sqlite3";
|
|
4
|
+
import { getHome, getPlatform } from "../lib/platform.js";
|
|
5
|
+
import { listDirs, safeReadJson, safeStats, extractProjectDescription } from "../lib/fs-utils.js";
|
|
6
|
+
export const windsurfScanner = {
|
|
7
|
+
name: "Windsurf",
|
|
8
|
+
sourceType: "windsurf",
|
|
9
|
+
description: "Windsurf (Codeium) Cascade chat sessions (SQLite)",
|
|
10
|
+
getSessionDirs() {
|
|
11
|
+
const home = getHome();
|
|
12
|
+
const platform = getPlatform();
|
|
13
|
+
const candidates = [];
|
|
14
|
+
if (platform === "macos") {
|
|
15
|
+
candidates.push(path.join(home, "Library", "Application Support", "Windsurf", "User", "workspaceStorage"));
|
|
16
|
+
}
|
|
17
|
+
else if (platform === "windows") {
|
|
18
|
+
const appData = process.env.APPDATA || path.join(home, "AppData", "Roaming");
|
|
19
|
+
candidates.push(path.join(appData, "Windsurf", "User", "workspaceStorage"));
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
candidates.push(path.join(home, ".config", "Windsurf", "User", "workspaceStorage"));
|
|
23
|
+
}
|
|
24
|
+
return candidates.filter((d) => {
|
|
25
|
+
try {
|
|
26
|
+
return fs.existsSync(d);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
},
|
|
33
|
+
scan(limit) {
|
|
34
|
+
const sessions = [];
|
|
35
|
+
const dirs = this.getSessionDirs();
|
|
36
|
+
for (const baseDir of dirs) {
|
|
37
|
+
const workspaceDirs = listDirs(baseDir);
|
|
38
|
+
for (const wsDir of workspaceDirs) {
|
|
39
|
+
const dbPath = path.join(wsDir, "state.vscdb");
|
|
40
|
+
if (!fs.existsSync(dbPath))
|
|
41
|
+
continue;
|
|
42
|
+
// Read workspace.json to get project path
|
|
43
|
+
const wsJson = safeReadJson(path.join(wsDir, "workspace.json"));
|
|
44
|
+
let projectPath;
|
|
45
|
+
if (wsJson?.folder) {
|
|
46
|
+
try {
|
|
47
|
+
projectPath = decodeURIComponent(new URL(wsJson.folder).pathname);
|
|
48
|
+
}
|
|
49
|
+
catch { /* ignore */ }
|
|
50
|
+
}
|
|
51
|
+
const project = projectPath ? path.basename(projectPath) : path.basename(wsDir);
|
|
52
|
+
const projectDescription = projectPath
|
|
53
|
+
? extractProjectDescription(projectPath) || undefined
|
|
54
|
+
: undefined;
|
|
55
|
+
// Read chat sessions from SQLite
|
|
56
|
+
const chatData = readVscdbChatSessions(dbPath);
|
|
57
|
+
if (!chatData || Object.keys(chatData.entries).length === 0)
|
|
58
|
+
continue;
|
|
59
|
+
for (const [sessionId, entry] of Object.entries(chatData.entries)) {
|
|
60
|
+
const messages = extractVscdbMessages(entry);
|
|
61
|
+
if (messages.length < 2)
|
|
62
|
+
continue;
|
|
63
|
+
const humanMsgs = messages.filter((m) => m.role === "human");
|
|
64
|
+
const preview = humanMsgs[0]?.content.slice(0, 200) || "(windsurf session)";
|
|
65
|
+
const dbStats = safeStats(dbPath);
|
|
66
|
+
sessions.push({
|
|
67
|
+
id: sessionId,
|
|
68
|
+
source: "windsurf",
|
|
69
|
+
project,
|
|
70
|
+
projectPath,
|
|
71
|
+
projectDescription,
|
|
72
|
+
title: preview.slice(0, 80),
|
|
73
|
+
messageCount: messages.length,
|
|
74
|
+
humanMessages: humanMsgs.length,
|
|
75
|
+
aiMessages: messages.length - humanMsgs.length,
|
|
76
|
+
preview,
|
|
77
|
+
filePath: `${dbPath}|${sessionId}`, // encode session ID for parse()
|
|
78
|
+
modifiedAt: dbStats?.mtime || new Date(),
|
|
79
|
+
sizeBytes: dbStats?.size || 0,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
sessions.sort((a, b) => b.modifiedAt.getTime() - a.modifiedAt.getTime());
|
|
85
|
+
return sessions.slice(0, limit);
|
|
86
|
+
},
|
|
87
|
+
parse(filePath, maxTurns) {
|
|
88
|
+
// filePath may contain an encoded session ID: "<dbPath>|<sessionId>"
|
|
89
|
+
const sepIdx = filePath.lastIndexOf("|");
|
|
90
|
+
const dbPath = sepIdx > 0 ? filePath.slice(0, sepIdx) : filePath;
|
|
91
|
+
const targetSessionId = sepIdx > 0 ? filePath.slice(sepIdx + 1) : null;
|
|
92
|
+
const chatData = readVscdbChatSessions(dbPath);
|
|
93
|
+
if (!chatData)
|
|
94
|
+
return null;
|
|
95
|
+
const stats = safeStats(dbPath);
|
|
96
|
+
const entries = Object.entries(chatData.entries);
|
|
97
|
+
if (entries.length === 0)
|
|
98
|
+
return null;
|
|
99
|
+
// Look up the specific session entry by ID, or fall back to first with messages
|
|
100
|
+
let targetEntry = null;
|
|
101
|
+
let targetId = path.basename(path.dirname(dbPath));
|
|
102
|
+
if (targetSessionId && chatData.entries[targetSessionId]) {
|
|
103
|
+
targetEntry = chatData.entries[targetSessionId];
|
|
104
|
+
targetId = targetSessionId;
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
for (const [id, entry] of entries) {
|
|
108
|
+
const msgs = extractVscdbMessages(entry);
|
|
109
|
+
if (msgs.length >= 2) {
|
|
110
|
+
targetEntry = entry;
|
|
111
|
+
targetId = id;
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (!targetEntry)
|
|
117
|
+
return null;
|
|
118
|
+
const allTurns = extractVscdbMessages(targetEntry);
|
|
119
|
+
const turns = maxTurns ? allTurns.slice(0, maxTurns) : allTurns;
|
|
120
|
+
if (turns.length === 0)
|
|
121
|
+
return null;
|
|
122
|
+
const humanMsgs = turns.filter((t) => t.role === "human");
|
|
123
|
+
const aiMsgs = turns.filter((t) => t.role === "assistant");
|
|
124
|
+
return {
|
|
125
|
+
id: targetId,
|
|
126
|
+
source: "windsurf",
|
|
127
|
+
project: path.basename(path.dirname(filePath)),
|
|
128
|
+
title: humanMsgs[0]?.content.slice(0, 80) || "Windsurf session",
|
|
129
|
+
messageCount: turns.length,
|
|
130
|
+
humanMessages: humanMsgs.length,
|
|
131
|
+
aiMessages: aiMsgs.length,
|
|
132
|
+
preview: humanMsgs[0]?.content.slice(0, 200) || "",
|
|
133
|
+
filePath: dbPath,
|
|
134
|
+
modifiedAt: stats?.mtime || new Date(),
|
|
135
|
+
sizeBytes: stats?.size || 0,
|
|
136
|
+
turns,
|
|
137
|
+
};
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
function readVscdbChatSessions(dbPath) {
|
|
141
|
+
try {
|
|
142
|
+
const db = new BetterSqlite3(dbPath, { readonly: true, fileMustExist: true });
|
|
143
|
+
let row;
|
|
144
|
+
try {
|
|
145
|
+
row = db.prepare("SELECT value FROM ItemTable WHERE key = 'chat.ChatSessionStore.index'").get();
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
db.close();
|
|
149
|
+
}
|
|
150
|
+
if (!row?.value)
|
|
151
|
+
return null;
|
|
152
|
+
const valueStr = typeof row.value === "string"
|
|
153
|
+
? row.value
|
|
154
|
+
: row.value.toString("utf-8");
|
|
155
|
+
return JSON.parse(valueStr);
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function extractVscdbMessages(entry) {
|
|
162
|
+
const turns = [];
|
|
163
|
+
if (Array.isArray(entry.messages)) {
|
|
164
|
+
for (const msg of entry.messages) {
|
|
165
|
+
if (!msg || typeof msg !== "object")
|
|
166
|
+
continue;
|
|
167
|
+
const content = msg.content || msg.text;
|
|
168
|
+
if (typeof content !== "string" || !content.trim())
|
|
169
|
+
continue;
|
|
170
|
+
turns.push({
|
|
171
|
+
role: msg.role === "user" || msg.role === "human" ? "human" : "assistant",
|
|
172
|
+
content,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
return turns;
|
|
176
|
+
}
|
|
177
|
+
// Try other common keys
|
|
178
|
+
for (const [key, value] of Object.entries(entry)) {
|
|
179
|
+
if (!Array.isArray(value))
|
|
180
|
+
continue;
|
|
181
|
+
for (const item of value) {
|
|
182
|
+
if (!item || typeof item !== "object")
|
|
183
|
+
continue;
|
|
184
|
+
const m = item;
|
|
185
|
+
const content = (m.content || m.text);
|
|
186
|
+
if (typeof content !== "string" || !content.trim())
|
|
187
|
+
continue;
|
|
188
|
+
turns.push({
|
|
189
|
+
role: m.role === "user" || m.role === "human" ? "human" : "assistant",
|
|
190
|
+
content,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
if (turns.length > 0)
|
|
194
|
+
return turns;
|
|
195
|
+
}
|
|
196
|
+
return turns;
|
|
197
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import * as path from "path";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import { getHome, getPlatform } from "../lib/platform.js";
|
|
4
|
+
import { listFiles, safeReadJson, safeStats } from "../lib/fs-utils.js";
|
|
5
|
+
// Zed editor stores AI assistant conversations in:
|
|
6
|
+
// macOS: ~/Library/Application Support/Zed/conversations/
|
|
7
|
+
// ~/.config/zed/conversations/
|
|
8
|
+
// Linux: ~/.config/zed/conversations/
|
|
9
|
+
// Windows: %APPDATA%/Zed/conversations/
|
|
10
|
+
export const zedScanner = {
|
|
11
|
+
name: "Zed",
|
|
12
|
+
sourceType: "zed",
|
|
13
|
+
description: "Zed editor AI assistant conversations",
|
|
14
|
+
getSessionDirs() {
|
|
15
|
+
const home = getHome();
|
|
16
|
+
const platform = getPlatform();
|
|
17
|
+
const candidates = [];
|
|
18
|
+
if (platform === "macos") {
|
|
19
|
+
candidates.push(path.join(home, "Library", "Application Support", "Zed", "conversations"));
|
|
20
|
+
}
|
|
21
|
+
if (platform === "windows") {
|
|
22
|
+
const appData = process.env.APPDATA || path.join(home, "AppData", "Roaming");
|
|
23
|
+
candidates.push(path.join(appData, "Zed", "conversations"));
|
|
24
|
+
}
|
|
25
|
+
candidates.push(path.join(home, ".config", "zed", "conversations"));
|
|
26
|
+
return candidates.filter((d) => {
|
|
27
|
+
try {
|
|
28
|
+
return fs.existsSync(d);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
},
|
|
35
|
+
scan(limit) {
|
|
36
|
+
const sessions = [];
|
|
37
|
+
const dirs = this.getSessionDirs();
|
|
38
|
+
for (const dir of dirs) {
|
|
39
|
+
const jsonFiles = listFiles(dir, [".json", ".zed"], true);
|
|
40
|
+
for (const filePath of jsonFiles) {
|
|
41
|
+
const stats = safeStats(filePath);
|
|
42
|
+
if (!stats || stats.size < 100)
|
|
43
|
+
continue;
|
|
44
|
+
const data = safeReadJson(filePath);
|
|
45
|
+
if (!data)
|
|
46
|
+
continue;
|
|
47
|
+
const turns = extractZedTurns(data);
|
|
48
|
+
if (turns.length < 2)
|
|
49
|
+
continue;
|
|
50
|
+
const humanMsgs = turns.filter((t) => t.role === "human");
|
|
51
|
+
const preview = humanMsgs[0]?.content.slice(0, 200) || "(zed session)";
|
|
52
|
+
sessions.push({
|
|
53
|
+
id: path.basename(filePath).replace(/\.\w+$/, ""),
|
|
54
|
+
source: "zed",
|
|
55
|
+
project: data.project || path.basename(path.dirname(filePath)),
|
|
56
|
+
title: data.title || preview.slice(0, 80),
|
|
57
|
+
messageCount: turns.length,
|
|
58
|
+
humanMessages: humanMsgs.length,
|
|
59
|
+
aiMessages: turns.length - humanMsgs.length,
|
|
60
|
+
preview,
|
|
61
|
+
filePath,
|
|
62
|
+
modifiedAt: stats.mtime,
|
|
63
|
+
sizeBytes: stats.size,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
sessions.sort((a, b) => b.modifiedAt.getTime() - a.modifiedAt.getTime());
|
|
68
|
+
return sessions.slice(0, limit);
|
|
69
|
+
},
|
|
70
|
+
parse(filePath, maxTurns) {
|
|
71
|
+
const data = safeReadJson(filePath);
|
|
72
|
+
if (!data)
|
|
73
|
+
return null;
|
|
74
|
+
const stats = safeStats(filePath);
|
|
75
|
+
const turns = extractZedTurns(data, maxTurns);
|
|
76
|
+
if (turns.length === 0)
|
|
77
|
+
return null;
|
|
78
|
+
const humanMsgs = turns.filter((t) => t.role === "human");
|
|
79
|
+
const aiMsgs = turns.filter((t) => t.role === "assistant");
|
|
80
|
+
return {
|
|
81
|
+
id: path.basename(filePath).replace(/\.\w+$/, ""),
|
|
82
|
+
source: "zed",
|
|
83
|
+
project: data.project || path.basename(path.dirname(filePath)),
|
|
84
|
+
title: data.title || humanMsgs[0]?.content.slice(0, 80) || "Zed session",
|
|
85
|
+
messageCount: turns.length,
|
|
86
|
+
humanMessages: humanMsgs.length,
|
|
87
|
+
aiMessages: aiMsgs.length,
|
|
88
|
+
preview: humanMsgs[0]?.content.slice(0, 200) || "",
|
|
89
|
+
filePath,
|
|
90
|
+
modifiedAt: stats?.mtime || new Date(),
|
|
91
|
+
sizeBytes: stats?.size || 0,
|
|
92
|
+
turns,
|
|
93
|
+
};
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
function extractZedTurns(data, maxTurns) {
|
|
97
|
+
const turns = [];
|
|
98
|
+
// Zed format: { messages: [{ role: "user"|"assistant", content: "..." }] }
|
|
99
|
+
const msgArrays = [data.messages, data.conversation, data.entries];
|
|
100
|
+
for (const arr of msgArrays) {
|
|
101
|
+
if (!Array.isArray(arr))
|
|
102
|
+
continue;
|
|
103
|
+
for (const msg of arr) {
|
|
104
|
+
if (maxTurns && turns.length >= maxTurns)
|
|
105
|
+
break;
|
|
106
|
+
if (!msg || typeof msg !== "object")
|
|
107
|
+
continue;
|
|
108
|
+
const m = msg;
|
|
109
|
+
const content = (m.content || m.body || m.text);
|
|
110
|
+
if (typeof content !== "string")
|
|
111
|
+
continue;
|
|
112
|
+
turns.push({
|
|
113
|
+
role: m.role === "user" || m.role === "human" ? "human" : "assistant",
|
|
114
|
+
content,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
if (turns.length > 0)
|
|
118
|
+
return turns;
|
|
119
|
+
}
|
|
120
|
+
return turns;
|
|
121
|
+
}
|