@getlore/cli 0.2.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 +13 -0
- package/README.md +80 -0
- package/dist/cli/colors.d.ts +48 -0
- package/dist/cli/colors.js +48 -0
- package/dist/cli/commands/ask.d.ts +7 -0
- package/dist/cli/commands/ask.js +97 -0
- package/dist/cli/commands/auth.d.ts +10 -0
- package/dist/cli/commands/auth.js +484 -0
- package/dist/cli/commands/daemon.d.ts +22 -0
- package/dist/cli/commands/daemon.js +244 -0
- package/dist/cli/commands/docs.d.ts +7 -0
- package/dist/cli/commands/docs.js +188 -0
- package/dist/cli/commands/extensions.d.ts +7 -0
- package/dist/cli/commands/extensions.js +204 -0
- package/dist/cli/commands/misc.d.ts +7 -0
- package/dist/cli/commands/misc.js +172 -0
- package/dist/cli/commands/pending.d.ts +7 -0
- package/dist/cli/commands/pending.js +63 -0
- package/dist/cli/commands/projects.d.ts +7 -0
- package/dist/cli/commands/projects.js +136 -0
- package/dist/cli/commands/search.d.ts +7 -0
- package/dist/cli/commands/search.js +102 -0
- package/dist/cli/commands/skills.d.ts +24 -0
- package/dist/cli/commands/skills.js +447 -0
- package/dist/cli/commands/sources.d.ts +7 -0
- package/dist/cli/commands/sources.js +121 -0
- package/dist/cli/commands/sync.d.ts +31 -0
- package/dist/cli/commands/sync.js +768 -0
- package/dist/cli/helpers.d.ts +30 -0
- package/dist/cli/helpers.js +119 -0
- package/dist/core/auth.d.ts +62 -0
- package/dist/core/auth.js +330 -0
- package/dist/core/config.d.ts +41 -0
- package/dist/core/config.js +96 -0
- package/dist/core/data-repo.d.ts +31 -0
- package/dist/core/data-repo.js +146 -0
- package/dist/core/embedder.d.ts +22 -0
- package/dist/core/embedder.js +104 -0
- package/dist/core/git.d.ts +37 -0
- package/dist/core/git.js +140 -0
- package/dist/core/index.d.ts +4 -0
- package/dist/core/index.js +5 -0
- package/dist/core/insight-extractor.d.ts +26 -0
- package/dist/core/insight-extractor.js +114 -0
- package/dist/core/local-search.d.ts +43 -0
- package/dist/core/local-search.js +221 -0
- package/dist/core/themes.d.ts +15 -0
- package/dist/core/themes.js +77 -0
- package/dist/core/types.d.ts +177 -0
- package/dist/core/types.js +9 -0
- package/dist/core/user-settings.d.ts +15 -0
- package/dist/core/user-settings.js +42 -0
- package/dist/core/vector-store-lance.d.ts +98 -0
- package/dist/core/vector-store-lance.js +384 -0
- package/dist/core/vector-store-supabase.d.ts +89 -0
- package/dist/core/vector-store-supabase.js +295 -0
- package/dist/core/vector-store.d.ts +131 -0
- package/dist/core/vector-store.js +503 -0
- package/dist/daemon-runner.d.ts +8 -0
- package/dist/daemon-runner.js +246 -0
- package/dist/extensions/config.d.ts +22 -0
- package/dist/extensions/config.js +102 -0
- package/dist/extensions/proposals.d.ts +30 -0
- package/dist/extensions/proposals.js +178 -0
- package/dist/extensions/registry.d.ts +35 -0
- package/dist/extensions/registry.js +309 -0
- package/dist/extensions/sandbox.d.ts +16 -0
- package/dist/extensions/sandbox.js +17 -0
- package/dist/extensions/types.d.ts +114 -0
- package/dist/extensions/types.js +4 -0
- package/dist/extensions/worker.d.ts +1 -0
- package/dist/extensions/worker.js +49 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +105 -0
- package/dist/mcp/handlers/archive-project.d.ts +51 -0
- package/dist/mcp/handlers/archive-project.js +112 -0
- package/dist/mcp/handlers/get-quotes.d.ts +27 -0
- package/dist/mcp/handlers/get-quotes.js +61 -0
- package/dist/mcp/handlers/get-source.d.ts +9 -0
- package/dist/mcp/handlers/get-source.js +40 -0
- package/dist/mcp/handlers/ingest.d.ts +25 -0
- package/dist/mcp/handlers/ingest.js +305 -0
- package/dist/mcp/handlers/list-projects.d.ts +4 -0
- package/dist/mcp/handlers/list-projects.js +16 -0
- package/dist/mcp/handlers/list-sources.d.ts +11 -0
- package/dist/mcp/handlers/list-sources.js +20 -0
- package/dist/mcp/handlers/research-agent.d.ts +21 -0
- package/dist/mcp/handlers/research-agent.js +369 -0
- package/dist/mcp/handlers/research.d.ts +22 -0
- package/dist/mcp/handlers/research.js +225 -0
- package/dist/mcp/handlers/retain.d.ts +18 -0
- package/dist/mcp/handlers/retain.js +92 -0
- package/dist/mcp/handlers/search.d.ts +52 -0
- package/dist/mcp/handlers/search.js +145 -0
- package/dist/mcp/handlers/sync.d.ts +47 -0
- package/dist/mcp/handlers/sync.js +211 -0
- package/dist/mcp/server.d.ts +10 -0
- package/dist/mcp/server.js +268 -0
- package/dist/mcp/tools.d.ts +16 -0
- package/dist/mcp/tools.js +297 -0
- package/dist/sync/config.d.ts +26 -0
- package/dist/sync/config.js +140 -0
- package/dist/sync/discover.d.ts +51 -0
- package/dist/sync/discover.js +190 -0
- package/dist/sync/index.d.ts +11 -0
- package/dist/sync/index.js +11 -0
- package/dist/sync/process.d.ts +50 -0
- package/dist/sync/process.js +285 -0
- package/dist/sync/processors.d.ts +24 -0
- package/dist/sync/processors.js +351 -0
- package/dist/tui/browse-handlers-ask.d.ts +30 -0
- package/dist/tui/browse-handlers-ask.js +372 -0
- package/dist/tui/browse-handlers-autocomplete.d.ts +49 -0
- package/dist/tui/browse-handlers-autocomplete.js +270 -0
- package/dist/tui/browse-handlers-extensions.d.ts +18 -0
- package/dist/tui/browse-handlers-extensions.js +107 -0
- package/dist/tui/browse-handlers-pending.d.ts +22 -0
- package/dist/tui/browse-handlers-pending.js +100 -0
- package/dist/tui/browse-handlers-research.d.ts +32 -0
- package/dist/tui/browse-handlers-research.js +363 -0
- package/dist/tui/browse-handlers-tools.d.ts +42 -0
- package/dist/tui/browse-handlers-tools.js +289 -0
- package/dist/tui/browse-handlers.d.ts +239 -0
- package/dist/tui/browse-handlers.js +1944 -0
- package/dist/tui/browse-render-extensions.d.ts +14 -0
- package/dist/tui/browse-render-extensions.js +114 -0
- package/dist/tui/browse-render-tools.d.ts +18 -0
- package/dist/tui/browse-render-tools.js +259 -0
- package/dist/tui/browse-render.d.ts +51 -0
- package/dist/tui/browse-render.js +599 -0
- package/dist/tui/browse-types.d.ts +142 -0
- package/dist/tui/browse-types.js +70 -0
- package/dist/tui/browse-ui.d.ts +10 -0
- package/dist/tui/browse-ui.js +432 -0
- package/dist/tui/browse.d.ts +17 -0
- package/dist/tui/browse.js +625 -0
- package/dist/tui/markdown.d.ts +22 -0
- package/dist/tui/markdown.js +223 -0
- package/package.json +71 -0
- package/plugins/claude-code/.claude-plugin/plugin.json +10 -0
- package/plugins/claude-code/.mcp.json +6 -0
- package/plugins/claude-code/skills/lore/SKILL.md +63 -0
- package/plugins/codex/SKILL.md +36 -0
- package/plugins/codex/agents/openai.yaml +10 -0
- package/plugins/gemini/GEMINI.md +31 -0
- package/plugins/gemini/gemini-extension.json +11 -0
- package/skills/generic-agent.md +99 -0
- package/skills/openclaw.md +67 -0
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lore - Vector Store (Supabase + pgvector)
|
|
3
|
+
*
|
|
4
|
+
* Cloud-hosted vector storage for semantic search across sources and chunks.
|
|
5
|
+
* Replaces LanceDB for multi-machine, multi-agent support.
|
|
6
|
+
*/
|
|
7
|
+
import { createClient } from '@supabase/supabase-js';
|
|
8
|
+
import { getValidSession } from './auth.js';
|
|
9
|
+
let supabase = null;
|
|
10
|
+
let supabaseMode = null;
|
|
11
|
+
/**
|
|
12
|
+
* Get an authenticated Supabase client. Three modes:
|
|
13
|
+
* 1. Service key (env var set) → bypasses RLS, backward compatible
|
|
14
|
+
* 2. Authenticated user → publishable key + auth session token → RLS applies
|
|
15
|
+
* 3. Neither → throws with helpful message
|
|
16
|
+
*/
|
|
17
|
+
export async function getSupabase() {
|
|
18
|
+
if (supabase)
|
|
19
|
+
return supabase;
|
|
20
|
+
const url = process.env.SUPABASE_URL;
|
|
21
|
+
const serviceKey = process.env.SUPABASE_SERVICE_KEY;
|
|
22
|
+
const publishableKey = process.env.SUPABASE_PUBLISHABLE_KEY || process.env.SUPABASE_ANON_KEY;
|
|
23
|
+
if (!url) {
|
|
24
|
+
throw new Error('SUPABASE_URL is required. Run \'lore setup\' to configure.');
|
|
25
|
+
}
|
|
26
|
+
// Mode 1: Service key (bypasses RLS)
|
|
27
|
+
if (serviceKey) {
|
|
28
|
+
supabase = createClient(url, serviceKey);
|
|
29
|
+
supabaseMode = 'service';
|
|
30
|
+
return supabase;
|
|
31
|
+
}
|
|
32
|
+
// Mode 2: Authenticated user (RLS applies)
|
|
33
|
+
if (publishableKey) {
|
|
34
|
+
const session = await getValidSession();
|
|
35
|
+
if (session) {
|
|
36
|
+
supabase = createClient(url, publishableKey, {
|
|
37
|
+
global: {
|
|
38
|
+
headers: {
|
|
39
|
+
Authorization: `Bearer ${session.access_token}`,
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
supabaseMode = 'auth';
|
|
44
|
+
return supabase;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// Mode 3: No auth
|
|
48
|
+
throw new Error('Not authenticated. Run \'lore login\' to sign in, or set SUPABASE_SERVICE_KEY for service mode.');
|
|
49
|
+
}
|
|
50
|
+
// ============================================================================
|
|
51
|
+
// Index Management (compatibility layer - not needed for Supabase)
|
|
52
|
+
// ============================================================================
|
|
53
|
+
export async function indexExists(_dbPath) {
|
|
54
|
+
// With Supabase, the index always "exists" if we can connect
|
|
55
|
+
try {
|
|
56
|
+
const client = await getSupabase();
|
|
57
|
+
const { error } = await client.from('sources').select('id').limit(1);
|
|
58
|
+
return !error;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export async function initializeTables(_dbPath) {
|
|
65
|
+
// Tables are managed via migrations in Supabase
|
|
66
|
+
// This is a no-op for compatibility
|
|
67
|
+
}
|
|
68
|
+
export function resetDatabaseConnection() {
|
|
69
|
+
// Reset the client to force reconnection
|
|
70
|
+
supabase = null;
|
|
71
|
+
supabaseMode = null;
|
|
72
|
+
}
|
|
73
|
+
export async function closeDatabase() {
|
|
74
|
+
supabase = null;
|
|
75
|
+
supabaseMode = null;
|
|
76
|
+
}
|
|
77
|
+
// For compatibility - Supabase doesn't use a local path
|
|
78
|
+
export async function getDatabase(_dbPath) {
|
|
79
|
+
return await getSupabase();
|
|
80
|
+
}
|
|
81
|
+
// ============================================================================
|
|
82
|
+
// Source Storage
|
|
83
|
+
// ============================================================================
|
|
84
|
+
export async function addSource(_dbPath, source, vector, extras) {
|
|
85
|
+
const client = await getSupabase();
|
|
86
|
+
const record = {
|
|
87
|
+
id: source.id,
|
|
88
|
+
title: source.title,
|
|
89
|
+
source_type: source.source_type,
|
|
90
|
+
content_type: source.content_type,
|
|
91
|
+
projects: JSON.parse(source.projects),
|
|
92
|
+
tags: JSON.parse(source.tags),
|
|
93
|
+
created_at: source.created_at,
|
|
94
|
+
summary: source.summary,
|
|
95
|
+
themes_json: JSON.parse(source.themes_json),
|
|
96
|
+
quotes_json: JSON.parse(source.quotes_json),
|
|
97
|
+
has_full_content: source.has_full_content,
|
|
98
|
+
embedding: vector,
|
|
99
|
+
indexed_at: new Date().toISOString(),
|
|
100
|
+
};
|
|
101
|
+
// Add optional dedup and metadata fields
|
|
102
|
+
if (extras?.content_hash) {
|
|
103
|
+
record.content_hash = extras.content_hash;
|
|
104
|
+
}
|
|
105
|
+
if (extras?.source_path) {
|
|
106
|
+
record.source_path = extras.source_path;
|
|
107
|
+
}
|
|
108
|
+
if (extras?.source_url) {
|
|
109
|
+
record.source_url = extras.source_url;
|
|
110
|
+
}
|
|
111
|
+
if (extras?.source_name) {
|
|
112
|
+
record.source_name = extras.source_name;
|
|
113
|
+
}
|
|
114
|
+
const { error } = await client.from('sources').upsert(record, {
|
|
115
|
+
ignoreDuplicates: true,
|
|
116
|
+
});
|
|
117
|
+
if (error) {
|
|
118
|
+
// Duplicate content_hash for this user — document already exists, skip silently
|
|
119
|
+
if (error.code === '23505') {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
console.error('[addSource] Error:', error);
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
export async function storeSources(_dbPath, sources) {
|
|
127
|
+
const client = await getSupabase();
|
|
128
|
+
const records = sources.map(({ source, vector, extras }) => {
|
|
129
|
+
const record = {
|
|
130
|
+
id: source.id,
|
|
131
|
+
title: source.title,
|
|
132
|
+
source_type: source.source_type,
|
|
133
|
+
content_type: source.content_type,
|
|
134
|
+
projects: JSON.parse(source.projects),
|
|
135
|
+
tags: JSON.parse(source.tags),
|
|
136
|
+
created_at: source.created_at,
|
|
137
|
+
summary: source.summary,
|
|
138
|
+
themes_json: JSON.parse(source.themes_json),
|
|
139
|
+
quotes_json: JSON.parse(source.quotes_json),
|
|
140
|
+
has_full_content: source.has_full_content,
|
|
141
|
+
embedding: vector,
|
|
142
|
+
indexed_at: new Date().toISOString(),
|
|
143
|
+
};
|
|
144
|
+
if (extras?.content_hash) {
|
|
145
|
+
record.content_hash = extras.content_hash;
|
|
146
|
+
}
|
|
147
|
+
if (extras?.source_path) {
|
|
148
|
+
record.source_path = extras.source_path;
|
|
149
|
+
}
|
|
150
|
+
if (extras?.source_url) {
|
|
151
|
+
record.source_url = extras.source_url;
|
|
152
|
+
}
|
|
153
|
+
if (extras?.source_name) {
|
|
154
|
+
record.source_name = extras.source_name;
|
|
155
|
+
}
|
|
156
|
+
return record;
|
|
157
|
+
});
|
|
158
|
+
const { error } = await client.from('sources').upsert(records);
|
|
159
|
+
if (error) {
|
|
160
|
+
console.error('[storeSources] Error:', error);
|
|
161
|
+
throw error;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// ============================================================================
|
|
165
|
+
// Source Path Operations (for edit detection)
|
|
166
|
+
// ============================================================================
|
|
167
|
+
export async function findSourceByPath(_dbPath, sourcePath) {
|
|
168
|
+
const client = await getSupabase();
|
|
169
|
+
const { data, error } = await client
|
|
170
|
+
.from('sources')
|
|
171
|
+
.select('id, content_hash')
|
|
172
|
+
.eq('source_path', sourcePath)
|
|
173
|
+
.limit(1)
|
|
174
|
+
.single();
|
|
175
|
+
if (error || !data) {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
id: data.id,
|
|
180
|
+
content_hash: data.content_hash,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
export async function getSourcePathMappings(_dbPath, paths) {
|
|
184
|
+
if (paths.length === 0)
|
|
185
|
+
return new Map();
|
|
186
|
+
const client = await getSupabase();
|
|
187
|
+
const mappings = new Map();
|
|
188
|
+
// Query in batches
|
|
189
|
+
const batchSize = 100;
|
|
190
|
+
for (let i = 0; i < paths.length; i += batchSize) {
|
|
191
|
+
const batch = paths.slice(i, i + batchSize);
|
|
192
|
+
const { data, error } = await client
|
|
193
|
+
.from('sources')
|
|
194
|
+
.select('id, source_path, content_hash')
|
|
195
|
+
.in('source_path', batch);
|
|
196
|
+
if (error) {
|
|
197
|
+
console.error('Error getting source path mappings:', error);
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
for (const row of data || []) {
|
|
201
|
+
if (row.source_path) {
|
|
202
|
+
mappings.set(row.source_path, {
|
|
203
|
+
id: row.id,
|
|
204
|
+
content_hash: row.content_hash,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return mappings;
|
|
210
|
+
}
|
|
211
|
+
// ============================================================================
|
|
212
|
+
// Content Hash Operations (for deduplication)
|
|
213
|
+
// ============================================================================
|
|
214
|
+
export async function checkContentHashExists(_dbPath, contentHash) {
|
|
215
|
+
const client = await getSupabase();
|
|
216
|
+
const { data, error } = await client
|
|
217
|
+
.from('sources')
|
|
218
|
+
.select('id')
|
|
219
|
+
.eq('content_hash', contentHash)
|
|
220
|
+
.limit(1);
|
|
221
|
+
if (error) {
|
|
222
|
+
console.error('Error checking content hash:', error);
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
return (data?.length || 0) > 0;
|
|
226
|
+
}
|
|
227
|
+
export async function getExistingContentHashes(_dbPath, hashes) {
|
|
228
|
+
if (hashes.length === 0)
|
|
229
|
+
return new Set();
|
|
230
|
+
const client = await getSupabase();
|
|
231
|
+
const existing = new Set();
|
|
232
|
+
// Query in batches to avoid limits
|
|
233
|
+
const batchSize = 100;
|
|
234
|
+
for (let i = 0; i < hashes.length; i += batchSize) {
|
|
235
|
+
const batch = hashes.slice(i, i + batchSize);
|
|
236
|
+
const { data, error } = await client
|
|
237
|
+
.from('sources')
|
|
238
|
+
.select('content_hash')
|
|
239
|
+
.in('content_hash', batch);
|
|
240
|
+
if (error) {
|
|
241
|
+
console.error('Error checking content hashes:', error);
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
for (const row of data || []) {
|
|
245
|
+
if (row.content_hash) {
|
|
246
|
+
existing.add(row.content_hash);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return existing;
|
|
251
|
+
}
|
|
252
|
+
export async function searchSources(_dbPath, queryVector, options = {}) {
|
|
253
|
+
const { limit = 10, project, source_type, content_type, recency_boost = 0.15, mode = 'hybrid', queryText = '', rrf_k = 60, } = options;
|
|
254
|
+
const client = await getSupabase();
|
|
255
|
+
// For backward compatibility: use legacy search if no query text or semantic-only
|
|
256
|
+
if (mode === 'semantic' || !queryText) {
|
|
257
|
+
const { data, error } = await client.rpc('search_sources', {
|
|
258
|
+
query_embedding: queryVector,
|
|
259
|
+
match_count: limit,
|
|
260
|
+
filter_project: project || null,
|
|
261
|
+
filter_source_type: source_type || null,
|
|
262
|
+
filter_content_type: content_type || null,
|
|
263
|
+
recency_boost,
|
|
264
|
+
});
|
|
265
|
+
if (error) {
|
|
266
|
+
console.error('Error searching sources:', error);
|
|
267
|
+
return [];
|
|
268
|
+
}
|
|
269
|
+
return (data || []).map((row) => ({
|
|
270
|
+
id: row.id,
|
|
271
|
+
title: row.title,
|
|
272
|
+
source_type: row.source_type,
|
|
273
|
+
content_type: row.content_type,
|
|
274
|
+
projects: row.projects,
|
|
275
|
+
tags: row.tags,
|
|
276
|
+
created_at: row.created_at,
|
|
277
|
+
summary: row.summary,
|
|
278
|
+
themes: (row.themes_json || []),
|
|
279
|
+
quotes: (row.quotes_json || []),
|
|
280
|
+
score: row.score,
|
|
281
|
+
}));
|
|
282
|
+
}
|
|
283
|
+
// Use hybrid search RPC
|
|
284
|
+
const { data, error } = await client.rpc('search_sources_hybrid', {
|
|
285
|
+
query_embedding: queryVector,
|
|
286
|
+
query_text: queryText,
|
|
287
|
+
match_count: limit,
|
|
288
|
+
filter_project: project || null,
|
|
289
|
+
filter_source_type: source_type || null,
|
|
290
|
+
filter_content_type: content_type || null,
|
|
291
|
+
recency_boost,
|
|
292
|
+
search_mode: mode,
|
|
293
|
+
rrf_k,
|
|
294
|
+
});
|
|
295
|
+
if (error) {
|
|
296
|
+
// Fall back to legacy search if hybrid RPC doesn't exist
|
|
297
|
+
if (error.message?.includes('function search_sources_hybrid') || error.code === '42883') {
|
|
298
|
+
console.warn('Hybrid search not available, falling back to semantic search');
|
|
299
|
+
return searchSources(_dbPath, queryVector, { ...options, mode: 'semantic' });
|
|
300
|
+
}
|
|
301
|
+
console.error('Error in hybrid search:', error);
|
|
302
|
+
return [];
|
|
303
|
+
}
|
|
304
|
+
return (data || []).map((row) => ({
|
|
305
|
+
id: row.id,
|
|
306
|
+
title: row.title,
|
|
307
|
+
source_type: row.source_type,
|
|
308
|
+
content_type: row.content_type,
|
|
309
|
+
projects: row.projects,
|
|
310
|
+
tags: row.tags,
|
|
311
|
+
created_at: row.created_at,
|
|
312
|
+
summary: row.summary,
|
|
313
|
+
themes: (row.themes_json || []),
|
|
314
|
+
quotes: (row.quotes_json || []),
|
|
315
|
+
score: row.score,
|
|
316
|
+
semantic_rank: row.semantic_rank,
|
|
317
|
+
keyword_rank: row.keyword_rank,
|
|
318
|
+
}));
|
|
319
|
+
}
|
|
320
|
+
// ============================================================================
|
|
321
|
+
// Retrieval Operations
|
|
322
|
+
// ============================================================================
|
|
323
|
+
export async function getAllSources(_dbPath, options = {}) {
|
|
324
|
+
const { project, source_type, limit } = options;
|
|
325
|
+
const client = await getSupabase();
|
|
326
|
+
let query = client
|
|
327
|
+
.from('sources')
|
|
328
|
+
.select('id, title, source_type, content_type, projects, created_at, summary')
|
|
329
|
+
.order('created_at', { ascending: false });
|
|
330
|
+
if (source_type) {
|
|
331
|
+
query = query.eq('source_type', source_type);
|
|
332
|
+
}
|
|
333
|
+
if (project) {
|
|
334
|
+
query = query.contains('projects', [project]);
|
|
335
|
+
}
|
|
336
|
+
if (limit) {
|
|
337
|
+
query = query.limit(limit);
|
|
338
|
+
}
|
|
339
|
+
const { data, error } = await query;
|
|
340
|
+
if (error) {
|
|
341
|
+
console.error('Error getting all sources:', error);
|
|
342
|
+
return [];
|
|
343
|
+
}
|
|
344
|
+
return (data || []).map((row) => ({
|
|
345
|
+
id: row.id,
|
|
346
|
+
title: row.title,
|
|
347
|
+
source_type: row.source_type,
|
|
348
|
+
content_type: row.content_type,
|
|
349
|
+
projects: row.projects,
|
|
350
|
+
created_at: row.created_at,
|
|
351
|
+
summary: row.summary,
|
|
352
|
+
}));
|
|
353
|
+
}
|
|
354
|
+
export async function getSourceById(_dbPath, sourceId) {
|
|
355
|
+
const client = await getSupabase();
|
|
356
|
+
const { data, error } = await client
|
|
357
|
+
.from('sources')
|
|
358
|
+
.select('*')
|
|
359
|
+
.eq('id', sourceId)
|
|
360
|
+
.single();
|
|
361
|
+
if (error || !data) {
|
|
362
|
+
console.error('Error getting source by ID:', error);
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
return {
|
|
366
|
+
id: data.id,
|
|
367
|
+
title: data.title,
|
|
368
|
+
source_type: data.source_type,
|
|
369
|
+
content_type: data.content_type,
|
|
370
|
+
projects: data.projects,
|
|
371
|
+
tags: data.tags,
|
|
372
|
+
created_at: data.created_at,
|
|
373
|
+
summary: data.summary,
|
|
374
|
+
themes: data.themes_json || [],
|
|
375
|
+
quotes: data.quotes_json || [],
|
|
376
|
+
source_url: data.source_url || undefined,
|
|
377
|
+
source_name: data.source_name || undefined,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
export async function deleteSource(_dbPath, sourceId) {
|
|
381
|
+
const client = await getSupabase();
|
|
382
|
+
// Fetch content_hash and source_path before deleting so callers can
|
|
383
|
+
// record the hash in the blocklist and remove the original file
|
|
384
|
+
const { data } = await client
|
|
385
|
+
.from('sources')
|
|
386
|
+
.select('content_hash, source_path')
|
|
387
|
+
.eq('id', sourceId)
|
|
388
|
+
.single();
|
|
389
|
+
const contentHash = data?.content_hash;
|
|
390
|
+
const sourcePath = data?.source_path;
|
|
391
|
+
const { error } = await client
|
|
392
|
+
.from('sources')
|
|
393
|
+
.delete()
|
|
394
|
+
.eq('id', sourceId);
|
|
395
|
+
if (error) {
|
|
396
|
+
console.error('Error deleting source:', error);
|
|
397
|
+
return { deleted: false };
|
|
398
|
+
}
|
|
399
|
+
return { deleted: true, contentHash, sourcePath };
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Update a source's projects array
|
|
403
|
+
*/
|
|
404
|
+
export async function updateSourceProjects(_dbPath, sourceId, projects) {
|
|
405
|
+
const client = await getSupabase();
|
|
406
|
+
const { error } = await client
|
|
407
|
+
.from('sources')
|
|
408
|
+
.update({ projects })
|
|
409
|
+
.eq('id', sourceId);
|
|
410
|
+
if (error) {
|
|
411
|
+
console.error('Error updating source projects:', error);
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
414
|
+
return true;
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Update a source's title
|
|
418
|
+
*/
|
|
419
|
+
export async function updateSourceTitle(_dbPath, sourceId, title) {
|
|
420
|
+
const client = await getSupabase();
|
|
421
|
+
const { error } = await client
|
|
422
|
+
.from('sources')
|
|
423
|
+
.update({ title })
|
|
424
|
+
.eq('id', sourceId);
|
|
425
|
+
if (error) {
|
|
426
|
+
console.error('Error updating source title:', error);
|
|
427
|
+
return false;
|
|
428
|
+
}
|
|
429
|
+
return true;
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Update a source's content type
|
|
433
|
+
*/
|
|
434
|
+
export async function updateSourceContentType(_dbPath, sourceId, contentType) {
|
|
435
|
+
const client = await getSupabase();
|
|
436
|
+
const { error } = await client
|
|
437
|
+
.from('sources')
|
|
438
|
+
.update({ content_type: contentType })
|
|
439
|
+
.eq('id', sourceId);
|
|
440
|
+
if (error) {
|
|
441
|
+
console.error('Error updating source content type:', error);
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
return true;
|
|
445
|
+
}
|
|
446
|
+
// ============================================================================
|
|
447
|
+
// Statistics
|
|
448
|
+
// ============================================================================
|
|
449
|
+
export async function getThemeStats(_dbPath, project) {
|
|
450
|
+
const client = await getSupabase();
|
|
451
|
+
const stats = new Map();
|
|
452
|
+
let query = client.from('sources').select('themes_json, quotes_json, projects');
|
|
453
|
+
if (project) {
|
|
454
|
+
query = query.contains('projects', [project]);
|
|
455
|
+
}
|
|
456
|
+
const { data, error } = await query;
|
|
457
|
+
if (error) {
|
|
458
|
+
console.error('Error getting theme stats:', error);
|
|
459
|
+
return stats;
|
|
460
|
+
}
|
|
461
|
+
for (const row of data || []) {
|
|
462
|
+
const themes = (row.themes_json || []);
|
|
463
|
+
for (const theme of themes) {
|
|
464
|
+
const existing = stats.get(theme.name) || { source_count: 0, quote_count: 0 };
|
|
465
|
+
existing.source_count++;
|
|
466
|
+
existing.quote_count += theme.evidence?.length || 0;
|
|
467
|
+
stats.set(theme.name, existing);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
return stats;
|
|
471
|
+
}
|
|
472
|
+
export async function getProjectStats(_dbPath) {
|
|
473
|
+
const client = await getSupabase();
|
|
474
|
+
const projectMap = new Map();
|
|
475
|
+
const { data, error } = await client
|
|
476
|
+
.from('sources')
|
|
477
|
+
.select('projects, quotes_json, created_at');
|
|
478
|
+
if (error) {
|
|
479
|
+
console.error('Error getting project stats:', error);
|
|
480
|
+
return [];
|
|
481
|
+
}
|
|
482
|
+
for (const row of data || []) {
|
|
483
|
+
const projects = row.projects;
|
|
484
|
+
const quotes = (row.quotes_json || []);
|
|
485
|
+
const created_at = row.created_at;
|
|
486
|
+
for (const project of projects) {
|
|
487
|
+
const existing = projectMap.get(project) || {
|
|
488
|
+
source_count: 0,
|
|
489
|
+
quote_count: 0,
|
|
490
|
+
latest_activity: created_at,
|
|
491
|
+
};
|
|
492
|
+
existing.source_count++;
|
|
493
|
+
existing.quote_count += quotes.length;
|
|
494
|
+
if (new Date(created_at) > new Date(existing.latest_activity)) {
|
|
495
|
+
existing.latest_activity = created_at;
|
|
496
|
+
}
|
|
497
|
+
projectMap.set(project, existing);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return Array.from(projectMap.entries())
|
|
501
|
+
.map(([project, stats]) => ({ project, ...stats }))
|
|
502
|
+
.sort((a, b) => new Date(b.latest_activity).getTime() - new Date(a.latest_activity).getTime());
|
|
503
|
+
}
|