@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.
Files changed (148) hide show
  1. package/LICENSE +13 -0
  2. package/README.md +80 -0
  3. package/dist/cli/colors.d.ts +48 -0
  4. package/dist/cli/colors.js +48 -0
  5. package/dist/cli/commands/ask.d.ts +7 -0
  6. package/dist/cli/commands/ask.js +97 -0
  7. package/dist/cli/commands/auth.d.ts +10 -0
  8. package/dist/cli/commands/auth.js +484 -0
  9. package/dist/cli/commands/daemon.d.ts +22 -0
  10. package/dist/cli/commands/daemon.js +244 -0
  11. package/dist/cli/commands/docs.d.ts +7 -0
  12. package/dist/cli/commands/docs.js +188 -0
  13. package/dist/cli/commands/extensions.d.ts +7 -0
  14. package/dist/cli/commands/extensions.js +204 -0
  15. package/dist/cli/commands/misc.d.ts +7 -0
  16. package/dist/cli/commands/misc.js +172 -0
  17. package/dist/cli/commands/pending.d.ts +7 -0
  18. package/dist/cli/commands/pending.js +63 -0
  19. package/dist/cli/commands/projects.d.ts +7 -0
  20. package/dist/cli/commands/projects.js +136 -0
  21. package/dist/cli/commands/search.d.ts +7 -0
  22. package/dist/cli/commands/search.js +102 -0
  23. package/dist/cli/commands/skills.d.ts +24 -0
  24. package/dist/cli/commands/skills.js +447 -0
  25. package/dist/cli/commands/sources.d.ts +7 -0
  26. package/dist/cli/commands/sources.js +121 -0
  27. package/dist/cli/commands/sync.d.ts +31 -0
  28. package/dist/cli/commands/sync.js +768 -0
  29. package/dist/cli/helpers.d.ts +30 -0
  30. package/dist/cli/helpers.js +119 -0
  31. package/dist/core/auth.d.ts +62 -0
  32. package/dist/core/auth.js +330 -0
  33. package/dist/core/config.d.ts +41 -0
  34. package/dist/core/config.js +96 -0
  35. package/dist/core/data-repo.d.ts +31 -0
  36. package/dist/core/data-repo.js +146 -0
  37. package/dist/core/embedder.d.ts +22 -0
  38. package/dist/core/embedder.js +104 -0
  39. package/dist/core/git.d.ts +37 -0
  40. package/dist/core/git.js +140 -0
  41. package/dist/core/index.d.ts +4 -0
  42. package/dist/core/index.js +5 -0
  43. package/dist/core/insight-extractor.d.ts +26 -0
  44. package/dist/core/insight-extractor.js +114 -0
  45. package/dist/core/local-search.d.ts +43 -0
  46. package/dist/core/local-search.js +221 -0
  47. package/dist/core/themes.d.ts +15 -0
  48. package/dist/core/themes.js +77 -0
  49. package/dist/core/types.d.ts +177 -0
  50. package/dist/core/types.js +9 -0
  51. package/dist/core/user-settings.d.ts +15 -0
  52. package/dist/core/user-settings.js +42 -0
  53. package/dist/core/vector-store-lance.d.ts +98 -0
  54. package/dist/core/vector-store-lance.js +384 -0
  55. package/dist/core/vector-store-supabase.d.ts +89 -0
  56. package/dist/core/vector-store-supabase.js +295 -0
  57. package/dist/core/vector-store.d.ts +131 -0
  58. package/dist/core/vector-store.js +503 -0
  59. package/dist/daemon-runner.d.ts +8 -0
  60. package/dist/daemon-runner.js +246 -0
  61. package/dist/extensions/config.d.ts +22 -0
  62. package/dist/extensions/config.js +102 -0
  63. package/dist/extensions/proposals.d.ts +30 -0
  64. package/dist/extensions/proposals.js +178 -0
  65. package/dist/extensions/registry.d.ts +35 -0
  66. package/dist/extensions/registry.js +309 -0
  67. package/dist/extensions/sandbox.d.ts +16 -0
  68. package/dist/extensions/sandbox.js +17 -0
  69. package/dist/extensions/types.d.ts +114 -0
  70. package/dist/extensions/types.js +4 -0
  71. package/dist/extensions/worker.d.ts +1 -0
  72. package/dist/extensions/worker.js +49 -0
  73. package/dist/index.d.ts +17 -0
  74. package/dist/index.js +105 -0
  75. package/dist/mcp/handlers/archive-project.d.ts +51 -0
  76. package/dist/mcp/handlers/archive-project.js +112 -0
  77. package/dist/mcp/handlers/get-quotes.d.ts +27 -0
  78. package/dist/mcp/handlers/get-quotes.js +61 -0
  79. package/dist/mcp/handlers/get-source.d.ts +9 -0
  80. package/dist/mcp/handlers/get-source.js +40 -0
  81. package/dist/mcp/handlers/ingest.d.ts +25 -0
  82. package/dist/mcp/handlers/ingest.js +305 -0
  83. package/dist/mcp/handlers/list-projects.d.ts +4 -0
  84. package/dist/mcp/handlers/list-projects.js +16 -0
  85. package/dist/mcp/handlers/list-sources.d.ts +11 -0
  86. package/dist/mcp/handlers/list-sources.js +20 -0
  87. package/dist/mcp/handlers/research-agent.d.ts +21 -0
  88. package/dist/mcp/handlers/research-agent.js +369 -0
  89. package/dist/mcp/handlers/research.d.ts +22 -0
  90. package/dist/mcp/handlers/research.js +225 -0
  91. package/dist/mcp/handlers/retain.d.ts +18 -0
  92. package/dist/mcp/handlers/retain.js +92 -0
  93. package/dist/mcp/handlers/search.d.ts +52 -0
  94. package/dist/mcp/handlers/search.js +145 -0
  95. package/dist/mcp/handlers/sync.d.ts +47 -0
  96. package/dist/mcp/handlers/sync.js +211 -0
  97. package/dist/mcp/server.d.ts +10 -0
  98. package/dist/mcp/server.js +268 -0
  99. package/dist/mcp/tools.d.ts +16 -0
  100. package/dist/mcp/tools.js +297 -0
  101. package/dist/sync/config.d.ts +26 -0
  102. package/dist/sync/config.js +140 -0
  103. package/dist/sync/discover.d.ts +51 -0
  104. package/dist/sync/discover.js +190 -0
  105. package/dist/sync/index.d.ts +11 -0
  106. package/dist/sync/index.js +11 -0
  107. package/dist/sync/process.d.ts +50 -0
  108. package/dist/sync/process.js +285 -0
  109. package/dist/sync/processors.d.ts +24 -0
  110. package/dist/sync/processors.js +351 -0
  111. package/dist/tui/browse-handlers-ask.d.ts +30 -0
  112. package/dist/tui/browse-handlers-ask.js +372 -0
  113. package/dist/tui/browse-handlers-autocomplete.d.ts +49 -0
  114. package/dist/tui/browse-handlers-autocomplete.js +270 -0
  115. package/dist/tui/browse-handlers-extensions.d.ts +18 -0
  116. package/dist/tui/browse-handlers-extensions.js +107 -0
  117. package/dist/tui/browse-handlers-pending.d.ts +22 -0
  118. package/dist/tui/browse-handlers-pending.js +100 -0
  119. package/dist/tui/browse-handlers-research.d.ts +32 -0
  120. package/dist/tui/browse-handlers-research.js +363 -0
  121. package/dist/tui/browse-handlers-tools.d.ts +42 -0
  122. package/dist/tui/browse-handlers-tools.js +289 -0
  123. package/dist/tui/browse-handlers.d.ts +239 -0
  124. package/dist/tui/browse-handlers.js +1944 -0
  125. package/dist/tui/browse-render-extensions.d.ts +14 -0
  126. package/dist/tui/browse-render-extensions.js +114 -0
  127. package/dist/tui/browse-render-tools.d.ts +18 -0
  128. package/dist/tui/browse-render-tools.js +259 -0
  129. package/dist/tui/browse-render.d.ts +51 -0
  130. package/dist/tui/browse-render.js +599 -0
  131. package/dist/tui/browse-types.d.ts +142 -0
  132. package/dist/tui/browse-types.js +70 -0
  133. package/dist/tui/browse-ui.d.ts +10 -0
  134. package/dist/tui/browse-ui.js +432 -0
  135. package/dist/tui/browse.d.ts +17 -0
  136. package/dist/tui/browse.js +625 -0
  137. package/dist/tui/markdown.d.ts +22 -0
  138. package/dist/tui/markdown.js +223 -0
  139. package/package.json +71 -0
  140. package/plugins/claude-code/.claude-plugin/plugin.json +10 -0
  141. package/plugins/claude-code/.mcp.json +6 -0
  142. package/plugins/claude-code/skills/lore/SKILL.md +63 -0
  143. package/plugins/codex/SKILL.md +36 -0
  144. package/plugins/codex/agents/openai.yaml +10 -0
  145. package/plugins/gemini/GEMINI.md +31 -0
  146. package/plugins/gemini/gemini-extension.json +11 -0
  147. package/skills/generic-agent.md +99 -0
  148. 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
+ }
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Daemon Runner
4
+ *
5
+ * This script runs as a background process, handling file watching and periodic sync.
6
+ * It writes logs to ~/.config/lore/daemon.log and updates status in daemon.status.json.
7
+ */
8
+ export {};