k0ntext 3.3.0 → 3.3.1

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.
@@ -0,0 +1,324 @@
1
+ /**
2
+ * Indexing Progress Visualization
3
+ *
4
+ * Real-time progress display for indexing operations
5
+ */
6
+
7
+ import chalk from 'chalk';
8
+ import { K0NTEXT_THEME } from '../theme.js';
9
+ import ora, { Ora } from 'ora';
10
+
11
+ /**
12
+ * Indexing progress state
13
+ */
14
+ interface IndexingProgress {
15
+ total: number;
16
+ processed: number;
17
+ currentFile?: string;
18
+ stage: 'discovering' | 'indexing_docs' | 'indexing_code' | 'indexing_tools' | 'generating_embeddings' | 'complete';
19
+ startTime: number;
20
+ errors: string[];
21
+ }
22
+
23
+ /**
24
+ * Indexing statistics
25
+ */
26
+ interface IndexingStats {
27
+ docsIndexed: number;
28
+ codeIndexed: number;
29
+ configsIndexed: number;
30
+ embeddingsGenerated: number;
31
+ filesSkipped: number;
32
+ duration: number;
33
+ }
34
+
35
+ /**
36
+ * Indexing Progress Visualizer
37
+ */
38
+ export class IndexingProgressVisualizer {
39
+ private progress: IndexingProgress;
40
+ private stats: IndexingStats;
41
+ private spinner: Ora | null = null;
42
+
43
+ constructor() {
44
+ this.progress = {
45
+ total: 0,
46
+ processed: 0,
47
+ stage: 'discovering',
48
+ startTime: Date.now(),
49
+ errors: []
50
+ };
51
+
52
+ this.stats = {
53
+ docsIndexed: 0,
54
+ codeIndexed: 0,
55
+ configsIndexed: 0,
56
+ embeddingsGenerated: 0,
57
+ filesSkipped: 0,
58
+ duration: 0
59
+ };
60
+ }
61
+
62
+ /**
63
+ * Start a new indexing operation
64
+ */
65
+ start(totalFiles: number): void {
66
+ this.progress = {
67
+ total: totalFiles,
68
+ processed: 0,
69
+ stage: 'discovering',
70
+ startTime: Date.now(),
71
+ errors: []
72
+ };
73
+
74
+ this.stats = {
75
+ docsIndexed: 0,
76
+ codeIndexed: 0,
77
+ configsIndexed: 0,
78
+ embeddingsGenerated: 0,
79
+ filesSkipped: 0,
80
+ duration: 0
81
+ };
82
+
83
+ this.spinner = ora('Preparing to index...').start();
84
+ }
85
+
86
+ /**
87
+ * Update progress
88
+ */
89
+ update(stage: IndexingProgress['stage'], updates: {
90
+ processed?: number;
91
+ currentFile?: string;
92
+ error?: string;
93
+ }): void {
94
+ this.progress.stage = stage;
95
+
96
+ if (updates.processed !== undefined) {
97
+ this.progress.processed = updates.processed;
98
+ }
99
+
100
+ if (updates.currentFile) {
101
+ this.progress.currentFile = updates.currentFile;
102
+ }
103
+
104
+ if (updates.error) {
105
+ this.progress.errors.push(updates.error);
106
+ }
107
+
108
+ this.render();
109
+ }
110
+
111
+ /**
112
+ * Render current progress
113
+ */
114
+ private render(): void {
115
+ if (!this.spinner) return;
116
+
117
+ const percentage = Math.min(100, Math.round((this.progress.processed / this.progress.total) * 100));
118
+ const elapsed = Date.now() - this.progress.startTime;
119
+ const eta = this.progress.processed > 0
120
+ ? (elapsed / this.progress.processed) * (this.progress.total - this.progress.processed)
121
+ : 0;
122
+
123
+ // Update spinner text
124
+ const stageEmoji = {
125
+ discovering: '🔍',
126
+ indexing_docs: '📄',
127
+ indexing_code: '💻',
128
+ indexing_tools: '⚙️',
129
+ generating_embeddings: '🧠',
130
+ complete: '✓'
131
+ }[this.progress.stage];
132
+
133
+ let spinnerText = `${stageEmoji} ${this.getStageName(this.progress.stage)}: ${percentage}%`;
134
+
135
+ if (this.progress.currentFile) {
136
+ const fileName = this.progress.currentFile.split('/').pop()!;
137
+ spinnerText += ` (${K0NTEXT_THEME.dim(fileName.slice(0, 30))}${this.progress.currentFile.length > 30 ? '...' : ''})`;
138
+ }
139
+
140
+ if (eta > 1000) {
141
+ const etaText = this.formatDuration(eta);
142
+ spinnerText += ` ${K0NTEXT_THEME.dim(`ETA: ${etaText}`)}`;
143
+ }
144
+
145
+ this.spinner.text = spinnerText;
146
+
147
+ // Show detailed progress on intervals
148
+ if (this.progress.processed % 50 === 0 || this.progress.stage === 'complete') {
149
+ this.showDetailedProgress();
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Get stage name
155
+ */
156
+ private getStageName(stage: IndexingProgress['stage']): string {
157
+ const names = {
158
+ discovering: 'Discovering files',
159
+ indexing_docs: 'Indexing documents',
160
+ indexing_code: 'Indexing code',
161
+ indexing_tools: 'Indexing configs',
162
+ generating_embeddings: 'Generating embeddings',
163
+ complete: 'Complete'
164
+ };
165
+ return names[stage];
166
+ }
167
+
168
+ /**
169
+ * Show detailed progress panel
170
+ */
171
+ showDetailedProgress(): void {
172
+ const lines: string[] = [];
173
+
174
+ lines.push('');
175
+ lines.push(K0NTEXT_THEME.header('━━━ Indexing Progress ━━━'));
176
+
177
+ // Progress bar
178
+ lines.push(` Progress: ${this.renderProgressBar()}`);
179
+ lines.push(` Stage: ${this.getStageName(this.progress.stage)}`);
180
+ lines.push('');
181
+
182
+ // Stats
183
+ lines.push(K0NTEXT_THEME.header('━━━ Statistics ━──'));
184
+ lines.push(` Documents Indexed: ${K0NTEXT_THEME.cyan(this.stats.docsIndexed.toString())}`);
185
+ lines.push(` Code Files Indexed: ${K0NTEXT_THEME.cyan(this.stats.codeIndexed.toString())}`);
186
+ lines.push(` Configs Indexed: ${K0NTEXT_THEME.cyan(this.stats.configsIndexed.toString())}`);
187
+ lines.push(` Embeddings: ${K0NTEXT_THEME.cyan(this.stats.embeddingsGenerated.toString())}`);
188
+ lines.push(` Files Skipped: ${this.stats.filesSkipped > 0 ? K0NTEXT_THEME.warning(this.stats.filesSkipped.toString()) : K0NTEXT_THEME.dim('0')}`);
189
+ lines.push('');
190
+
191
+ // Errors
192
+ if (this.progress.errors.length > 0) {
193
+ lines.push(K0NTEXT_THEME.header('━━━ Errors ━──'));
194
+ for (const error of this.progress.errors.slice(-5)) {
195
+ lines.push(` ${K0NTEXT_THEME.error('✖')} ${error}`);
196
+ }
197
+ lines.push('');
198
+ }
199
+
200
+ // Clear and render
201
+ console.log(lines.join('\n'));
202
+ }
203
+
204
+ /**
205
+ * Render progress bar
206
+ */
207
+ private renderProgressBar(): string {
208
+ const percentage = Math.min(100, Math.round((this.progress.processed / this.progress.total) * 100));
209
+ const width = 30;
210
+ const filled = Math.floor((percentage / 100) * width);
211
+
212
+ const bar = K0NTEXT_THEME.primary('█'.repeat(filled)) + K0NTEXT_THEME.dim('░'.repeat(width - filled));
213
+ const pct = K0NTEXT_THEME.primary(`${percentage.toString().padStart(3)}%`);
214
+ const counts = `${this.progress.processed}/${this.progress.total}`;
215
+
216
+ return `${bar} ${pct} (${K0NTEXT_THEME.dim(counts)})`;
217
+ }
218
+
219
+ /**
220
+ * Format duration
221
+ */
222
+ private formatDuration(ms: number): string {
223
+ const seconds = Math.floor(ms / 1000);
224
+ const minutes = Math.floor(seconds / 60);
225
+ const hours = Math.floor(minutes / 60);
226
+
227
+ if (hours > 0) {
228
+ return `${hours}h ${minutes % 60}m`;
229
+ } else if (minutes > 0) {
230
+ return `${minutes}m ${seconds % 60}s`;
231
+ } else {
232
+ return `${seconds}s`;
233
+ }
234
+ }
235
+
236
+ /**
237
+ * Complete indexing
238
+ */
239
+ complete(stats: Partial<IndexingStats>): void {
240
+ this.progress.stage = 'complete';
241
+ this.progress.processed = this.progress.total;
242
+
243
+ this.stats = {
244
+ docsIndexed: stats.docsIndexed || 0,
245
+ codeIndexed: stats.codeIndexed || 0,
246
+ configsIndexed: stats.configsIndexed || 0,
247
+ embeddingsGenerated: stats.embeddingsGenerated || 0,
248
+ filesSkipped: stats.filesSkipped || 0,
249
+ duration: Date.now() - this.progress.startTime
250
+ };
251
+
252
+ if (this.spinner) {
253
+ this.spinner.succeed('Indexing complete!');
254
+ }
255
+
256
+ this.showFinalStats();
257
+ }
258
+
259
+ /**
260
+ * Show final statistics
261
+ */
262
+ showFinalStats(): void {
263
+ const lines: string[] = [];
264
+
265
+ lines.push('');
266
+ lines.push(K0NTEXT_THEME.success('━━━ Indexing Complete ━━━'));
267
+ lines.push('');
268
+
269
+ lines.push(` ${K0NTEXT_THEME.header('Files Processed:')}`);
270
+ lines.push(` Documents: ${K0NTEXT_THEME.cyan(this.stats.docsIndexed.toString())}`);
271
+ lines.push(` Code Files: ${K0NTEXT_THEME.cyan(this.stats.codeIndexed.toString())}`);
272
+ lines.push(` Configs: ${K0NTEXT_THEME.cyan(this.stats.configsIndexed.toString())}`);
273
+ lines.push(` Total: ${K0NTEXT_THEME.primary((this.stats.docsIndexed + this.stats.codeIndexed + this.stats.configsIndexed).toString())}`);
274
+ lines.push('');
275
+
276
+ if (this.stats.embeddingsGenerated > 0) {
277
+ lines.push(` ${K0NTEXT_THEME.header('Embeddings:')}`);
278
+ lines.push(` Generated: ${K0NTEXT_THEME.cyan(this.stats.embeddingsGenerated.toString())}`);
279
+ lines.push('');
280
+ }
281
+
282
+ if (this.stats.filesSkipped > 0) {
283
+ lines.push(` ${K0NTEXT_THEME.warning('⚠ Files Skipped:')}`);
284
+ lines.push(` ${this.stats.filesSkipped} files hit limit (use --max-files to increase)`);
285
+ lines.push('');
286
+ }
287
+
288
+ lines.push(` ${K0NTEXT_THEME.header('Time:')}`);
289
+ lines.push(` Duration: ${K0NTEXT_THEME.formatTimestamp(new Date(Date.now() - this.stats.duration))}`);
290
+ lines.push('');
291
+
292
+ console.log(lines.join('\n'));
293
+ }
294
+
295
+ /**
296
+ * Cancel indexing
297
+ */
298
+ cancel(): void {
299
+ if (this.spinner) {
300
+ this.spinner.warn('Indexing cancelled');
301
+ }
302
+
303
+ console.log('');
304
+ console.log(K0NTEXT_THEME.info('Indexing was cancelled.'));
305
+ console.log(K0NTEXT_THEME.dim('Partial results may have been saved.'));
306
+ }
307
+
308
+ /**
309
+ * Format a timestamp relative to now
310
+ */
311
+ private formatTimestamp(ms: number): string {
312
+ const seconds = Math.floor(ms / 1000);
313
+ const minutes = Math.floor(seconds / 60);
314
+ const hours = Math.floor(minutes / 60);
315
+
316
+ if (hours > 0) {
317
+ return `${hours}h ${minutes % 60}m ${seconds % 60}s ago`;
318
+ } else if (minutes > 0) {
319
+ return `${minutes}m ${seconds % 60}s ago`;
320
+ } else {
321
+ return `${seconds}s ago`;
322
+ }
323
+ }
324
+ }
@@ -0,0 +1,272 @@
1
+ /**
2
+ * Advanced Search Panel
3
+ *
4
+ * Enhanced search with filtering, sorting, and detailed results
5
+ */
6
+
7
+ import chalk from 'chalk';
8
+ import { K0NTEXT_THEME } from '../theme.js';
9
+ import type { ContextItem } from '../../../../db/client.js';
10
+ import type { ContextType } from '../../../../db/schema.js';
11
+
12
+ /**
13
+ * Search filter options
14
+ */
15
+ export interface SearchFilters {
16
+ type?: ContextType;
17
+ sortBy?: 'relevance' | 'name' | 'date' | 'size';
18
+ sortOrder?: 'asc' | 'desc';
19
+ limit?: number;
20
+ }
21
+
22
+ /**
23
+ * Enhanced search result with metadata
24
+ */
25
+ export interface EnhancedSearchResult {
26
+ item: ContextItem;
27
+ score?: number;
28
+ highlights: string[];
29
+ }
30
+
31
+ /**
32
+ * Advanced Search Panel
33
+ */
34
+ export class AdvancedSearchPanel {
35
+ /**
36
+ * Display advanced search results with formatting
37
+ */
38
+ displayResults(results: EnhancedSearchResult[], query: string, filters: SearchFilters): string {
39
+ const lines: string[] = [];
40
+
41
+ // Header with query and filters
42
+ lines.push('');
43
+ lines.push(K0NTEXT_THEME.header(`━━━ Search Results: "${query}" ━━━`));
44
+
45
+ if (filters.type) {
46
+ lines.push(` ${K0NTEXT_THEME.dim('Filter:')} ${K0NTEXT_THEME.cyan(filters.type)}`);
47
+ }
48
+ if (filters.sortBy) {
49
+ lines.push(` ${K0NTEXT_THEME.dim('Sorted by:')} ${K0NTEXT_THEME.cyan(filters.sortBy)} ${K0NTEXT_THEME.dim('(')}${filters.sortOrder}${K0NTEXT_THEME.dim(')')}`);
50
+ }
51
+ lines.push(` ${K0NTEXT_THEME.dim('Found:')} ${K0NTEXT_THEME.highlight(results.length.toString())} results`);
52
+ lines.push('');
53
+
54
+ // Type legend
55
+ lines.push(K0NTEXT_THEME.dim('Types: 📄=doc 💻=code ⚙️=config 📋=workflow 🤖=agent 🔧=command'));
56
+ lines.push('');
57
+
58
+ // Display results
59
+ for (let i = 0; i < Math.min(results.length, filters.limit || 20); i++) {
60
+ const { item, score, highlights } = results[i];
61
+
62
+ // Type emoji
63
+ const typeEmoji = this.getTypeEmoji(item.type);
64
+ const relevance = score !== undefined ? ` ${K0NTEXT_THEME.dim(`(${Math.round(score * 100)}%)`)}` : '';
65
+
66
+ lines.push(`${K0NTEXT_THEME.primary(`${i + 1}.`)} ${typeEmoji} ${K0NTEXT_THEME.highlight(item.name)}${relevance}`);
67
+
68
+ if (item.filePath) {
69
+ lines.push(` ${K0NTEXT_THEME.dim(item.filePath)}`);
70
+ }
71
+
72
+ // Show content preview
73
+ if (item.content) {
74
+ const preview = this.getContentPreview(item.content, query);
75
+ if (preview) {
76
+ lines.push(` ${K0NTEXT_THEME.dim(preview)}`);
77
+ }
78
+ }
79
+
80
+ // Show metadata
81
+ if (item.metadata) {
82
+ const meta = this.formatMetadata(item.metadata as Record<string, unknown>);
83
+ if (meta) {
84
+ lines.push(` ${K0NTEXT_THEME.dim(meta)}`);
85
+ }
86
+ }
87
+
88
+ lines.push('');
89
+ }
90
+
91
+ if (results.length > (filters.limit || 20)) {
92
+ const remaining = results.length - (filters.limit || 20);
93
+ lines.push(K0NTEXT_THEME.dim(` ... and ${remaining} more results`));
94
+ lines.push('');
95
+ }
96
+
97
+ // Search tips
98
+ lines.push(K0NTEXT_THEME.header('━━━ Search Tips ━━━'));
99
+ lines.push(` ${K0NTEXT_THEME.cyan('•')} Use ${K0NTEXT_THEME.highlight('search <query> --type <type>')} to filter`);
100
+ lines.push(` ${K0NTEXT_THEME.cyan('•')} Use ${K0NTEXT_THEME.highlight('search <query> --sort <field>')} to sort`);
101
+ lines.push(` ${K0NTEXT_THEME.cyan('•')} Use ${K0NTEXT_THEME.highlight('search <query> --limit <n>')} for more results`);
102
+ lines.push('');
103
+
104
+ return lines.join('\n');
105
+ }
106
+
107
+ /**
108
+ * Get emoji for context type
109
+ */
110
+ private getTypeEmoji(type: ContextType): string {
111
+ const emojis: Record<string, string> = {
112
+ doc: '📄',
113
+ code: '💻',
114
+ tool_config: '⚙️',
115
+ workflow: '📋',
116
+ agent: '🤖',
117
+ command: '🔧',
118
+ commit: '📝',
119
+ knowledge: '🧠',
120
+ config: '⚙️'
121
+ };
122
+ return emojis[type] || '📄';
123
+ }
124
+
125
+ /**
126
+ * Get content preview with highlighted query terms
127
+ */
128
+ private getContentPreview(content: string, query: string): string {
129
+ const maxLength = 100;
130
+ const preview = content.slice(0, maxLength);
131
+
132
+ // Highlight query terms
133
+ const terms = query.toLowerCase().split(/\s+/).filter(t => t.length > 2);
134
+ let highlighted = preview;
135
+
136
+ for (const term of terms) {
137
+ const regex = new RegExp(`(${this.escapeRegex(term)})`, 'gi');
138
+ highlighted = highlighted.replace(regex, K0NTEXT_THEME.highlight('$1'));
139
+ }
140
+
141
+ return highlighted + (content.length > maxLength ? '...' : '');
142
+ }
143
+
144
+ /**
145
+ * Escape special regex characters
146
+ */
147
+ private escapeRegex(string: string): string {
148
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
149
+ }
150
+
151
+ /**
152
+ * Format metadata for display
153
+ */
154
+ private formatMetadata(metadata: Record<string, unknown>): string | null {
155
+ const parts: string[] = [];
156
+
157
+ if (metadata.size !== undefined) {
158
+ parts.push(K0NTEXT_THEME.formatFileSize(Number(metadata.size)));
159
+ }
160
+
161
+ if (metadata.module) {
162
+ parts.push(`module:${metadata.module}`);
163
+ }
164
+
165
+ if (metadata.tool) {
166
+ parts.push(`tool:${metadata.tool}`);
167
+ }
168
+
169
+ return parts.length > 0 ? parts.join(' • ') : null;
170
+ }
171
+
172
+ /**
173
+ * Sort results by various criteria
174
+ */
175
+ sortResults(results: EnhancedSearchResult[], sortBy: 'relevance' | 'name' | 'date' | 'size', order: 'asc' | 'desc'): EnhancedSearchResult[] {
176
+ return results.sort((a, b) => {
177
+ let comparison = 0;
178
+
179
+ switch (sortBy) {
180
+ case 'relevance':
181
+ comparison = (b.score || 0) - (a.score || 0);
182
+ break;
183
+ case 'name':
184
+ comparison = a.item.name.localeCompare(b.item.name);
185
+ break;
186
+ case 'date':
187
+ const aDate = a.item.updatedAt ? new Date(a.item.updatedAt).getTime() : 0;
188
+ const bDate = b.item.updatedAt ? new Date(b.item.updatedAt).getTime() : 0;
189
+ comparison = bDate - aDate;
190
+ break;
191
+ case 'size':
192
+ const aSize = (a.item.metadata as Record<string, unknown>)?.size || 0;
193
+ const bSize = (b.item.metadata as Record<string, unknown>)?.size || 0;
194
+ comparison = Number(bSize) - Number(aSize);
195
+ break;
196
+ }
197
+
198
+ return order === 'desc' ? -comparison : comparison;
199
+ });
200
+ }
201
+
202
+ /**
203
+ * Filter results by type
204
+ */
205
+ filterByType(results: EnhancedSearchResult[], type: ContextType): EnhancedSearchResult[] {
206
+ return results.filter(r => r.item.type === type);
207
+ }
208
+
209
+ /**
210
+ * Parse search flags from command arguments
211
+ */
212
+ parseSearchFlags(args: string[]): SearchFilters {
213
+ const filters: SearchFilters = {};
214
+
215
+ for (let i = 0; i < args.length; i++) {
216
+ const arg = args[i];
217
+
218
+ switch (arg) {
219
+ case '--type':
220
+ case '-t':
221
+ filters.type = (args[++i] || 'all') as ContextType;
222
+ break;
223
+ case '--sort':
224
+ case '-s':
225
+ filters.sortBy = (args[++i] || 'relevance') as SearchFilters['sortBy'];
226
+ break;
227
+ case '--order':
228
+ case '-o':
229
+ filters.sortOrder = (args[++i] || 'desc') as SearchFilters['sortOrder'];
230
+ break;
231
+ case '--limit':
232
+ case '-l':
233
+ filters.limit = Number(args[++i]) || 20;
234
+ break;
235
+ }
236
+ }
237
+
238
+ return filters;
239
+ }
240
+
241
+ /**
242
+ * Show search help
243
+ */
244
+ showSearchHelp(): string {
245
+ const lines = [
246
+ '',
247
+ K0NTEXT_THEME.header('━━━ Advanced Search Help ━━━'),
248
+ '',
249
+ ' Usage:',
250
+ ' search <query> [options]',
251
+ '',
252
+ ' Options:',
253
+ ' --type, -t <type> Filter by type (doc, code, tool_config, workflow, agent, command)',
254
+ ' --sort, -s <field> Sort by (relevance, name, date, size)',
255
+ ' --order, -o <dir> Sort order (asc, desc)',
256
+ ' --limit, -l <n> Max results (default: 20)',
257
+ '',
258
+ ' Examples:',
259
+ ' search auth --type code',
260
+ ' search "user login" --sort date --order desc',
261
+ ' search config --limit 50',
262
+ '',
263
+ ' Shortcuts:',
264
+ ' s <query> Alias for search',
265
+ ' f <query> Search and filter',
266
+ ' ? Show this help',
267
+ ''
268
+ ];
269
+
270
+ return lines.join('\n');
271
+ }
272
+ }