memhouse 0.0.1 → 0.3.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/LICENSE +22 -0
- package/README.md +59 -3
- package/bin/memhouse.js +487 -0
- package/cache.js +850 -0
- package/ch-ingest.js +329 -0
- package/ch-schema.js +152 -0
- package/clickhouse.js +87 -0
- package/editors/antigravity.js +1019 -0
- package/editors/base.js +321 -0
- package/editors/claude.js +386 -0
- package/editors/codebuff.js +338 -0
- package/editors/codex.js +537 -0
- package/editors/commandcode.js +166 -0
- package/editors/copilot-jetbrains.js +207 -0
- package/editors/copilot.js +260 -0
- package/editors/cursor-agent.js +202 -0
- package/editors/cursor.js +438 -0
- package/editors/gemini.js +231 -0
- package/editors/goose.js +358 -0
- package/editors/gsd.js +366 -0
- package/editors/index.js +179 -0
- package/editors/kiro.js +315 -0
- package/editors/opencode.js +374 -0
- package/editors/vscode.js +413 -0
- package/editors/windsurf.js +634 -0
- package/editors/zed.js +213 -0
- package/index.js +424 -2
- package/mcp-server.js +294 -0
- package/mem-house/DESIGN.md +119 -0
- package/mem-house/delivery/AGENT-INSTALL.md +75 -0
- package/mem-house/delivery/PROMPT.md +42 -0
- package/mem-house/delivery/install.sh +181 -0
- package/mem-house/delivery/kernel-install.md +76 -0
- package/mem-house/delivery/plugin/.claude-plugin/plugin.json +8 -0
- package/mem-house/delivery/skills/memhouse-search/SKILL.md +104 -0
- package/mem-house/delivery/skills/memhouse-sessions/SKILL.md +94 -0
- package/mem-house/delivery/skills/memhouse-sql/SKILL.md +115 -0
- package/mem-house/rls.sql +17 -0
- package/mem-house/schema.sql +145 -0
- package/mem-house/server/queries.js +878 -0
- package/mem-house/server/server.js +228 -0
- package/mem-house/shipper/ship.js +402 -0
- package/misc/syntax-check.js +38 -0
- package/package.json +56 -7
- package/pricing.js +88 -0
- package/pricing.json +992 -0
- package/public/assets/index-BsgjOSTv.js +75 -0
- package/public/assets/index-DV6ONi_F.css +2 -0
- package/public/index.html +16 -0
- package/relay-client.js +306 -0
- package/relay-server.js +577 -0
- package/server.js +860 -0
- package/share-image.js +367 -0
- package/ui/eslint.config.js +29 -0
- package/ui/index.html +15 -0
- package/ui/package-lock.json +4229 -0
- package/ui/package.json +38 -0
- package/ui/src/App.jsx +364 -0
- package/ui/src/components/ActivityHeatmap.jsx +206 -0
- package/ui/src/components/AiAuditCard.jsx +254 -0
- package/ui/src/components/AnimatedLoader.jsx +14 -0
- package/ui/src/components/AnimatedLogo.jsx +96 -0
- package/ui/src/components/ChatSidebar.jsx +226 -0
- package/ui/src/components/DateRangePicker.jsx +104 -0
- package/ui/src/components/EditorDot.jsx +11 -0
- package/ui/src/components/EditorIcon.jsx +117 -0
- package/ui/src/components/KpiCard.jsx +9 -0
- package/ui/src/components/LiveFeed.jsx +128 -0
- package/ui/src/components/LoginScreen.jsx +81 -0
- package/ui/src/components/MessageRenderer.jsx +148 -0
- package/ui/src/components/PageHeader.jsx +11 -0
- package/ui/src/components/SectionTitle.jsx +3 -0
- package/ui/src/components/ShareModal.jsx +305 -0
- package/ui/src/components/TokenTimeline.jsx +258 -0
- package/ui/src/hooks/useLive.jsx +15 -0
- package/ui/src/index.css +83 -0
- package/ui/src/lib/api.js +343 -0
- package/ui/src/lib/constants.js +88 -0
- package/ui/src/lib/theme.jsx +23 -0
- package/ui/src/main.jsx +16 -0
- package/ui/src/pages/Artifacts.jsx +575 -0
- package/ui/src/pages/Compare.jsx +232 -0
- package/ui/src/pages/CostAnalysis.jsx +354 -0
- package/ui/src/pages/Dashboard.jsx +529 -0
- package/ui/src/pages/DeepAnalysis.jsx +495 -0
- package/ui/src/pages/GSD.jsx +726 -0
- package/ui/src/pages/MCPs.jsx +744 -0
- package/ui/src/pages/ProjectDetail.jsx +325 -0
- package/ui/src/pages/Projects.jsx +249 -0
- package/ui/src/pages/RelayDashboard.jsx +590 -0
- package/ui/src/pages/RelayUserDetail.jsx +466 -0
- package/ui/src/pages/Sessions.jsx +535 -0
- package/ui/src/pages/Settings.jsx +241 -0
- package/ui/src/pages/SqlViewer.jsx +344 -0
- package/ui/src/pages/Subscriptions.jsx +411 -0
- package/ui/vite.config.js +18 -0
package/editors/kiro.js
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const { getAppDataPath } = require('./base');
|
|
5
|
+
|
|
6
|
+
// ============================================================
|
|
7
|
+
// Kiro editor adapter
|
|
8
|
+
// ============================================================
|
|
9
|
+
|
|
10
|
+
const name = 'kiro';
|
|
11
|
+
|
|
12
|
+
const KIRO_AGENT_DIR = path.join(
|
|
13
|
+
getAppDataPath('Kiro'), 'User', 'globalStorage', 'kiro.kiroagent'
|
|
14
|
+
);
|
|
15
|
+
const WORKSPACE_SESSIONS_DIR = path.join(KIRO_AGENT_DIR, 'workspace-sessions');
|
|
16
|
+
|
|
17
|
+
function getChats() {
|
|
18
|
+
const chats = [];
|
|
19
|
+
if (!fs.existsSync(KIRO_AGENT_DIR)) return chats;
|
|
20
|
+
|
|
21
|
+
// Strategy 1: workspace-sessions (structured, has workspace info)
|
|
22
|
+
if (fs.existsSync(WORKSPACE_SESSIONS_DIR)) {
|
|
23
|
+
try {
|
|
24
|
+
for (const folder of fs.readdirSync(WORKSPACE_SESSIONS_DIR)) {
|
|
25
|
+
const wsDir = path.join(WORKSPACE_SESSIONS_DIR, folder);
|
|
26
|
+
if (!fs.statSync(wsDir).isDirectory()) continue;
|
|
27
|
+
|
|
28
|
+
// Decode base64 folder name to get workspace path
|
|
29
|
+
let workspacePath = null;
|
|
30
|
+
try {
|
|
31
|
+
workspacePath = Buffer.from(folder, 'base64').toString('utf-8');
|
|
32
|
+
} catch {}
|
|
33
|
+
|
|
34
|
+
const indexPath = path.join(wsDir, 'sessions.json');
|
|
35
|
+
let sessions = [];
|
|
36
|
+
try {
|
|
37
|
+
sessions = JSON.parse(fs.readFileSync(indexPath, 'utf-8'));
|
|
38
|
+
} catch { continue; }
|
|
39
|
+
|
|
40
|
+
for (const session of sessions) {
|
|
41
|
+
const sessionFile = path.join(wsDir, `${session.sessionId}.json`);
|
|
42
|
+
const exists = fs.existsSync(sessionFile);
|
|
43
|
+
|
|
44
|
+
chats.push({
|
|
45
|
+
source: 'kiro',
|
|
46
|
+
composerId: session.sessionId,
|
|
47
|
+
name: cleanTitle(session.title),
|
|
48
|
+
createdAt: parseInt(session.dateCreated) || null,
|
|
49
|
+
lastUpdatedAt: exists ? getFileMtime(sessionFile) : parseInt(session.dateCreated) || null,
|
|
50
|
+
mode: 'kiro',
|
|
51
|
+
folder: session.workspaceDirectory || workspacePath || null,
|
|
52
|
+
encrypted: false,
|
|
53
|
+
bubbleCount: 0,
|
|
54
|
+
_fullPath: exists ? sessionFile : null,
|
|
55
|
+
_type: 'workspace-session',
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
} catch {}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Strategy 2: .chat files in hash directories (individual agent executions)
|
|
63
|
+
// Kiro saves a snapshot of the conversation after each API call, so multiple
|
|
64
|
+
// .chat files can share the same executionId. We group by executionId and
|
|
65
|
+
// keep only the latest snapshot (highest message count) per conversation.
|
|
66
|
+
const seenIds = new Set(chats.map(c => c.composerId));
|
|
67
|
+
const executionMap = new Map(); // executionId -> best candidate
|
|
68
|
+
try {
|
|
69
|
+
for (const dir of fs.readdirSync(KIRO_AGENT_DIR)) {
|
|
70
|
+
// Skip known non-workspace directories
|
|
71
|
+
if (['default', 'dev_data', 'index', 'sessions', 'workspace-sessions'].includes(dir)) continue;
|
|
72
|
+
const fullDir = path.join(KIRO_AGENT_DIR, dir);
|
|
73
|
+
if (!fs.statSync(fullDir).isDirectory()) continue;
|
|
74
|
+
|
|
75
|
+
let files;
|
|
76
|
+
try { files = fs.readdirSync(fullDir).filter(f => f.endsWith('.chat')); } catch { continue; }
|
|
77
|
+
|
|
78
|
+
for (const file of files) {
|
|
79
|
+
const fullPath = path.join(fullDir, file);
|
|
80
|
+
try {
|
|
81
|
+
const stat = fs.statSync(fullPath);
|
|
82
|
+
const meta = peekChatMeta(fullPath);
|
|
83
|
+
const chatId = meta.executionId || `${dir}/${file.replace('.chat', '')}`;
|
|
84
|
+
if (seenIds.has(chatId)) continue;
|
|
85
|
+
|
|
86
|
+
const candidate = {
|
|
87
|
+
source: 'kiro',
|
|
88
|
+
composerId: chatId,
|
|
89
|
+
name: meta.title || null,
|
|
90
|
+
createdAt: meta.startTime || stat.birthtime.getTime(),
|
|
91
|
+
lastUpdatedAt: meta.endTime || stat.mtime.getTime(),
|
|
92
|
+
mode: meta.workflow || 'kiro',
|
|
93
|
+
folder: meta.folder || null,
|
|
94
|
+
encrypted: false,
|
|
95
|
+
bubbleCount: meta.messageCount || 0,
|
|
96
|
+
_fullPath: fullPath,
|
|
97
|
+
_type: 'chat-file',
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// Keep the snapshot with the most messages per executionId
|
|
101
|
+
if (meta.executionId) {
|
|
102
|
+
const existing = executionMap.get(meta.executionId);
|
|
103
|
+
if (!existing || meta.messageCount > existing.bubbleCount) {
|
|
104
|
+
// Update createdAt to the earliest startTime seen
|
|
105
|
+
if (existing && existing.createdAt < candidate.createdAt) {
|
|
106
|
+
candidate.createdAt = existing.createdAt;
|
|
107
|
+
}
|
|
108
|
+
executionMap.set(meta.executionId, candidate);
|
|
109
|
+
} else if (existing && meta.startTime && meta.startTime < existing.createdAt) {
|
|
110
|
+
existing.createdAt = meta.startTime;
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
chats.push(candidate);
|
|
114
|
+
}
|
|
115
|
+
} catch {}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
} catch {}
|
|
119
|
+
|
|
120
|
+
// Add the deduplicated execution sessions
|
|
121
|
+
for (const chat of executionMap.values()) {
|
|
122
|
+
chats.push(chat);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return chats;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function peekChatMeta(filePath) {
|
|
129
|
+
const meta = { title: null, folder: null, startTime: null, endTime: null, workflow: null, messageCount: 0, executionId: null };
|
|
130
|
+
try {
|
|
131
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
132
|
+
const data = JSON.parse(raw);
|
|
133
|
+
|
|
134
|
+
meta.executionId = data.executionId || null;
|
|
135
|
+
|
|
136
|
+
if (data.metadata) {
|
|
137
|
+
meta.startTime = data.metadata.startTime || null;
|
|
138
|
+
meta.endTime = data.metadata.endTime || null;
|
|
139
|
+
meta.workflow = data.metadata.workflow || null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const chat = data.chat || [];
|
|
143
|
+
for (const msg of chat) {
|
|
144
|
+
if (msg.role === 'human') {
|
|
145
|
+
// Try to extract user request from rules block
|
|
146
|
+
const userReq = extractUserRequest(msg.content);
|
|
147
|
+
if (userReq && !meta.title) {
|
|
148
|
+
meta.title = cleanTitle(userReq);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (msg.role === 'bot' || msg.role === 'human') meta.messageCount++;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Try to extract folder from context
|
|
155
|
+
for (const ctx of data.context || []) {
|
|
156
|
+
if (ctx.type === 'steering' && ctx.id) {
|
|
157
|
+
// Extract workspace from steering file path
|
|
158
|
+
const match = ctx.id.match(/file:\/\/(.*?)\/.kiro\//);
|
|
159
|
+
if (match) meta.folder = match[1];
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
} catch {}
|
|
163
|
+
return meta;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function isSystemPrompt(content) {
|
|
167
|
+
if (typeof content !== 'string') return false;
|
|
168
|
+
return content.startsWith('<identity>') || content.startsWith('# ');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function extractUserRequest(content) {
|
|
172
|
+
if (typeof content !== 'string') return null;
|
|
173
|
+
// "## Included Rules" messages contain the actual user request after </user-rule>
|
|
174
|
+
const ruleEnd = content.lastIndexOf('</user-rule>');
|
|
175
|
+
if (ruleEnd >= 0) {
|
|
176
|
+
let userPart = content.substring(ruleEnd + '</user-rule>'.length).trim();
|
|
177
|
+
// Strip trailing EnvironmentContext block
|
|
178
|
+
const envIdx = userPart.indexOf('<EnvironmentContext>');
|
|
179
|
+
if (envIdx >= 0) userPart = userPart.substring(0, envIdx).trim();
|
|
180
|
+
// Strip steering-reminder blocks
|
|
181
|
+
const steerIdx = userPart.indexOf('<steering-reminder>');
|
|
182
|
+
if (steerIdx >= 0) userPart = userPart.substring(0, steerIdx).trim();
|
|
183
|
+
if (userPart) return userPart;
|
|
184
|
+
}
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function getMessages(chat) {
|
|
189
|
+
if (!chat._fullPath || !fs.existsSync(chat._fullPath)) return [];
|
|
190
|
+
|
|
191
|
+
if (chat._type === 'workspace-session') {
|
|
192
|
+
return getWorkspaceSessionMessages(chat._fullPath);
|
|
193
|
+
}
|
|
194
|
+
return getChatFileMessages(chat._fullPath);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function getWorkspaceSessionMessages(filePath) {
|
|
198
|
+
const messages = [];
|
|
199
|
+
try {
|
|
200
|
+
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
201
|
+
const history = data.history || [];
|
|
202
|
+
|
|
203
|
+
for (const entry of history) {
|
|
204
|
+
const msg = entry.message;
|
|
205
|
+
if (!msg) continue;
|
|
206
|
+
|
|
207
|
+
const role = msg.role === 'user' ? 'user' : msg.role === 'assistant' ? 'assistant' : null;
|
|
208
|
+
if (!role) continue;
|
|
209
|
+
|
|
210
|
+
const content = extractContentFromMessage(msg.content);
|
|
211
|
+
if (!content) continue;
|
|
212
|
+
|
|
213
|
+
const result = { role, content };
|
|
214
|
+
|
|
215
|
+
// Extract model info from promptLogs
|
|
216
|
+
if (role === 'assistant' && entry.promptLogs && entry.promptLogs.length > 0) {
|
|
217
|
+
const log = entry.promptLogs[0];
|
|
218
|
+
result._model = log.modelTitle || null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
messages.push(result);
|
|
222
|
+
}
|
|
223
|
+
} catch {}
|
|
224
|
+
return messages;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function getChatFileMessages(filePath) {
|
|
228
|
+
const messages = [];
|
|
229
|
+
try {
|
|
230
|
+
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
231
|
+
const chat = data.chat || [];
|
|
232
|
+
const model = data.metadata?.modelId || null;
|
|
233
|
+
|
|
234
|
+
for (const msg of chat) {
|
|
235
|
+
if (msg.role === 'human') {
|
|
236
|
+
if (isSystemPrompt(msg.content)) continue;
|
|
237
|
+
// Try extracting user request from rules block first
|
|
238
|
+
const userReq = extractUserRequest(msg.content);
|
|
239
|
+
const content = userReq || extractUserText(msg.content);
|
|
240
|
+
if (content) messages.push({ role: 'user', content });
|
|
241
|
+
} else if (msg.role === 'bot') {
|
|
242
|
+
const content = typeof msg.content === 'string' ? msg.content : '';
|
|
243
|
+
if (content) messages.push({ role: 'assistant', content, _model: model });
|
|
244
|
+
} else if (msg.role === 'tool') {
|
|
245
|
+
const content = typeof msg.content === 'string' ? msg.content : '';
|
|
246
|
+
if (content) messages.push({ role: 'tool', content: content.substring(0, 2000) });
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
} catch {}
|
|
250
|
+
return messages;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function extractContentFromMessage(content) {
|
|
254
|
+
if (typeof content === 'string') return content;
|
|
255
|
+
if (!Array.isArray(content)) return '';
|
|
256
|
+
return content
|
|
257
|
+
.filter(c => c.type === 'text' || c.type === 'mention')
|
|
258
|
+
.map(c => c.text)
|
|
259
|
+
.join('\n') || '';
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function extractUserText(content) {
|
|
263
|
+
if (typeof content === 'string') {
|
|
264
|
+
// Skip system prompt content
|
|
265
|
+
if (isSystemPrompt(content)) return null;
|
|
266
|
+
// Strip XML tags and rules blocks
|
|
267
|
+
return cleanTitle(content);
|
|
268
|
+
}
|
|
269
|
+
if (Array.isArray(content)) {
|
|
270
|
+
return content
|
|
271
|
+
.filter(c => c.type === 'text' || c.type === 'mention')
|
|
272
|
+
.map(c => c.text)
|
|
273
|
+
.join('\n') || '';
|
|
274
|
+
}
|
|
275
|
+
return '';
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function cleanTitle(title) {
|
|
279
|
+
if (!title) return null;
|
|
280
|
+
let clean = title
|
|
281
|
+
.replace(/<[^>]+>[\s\S]*?<\/[^>]+>/g, '')
|
|
282
|
+
.replace(/<[^>]+>/g, '')
|
|
283
|
+
.replace(/## Included Rules[\s\S]*$/m, '')
|
|
284
|
+
.replace(/\s+/g, ' ')
|
|
285
|
+
.trim()
|
|
286
|
+
.substring(0, 120);
|
|
287
|
+
return clean || null;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function getFileMtime(filePath) {
|
|
291
|
+
try { return fs.statSync(filePath).mtime.getTime(); } catch { return null; }
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const labels = { 'kiro': 'Kiro' };
|
|
295
|
+
|
|
296
|
+
function getArtifacts(folder) {
|
|
297
|
+
const { scanArtifacts } = require('./base');
|
|
298
|
+
return scanArtifacts(folder, {
|
|
299
|
+
editor: 'kiro',
|
|
300
|
+
label: 'Kiro',
|
|
301
|
+
files: ['AGENTS.md'],
|
|
302
|
+
dirs: ['.kiro/specs', '.kiro/steering'],
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function getMCPServers() {
|
|
307
|
+
const { parseMcpConfigFile } = require('./base');
|
|
308
|
+
// Global: ~/.kiro/settings/mcp.json
|
|
309
|
+
const globalConfig = path.join(os.homedir(), '.kiro', 'settings', 'mcp.json');
|
|
310
|
+
return [
|
|
311
|
+
...parseMcpConfigFile(globalConfig, { editor: 'kiro', label: 'Kiro', scope: 'global' }),
|
|
312
|
+
];
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
module.exports = { name, labels, getChats, getMessages, getArtifacts, getMCPServers };
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const Database = require('better-sqlite3');
|
|
5
|
+
|
|
6
|
+
// OpenCode stores data in XDG-style paths across all platforms
|
|
7
|
+
function getOpenCodeStoragePath() {
|
|
8
|
+
return path.join(os.homedir(), '.local', 'share', 'opencode', 'storage');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const STORAGE_DIR = getOpenCodeStoragePath();
|
|
12
|
+
const SESSION_DIR = path.join(STORAGE_DIR, 'session');
|
|
13
|
+
const MESSAGE_DIR = path.join(STORAGE_DIR, 'message');
|
|
14
|
+
const PART_DIR = path.join(STORAGE_DIR, 'part');
|
|
15
|
+
|
|
16
|
+
// OpenCode also stores data in a SQLite database
|
|
17
|
+
function getOpenCodeDbPath() {
|
|
18
|
+
return path.join(os.homedir(), '.local', 'share', 'opencode', 'opencode.db');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const DB_PATH = getOpenCodeDbPath();
|
|
22
|
+
|
|
23
|
+
// ============================================================
|
|
24
|
+
// Query SQLite using better-sqlite3
|
|
25
|
+
// ============================================================
|
|
26
|
+
|
|
27
|
+
function queryDb(sql) {
|
|
28
|
+
if (!fs.existsSync(DB_PATH)) return [];
|
|
29
|
+
try {
|
|
30
|
+
const db = new Database(DB_PATH, { readonly: true });
|
|
31
|
+
const rows = db.prepare(sql).all();
|
|
32
|
+
db.close();
|
|
33
|
+
return rows;
|
|
34
|
+
} catch {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function extractModelInfo(data) {
|
|
40
|
+
let modelValue = null;
|
|
41
|
+
let providerValue = null;
|
|
42
|
+
|
|
43
|
+
if (typeof data?.modelID === 'string') {
|
|
44
|
+
modelValue = data.modelID;
|
|
45
|
+
providerValue = typeof data.providerID === 'string' ? data.providerID : null;
|
|
46
|
+
} else if (data?.model && typeof data.model === 'object') {
|
|
47
|
+
modelValue = typeof data.model.modelID === 'string' ? data.model.modelID : null;
|
|
48
|
+
providerValue = typeof data.providerID === 'string'
|
|
49
|
+
? data.providerID
|
|
50
|
+
: (typeof data.model.providerID === 'string' ? data.model.providerID : null);
|
|
51
|
+
} else if (typeof data?.model === 'string') {
|
|
52
|
+
modelValue = data.model;
|
|
53
|
+
providerValue = typeof data.providerID === 'string' ? data.providerID : null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return { modelValue, providerValue };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function extractTokenInfo(data) {
|
|
60
|
+
const tokens = data?.tokens && typeof data.tokens === 'object' ? data.tokens : null;
|
|
61
|
+
const cache = tokens?.cache && typeof tokens.cache === 'object' ? tokens.cache : null;
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
inputTokens: tokens?.input,
|
|
65
|
+
outputTokens: tokens?.output,
|
|
66
|
+
cacheRead: cache?.read,
|
|
67
|
+
cacheWrite: cache?.write,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function getSqliteSessions() {
|
|
72
|
+
return queryDb(
|
|
73
|
+
`SELECT s.id, s.title, s.directory, s.time_created, s.time_updated,
|
|
74
|
+
p.worktree, p.name as project_name,
|
|
75
|
+
(SELECT count(*) FROM message m WHERE m.session_id = s.id) as msg_count
|
|
76
|
+
FROM session s LEFT JOIN project p ON s.project_id = p.id
|
|
77
|
+
ORDER BY s.time_updated DESC`
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function getSqliteMessages(sessionId) {
|
|
82
|
+
if (!fs.existsSync(DB_PATH)) return [];
|
|
83
|
+
try {
|
|
84
|
+
const db = new Database(DB_PATH, { readonly: true });
|
|
85
|
+
const messages = db.prepare(
|
|
86
|
+
`SELECT m.id as msg_id, m.data as msg_data, m.time_created
|
|
87
|
+
FROM message m WHERE m.session_id = ? ORDER BY m.time_created ASC`
|
|
88
|
+
).all(sessionId);
|
|
89
|
+
|
|
90
|
+
const result = [];
|
|
91
|
+
for (const msg of messages) {
|
|
92
|
+
let msgData;
|
|
93
|
+
try { msgData = JSON.parse(msg.msg_data); } catch { continue; }
|
|
94
|
+
|
|
95
|
+
const role = msgData.role;
|
|
96
|
+
if (!role) continue;
|
|
97
|
+
|
|
98
|
+
const parts = db.prepare(
|
|
99
|
+
`SELECT data FROM part WHERE message_id = ? ORDER BY time_created ASC`
|
|
100
|
+
).all(msg.msg_id);
|
|
101
|
+
|
|
102
|
+
const contentParts = [];
|
|
103
|
+
for (const part of parts) {
|
|
104
|
+
let partData;
|
|
105
|
+
try { partData = JSON.parse(part.data); } catch { continue; }
|
|
106
|
+
const type = partData.type;
|
|
107
|
+
|
|
108
|
+
if (type === 'text' && partData.text) {
|
|
109
|
+
contentParts.push(partData.text);
|
|
110
|
+
} else if (type === 'thinking' || type === 'reasoning') {
|
|
111
|
+
if (partData.text) contentParts.push(`[thinking] ${partData.text}`);
|
|
112
|
+
} else if (type === 'tool-call' || type === 'tool_use' || type === 'tool-use' || type === 'tool') {
|
|
113
|
+
const toolName = partData.name || partData.toolName || partData.tool || 'tool';
|
|
114
|
+
let argKeys = '';
|
|
115
|
+
try {
|
|
116
|
+
const input = typeof partData.input === 'string' ? JSON.parse(partData.input) : (partData.input || partData.args || partData.arguments || partData.state?.input || {});
|
|
117
|
+
argKeys = typeof input === 'object' ? Object.keys(input).join(', ') : '';
|
|
118
|
+
} catch {}
|
|
119
|
+
contentParts.push(`[tool-call: ${toolName}(${argKeys})]`);
|
|
120
|
+
} else if (type === 'tool-result' || type === 'tool_result') {
|
|
121
|
+
const preview = (partData.text || partData.output || partData.state?.output || '').substring(0, 500);
|
|
122
|
+
contentParts.push(`[tool-result] ${preview}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const content = contentParts.join('\n');
|
|
127
|
+
if (!content) continue;
|
|
128
|
+
|
|
129
|
+
const { modelValue, providerValue } = extractModelInfo(msgData);
|
|
130
|
+
const { inputTokens, outputTokens, cacheRead, cacheWrite } = extractTokenInfo(msgData);
|
|
131
|
+
|
|
132
|
+
result.push({
|
|
133
|
+
role: role === 'user' ? 'user' : role === 'assistant' ? 'assistant' : role,
|
|
134
|
+
content,
|
|
135
|
+
_model: modelValue,
|
|
136
|
+
_provider: providerValue,
|
|
137
|
+
_inputTokens: inputTokens,
|
|
138
|
+
_outputTokens: outputTokens,
|
|
139
|
+
_cacheRead: cacheRead,
|
|
140
|
+
_cacheWrite: cacheWrite,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
db.close();
|
|
145
|
+
return result;
|
|
146
|
+
} catch {
|
|
147
|
+
return [];
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ============================================================
|
|
152
|
+
// Scan JSON files from OpenCode storage
|
|
153
|
+
// ============================================================
|
|
154
|
+
|
|
155
|
+
function readJson(filePath) {
|
|
156
|
+
try {
|
|
157
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
158
|
+
} catch { return null; }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function getAllSessions() {
|
|
162
|
+
const sessions = [];
|
|
163
|
+
if (!fs.existsSync(SESSION_DIR)) return sessions;
|
|
164
|
+
|
|
165
|
+
for (const projectHash of fs.readdirSync(SESSION_DIR)) {
|
|
166
|
+
const projectDir = path.join(SESSION_DIR, projectHash);
|
|
167
|
+
if (!fs.statSync(projectDir).isDirectory()) continue;
|
|
168
|
+
|
|
169
|
+
let files;
|
|
170
|
+
try { files = fs.readdirSync(projectDir).filter(f => f.startsWith('ses_') && f.endsWith('.json')); } catch { continue; }
|
|
171
|
+
|
|
172
|
+
for (const file of files) {
|
|
173
|
+
const filePath = path.join(projectDir, file);
|
|
174
|
+
const data = readJson(filePath);
|
|
175
|
+
if (data && data.id) {
|
|
176
|
+
sessions.push({ ...data, _filePath: filePath });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return sessions;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function getMessageCount(sessionId) {
|
|
184
|
+
const sessionMsgDir = path.join(MESSAGE_DIR, sessionId);
|
|
185
|
+
if (!fs.existsSync(sessionMsgDir)) return 0;
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
return fs.readdirSync(sessionMsgDir).filter(f => f.startsWith('msg_') && f.endsWith('.json')).length;
|
|
189
|
+
} catch { return 0; }
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function getMessagesForSession(sessionId) {
|
|
193
|
+
const sessionMsgDir = path.join(MESSAGE_DIR, sessionId);
|
|
194
|
+
if (!fs.existsSync(sessionMsgDir)) return [];
|
|
195
|
+
|
|
196
|
+
let files;
|
|
197
|
+
try { files = fs.readdirSync(sessionMsgDir).filter(f => f.startsWith('msg_') && f.endsWith('.json')); } catch { return []; }
|
|
198
|
+
|
|
199
|
+
const rawMsgs = [];
|
|
200
|
+
for (const file of files) {
|
|
201
|
+
const msgPath = path.join(sessionMsgDir, file);
|
|
202
|
+
const msg = readJson(msgPath);
|
|
203
|
+
if (!msg || !msg.id) continue;
|
|
204
|
+
rawMsgs.push(msg);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Sort by creation time before building output
|
|
208
|
+
rawMsgs.sort((a, b) => (a.time?.created || 0) - (b.time?.created || 0));
|
|
209
|
+
|
|
210
|
+
const messages = [];
|
|
211
|
+
for (const msg of rawMsgs) {
|
|
212
|
+
// Get parts for this message
|
|
213
|
+
const msgPartDir = path.join(PART_DIR, msg.id);
|
|
214
|
+
const parts = [];
|
|
215
|
+
if (fs.existsSync(msgPartDir)) {
|
|
216
|
+
try {
|
|
217
|
+
const partFiles = fs.readdirSync(msgPartDir).filter(f => f.startsWith('prt_') && f.endsWith('.json'));
|
|
218
|
+
for (const partFile of partFiles) {
|
|
219
|
+
const part = readJson(path.join(msgPartDir, partFile));
|
|
220
|
+
if (part) parts.push(part);
|
|
221
|
+
}
|
|
222
|
+
} catch { /* skip */ }
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Build content from parts
|
|
226
|
+
const contentParts = [];
|
|
227
|
+
for (const part of parts) {
|
|
228
|
+
const type = part.type;
|
|
229
|
+
|
|
230
|
+
if (type === 'text' && part.text) {
|
|
231
|
+
contentParts.push(part.text);
|
|
232
|
+
} else if (type === 'thinking' || type === 'reasoning') {
|
|
233
|
+
if (part.text) contentParts.push(`[thinking] ${part.text}`);
|
|
234
|
+
} else if (type === 'tool-call' || type === 'tool_use' || type === 'tool') {
|
|
235
|
+
const toolName = part.name || part.toolName || part.tool || 'tool';
|
|
236
|
+
const args = part.args || part.arguments || part.state?.input || {};
|
|
237
|
+
const argKeys = typeof args === 'object' ? Object.keys(args).join(', ') : '';
|
|
238
|
+
contentParts.push(`[tool-call: ${toolName}(${argKeys})]`);
|
|
239
|
+
} else if (type === 'tool-result' || type === 'tool_result') {
|
|
240
|
+
const preview = (part.text || part.output || part.state?.output || '').substring(0, 500);
|
|
241
|
+
contentParts.push(`[tool-result] ${preview}`);
|
|
242
|
+
} else if (type === 'step-start' || type === 'step-finish') {
|
|
243
|
+
// Skip metadata parts
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// If no parts with content, check if message itself has content
|
|
248
|
+
if (contentParts.length === 0 && msg.role) {
|
|
249
|
+
contentParts.push(`[${msg.role}]`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const content = contentParts.join('\n');
|
|
253
|
+
if (content) {
|
|
254
|
+
const { modelValue, providerValue } = extractModelInfo(msg);
|
|
255
|
+
const { inputTokens, outputTokens, cacheRead, cacheWrite } = extractTokenInfo(msg);
|
|
256
|
+
|
|
257
|
+
messages.push({
|
|
258
|
+
role: msg.role || 'assistant',
|
|
259
|
+
content,
|
|
260
|
+
_model: modelValue,
|
|
261
|
+
_provider: providerValue,
|
|
262
|
+
_inputTokens: inputTokens,
|
|
263
|
+
_outputTokens: outputTokens,
|
|
264
|
+
_cacheRead: cacheRead,
|
|
265
|
+
_cacheWrite: cacheWrite,
|
|
266
|
+
_finish: msg.finish,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return messages;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ============================================================
|
|
275
|
+
// Adapter interface
|
|
276
|
+
// ============================================================
|
|
277
|
+
|
|
278
|
+
const name = 'opencode';
|
|
279
|
+
|
|
280
|
+
function getChats() {
|
|
281
|
+
const seen = new Set();
|
|
282
|
+
const chats = [];
|
|
283
|
+
|
|
284
|
+
// 1. JSON file-based sessions (newer storage format)
|
|
285
|
+
const fileSessions = getAllSessions();
|
|
286
|
+
for (const s of fileSessions) {
|
|
287
|
+
seen.add(s.id);
|
|
288
|
+
chats.push({
|
|
289
|
+
source: 'opencode',
|
|
290
|
+
composerId: s.id,
|
|
291
|
+
name: s.title || null,
|
|
292
|
+
createdAt: s.time?.created || null,
|
|
293
|
+
lastUpdatedAt: s.time?.updated || null,
|
|
294
|
+
mode: s.mode || 'opencode',
|
|
295
|
+
folder: s.directory || null,
|
|
296
|
+
encrypted: false,
|
|
297
|
+
bubbleCount: getMessageCount(s.id),
|
|
298
|
+
_agent: s.agent,
|
|
299
|
+
_model: s.modelID,
|
|
300
|
+
_provider: s.providerID,
|
|
301
|
+
_sessionData: s,
|
|
302
|
+
_storageType: 'file',
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// 2. SQLite sessions (older/primary store) — add any not already found in files
|
|
307
|
+
const dbSessions = getSqliteSessions();
|
|
308
|
+
for (const row of dbSessions) {
|
|
309
|
+
if (seen.has(row.id)) continue;
|
|
310
|
+
seen.add(row.id);
|
|
311
|
+
chats.push({
|
|
312
|
+
source: 'opencode',
|
|
313
|
+
composerId: row.id,
|
|
314
|
+
name: cleanTitle(row.title),
|
|
315
|
+
createdAt: row.time_created || null,
|
|
316
|
+
lastUpdatedAt: row.time_updated || null,
|
|
317
|
+
mode: 'opencode',
|
|
318
|
+
folder: row.worktree || row.directory || null,
|
|
319
|
+
encrypted: false,
|
|
320
|
+
bubbleCount: row.msg_count || 0,
|
|
321
|
+
_storageType: 'sqlite',
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return chats.sort((a, b) => (b.lastUpdatedAt || 0) - (a.lastUpdatedAt || 0));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function cleanTitle(title) {
|
|
329
|
+
if (!title) return null;
|
|
330
|
+
if (title.startsWith('New session - ')) return null;
|
|
331
|
+
return title.substring(0, 120) || null;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function getMessages(chat) {
|
|
335
|
+
// Prefer file-based messages; fall back to SQLite
|
|
336
|
+
const fileMessages = getMessagesForSession(chat.composerId);
|
|
337
|
+
if (fileMessages.length > 0) return fileMessages;
|
|
338
|
+
return getSqliteMessages(chat.composerId);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const labels = { 'opencode': 'OpenCode' };
|
|
342
|
+
|
|
343
|
+
function getMCPServers() {
|
|
344
|
+
const { parseMcpConfigFile } = require('./base');
|
|
345
|
+
// OpenCode: ~/.config/opencode/opencode.json (mcp key maps to mcpServers format)
|
|
346
|
+
const globalConfig = path.join(os.homedir(), '.config', 'opencode', 'opencode.json');
|
|
347
|
+
const results = [];
|
|
348
|
+
if (fs.existsSync(globalConfig)) {
|
|
349
|
+
try {
|
|
350
|
+
const data = JSON.parse(fs.readFileSync(globalConfig, 'utf-8'));
|
|
351
|
+
const servers = data.mcp || {};
|
|
352
|
+
for (const [name, cfg] of Object.entries(servers)) {
|
|
353
|
+
if (typeof cfg !== 'object') continue;
|
|
354
|
+
results.push({
|
|
355
|
+
name,
|
|
356
|
+
editor: 'opencode',
|
|
357
|
+
editorLabel: 'OpenCode',
|
|
358
|
+
scope: 'global',
|
|
359
|
+
configPath: globalConfig,
|
|
360
|
+
command: cfg.command || null,
|
|
361
|
+
args: cfg.args || [],
|
|
362
|
+
env: cfg.env ? Object.keys(cfg.env) : [],
|
|
363
|
+
url: cfg.url || null,
|
|
364
|
+
transport: cfg.type || (cfg.url ? 'http' : 'stdio'),
|
|
365
|
+
disabled: false,
|
|
366
|
+
disabledTools: [],
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
} catch {}
|
|
370
|
+
}
|
|
371
|
+
return results;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
module.exports = { name, labels, getChats, getMessages, getMCPServers };
|