@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,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
|
+
}
|