@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,372 @@
1
+ /**
2
+ * Ask handlers for the Lore Document Browser TUI
3
+ *
4
+ * Handles AI-powered queries with streaming responses.
5
+ * Supports slash commands and multi-turn conversations.
6
+ *
7
+ * Slash commands:
8
+ * /project <name> or /p <name> - Set project filter
9
+ * /type <type> or /t <type> - Set content type filter
10
+ * /clear - Clear all filters
11
+ * /new - Start new conversation
12
+ * /help or /? - Show available commands
13
+ */
14
+ import Anthropic from '@anthropic-ai/sdk';
15
+ import { searchSources } from '../core/vector-store.js';
16
+ import { generateEmbedding } from '../core/embedder.js';
17
+ import { showProjectPicker, showContentTypeFilter } from './browse-handlers.js';
18
+ const SYSTEM_PROMPT = `You are a research assistant with access to a knowledge base.
19
+ Your job is to answer questions based on the provided sources.
20
+
21
+ When answering:
22
+ - Cite specific sources when making claims
23
+ - Be concise but thorough
24
+ - If the sources don't contain enough information, say so
25
+ - Consider previous conversation context when answering follow-up questions
26
+
27
+ Source format: Each source has an ID, title, and content summary.`;
28
+ const CONTENT_TYPES = ['interview', 'meeting', 'conversation', 'document', 'note', 'analysis'];
29
+ /**
30
+ * Build the filter display string
31
+ */
32
+ function getFilterDisplay(state) {
33
+ const filters = [];
34
+ if (state.currentProject)
35
+ filters.push(`project: ${state.currentProject}`);
36
+ if (state.currentContentType)
37
+ filters.push(`type: ${state.currentContentType}`);
38
+ const filterInfo = filters.length > 0
39
+ ? `{yellow-fg}Scope: ${filters.join(', ')}{/yellow-fg}`
40
+ : '{blue-fg}No filters{/blue-fg}';
41
+ const footerNote = filters.length > 0
42
+ ? `{yellow-fg}${filters.join(', ')}{/yellow-fg}`
43
+ : '{blue-fg}all sources{/blue-fg}';
44
+ return { filters, filterInfo, footerNote };
45
+ }
46
+ /**
47
+ * Render the conversation history and prompt
48
+ */
49
+ function renderAskPane(state, ui, showInput = true) {
50
+ const { filterInfo } = getFilterDisplay(state);
51
+ const lines = [];
52
+ // Show filter status at top (using brighter colors for dark terminals)
53
+ lines.push(`${filterInfo} {blue-fg}│{/blue-fg} {white-fg}/help{/white-fg} for commands {blue-fg}│{/blue-fg} {white-fg}/new{/white-fg} to start fresh`);
54
+ lines.push('');
55
+ // Show conversation history
56
+ if (state.askHistory.length > 0) {
57
+ for (const msg of state.askHistory) {
58
+ if (msg.role === 'user') {
59
+ lines.push(`{cyan-fg}You:{/cyan-fg} ${msg.content}`);
60
+ }
61
+ else {
62
+ // Escape blessed tags in assistant response
63
+ const escaped = msg.content
64
+ .replace(/\{/g, '\\{')
65
+ .replace(/\}/g, '\\}');
66
+ lines.push(`{green-fg}Assistant:{/green-fg}`);
67
+ lines.push(escaped);
68
+ }
69
+ lines.push('');
70
+ }
71
+ }
72
+ // Show current streaming response if any
73
+ if (state.askStreaming && state.askResponse) {
74
+ lines.push(`{cyan-fg}You:{/cyan-fg} ${state.askQuery}`);
75
+ lines.push(`{green-fg}Assistant:{/green-fg}`);
76
+ const escaped = state.askResponse
77
+ .replace(/\{/g, '\\{')
78
+ .replace(/\}/g, '\\}');
79
+ lines.push(escaped);
80
+ }
81
+ else if (showInput && state.askHistory.length === 0) {
82
+ lines.push('{blue-fg}Ask a question about your knowledge base...{/blue-fg}');
83
+ }
84
+ ui.askPane.setContent(lines.join('\n'));
85
+ }
86
+ /**
87
+ * Update the footer for ask mode
88
+ */
89
+ function updateAskFooter(ui, state, status) {
90
+ const { footerNote } = getFilterDisplay(state);
91
+ if (status) {
92
+ ui.footer.setContent(` ${status} │ Scope: ${footerNote}`);
93
+ }
94
+ else if (state.askStreaming) {
95
+ ui.footer.setContent(` Generating... │ Scope: ${footerNote}`);
96
+ }
97
+ else {
98
+ const historyNote = state.askHistory.length > 0 ? `${state.askHistory.length / 2} Q&A │ ` : '';
99
+ ui.footer.setContent(` ${historyNote}Enter: Send │ Esc: Back │ Scope: ${footerNote}`);
100
+ }
101
+ }
102
+ /**
103
+ * Enter ask mode - show input for query
104
+ */
105
+ export function enterAskMode(state, ui) {
106
+ state.mode = 'ask';
107
+ state.askQuery = '';
108
+ state.askResponse = '';
109
+ state.askStreaming = false;
110
+ // Don't clear history - allow continuing conversation
111
+ // Hide list/preview panes
112
+ ui.listPane.hide();
113
+ ui.previewPane.hide();
114
+ // Show ask UI
115
+ ui.askInput.show();
116
+ ui.askInput.setValue('');
117
+ ui.askPane.show();
118
+ ui.askPane.setLabel(' Ask Lore ');
119
+ renderAskPane(state, ui);
120
+ updateAskFooter(ui, state);
121
+ ui.askInput.focus();
122
+ ui.askInput.readInput();
123
+ ui.screen.render();
124
+ }
125
+ /**
126
+ * Exit ask mode - return to list
127
+ */
128
+ export function exitAskMode(state, ui) {
129
+ state.mode = 'list';
130
+ state.askQuery = '';
131
+ state.askResponse = '';
132
+ state.askStreaming = false;
133
+ // Keep askHistory for when they come back
134
+ // Clear ask-mode filters so they don't affect list operations
135
+ state.currentProject = undefined;
136
+ state.currentContentType = undefined;
137
+ ui.askInput.hide();
138
+ ui.askInput.setValue('');
139
+ ui.askPane.hide();
140
+ ui.listPane.show();
141
+ ui.previewPane.show();
142
+ ui.footer.setContent(' j/k Nav │ / Search │ a Ask │ R Research │ p Proj │ c Type │ m Move │ i Edit │ Esc Quit │ ? Help');
143
+ ui.listContent.focus();
144
+ ui.screen.render();
145
+ }
146
+ /**
147
+ * Show help for slash commands
148
+ */
149
+ function showCommandHelp(ui, state) {
150
+ const helpText = `{bold}Slash Commands:{/bold}
151
+
152
+ /p or /project Show project picker
153
+ /p <name> Set project filter directly
154
+ /t or /type Show content type picker
155
+ /t <type> Set type filter directly
156
+ (interview, meeting, conversation,
157
+ document, note, analysis)
158
+ /clear Clear all filters
159
+ /new Start new conversation (clear history)
160
+ /help or /? Show this help
161
+
162
+ {bold}Current filters:{/bold}
163
+ Project: ${state.currentProject || '(none)'}
164
+ Type: ${state.currentContentType || '(none)'}
165
+
166
+ {blue-fg}Press Enter to continue...{/blue-fg}`;
167
+ ui.askPane.setContent(helpText);
168
+ ui.screen.render();
169
+ }
170
+ /**
171
+ * Handle slash commands
172
+ * Returns true if input was a command (handled), false if it's a question
173
+ */
174
+ async function handleSlashCommand(input, state, ui, dbPath) {
175
+ const trimmed = input.trim();
176
+ if (!trimmed.startsWith('/'))
177
+ return false;
178
+ const parts = trimmed.slice(1).split(/\s+/);
179
+ const cmd = parts[0].toLowerCase();
180
+ const arg = parts.slice(1).join(' ');
181
+ switch (cmd) {
182
+ case 'help':
183
+ case '?':
184
+ showCommandHelp(ui, state);
185
+ return true;
186
+ case 'new':
187
+ state.askHistory = [];
188
+ state.askResponse = '';
189
+ renderAskPane(state, ui);
190
+ updateAskFooter(ui, state, 'Conversation cleared');
191
+ ui.screen.render();
192
+ return true;
193
+ case 'clear':
194
+ state.currentProject = undefined;
195
+ state.currentContentType = undefined;
196
+ renderAskPane(state, ui);
197
+ updateAskFooter(ui, state, 'Filters cleared');
198
+ ui.screen.render();
199
+ return true;
200
+ case 'p':
201
+ case 'project': {
202
+ if (!arg) {
203
+ // Show interactive project picker
204
+ state.pickerReturnMode = 'ask';
205
+ ui.askInput.hide();
206
+ ui.askPane.hide();
207
+ showProjectPicker(state, ui, dbPath);
208
+ return true;
209
+ }
210
+ state.currentProject = arg;
211
+ renderAskPane(state, ui);
212
+ updateAskFooter(ui, state, `Project set to: ${arg}`);
213
+ ui.screen.render();
214
+ return true;
215
+ }
216
+ case 't':
217
+ case 'type': {
218
+ if (!arg) {
219
+ // Show interactive content type picker
220
+ state.pickerReturnMode = 'ask';
221
+ ui.askInput.hide();
222
+ ui.askPane.hide();
223
+ showContentTypeFilter(state, ui);
224
+ return true;
225
+ }
226
+ if (!CONTENT_TYPES.includes(arg)) {
227
+ ui.askPane.setContent(`{red-fg}Unknown type: ${arg}{/red-fg}\n\nAvailable: ${CONTENT_TYPES.join(', ')}`);
228
+ ui.screen.render();
229
+ return true;
230
+ }
231
+ state.currentContentType = arg;
232
+ renderAskPane(state, ui);
233
+ updateAskFooter(ui, state, `Type set to: ${arg}`);
234
+ ui.screen.render();
235
+ return true;
236
+ }
237
+ default:
238
+ ui.askPane.setContent(`{red-fg}Unknown command: /${cmd}{/red-fg}\n\nType /help for available commands.`);
239
+ ui.screen.render();
240
+ return true;
241
+ }
242
+ }
243
+ /**
244
+ * Prompt for next input after a response
245
+ */
246
+ export function promptForFollowUp(state, ui) {
247
+ ui.askInput.show();
248
+ ui.askInput.setValue('');
249
+ ui.askInput.focus();
250
+ ui.askInput.readInput();
251
+ updateAskFooter(ui, state);
252
+ ui.screen.render();
253
+ }
254
+ /**
255
+ * Execute the ask query with streaming
256
+ */
257
+ export async function executeAsk(state, ui, dbPath, query) {
258
+ const trimmed = query.trim();
259
+ if (!trimmed) {
260
+ renderAskPane(state, ui);
261
+ promptForFollowUp(state, ui);
262
+ return;
263
+ }
264
+ // Check for slash commands
265
+ if (await handleSlashCommand(trimmed, state, ui, dbPath)) {
266
+ promptForFollowUp(state, ui);
267
+ return;
268
+ }
269
+ if (!process.env.ANTHROPIC_API_KEY) {
270
+ ui.askPane.setContent('{red-fg}Error: ANTHROPIC_API_KEY not set{/red-fg}');
271
+ ui.screen.render();
272
+ return;
273
+ }
274
+ state.askQuery = trimmed;
275
+ state.askStreaming = true;
276
+ state.askResponse = '';
277
+ ui.askInput.hide();
278
+ renderAskPane(state, ui);
279
+ updateAskFooter(ui, state, 'Searching knowledge base...');
280
+ ui.screen.render();
281
+ try {
282
+ // Search for relevant sources
283
+ const embedding = await generateEmbedding(trimmed);
284
+ let sources = await searchSources(dbPath, embedding, {
285
+ limit: 20,
286
+ project: state.currentProject || undefined,
287
+ content_type: state.currentContentType || undefined,
288
+ queryText: trimmed,
289
+ mode: 'hybrid',
290
+ });
291
+ // Double-check content type filter
292
+ if (state.currentContentType) {
293
+ sources = sources.filter(s => s.content_type === state.currentContentType);
294
+ }
295
+ if (sources.length === 0) {
296
+ state.askStreaming = false;
297
+ // Add to history as a failed query
298
+ state.askHistory.push({ role: 'user', content: trimmed });
299
+ state.askHistory.push({ role: 'assistant', content: 'No relevant sources found for this query.' });
300
+ renderAskPane(state, ui);
301
+ promptForFollowUp(state, ui);
302
+ return;
303
+ }
304
+ // Build source context
305
+ const sourceContext = sources.map((s, i) => {
306
+ const parts = [`[Source ${i + 1}: ${s.title}]`];
307
+ if (s.summary)
308
+ parts.push(`Summary: ${s.summary}`);
309
+ if (s.themes?.length) {
310
+ parts.push(`Themes: ${s.themes.map(t => t.name).join(', ')}`);
311
+ for (const theme of s.themes.slice(0, 3)) {
312
+ if (theme.summary) {
313
+ parts.push(` ${theme.name}: ${theme.summary}`);
314
+ }
315
+ }
316
+ }
317
+ if (s.quotes?.length) {
318
+ parts.push('Key quotes:');
319
+ for (const q of s.quotes.slice(0, 8)) {
320
+ const speaker = q.speaker_name || q.speaker || '';
321
+ const speakerPrefix = speaker ? `[${speaker}] ` : '';
322
+ parts.push(` ${speakerPrefix}"${q.text}"`);
323
+ }
324
+ }
325
+ return parts.join('\n');
326
+ }).join('\n\n---\n\n');
327
+ updateAskFooter(ui, state, `Found ${sources.length} sources. Thinking...`);
328
+ ui.screen.render();
329
+ // Build messages array with history for multi-turn
330
+ const messages = [];
331
+ // Add conversation history (without source context for brevity)
332
+ for (const msg of state.askHistory) {
333
+ messages.push({ role: msg.role, content: msg.content });
334
+ }
335
+ // Add current question with source context
336
+ messages.push({
337
+ role: 'user',
338
+ content: `Question: ${trimmed}\n\n---\nSources:\n${sourceContext}`,
339
+ });
340
+ // Stream response
341
+ const anthropic = new Anthropic();
342
+ const stream = anthropic.messages.stream({
343
+ model: 'claude-sonnet-4-20250514',
344
+ max_tokens: 4096,
345
+ system: SYSTEM_PROMPT,
346
+ messages,
347
+ });
348
+ let response = '';
349
+ stream.on('text', (text) => {
350
+ response += text;
351
+ state.askResponse = response;
352
+ renderAskPane(state, ui);
353
+ ui.screen.render();
354
+ });
355
+ await stream.finalMessage();
356
+ state.askStreaming = false;
357
+ // Add to conversation history
358
+ state.askHistory.push({ role: 'user', content: trimmed });
359
+ state.askHistory.push({ role: 'assistant', content: response });
360
+ state.askResponse = '';
361
+ renderAskPane(state, ui);
362
+ promptForFollowUp(state, ui);
363
+ }
364
+ catch (error) {
365
+ state.askStreaming = false;
366
+ const errorMsg = error instanceof Error ? error.message : String(error);
367
+ state.askHistory.push({ role: 'user', content: trimmed });
368
+ state.askHistory.push({ role: 'assistant', content: `Error: ${errorMsg}` });
369
+ renderAskPane(state, ui);
370
+ promptForFollowUp(state, ui);
371
+ }
372
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Autocomplete handlers for Ask/Research mode slash commands
3
+ *
4
+ * Provides typeahead suggestions as you type:
5
+ * - "/" shows available commands
6
+ * - "/p " or "/project " shows projects
7
+ * - "/t " or "/type " shows content types
8
+ */
9
+ import type { BrowserState, UIComponents } from './browse-types.js';
10
+ /**
11
+ * Update autocomplete based on current input
12
+ */
13
+ export declare function updateAutocomplete(state: BrowserState, ui: UIComponents, dbPath: string, inputValue: string): Promise<void>;
14
+ /**
15
+ * Render the autocomplete dropdown
16
+ */
17
+ export declare function renderAutocomplete(state: BrowserState, ui: UIComponents): void;
18
+ /**
19
+ * Hide the autocomplete dropdown
20
+ */
21
+ export declare function hideAutocomplete(state: BrowserState, ui: UIComponents): void;
22
+ /**
23
+ * Navigate autocomplete down
24
+ */
25
+ export declare function autocompleteDown(state: BrowserState, ui: UIComponents): boolean;
26
+ /**
27
+ * Navigate autocomplete up
28
+ */
29
+ export declare function autocompleteUp(state: BrowserState, ui: UIComponents): boolean;
30
+ /**
31
+ * Result from autocomplete selection
32
+ */
33
+ export interface AutocompleteResult {
34
+ type: 'input' | 'project' | 'contentType';
35
+ value: string;
36
+ }
37
+ /**
38
+ * Select current autocomplete option
39
+ * Returns what was selected so caller can handle appropriately
40
+ */
41
+ export declare function autocompleteSelect(state: BrowserState, ui: UIComponents): AutocompleteResult | null;
42
+ /**
43
+ * Check if autocomplete handled a key event
44
+ * Returns true if the key was handled (should not propagate)
45
+ */
46
+ export declare function handleAutocompleteKey(state: BrowserState, ui: UIComponents, key: string): {
47
+ handled: boolean;
48
+ result?: AutocompleteResult;
49
+ };
@@ -0,0 +1,270 @@
1
+ /**
2
+ * Autocomplete handlers for Ask/Research mode slash commands
3
+ *
4
+ * Provides typeahead suggestions as you type:
5
+ * - "/" shows available commands
6
+ * - "/p " or "/project " shows projects
7
+ * - "/t " or "/type " shows content types
8
+ */
9
+ import { getAllSources } from '../core/vector-store.js';
10
+ // Command definitions
11
+ const SLASH_COMMANDS = [
12
+ { value: '/p', label: '/p', description: 'Select project filter' },
13
+ { value: '/project', label: '/project', description: 'Select project filter' },
14
+ { value: '/t', label: '/t', description: 'Select content type filter' },
15
+ { value: '/type', label: '/type', description: 'Select content type filter' },
16
+ { value: '/clear', label: '/clear', description: 'Clear all filters' },
17
+ { value: '/new', label: '/new', description: 'Start fresh conversation' },
18
+ { value: '/help', label: '/help', description: 'Show command help' },
19
+ ];
20
+ // Content types
21
+ const CONTENT_TYPES = [
22
+ { value: 'interview', label: 'interview', description: 'User interviews' },
23
+ { value: 'meeting', label: 'meeting', description: 'Meeting notes' },
24
+ { value: 'conversation', label: 'conversation', description: 'AI conversations' },
25
+ { value: 'document', label: 'document', description: 'Documents' },
26
+ { value: 'note', label: 'note', description: 'Notes' },
27
+ { value: 'analysis', label: 'analysis', description: 'Analysis documents' },
28
+ ];
29
+ // Cache for projects
30
+ let projectsCache = null;
31
+ let projectsCacheTime = 0;
32
+ const CACHE_TTL = 60000; // 1 minute
33
+ /**
34
+ * Load projects (with caching)
35
+ */
36
+ async function loadProjects(dbPath) {
37
+ const now = Date.now();
38
+ if (projectsCache && now - projectsCacheTime < CACHE_TTL) {
39
+ return projectsCache.map(p => ({
40
+ value: p.name,
41
+ label: p.name,
42
+ description: `${p.count} docs`,
43
+ }));
44
+ }
45
+ try {
46
+ const sources = await getAllSources(dbPath, { limit: 1000 });
47
+ const projectMap = new Map();
48
+ for (const source of sources) {
49
+ for (const project of source.projects) {
50
+ const existing = projectMap.get(project);
51
+ if (!existing) {
52
+ projectMap.set(project, { count: 1, latest: source.created_at });
53
+ }
54
+ else {
55
+ existing.count++;
56
+ if (source.created_at > existing.latest) {
57
+ existing.latest = source.created_at;
58
+ }
59
+ }
60
+ }
61
+ }
62
+ projectsCache = Array.from(projectMap.entries())
63
+ .map(([name, data]) => ({
64
+ name,
65
+ count: data.count,
66
+ latestActivity: data.latest,
67
+ }))
68
+ .sort((a, b) => b.count - a.count);
69
+ projectsCacheTime = now;
70
+ return projectsCache.map(p => ({
71
+ value: p.name,
72
+ label: p.name,
73
+ description: `${p.count} docs`,
74
+ }));
75
+ }
76
+ catch {
77
+ return [];
78
+ }
79
+ }
80
+ /**
81
+ * Update autocomplete based on current input
82
+ */
83
+ export async function updateAutocomplete(state, ui, dbPath, inputValue) {
84
+ const trimmed = inputValue.trim();
85
+ // No autocomplete for empty or non-slash input
86
+ if (!trimmed.startsWith('/')) {
87
+ hideAutocomplete(state, ui);
88
+ return;
89
+ }
90
+ // Check if we're typing a command or have completed one
91
+ const parts = trimmed.split(/\s+/);
92
+ const cmd = parts[0].toLowerCase();
93
+ const hasSpace = trimmed.includes(' ');
94
+ const arg = hasSpace ? parts.slice(1).join(' ') : '';
95
+ // Project selection: /p <arg> or /project <arg>
96
+ if ((cmd === '/p' || cmd === '/project') && hasSpace) {
97
+ const projects = await loadProjects(dbPath);
98
+ const filtered = arg
99
+ ? projects.filter(p => p.label.toLowerCase().includes(arg.toLowerCase()))
100
+ : projects;
101
+ if (filtered.length > 0) {
102
+ state.autocompleteType = 'project';
103
+ state.autocompleteOptions = filtered.slice(0, 8);
104
+ state.autocompleteIndex = 0;
105
+ state.autocompleteVisible = true;
106
+ renderAutocomplete(state, ui);
107
+ }
108
+ else {
109
+ hideAutocomplete(state, ui);
110
+ }
111
+ return;
112
+ }
113
+ // Type selection: /t <arg> or /type <arg>
114
+ if ((cmd === '/t' || cmd === '/type') && hasSpace) {
115
+ const filtered = arg
116
+ ? CONTENT_TYPES.filter(t => t.label.toLowerCase().includes(arg.toLowerCase()))
117
+ : CONTENT_TYPES;
118
+ if (filtered.length > 0) {
119
+ state.autocompleteType = 'type';
120
+ state.autocompleteOptions = filtered;
121
+ state.autocompleteIndex = 0;
122
+ state.autocompleteVisible = true;
123
+ renderAutocomplete(state, ui);
124
+ }
125
+ else {
126
+ hideAutocomplete(state, ui);
127
+ }
128
+ return;
129
+ }
130
+ // Command suggestions: starts with / but no space yet
131
+ if (!hasSpace) {
132
+ const filtered = SLASH_COMMANDS.filter(c => c.value.toLowerCase().startsWith(cmd.toLowerCase()));
133
+ if (filtered.length > 0 && cmd !== filtered[0].value) {
134
+ // Only show if not exact match
135
+ state.autocompleteType = 'command';
136
+ state.autocompleteOptions = filtered;
137
+ state.autocompleteIndex = 0;
138
+ state.autocompleteVisible = true;
139
+ renderAutocomplete(state, ui);
140
+ }
141
+ else {
142
+ hideAutocomplete(state, ui);
143
+ }
144
+ return;
145
+ }
146
+ hideAutocomplete(state, ui);
147
+ }
148
+ /**
149
+ * Render the autocomplete dropdown
150
+ */
151
+ export function renderAutocomplete(state, ui) {
152
+ if (!state.autocompleteVisible || state.autocompleteOptions.length === 0) {
153
+ ui.autocompleteDropdown.hide();
154
+ return;
155
+ }
156
+ const lines = [];
157
+ for (let i = 0; i < state.autocompleteOptions.length; i++) {
158
+ const opt = state.autocompleteOptions[i];
159
+ const isSelected = i === state.autocompleteIndex;
160
+ const prefix = isSelected ? '{inverse}' : '';
161
+ const suffix = isSelected ? '{/inverse}' : '';
162
+ const desc = opt.description ? ` {blue-fg}${opt.description}{/blue-fg}` : '';
163
+ lines.push(`${prefix} ${opt.label}${desc} ${suffix}`);
164
+ }
165
+ lines.push('');
166
+ lines.push('{blue-fg}Up/Down navigate Tab select Esc cancel{/blue-fg}');
167
+ ui.autocompleteDropdown.setContent(lines.join('\n'));
168
+ // Adjust height based on content
169
+ const height = Math.min(state.autocompleteOptions.length + 3, 12);
170
+ ui.autocompleteDropdown.height = height;
171
+ ui.autocompleteDropdown.show();
172
+ ui.screen.render();
173
+ }
174
+ /**
175
+ * Hide the autocomplete dropdown
176
+ */
177
+ export function hideAutocomplete(state, ui) {
178
+ if (state.autocompleteVisible) {
179
+ state.autocompleteVisible = false;
180
+ state.autocompleteOptions = [];
181
+ state.autocompleteIndex = 0;
182
+ state.autocompleteType = null;
183
+ ui.autocompleteDropdown.hide();
184
+ ui.screen.render();
185
+ }
186
+ }
187
+ /**
188
+ * Navigate autocomplete down
189
+ */
190
+ export function autocompleteDown(state, ui) {
191
+ if (!state.autocompleteVisible)
192
+ return false;
193
+ if (state.autocompleteIndex < state.autocompleteOptions.length - 1) {
194
+ state.autocompleteIndex++;
195
+ renderAutocomplete(state, ui);
196
+ }
197
+ return true;
198
+ }
199
+ /**
200
+ * Navigate autocomplete up
201
+ */
202
+ export function autocompleteUp(state, ui) {
203
+ if (!state.autocompleteVisible)
204
+ return false;
205
+ if (state.autocompleteIndex > 0) {
206
+ state.autocompleteIndex--;
207
+ renderAutocomplete(state, ui);
208
+ }
209
+ return true;
210
+ }
211
+ /**
212
+ * Select current autocomplete option
213
+ * Returns what was selected so caller can handle appropriately
214
+ */
215
+ export function autocompleteSelect(state, ui) {
216
+ if (!state.autocompleteVisible || state.autocompleteOptions.length === 0) {
217
+ return null;
218
+ }
219
+ const selected = state.autocompleteOptions[state.autocompleteIndex];
220
+ hideAutocomplete(state, ui);
221
+ switch (state.autocompleteType) {
222
+ case 'command':
223
+ // For commands like /p, /t, add a space to trigger next autocomplete
224
+ if (selected.value === '/p' || selected.value === '/project' ||
225
+ selected.value === '/t' || selected.value === '/type') {
226
+ return { type: 'input', value: selected.value + ' ' };
227
+ }
228
+ else {
229
+ return { type: 'input', value: selected.value };
230
+ }
231
+ case 'project':
232
+ // Directly return project name for immediate application
233
+ return { type: 'project', value: selected.value };
234
+ case 'type':
235
+ // Directly return type for immediate application
236
+ return { type: 'contentType', value: selected.value };
237
+ default:
238
+ return null;
239
+ }
240
+ }
241
+ /**
242
+ * Check if autocomplete handled a key event
243
+ * Returns true if the key was handled (should not propagate)
244
+ */
245
+ export function handleAutocompleteKey(state, ui, key) {
246
+ if (!state.autocompleteVisible) {
247
+ return { handled: false };
248
+ }
249
+ switch (key) {
250
+ case 'down':
251
+ autocompleteDown(state, ui);
252
+ return { handled: true };
253
+ case 'up':
254
+ autocompleteUp(state, ui);
255
+ return { handled: true };
256
+ case 'tab':
257
+ case 'return': {
258
+ const result = autocompleteSelect(state, ui);
259
+ if (result !== null) {
260
+ return { handled: true, result };
261
+ }
262
+ return { handled: false };
263
+ }
264
+ case 'escape':
265
+ hideAutocomplete(state, ui);
266
+ return { handled: true };
267
+ default:
268
+ return { handled: false };
269
+ }
270
+ }