@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,369 @@
1
+ /**
2
+ * Lore - Agentic Research using Claude Agent SDK
3
+ *
4
+ * This is the "real" agent that:
5
+ * 1. Takes a research task
6
+ * 2. Uses Lore's own tools iteratively (search, get_source, list_sources)
7
+ * 3. Follows leads, cross-references, refines queries
8
+ * 4. Synthesizes findings into a comprehensive research package
9
+ */
10
+ import { query, tool, createSdkMcpServer } from '@anthropic-ai/claude-agent-sdk';
11
+ import { z } from 'zod';
12
+ import { searchSources, getSourceById, getAllSources } from '../../core/vector-store.js';
13
+ import { generateEmbedding } from '../../core/embedder.js';
14
+ import { loadArchivedProjects } from './archive-project.js';
15
+ // Agent will self-terminate when it has enough evidence
16
+ // This is a safety limit to prevent runaway loops
17
+ const MAX_TURNS = 50;
18
+ /**
19
+ * Create the Lore tools MCP server for the research agent
20
+ */
21
+ function createLoreToolsServer(dbPath, dataDir, archivedProjects) {
22
+ return createSdkMcpServer({
23
+ name: 'lore-tools',
24
+ version: '1.0.0',
25
+ tools: [
26
+ // Search tool - semantic search across sources
27
+ tool('search', 'Semantic search across all sources in the knowledge repository. Returns summaries with relevant quotes. Use this to find information related to a topic.', {
28
+ query: z.string().describe('Semantic search query - describe what you\'re looking for'),
29
+ source_type: z
30
+ .enum(['granola', 'claude-code', 'claude-desktop', 'chatgpt', 'markdown', 'document'])
31
+ .optional()
32
+ .describe('Filter by source type (e.g., "granola" for meeting transcripts)'),
33
+ content_type: z
34
+ .enum(['interview', 'meeting', 'conversation', 'document', 'note', 'analysis'])
35
+ .optional()
36
+ .describe('Filter by content type (e.g., "interview" for user interviews)'),
37
+ project: z.string().optional().describe('Filter to specific project'),
38
+ limit: z.number().optional().describe('Max results (default 10)'),
39
+ }, async (args) => {
40
+ try {
41
+ const queryVector = await generateEmbedding(args.query);
42
+ const results = await searchSources(dbPath, queryVector, {
43
+ limit: args.limit || 10,
44
+ project: args.project,
45
+ source_type: args.source_type,
46
+ content_type: args.content_type,
47
+ });
48
+ // Filter out archived projects
49
+ const filtered = results.filter((r) => {
50
+ return !r.projects.some((p) => archivedProjects.includes(p.toLowerCase()));
51
+ });
52
+ if (filtered.length === 0) {
53
+ return {
54
+ content: [{ type: 'text', text: 'No results found for this query.' }],
55
+ };
56
+ }
57
+ const resultText = filtered
58
+ .map((r, i) => {
59
+ const quotes = r.quotes.slice(0, 3).map((q) => ` - "${q.text.substring(0, 150)}..."`).join('\n');
60
+ return `${i + 1}. **${r.title}** (${r.source_type}, score: ${(r.score * 100).toFixed(0)}%)
61
+ ID: ${r.id}
62
+ Projects: ${r.projects.join(', ') || 'none'}
63
+ Summary: ${r.summary.substring(0, 200)}...
64
+ Key quotes:
65
+ ${quotes}`;
66
+ })
67
+ .join('\n\n');
68
+ return {
69
+ content: [{ type: 'text', text: `Found ${filtered.length} results:\n\n${resultText}` }],
70
+ };
71
+ }
72
+ catch (error) {
73
+ return {
74
+ content: [{ type: 'text', text: `Search error: ${error}` }],
75
+ };
76
+ }
77
+ }),
78
+ // Get source - retrieve full details of a specific source
79
+ tool('get_source', 'Get full details of a specific source document including all quotes, themes, and summary. Use this to dive deeper into a specific source found via search.', {
80
+ source_id: z.string().describe('ID of the source document (from search results)'),
81
+ }, async (args) => {
82
+ try {
83
+ const source = await getSourceById(dbPath, args.source_id);
84
+ if (!source) {
85
+ return {
86
+ content: [{ type: 'text', text: `Source not found: ${args.source_id}` }],
87
+ };
88
+ }
89
+ const themes = source.themes.map((t) => `- ${t.name}: ${t.evidence.length} pieces of evidence`).join('\n');
90
+ const quotes = source.quotes
91
+ .slice(0, 10)
92
+ .map((q) => `- [${q.speaker || 'unknown'}] "${q.text}"`)
93
+ .join('\n');
94
+ return {
95
+ content: [
96
+ {
97
+ type: 'text',
98
+ text: `# ${source.title}
99
+
100
+ **Type:** ${source.source_type} / ${source.content_type}
101
+ **Created:** ${source.created_at}
102
+ **Projects:** ${source.projects.join(', ') || 'none'}
103
+
104
+ ## Summary
105
+ ${source.summary}
106
+
107
+ ## Themes
108
+ ${themes || 'No themes extracted'}
109
+
110
+ ## Key Quotes
111
+ ${quotes || 'No quotes extracted'}`,
112
+ },
113
+ ],
114
+ };
115
+ }
116
+ catch (error) {
117
+ return {
118
+ content: [{ type: 'text', text: `Error getting source: ${error}` }],
119
+ };
120
+ }
121
+ }),
122
+ // List sources - browse available sources
123
+ tool('list_sources', 'List all sources in the repository. Use this to understand what knowledge is available before searching.', {
124
+ source_type: z
125
+ .enum(['granola', 'claude-code', 'claude-desktop', 'chatgpt', 'markdown', 'document'])
126
+ .optional()
127
+ .describe('Filter by source type'),
128
+ project: z.string().optional().describe('Filter to specific project'),
129
+ limit: z.number().optional().describe('Max results (default 20)'),
130
+ }, async (args) => {
131
+ try {
132
+ const sources = await getAllSources(dbPath, {
133
+ source_type: args.source_type,
134
+ project: args.project,
135
+ limit: args.limit || 20,
136
+ });
137
+ // Filter out archived
138
+ const filtered = sources.filter((s) => {
139
+ return !s.projects.some((p) => archivedProjects.includes(p.toLowerCase()));
140
+ });
141
+ if (filtered.length === 0) {
142
+ return {
143
+ content: [{ type: 'text', text: 'No sources found matching criteria.' }],
144
+ };
145
+ }
146
+ const listText = filtered
147
+ .map((s, i) => {
148
+ return `${i + 1}. **${s.title}** (${s.source_type})
149
+ ID: ${s.id}
150
+ Created: ${s.created_at}
151
+ Projects: ${s.projects.join(', ') || 'none'}`;
152
+ })
153
+ .join('\n\n');
154
+ return {
155
+ content: [{ type: 'text', text: `Found ${filtered.length} sources:\n\n${listText}` }],
156
+ };
157
+ }
158
+ catch (error) {
159
+ return {
160
+ content: [{ type: 'text', text: `Error listing sources: ${error}` }],
161
+ };
162
+ }
163
+ }),
164
+ ],
165
+ });
166
+ }
167
+ /**
168
+ * Research agent system prompt
169
+ */
170
+ function getResearchSystemPrompt(task, project) {
171
+ return `You are a research agent for Lore, a knowledge repository containing user interviews, meeting transcripts, AI conversations, and documents.
172
+
173
+ Your task is to conduct comprehensive research and produce a well-cited research package.
174
+
175
+ ## Research Task
176
+ ${task}
177
+ ${project ? `\nFocus on project: ${project}` : ''}
178
+
179
+ ## Your Tools
180
+ - **search**: Semantic search across all sources. Start broad, then refine.
181
+ - **get_source**: Dive deep into a specific source for full context and quotes.
182
+ - **list_sources**: See what knowledge is available.
183
+
184
+ ## Research Methodology
185
+
186
+ 1. **Explore First**: Start with broad searches to understand what's available.
187
+ 2. **Follow Leads**: When you find relevant sources, use get_source to read more and gather quotes.
188
+ 3. **Cross-Reference**: Look for patterns across multiple sources.
189
+ 4. **Identify Conflicts**: Note when sources disagree - prefer newer sources.
190
+ 5. **Synthesize When Ready**: When you feel you have sufficient evidence to answer the research task comprehensively, produce your findings.
191
+
192
+ ## Output Requirements
193
+
194
+ When you have gathered sufficient evidence, provide your findings in this exact JSON format:
195
+
196
+ \`\`\`json
197
+ {
198
+ "summary": "A comprehensive 2-3 paragraph summary of findings",
199
+ "key_findings": ["Finding 1 with citation", "Finding 2 with citation", ...],
200
+ "conflicts_resolved": ["Description of any conflicts and how they were resolved"],
201
+ "supporting_quotes": [
202
+ {
203
+ "text": "The exact quote",
204
+ "speaker": "who said it",
205
+ "source_id": "source document ID",
206
+ "source_title": "title of the source"
207
+ }
208
+ ],
209
+ "sources_consulted": [
210
+ {
211
+ "id": "source ID",
212
+ "title": "source title",
213
+ "relevance": "why this was useful"
214
+ }
215
+ ],
216
+ "gaps_identified": ["What information is missing or unclear"],
217
+ "suggested_queries": ["Follow-up research questions"]
218
+ }
219
+ \`\`\`
220
+
221
+ ## Important Guidelines
222
+
223
+ - ALWAYS cite your sources - every claim should reference a specific source
224
+ - When sources conflict, note the conflict and prefer the more recent source
225
+ - Be thorough - use multiple searches with different queries
226
+ - Include direct quotes as evidence
227
+ - Identify what you DIDN'T find (gaps)
228
+ - Suggest follow-up questions
229
+
230
+ Now begin your research. Use the tools iteratively until you have comprehensive findings.`;
231
+ }
232
+ /**
233
+ * Run the agentic research
234
+ */
235
+ export async function runResearchAgent(dbPath, dataDir, args) {
236
+ const { task, project, include_sources = true } = args;
237
+ // Load archived projects to filter (extract just the project names)
238
+ const archivedProjectsData = await loadArchivedProjects(dataDir);
239
+ const archivedProjects = archivedProjectsData.map((p) => p.project.toLowerCase());
240
+ // Create the Lore tools server
241
+ const loreTools = createLoreToolsServer(dbPath, dataDir, archivedProjects);
242
+ // System prompt
243
+ const systemPrompt = getResearchSystemPrompt(task, project);
244
+ let finalResult = null;
245
+ let lastAssistantMessage = '';
246
+ try {
247
+ // Run the agent
248
+ for await (const message of query({
249
+ prompt: `Research task: ${task}${project ? ` (project: ${project})` : ''}`,
250
+ options: {
251
+ systemPrompt,
252
+ mcpServers: {
253
+ 'lore-tools': loreTools,
254
+ },
255
+ allowedTools: [
256
+ 'mcp__lore-tools__search',
257
+ 'mcp__lore-tools__get_source',
258
+ 'mcp__lore-tools__list_sources',
259
+ ],
260
+ maxTurns: MAX_TURNS,
261
+ permissionMode: 'acceptEdits', // Auto-approve tool calls
262
+ },
263
+ })) {
264
+ // Capture assistant messages (intermediate)
265
+ if (message.type === 'assistant') {
266
+ const msg = message;
267
+ if (msg.message?.content) {
268
+ const content = msg.message.content;
269
+ if (typeof content === 'string') {
270
+ lastAssistantMessage = content;
271
+ }
272
+ else if (Array.isArray(content)) {
273
+ const textBlocks = content.filter((b) => b.type === 'text');
274
+ if (textBlocks.length > 0) {
275
+ lastAssistantMessage = textBlocks.map((b) => b.text).join('\n');
276
+ }
277
+ }
278
+ }
279
+ }
280
+ // Capture the final result message
281
+ if (message.type === 'result') {
282
+ const msg = message;
283
+ if (msg.subtype === 'success' && msg.result) {
284
+ lastAssistantMessage = msg.result;
285
+ console.error(`[research-agent] Completed in ${msg.num_turns} turns`);
286
+ }
287
+ else if (msg.subtype?.startsWith('error')) {
288
+ console.error(`[research-agent] Error: ${msg.subtype}`, msg.errors);
289
+ }
290
+ }
291
+ // Log tool usage for debugging
292
+ if (message.type === 'tool_use_summary') {
293
+ const msg = message;
294
+ console.error(`[research-agent] Tool: ${msg.tool_name || 'unknown'}`);
295
+ }
296
+ }
297
+ // Parse the final result from the agent's output
298
+ finalResult = parseResearchOutput(lastAssistantMessage, task, project);
299
+ }
300
+ catch (error) {
301
+ console.error('[research-agent] Error:', error);
302
+ // Return a minimal result on error
303
+ finalResult = {
304
+ query: task,
305
+ project,
306
+ generated_at: new Date().toISOString(),
307
+ summary: `Research failed: ${error}`,
308
+ key_findings: [],
309
+ supporting_quotes: [],
310
+ related_decisions: [],
311
+ sources_consulted: [],
312
+ gaps_identified: ['Research could not be completed due to an error'],
313
+ };
314
+ }
315
+ return finalResult;
316
+ }
317
+ /**
318
+ * Parse the agent's output into a ResearchPackage
319
+ */
320
+ function parseResearchOutput(output, task, project) {
321
+ // Try to extract JSON from the output
322
+ const jsonMatch = output.match(/```json\s*([\s\S]*?)\s*```/);
323
+ if (jsonMatch) {
324
+ try {
325
+ const parsed = JSON.parse(jsonMatch[1]);
326
+ return {
327
+ query: task,
328
+ project,
329
+ generated_at: new Date().toISOString(),
330
+ summary: parsed.summary || '',
331
+ key_findings: parsed.key_findings || [],
332
+ conflicts_resolved: parsed.conflicts_resolved || [],
333
+ supporting_quotes: (parsed.supporting_quotes || []).map((q) => ({
334
+ id: `quote_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
335
+ text: q.text,
336
+ speaker: q.speaker,
337
+ citation: {
338
+ source_id: q.source_id,
339
+ context: q.source_title,
340
+ },
341
+ })),
342
+ related_decisions: [],
343
+ sources_consulted: (parsed.sources_consulted || []).map((s) => ({
344
+ id: s.id,
345
+ title: s.title,
346
+ source_type: 'document',
347
+ relevance: 0.8,
348
+ })),
349
+ gaps_identified: parsed.gaps_identified || [],
350
+ suggested_queries: parsed.suggested_queries || [],
351
+ };
352
+ }
353
+ catch (e) {
354
+ console.error('[research-agent] Failed to parse JSON output:', e);
355
+ }
356
+ }
357
+ // Fallback: return the raw output as summary
358
+ return {
359
+ query: task,
360
+ project,
361
+ generated_at: new Date().toISOString(),
362
+ summary: output || 'No research output generated',
363
+ key_findings: [],
364
+ supporting_quotes: [],
365
+ related_decisions: [],
366
+ sources_consulted: [],
367
+ gaps_identified: ['Could not parse structured output from research agent'],
368
+ };
369
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Research Handler - LLM-powered research synthesis
3
+ *
4
+ * Two modes:
5
+ * 1. AGENTIC (default): Uses Claude Agent SDK for iterative, thorough research
6
+ * 2. SIMPLE (fallback): Single-pass search + GPT-4o-mini synthesis
7
+ *
8
+ * Set LORE_RESEARCH_MODE=simple to use the fallback mode.
9
+ */
10
+ import type { ResearchPackage } from '../../core/types.js';
11
+ interface ResearchArgs {
12
+ task: string;
13
+ project?: string;
14
+ content_type?: string;
15
+ include_sources?: boolean;
16
+ }
17
+ export declare function handleResearch(dbPath: string, dataDir: string, args: ResearchArgs, options?: {
18
+ hookContext?: {
19
+ mode: 'mcp' | 'cli';
20
+ };
21
+ }): Promise<ResearchPackage>;
22
+ export {};
@@ -0,0 +1,225 @@
1
+ /**
2
+ * Research Handler - LLM-powered research synthesis
3
+ *
4
+ * Two modes:
5
+ * 1. AGENTIC (default): Uses Claude Agent SDK for iterative, thorough research
6
+ * 2. SIMPLE (fallback): Single-pass search + GPT-4o-mini synthesis
7
+ *
8
+ * Set LORE_RESEARCH_MODE=simple to use the fallback mode.
9
+ */
10
+ import OpenAI from 'openai';
11
+ import { searchSources } from '../../core/vector-store.js';
12
+ import { generateEmbedding } from '../../core/embedder.js';
13
+ import { loadArchivedProjects } from './archive-project.js';
14
+ import { runResearchAgent } from './research-agent.js';
15
+ import { getExtensionRegistry } from '../../extensions/registry.js';
16
+ // Lazy initialization for OpenAI (only used in simple mode)
17
+ let openaiClient = null;
18
+ function getOpenAI() {
19
+ if (!openaiClient) {
20
+ openaiClient = new OpenAI();
21
+ }
22
+ return openaiClient;
23
+ }
24
+ /**
25
+ * Use LLM to synthesize research findings with conflict awareness
26
+ */
27
+ async function synthesizeFindings(task, sources, quotes, decisions) {
28
+ // Sort sources by date for context (newest first in display)
29
+ const sortedSources = [...sources].sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
30
+ const sourceSummaries = sortedSources
31
+ .map((s, i) => {
32
+ const date = new Date(s.created_at).toLocaleDateString();
33
+ return `[${i + 1}] "${s.title}" (${s.source_type}, ${date}): ${s.summary}`;
34
+ })
35
+ .join('\n\n');
36
+ const quoteTexts = quotes
37
+ .slice(0, 15)
38
+ .map((q, i) => {
39
+ const speaker = q.speaker === 'user' ? '[User]' : '[Participant]';
40
+ return `${i + 1}. ${speaker} "${q.text}"${q.theme ? ` (Theme: ${q.theme})` : ''}`;
41
+ })
42
+ .join('\n');
43
+ const decisionTexts = decisions
44
+ .map((d, i) => `${i + 1}. ${d.content}`)
45
+ .join('\n');
46
+ const prompt = `You are a research analyst synthesizing findings from user research and conversations.
47
+
48
+ RESEARCH TASK: ${task}
49
+
50
+ SOURCES FOUND (${sources.length}, sorted newest first):
51
+ ${sourceSummaries || 'No sources found.'}
52
+
53
+ KEY QUOTES (${quotes.length} total, showing top 15):
54
+ ${quoteTexts || 'No quotes found.'}
55
+
56
+ RELATED DECISIONS:
57
+ ${decisionTexts || 'No decisions found.'}
58
+
59
+ Based on this evidence, provide a research synthesis in the following JSON format:
60
+ {
61
+ "summary": "A 2-3 sentence executive summary answering the research task with the CURRENT understanding",
62
+ "key_findings": ["Finding 1 with evidence", "Finding 2 with evidence", "Finding 3 with evidence"],
63
+ "conflicts_resolved": ["Description of any conflicting info and how it was resolved, e.g., 'Earlier (Jan 5) the approach was X, but a later decision (Jan 15) changed to Y. Current approach: Y'"],
64
+ "gaps_identified": ["Gap or unanswered question 1", "Gap 2"],
65
+ "suggested_queries": ["Follow-up research query 1", "Query 2"]
66
+ }
67
+
68
+ CRITICAL GUIDELINES:
69
+ - When sources contain CONFLICTING information, ALWAYS prefer the more recent source
70
+ - If you detect a pivot, decision change, or evolution in thinking, note it in conflicts_resolved
71
+ - The summary should reflect the CURRENT understanding, not historical positions
72
+ - Key findings should reflect the latest thinking, while acknowledging evolution where relevant
73
+ - If older sources contradict newer decisions, the newer decision takes precedence
74
+ - Include dates when noting conflicts or changes (e.g., "As of Jan 15, the approach is X")
75
+ - Be transparent about what changed and when - this helps users understand the lineage
76
+
77
+ Respond with only the JSON object.`;
78
+ try {
79
+ const response = await getOpenAI().chat.completions.create({
80
+ model: 'gpt-4o-mini',
81
+ messages: [{ role: 'user', content: prompt }],
82
+ temperature: 0.3,
83
+ response_format: { type: 'json_object' },
84
+ });
85
+ const content = response.choices[0]?.message?.content;
86
+ if (!content) {
87
+ throw new Error('No response from LLM');
88
+ }
89
+ const result = JSON.parse(content);
90
+ return {
91
+ summary: result.summary || `Research on "${task}" found ${sources.length} sources.`,
92
+ key_findings: result.key_findings || [],
93
+ conflicts_resolved: result.conflicts_resolved || [],
94
+ gaps_identified: result.gaps_identified || [],
95
+ suggested_queries: result.suggested_queries || [],
96
+ };
97
+ }
98
+ catch (error) {
99
+ console.error('Error synthesizing findings:', error);
100
+ // Fallback to simple synthesis
101
+ return {
102
+ summary: `Research on "${task}" found ${sources.length} relevant sources with ${quotes.length} supporting quotes.`,
103
+ key_findings: sources.slice(0, 3).map((s) => s.summary),
104
+ conflicts_resolved: [],
105
+ gaps_identified: [],
106
+ suggested_queries: [],
107
+ };
108
+ }
109
+ }
110
+ export async function handleResearch(dbPath, dataDir, args, options = {}) {
111
+ const { task, project, include_sources = true } = args;
112
+ // Check if we should use agentic mode (default) or simple mode (fallback)
113
+ const useAgenticMode = process.env.LORE_RESEARCH_MODE !== 'simple';
114
+ if (useAgenticMode) {
115
+ console.error('[research] Using agentic mode (Claude Agent SDK)');
116
+ try {
117
+ const result = await runResearchAgent(dbPath, dataDir, args);
118
+ await runResearchCompletedHook(result, {
119
+ mode: options.hookContext?.mode || 'mcp',
120
+ dataDir,
121
+ dbPath,
122
+ });
123
+ return result;
124
+ }
125
+ catch (error) {
126
+ console.error('[research] Agentic mode failed, falling back to simple mode:', error);
127
+ // Fall through to simple mode
128
+ }
129
+ }
130
+ console.error('[research] Using simple mode (single-pass synthesis)');
131
+ const result = await handleResearchSimple(dbPath, dataDir, args);
132
+ await runResearchCompletedHook(result, {
133
+ mode: options.hookContext?.mode || 'mcp',
134
+ dataDir,
135
+ dbPath,
136
+ });
137
+ return result;
138
+ }
139
+ /**
140
+ * Simple research mode - single pass search + synthesis
141
+ * This is the fallback when agentic mode fails or is disabled
142
+ */
143
+ async function handleResearchSimple(dbPath, dataDir, args) {
144
+ const { task, project, include_sources = true } = args;
145
+ // Use sensible defaults for simple mode
146
+ const sourceLimit = 10;
147
+ const quoteLimit = 25;
148
+ // Load archived projects to filter them out
149
+ const archivedProjects = await loadArchivedProjects(dataDir);
150
+ const archivedNames = new Set(archivedProjects.map((p) => p.project.toLowerCase()));
151
+ // Step 1: Search for relevant sources (fetch extra to account for archived filtering)
152
+ const queryVector = await generateEmbedding(task);
153
+ const rawSources = await searchSources(dbPath, queryVector, {
154
+ limit: sourceLimit * 2,
155
+ project,
156
+ });
157
+ // Filter out archived projects
158
+ const sources = rawSources
159
+ .filter((s) => !s.projects.some((p) => archivedNames.has(p.toLowerCase())))
160
+ .slice(0, sourceLimit);
161
+ // Step 2: Gather quotes from found sources (quotes are stored in source.quotes_json)
162
+ const allQuotes = [];
163
+ for (const source of sources) {
164
+ for (const quote of source.quotes) {
165
+ allQuotes.push({
166
+ ...quote,
167
+ citation: {
168
+ source_id: source.id,
169
+ context: source.title,
170
+ },
171
+ });
172
+ }
173
+ }
174
+ // Step 3: Synthesize findings with LLM (conflict-aware)
175
+ // Note: Decisions are now extracted at query time by the agentic research mode
176
+ const synthesis = await synthesizeFindings(task, sources.map((s) => ({
177
+ id: s.id,
178
+ title: s.title,
179
+ summary: s.summary,
180
+ source_type: s.source_type,
181
+ created_at: s.created_at,
182
+ })), allQuotes, [] // No pre-indexed decisions - agentic mode extracts them dynamically
183
+ );
184
+ const researchPackage = {
185
+ query: task,
186
+ project,
187
+ generated_at: new Date().toISOString(),
188
+ // LLM-synthesized findings
189
+ summary: synthesis.summary,
190
+ key_findings: synthesis.key_findings,
191
+ // Conflict resolution (shows evolution of thinking)
192
+ conflicts_resolved: synthesis.conflicts_resolved.length > 0 ? synthesis.conflicts_resolved : undefined,
193
+ // Evidence
194
+ supporting_quotes: allQuotes.slice(0, quoteLimit),
195
+ related_decisions: [],
196
+ // Sources
197
+ sources_consulted: include_sources
198
+ ? sources.map((s) => ({
199
+ id: s.id,
200
+ title: s.title,
201
+ source_type: s.source_type,
202
+ relevance: s.score,
203
+ }))
204
+ : [],
205
+ // LLM-identified gaps and suggestions
206
+ gaps_identified: synthesis.gaps_identified,
207
+ suggested_queries: synthesis.suggested_queries,
208
+ };
209
+ return researchPackage;
210
+ }
211
+ async function runResearchCompletedHook(result, context) {
212
+ try {
213
+ const registry = await getExtensionRegistry({
214
+ logger: (message) => console.error(message),
215
+ });
216
+ await registry.runHook('onResearchCompleted', result, {
217
+ mode: context.mode,
218
+ dataDir: context.dataDir,
219
+ dbPath: context.dbPath,
220
+ });
221
+ }
222
+ catch (error) {
223
+ console.error('[extensions] Failed to run onResearchCompleted hook:', error);
224
+ }
225
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Retain Handler - Save insights, decisions, and notes
3
+ *
4
+ * This is the "push" mechanism for adding knowledge explicitly.
5
+ * Retained items are immediately added to the vector store for instant searchability.
6
+ * Auto-pushes to git remote if configured.
7
+ */
8
+ interface RetainArgs {
9
+ content: string;
10
+ project: string;
11
+ type: 'insight' | 'decision' | 'requirement' | 'note';
12
+ source_context?: string;
13
+ tags?: string[];
14
+ }
15
+ export declare function handleRetain(dbPath: string, dataDir: string, args: RetainArgs, options?: {
16
+ autoPush?: boolean;
17
+ }): Promise<unknown>;
18
+ export {};