memhouse 0.0.1 → 0.3.1

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.
Files changed (97) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +72 -3
  3. package/bin/memhouse.js +559 -0
  4. package/cache.js +850 -0
  5. package/ch-ingest.js +329 -0
  6. package/ch-schema.js +152 -0
  7. package/clickhouse.js +87 -0
  8. package/editors/adapter-errors.js +52 -0
  9. package/editors/antigravity.js +1048 -0
  10. package/editors/base.js +321 -0
  11. package/editors/claude.js +386 -0
  12. package/editors/codebuff.js +338 -0
  13. package/editors/codex.js +537 -0
  14. package/editors/commandcode.js +166 -0
  15. package/editors/copilot-jetbrains.js +207 -0
  16. package/editors/copilot.js +260 -0
  17. package/editors/cursor-agent.js +202 -0
  18. package/editors/cursor.js +498 -0
  19. package/editors/gemini.js +231 -0
  20. package/editors/goose.js +386 -0
  21. package/editors/gsd.js +366 -0
  22. package/editors/index.js +274 -0
  23. package/editors/kiro.js +315 -0
  24. package/editors/opencode.js +432 -0
  25. package/editors/vscode.js +413 -0
  26. package/editors/windsurf.js +634 -0
  27. package/editors/zed.js +229 -0
  28. package/index.js +424 -2
  29. package/mcp-server.js +294 -0
  30. package/mem-house/DESIGN.md +119 -0
  31. package/mem-house/delivery/AGENT-INSTALL.md +87 -0
  32. package/mem-house/delivery/PROMPT.md +42 -0
  33. package/mem-house/delivery/install.sh +181 -0
  34. package/mem-house/delivery/kernel-install.md +83 -0
  35. package/mem-house/delivery/plugin/.claude-plugin/plugin.json +8 -0
  36. package/mem-house/delivery/skills/memhouse-search/SKILL.md +104 -0
  37. package/mem-house/delivery/skills/memhouse-sessions/SKILL.md +94 -0
  38. package/mem-house/delivery/skills/memhouse-sql/SKILL.md +115 -0
  39. package/mem-house/rls.sql +17 -0
  40. package/mem-house/schema.sql +145 -0
  41. package/mem-house/server/queries.js +878 -0
  42. package/mem-house/server/server.js +228 -0
  43. package/mem-house/shipper/ship.js +461 -0
  44. package/misc/syntax-check.js +38 -0
  45. package/package.json +59 -7
  46. package/pricing.js +88 -0
  47. package/pricing.json +992 -0
  48. package/public/assets/index-CCPh7a2Z.js +75 -0
  49. package/public/assets/index-DV6ONi_F.css +2 -0
  50. package/public/index.html +16 -0
  51. package/relay-client.js +306 -0
  52. package/relay-server.js +577 -0
  53. package/server.js +860 -0
  54. package/share-image.js +367 -0
  55. package/ui/eslint.config.js +29 -0
  56. package/ui/index.html +15 -0
  57. package/ui/package-lock.json +4229 -0
  58. package/ui/package.json +38 -0
  59. package/ui/src/App.jsx +364 -0
  60. package/ui/src/components/ActivityHeatmap.jsx +206 -0
  61. package/ui/src/components/AiAuditCard.jsx +254 -0
  62. package/ui/src/components/AnimatedLoader.jsx +14 -0
  63. package/ui/src/components/AnimatedLogo.jsx +96 -0
  64. package/ui/src/components/ChatSidebar.jsx +226 -0
  65. package/ui/src/components/DateRangePicker.jsx +104 -0
  66. package/ui/src/components/EditorDot.jsx +11 -0
  67. package/ui/src/components/EditorIcon.jsx +117 -0
  68. package/ui/src/components/KpiCard.jsx +9 -0
  69. package/ui/src/components/LiveFeed.jsx +128 -0
  70. package/ui/src/components/LoginScreen.jsx +81 -0
  71. package/ui/src/components/MessageRenderer.jsx +148 -0
  72. package/ui/src/components/PageHeader.jsx +11 -0
  73. package/ui/src/components/SectionTitle.jsx +3 -0
  74. package/ui/src/components/ShareModal.jsx +305 -0
  75. package/ui/src/components/TokenTimeline.jsx +258 -0
  76. package/ui/src/hooks/useLive.jsx +15 -0
  77. package/ui/src/index.css +83 -0
  78. package/ui/src/lib/api.js +343 -0
  79. package/ui/src/lib/constants.js +88 -0
  80. package/ui/src/lib/theme.jsx +23 -0
  81. package/ui/src/main.jsx +16 -0
  82. package/ui/src/pages/Artifacts.jsx +575 -0
  83. package/ui/src/pages/Compare.jsx +232 -0
  84. package/ui/src/pages/CostAnalysis.jsx +354 -0
  85. package/ui/src/pages/Dashboard.jsx +529 -0
  86. package/ui/src/pages/DeepAnalysis.jsx +495 -0
  87. package/ui/src/pages/GSD.jsx +726 -0
  88. package/ui/src/pages/MCPs.jsx +744 -0
  89. package/ui/src/pages/ProjectDetail.jsx +325 -0
  90. package/ui/src/pages/Projects.jsx +249 -0
  91. package/ui/src/pages/RelayDashboard.jsx +590 -0
  92. package/ui/src/pages/RelayUserDetail.jsx +466 -0
  93. package/ui/src/pages/Sessions.jsx +535 -0
  94. package/ui/src/pages/Settings.jsx +241 -0
  95. package/ui/src/pages/SqlViewer.jsx +370 -0
  96. package/ui/src/pages/Subscriptions.jsx +411 -0
  97. package/ui/vite.config.js +18 -0
@@ -0,0 +1,231 @@
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+ const os = require('os');
4
+
5
+ const GEMINI_DIR = path.join(os.homedir(), '.gemini');
6
+ const TMP_DIR = path.join(GEMINI_DIR, 'tmp');
7
+ const PROJECTS_JSON = path.join(GEMINI_DIR, 'projects.json');
8
+
9
+ // ============================================================
10
+ // Adapter interface
11
+ // ============================================================
12
+
13
+ const name = 'gemini-cli';
14
+
15
+ /**
16
+ * Load project path mapping from ~/.gemini/projects.json
17
+ * Format: { "projects": { "/Users/dev/Code/myapp": "myapp" } }
18
+ * Returns Map<projectName, folderPath>
19
+ */
20
+ function loadProjectMap() {
21
+ const map = new Map();
22
+ try {
23
+ const data = JSON.parse(fs.readFileSync(PROJECTS_JSON, 'utf-8'));
24
+ if (data.projects) {
25
+ for (const [folderPath, projName] of Object.entries(data.projects)) {
26
+ map.set(projName, folderPath);
27
+ }
28
+ }
29
+ } catch {}
30
+ return map;
31
+ }
32
+
33
+ /**
34
+ * Load a Gemini session record, handling BOTH on-disk formats:
35
+ * - legacy single-object JSON: { sessionId, startTime, lastUpdated, messages: [...] }
36
+ * - newer .jsonl oplog: a header line, then interleaved message objects and
37
+ * {$set:{messages:[...]}} snapshots. Reconstruct = the latest messages snapshot
38
+ * (they accumulate) plus any message objects appended after it.
39
+ * Returns { sessionId, startTime, lastUpdated, messages }.
40
+ */
41
+ function loadGeminiRecord(filePath) {
42
+ const raw = fs.readFileSync(filePath, 'utf-8');
43
+ if (!filePath.endsWith('.jsonl')) {
44
+ const rec = JSON.parse(raw);
45
+ return { sessionId: rec.sessionId, startTime: rec.startTime, lastUpdated: rec.lastUpdated, messages: rec.messages || [] };
46
+ }
47
+ let header = {}, messages = null, tail = [];
48
+ for (const line of raw.split('\n')) {
49
+ if (!line.trim()) continue;
50
+ let o; try { o = JSON.parse(line); } catch { continue; }
51
+ if (o.$set) {
52
+ if (Array.isArray(o.$set.messages)) { messages = o.$set.messages; tail = []; }
53
+ if (o.$set.lastUpdated) header.lastUpdated = o.$set.lastUpdated;
54
+ } else if (o.type && o.content !== undefined) {
55
+ tail.push(o); // message appended after the last snapshot
56
+ } else if (o.sessionId || o.startTime) {
57
+ header = { ...header, ...o }; // header/meta line
58
+ }
59
+ }
60
+ return {
61
+ sessionId: header.sessionId,
62
+ startTime: header.startTime,
63
+ lastUpdated: header.lastUpdated,
64
+ messages: (messages || []).concat(tail),
65
+ };
66
+ }
67
+
68
+ function getChats() {
69
+ const chats = [];
70
+ if (!fs.existsSync(TMP_DIR)) return chats;
71
+
72
+ const projectMap = loadProjectMap();
73
+
74
+ // Each subdirectory under tmp/ is a project name (e.g. "codename-share")
75
+ let projectDirs;
76
+ try { projectDirs = fs.readdirSync(TMP_DIR); } catch { return chats; }
77
+
78
+ for (const projName of projectDirs) {
79
+ const projDir = path.join(TMP_DIR, projName);
80
+ try { if (!fs.statSync(projDir).isDirectory()) continue; } catch { continue; }
81
+
82
+ // Sessions are in <projDir>/chats/session-*.json
83
+ const chatsDir = path.join(projDir, 'chats');
84
+ if (!fs.existsSync(chatsDir)) continue;
85
+
86
+ let files;
87
+ try {
88
+ // Both formats: legacy session-*.json + newer session-*.jsonl (oplog).
89
+ // The chats/<uuid>/ checkpoint subdirs are skipped (not session- prefixed).
90
+ files = fs.readdirSync(chatsDir).filter(f => f.startsWith('session-') && (f.endsWith('.json') || f.endsWith('.jsonl')));
91
+ } catch { continue; }
92
+
93
+ // Resolve folder from projects.json mapping
94
+ const folder = projectMap.get(projName) || null;
95
+
96
+ for (const file of files) {
97
+ const fullPath = path.join(chatsDir, file);
98
+ try {
99
+ const record = loadGeminiRecord(fullPath);
100
+ if (!record || !record.messages || record.messages.length === 0) continue;
101
+
102
+ const sessionId = record.sessionId || file.replace(/\.jsonl?$/, '');
103
+ const messages = record.messages;
104
+
105
+ // Extract first user prompt for title
106
+ const firstUser = messages.find(m => m.type === 'user');
107
+ const firstPrompt = extractTextContent(firstUser?.content);
108
+
109
+ chats.push({
110
+ source: 'gemini-cli',
111
+ composerId: sessionId,
112
+ name: firstPrompt ? cleanPrompt(firstPrompt) : null,
113
+ createdAt: record.startTime ? new Date(record.startTime).getTime() : null,
114
+ lastUpdatedAt: record.lastUpdated ? new Date(record.lastUpdated).getTime() : null,
115
+ mode: 'gemini',
116
+ folder,
117
+ encrypted: false,
118
+ bubbleCount: messages.length,
119
+ _fullPath: fullPath,
120
+ });
121
+ } catch { /* skip malformed files */ }
122
+ }
123
+ }
124
+
125
+ return chats;
126
+ }
127
+
128
+ function cleanPrompt(text) {
129
+ if (!text) return null;
130
+ return text.replace(/\s+/g, ' ').trim().substring(0, 120) || null;
131
+ }
132
+
133
+ function extractTextContent(content) {
134
+ if (!content) return '';
135
+ if (typeof content === 'string') return content;
136
+ // Array of { text } parts (user messages)
137
+ if (Array.isArray(content)) {
138
+ return content
139
+ .filter(p => p.text)
140
+ .map(p => p.text)
141
+ .join('\n') || '';
142
+ }
143
+ if (content.text) return content.text;
144
+ return '';
145
+ }
146
+
147
+ function getMessages(chat) {
148
+ const filePath = chat._fullPath;
149
+ if (!filePath || !fs.existsSync(filePath)) return [];
150
+
151
+ let record;
152
+ try {
153
+ record = loadGeminiRecord(filePath);
154
+ } catch { return []; }
155
+
156
+ if (!record || !record.messages) return [];
157
+
158
+ const result = [];
159
+ for (const msg of record.messages) {
160
+ const type = msg.type;
161
+ const text = extractTextContent(msg.content || msg.displayContent);
162
+
163
+ if (type === 'user') {
164
+ if (text) result.push({ role: 'user', content: text });
165
+ } else if (type === 'gemini') {
166
+ const parts = [];
167
+ const toolCalls = [];
168
+
169
+ // Thoughts have { subject, description, timestamp }
170
+ if (msg.thoughts && Array.isArray(msg.thoughts)) {
171
+ for (const t of msg.thoughts) {
172
+ const thought = t.description || t.subject || '';
173
+ if (thought) parts.push(`[thinking] ${thought}`);
174
+ }
175
+ }
176
+
177
+ // Main text content (string for gemini messages)
178
+ if (text) parts.push(text);
179
+
180
+ // Tool calls
181
+ if (msg.toolCalls && Array.isArray(msg.toolCalls)) {
182
+ for (const tc of msg.toolCalls) {
183
+ const args = tc.args || {};
184
+ const argKeys = typeof args === 'object' ? Object.keys(args).join(', ') : '';
185
+ parts.push(`[tool-call: ${tc.name || 'unknown'}(${argKeys})]`);
186
+ toolCalls.push({ name: tc.name || 'unknown', args });
187
+ }
188
+ }
189
+
190
+ if (parts.length > 0) {
191
+ const tokens = msg.tokens || {};
192
+ result.push({
193
+ role: 'assistant',
194
+ content: parts.join('\n'),
195
+ _model: msg.model,
196
+ _inputTokens: tokens.input,
197
+ _outputTokens: tokens.output,
198
+ _cacheRead: tokens.cached,
199
+ _toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
200
+ });
201
+ }
202
+ } else if (type === 'info' || type === 'error' || type === 'warning') {
203
+ if (text) result.push({ role: 'system', content: `[${type}] ${text}` });
204
+ }
205
+ }
206
+
207
+ return result;
208
+ }
209
+
210
+ const labels = { 'gemini-cli': 'Gemini CLI' };
211
+
212
+ function getArtifacts(folder) {
213
+ const { scanArtifacts } = require('./base');
214
+ return scanArtifacts(folder, {
215
+ editor: 'gemini-cli',
216
+ label: 'Gemini CLI',
217
+ files: ['GEMINI.md'],
218
+ dirs: [],
219
+ });
220
+ }
221
+
222
+ function getMCPServers() {
223
+ const { parseMcpConfigFile } = require('./base');
224
+ // Global: ~/.gemini/settings.json (mcpServers key)
225
+ const globalSettings = path.join(os.homedir(), '.gemini', 'settings.json');
226
+ return [
227
+ ...parseMcpConfigFile(globalSettings, { editor: 'gemini-cli', label: 'Gemini CLI', scope: 'global' }),
228
+ ];
229
+ }
230
+
231
+ module.exports = { name, labels, getChats, getMessages, getArtifacts, getMCPServers };
@@ -0,0 +1,386 @@
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+ const os = require('os');
4
+
5
+ const adapterErrors = require('./adapter-errors');
6
+
7
+ const GOOSE_DIR = path.join(os.homedir(), '.local', 'share', 'goose', 'sessions');
8
+ const DB_PATH = path.join(GOOSE_DIR, 'sessions.db');
9
+ const CONFIG_PATH = path.join(os.homedir(), '.config', 'goose', 'config.yaml');
10
+
11
+ // ============================================================
12
+ // Query SQLite via better-sqlite3 (cross-platform)
13
+ // ============================================================
14
+
15
+ let Database;
16
+ function getDatabase() {
17
+ if (!Database) {
18
+ try {
19
+ Database = require('better-sqlite3');
20
+ } catch {
21
+ // better-sqlite3 not available
22
+ }
23
+ }
24
+ return Database;
25
+ }
26
+
27
+ function queryDb(sql) {
28
+ if (!fs.existsSync(DB_PATH)) return [];
29
+ const Db = getDatabase();
30
+ if (!Db) {
31
+ adapterErrors.record('goose', new Error('better-sqlite3 unavailable'), DB_PATH);
32
+ return [];
33
+ }
34
+ try {
35
+ const db = new Db(DB_PATH, { readonly: true });
36
+ const rows = db.prepare(sql).all();
37
+ db.close();
38
+ return rows;
39
+ } catch (e) {
40
+ adapterErrors.record('goose', e, DB_PATH);
41
+ return [];
42
+ }
43
+ }
44
+
45
+ // ============================================================
46
+ // Adapter interface
47
+ // ============================================================
48
+
49
+ const name = 'goose';
50
+
51
+ let _configModel = undefined; // lazy-loaded
52
+
53
+ function getConfigModel() {
54
+ if (_configModel !== undefined) return _configModel;
55
+ _configModel = null;
56
+ try {
57
+ const raw = fs.readFileSync(CONFIG_PATH, 'utf-8');
58
+ const modelMatch = raw.match(/^GOOSE_MODEL:\s*(.+)$/m);
59
+ if (modelMatch) _configModel = modelMatch[1].trim();
60
+ } catch {}
61
+ return _configModel;
62
+ }
63
+
64
+ function getChats() {
65
+ const chats = [];
66
+
67
+ const configModel = getConfigModel();
68
+
69
+ // --- SQLite sessions (v1.10.0+) ---
70
+ const dbSessions = queryDb(
71
+ `SELECT id, name, description, working_dir, created_at, updated_at,
72
+ total_tokens, input_tokens, output_tokens, provider_name, model_config_json,
73
+ (SELECT count(*) FROM messages m WHERE m.session_id = s.id) as msg_count
74
+ FROM sessions s ORDER BY updated_at DESC`
75
+ );
76
+
77
+ const dbSessionIds = new Set();
78
+ for (const row of dbSessions) {
79
+ dbSessionIds.add(row.id);
80
+ chats.push({
81
+ source: 'goose',
82
+ composerId: row.id,
83
+ name: cleanTitle(row.name || row.description),
84
+ createdAt: parseTimestamp(row.created_at),
85
+ lastUpdatedAt: parseTimestamp(row.updated_at),
86
+ mode: 'goose',
87
+ folder: row.working_dir || null,
88
+ encrypted: false,
89
+ bubbleCount: row.msg_count || 0,
90
+ _storage: 'db',
91
+ _inputTokens: row.input_tokens,
92
+ _outputTokens: row.output_tokens,
93
+ _model: extractSessionModel(row) || configModel,
94
+ });
95
+ }
96
+
97
+ // --- Legacy JSONL files ---
98
+ if (fs.existsSync(GOOSE_DIR)) {
99
+ let files;
100
+ // The directory exists (checked above); if it will not enumerate, every legacy
101
+ // jsonl session disappears at discovery and nothing downstream can miss what it
102
+ // never saw.
103
+ try { files = fs.readdirSync(GOOSE_DIR).filter(f => f.endsWith('.jsonl')); }
104
+ catch (e) { adapterErrors.record('goose', e, GOOSE_DIR); files = []; }
105
+
106
+ for (const file of files) {
107
+ const sessionId = file.replace('.jsonl', '');
108
+ if (dbSessionIds.has(sessionId)) continue; // already in DB
109
+
110
+ const fullPath = path.join(GOOSE_DIR, file);
111
+ try {
112
+ const stat = fs.statSync(fullPath);
113
+ const meta = peekJsonlMeta(fullPath);
114
+ chats.push({
115
+ source: 'goose',
116
+ composerId: sessionId,
117
+ name: meta.firstPrompt ? cleanTitle(meta.firstPrompt) : null,
118
+ createdAt: meta.timestamp || stat.birthtime.getTime(),
119
+ lastUpdatedAt: stat.mtime.getTime(),
120
+ mode: 'goose',
121
+ folder: meta.workingDir || null,
122
+ encrypted: false,
123
+ _storage: 'jsonl',
124
+ _fullPath: fullPath,
125
+ _model: configModel,
126
+ });
127
+ } catch (e) {
128
+ // Same shape one level down: an unstattable or unparseable session file is
129
+ // omitted from the listing entirely, so it is invisible to doctor and the
130
+ // shipper alike.
131
+ adapterErrors.record('goose', e, fullPath);
132
+ }
133
+ }
134
+ }
135
+
136
+ return chats;
137
+ }
138
+
139
+ function parseTimestamp(ts) {
140
+ if (!ts) return null;
141
+ if (typeof ts === 'number') return ts;
142
+ const d = new Date(ts);
143
+ return isNaN(d.getTime()) ? null : d.getTime();
144
+ }
145
+
146
+ function cleanTitle(title) {
147
+ if (!title) return null;
148
+ return title.substring(0, 120).trim() || null;
149
+ }
150
+
151
+ function peekJsonlMeta(filePath) {
152
+ const meta = { firstPrompt: null, workingDir: null, timestamp: null };
153
+ try {
154
+ const buf = fs.readFileSync(filePath, 'utf-8');
155
+ for (const line of buf.split('\n')) {
156
+ if (!line) continue;
157
+ const obj = JSON.parse(line);
158
+
159
+ if (!meta.timestamp && obj.created) {
160
+ meta.timestamp = parseTimestamp(obj.created);
161
+ }
162
+
163
+ if (!meta.workingDir && obj.working_dir) {
164
+ meta.workingDir = obj.working_dir;
165
+ }
166
+
167
+ // First user text message
168
+ if (!meta.firstPrompt && obj.role === 'user' && obj.content) {
169
+ let parts;
170
+ try { parts = typeof obj.content === 'string' ? JSON.parse(obj.content) : obj.content; } catch { continue; }
171
+ if (Array.isArray(parts)) {
172
+ const text = parts.filter(p => p.type === 'text').map(p => p.text).join(' ');
173
+ if (text) meta.firstPrompt = text.substring(0, 200);
174
+ }
175
+ }
176
+
177
+ if (meta.firstPrompt && meta.workingDir) break;
178
+ }
179
+ } catch {}
180
+ return meta;
181
+ }
182
+
183
+ function getMessages(chat) {
184
+ if (chat._storage === 'db') return getMessagesFromDb(chat);
185
+ if (chat._storage === 'jsonl') return getMessagesFromJsonl(chat);
186
+ // Try DB first, then JSONL
187
+ const dbMessages = getMessagesFromDb(chat);
188
+ if (dbMessages.length) return dbMessages;
189
+ return getMessagesFromJsonl(chat);
190
+ }
191
+
192
+ function getMessagesFromDb(chat) {
193
+ const rows = queryDb(
194
+ `SELECT role, content_json, created_timestamp FROM messages
195
+ WHERE session_id = '${chat.composerId}' ORDER BY created_timestamp ASC`
196
+ );
197
+
198
+ const result = [];
199
+ for (const row of rows) {
200
+ let parts;
201
+ // A row we cannot decode drops a message from the transcript. Silently, that is
202
+ // a truncation that looks complete, and a re-ship would replace the stored
203
+ // transcript with it. Report so the shipper withholds and retries instead.
204
+ try { parts = JSON.parse(row.content_json); }
205
+ catch (e) { adapterErrors.record('goose', e, `session ${chat.composerId}`); continue; }
206
+ if (!Array.isArray(parts)) {
207
+ adapterErrors.record('goose', new Error('message content_json is not an array'), `session ${chat.composerId}`);
208
+ continue;
209
+ }
210
+
211
+ const role = row.role;
212
+ const contentParts = [];
213
+ const toolCalls = [];
214
+
215
+ for (const part of parts) {
216
+ if (part.type === 'text' && part.text) {
217
+ contentParts.push(part.text);
218
+ } else if (part.type === 'toolRequest' && part.toolCall) {
219
+ const tc = part.toolCall.value || {};
220
+ const toolName = tc.name || 'tool';
221
+ let argKeys = '';
222
+ try { argKeys = Object.keys(tc.arguments || {}).join(', '); } catch {}
223
+ contentParts.push(`[tool-call: ${toolName}(${argKeys})]`);
224
+ toolCalls.push({ name: toolName, args: tc.arguments || {} });
225
+ } else if (part.type === 'toolResponse' && part.toolResult) {
226
+ const tr = part.toolResult;
227
+ let preview = '';
228
+ if (tr.value && Array.isArray(tr.value)) {
229
+ preview = tr.value
230
+ .filter(v => v.type === 'text')
231
+ .map(v => v.text)
232
+ .join('\n')
233
+ .substring(0, 500);
234
+ }
235
+ contentParts.push(`[tool-result: ${tr.status || 'done'}] ${preview}`);
236
+ }
237
+ }
238
+
239
+ const content = contentParts.join('\n');
240
+ if (!content) continue;
241
+
242
+ const mappedRole = role === 'user' ? 'user' : role === 'assistant' ? 'assistant' : role;
243
+ const msg = { role: mappedRole, content };
244
+ if (mappedRole === 'assistant' && chat._model) msg._model = chat._model;
245
+ if (toolCalls.length) msg._toolCalls = toolCalls;
246
+ result.push(msg);
247
+ }
248
+
249
+ return result;
250
+ }
251
+
252
+ function getMessagesFromJsonl(chat) {
253
+ const filePath = chat._fullPath || path.join(GOOSE_DIR, chat.composerId + '.jsonl');
254
+ if (!fs.existsSync(filePath)) return [];
255
+
256
+ const lines = fs.readFileSync(filePath, 'utf-8').split('\n').filter(Boolean);
257
+ const result = [];
258
+
259
+ for (const line of lines) {
260
+ let obj;
261
+ // The legacy JSONL store needs the same protection as the SQLite one: a corrupt
262
+ // line drops a message, and the truncated transcript would overwrite the stored
263
+ // one when the file's mtime changes or --full is used.
264
+ try { obj = JSON.parse(line); }
265
+ catch (e) { adapterErrors.record('goose', e, filePath); continue; }
266
+
267
+ if (!obj.role) continue;
268
+
269
+ let parts;
270
+ try {
271
+ parts = typeof obj.content === 'string' ? JSON.parse(obj.content) : obj.content;
272
+ } catch {
273
+ // content might be plain text
274
+ if (typeof obj.content === 'string' && obj.content) {
275
+ result.push({ role: obj.role, content: obj.content });
276
+ }
277
+ continue;
278
+ }
279
+
280
+ if (!Array.isArray(parts)) continue;
281
+
282
+ const contentParts = [];
283
+ for (const part of parts) {
284
+ if (part.type === 'text' && part.text) {
285
+ contentParts.push(part.text);
286
+ } else if (part.type === 'toolRequest') {
287
+ const tc = part.toolCall?.value || {};
288
+ contentParts.push(`[tool-call: ${tc.name || 'tool'}(${Object.keys(tc.arguments || {}).join(', ')})]`);
289
+ } else if (part.type === 'toolResponse') {
290
+ const tr = part.toolResult || {};
291
+ let preview = '';
292
+ if (Array.isArray(tr.value)) {
293
+ preview = tr.value.filter(v => v.type === 'text').map(v => v.text).join('\n').substring(0, 500);
294
+ }
295
+ contentParts.push(`[tool-result] ${preview}`);
296
+ }
297
+ }
298
+
299
+ const content = contentParts.join('\n');
300
+ if (content) {
301
+ const msg = { role: obj.role, content };
302
+ if (obj.role === 'assistant' && chat._model) msg._model = chat._model;
303
+ result.push(msg);
304
+ }
305
+ }
306
+
307
+ return result;
308
+ }
309
+
310
+ function extractSessionModel(row) {
311
+ if (row.model_config_json) {
312
+ try {
313
+ const cfg = JSON.parse(row.model_config_json);
314
+ if (cfg.model) return cfg.model;
315
+ if (cfg.model_id) return cfg.model_id;
316
+ } catch {}
317
+ }
318
+ return null;
319
+ }
320
+
321
+ function resetCache() {
322
+ _configModel = undefined;
323
+ }
324
+
325
+ const labels = { 'goose': 'Goose' };
326
+
327
+ function getArtifacts(folder) {
328
+ const { scanArtifacts } = require('./base');
329
+ return scanArtifacts(folder, {
330
+ editor: 'goose',
331
+ label: 'Goose',
332
+ files: ['.goosehints'],
333
+ dirs: [],
334
+ });
335
+ }
336
+
337
+ function getMCPServers() {
338
+ const results = [];
339
+ // Goose stores MCP config in ~/.config/goose/config.yaml under extensions
340
+ // Also check profiles.yaml for MCP server entries
341
+ const configFiles = [CONFIG_PATH, path.join(os.homedir(), '.config', 'goose', 'profiles.yaml')];
342
+ for (const cfgPath of configFiles) {
343
+ if (!fs.existsSync(cfgPath)) continue;
344
+ try {
345
+ const raw = fs.readFileSync(cfgPath, 'utf-8');
346
+ // Simple YAML parsing for mcpServers / extensions blocks
347
+ let currentServer = null;
348
+ let inMcp = false;
349
+ for (const line of raw.split('\n')) {
350
+ if (line.match(/^\s*(mcpServers|extensions):/)) { inMcp = true; continue; }
351
+ if (inMcp && line.match(/^\s{2}\w/) && !line.match(/^\s{4}/)) {
352
+ // New top-level key under mcpServers
353
+ if (currentServer) results.push(currentServer);
354
+ const nameMatch = line.match(/^\s{2}(\S+):/);
355
+ if (nameMatch) {
356
+ currentServer = {
357
+ name: nameMatch[1],
358
+ editor: 'goose',
359
+ editorLabel: 'Goose',
360
+ scope: 'global',
361
+ configPath: cfgPath,
362
+ command: null, args: [], env: [], url: null,
363
+ transport: 'stdio', disabled: false, disabledTools: [],
364
+ };
365
+ }
366
+ continue;
367
+ }
368
+ if (inMcp && currentServer) {
369
+ const cmdMatch = line.match(/^\s+command:\s*(.+)/);
370
+ if (cmdMatch) currentServer.command = cmdMatch[1].trim();
371
+ const urlMatch = line.match(/^\s+url:\s*(.+)/);
372
+ if (urlMatch) { currentServer.url = urlMatch[1].trim(); currentServer.transport = 'http'; }
373
+ }
374
+ if (line.match(/^\S/) && !line.match(/^\s/) && inMcp) {
375
+ if (currentServer) results.push(currentServer);
376
+ currentServer = null;
377
+ inMcp = false;
378
+ }
379
+ }
380
+ if (currentServer) results.push(currentServer);
381
+ } catch {}
382
+ }
383
+ return results;
384
+ }
385
+
386
+ module.exports = { name, labels, getChats, getMessages, resetCache, getArtifacts, getMCPServers };