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.
- package/LICENSE +22 -0
- package/README.md +72 -3
- package/bin/memhouse.js +559 -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/adapter-errors.js +52 -0
- package/editors/antigravity.js +1048 -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 +498 -0
- package/editors/gemini.js +231 -0
- package/editors/goose.js +386 -0
- package/editors/gsd.js +366 -0
- package/editors/index.js +274 -0
- package/editors/kiro.js +315 -0
- package/editors/opencode.js +432 -0
- package/editors/vscode.js +413 -0
- package/editors/windsurf.js +634 -0
- package/editors/zed.js +229 -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 +87 -0
- package/mem-house/delivery/PROMPT.md +42 -0
- package/mem-house/delivery/install.sh +181 -0
- package/mem-house/delivery/kernel-install.md +83 -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 +461 -0
- package/misc/syntax-check.js +38 -0
- package/package.json +59 -7
- package/pricing.js +88 -0
- package/pricing.json +992 -0
- package/public/assets/index-CCPh7a2Z.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 +370 -0
- package/ui/src/pages/Subscriptions.jsx +411 -0
- 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 };
|
package/editors/goose.js
ADDED
|
@@ -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 };
|