k0ntext 3.2.1 → 3.3.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 (39) hide show
  1. package/dist/cli/index.js +28 -2
  2. package/dist/cli/index.js.map +1 -1
  3. package/dist/cli/repl/core/parser.d.ts +84 -0
  4. package/dist/cli/repl/core/parser.d.ts.map +1 -0
  5. package/dist/cli/repl/core/parser.js +309 -0
  6. package/dist/cli/repl/core/parser.js.map +1 -0
  7. package/dist/cli/repl/core/session.d.ts +124 -0
  8. package/dist/cli/repl/core/session.d.ts.map +1 -0
  9. package/dist/cli/repl/core/session.js +196 -0
  10. package/dist/cli/repl/core/session.js.map +1 -0
  11. package/dist/cli/repl/index.d.ts +56 -0
  12. package/dist/cli/repl/index.d.ts.map +1 -0
  13. package/dist/cli/repl/index.js +468 -0
  14. package/dist/cli/repl/index.js.map +1 -0
  15. package/dist/cli/repl/init/wizard.d.ts +61 -0
  16. package/dist/cli/repl/init/wizard.d.ts.map +1 -0
  17. package/dist/cli/repl/init/wizard.js +245 -0
  18. package/dist/cli/repl/init/wizard.js.map +1 -0
  19. package/dist/cli/repl/tui/theme.d.ts +109 -0
  20. package/dist/cli/repl/tui/theme.d.ts.map +1 -0
  21. package/dist/cli/repl/tui/theme.js +225 -0
  22. package/dist/cli/repl/tui/theme.js.map +1 -0
  23. package/dist/cli/repl/update/checker.d.ts +64 -0
  24. package/dist/cli/repl/update/checker.d.ts.map +1 -0
  25. package/dist/cli/repl/update/checker.js +166 -0
  26. package/dist/cli/repl/update/checker.js.map +1 -0
  27. package/dist/db/client.d.ts +1 -0
  28. package/dist/db/client.d.ts.map +1 -1
  29. package/dist/db/client.js +2 -1
  30. package/dist/db/client.js.map +1 -1
  31. package/package.json +4 -1
  32. package/src/cli/index.ts +28 -2
  33. package/src/cli/repl/core/parser.ts +373 -0
  34. package/src/cli/repl/core/session.ts +269 -0
  35. package/src/cli/repl/index.ts +554 -0
  36. package/src/cli/repl/init/wizard.ts +300 -0
  37. package/src/cli/repl/tui/theme.ts +276 -0
  38. package/src/cli/repl/update/checker.ts +209 -0
  39. package/src/db/client.ts +9 -7
@@ -0,0 +1,554 @@
1
+ /**
2
+ * K0ntext REPL Shell
3
+ *
4
+ * Interactive shell for managing k0ntext context
5
+ */
6
+
7
+ import readline from 'readline';
8
+ import { REPLSessionManager, ProjectType } from './core/session.js';
9
+ import { REPLCommandParser } from './core/parser.js';
10
+ import { InitWizard } from './init/wizard.js';
11
+ import { UpdateChecker } from './update/checker.js';
12
+ import { K0NTEXT_THEME, terminal } from './tui/theme.js';
13
+ import { createIntelligentAnalyzer } from '../../analyzer/intelligent-analyzer.js';
14
+ import { DatabaseClient } from '../../db/client.js';
15
+ import { hasOpenRouterKey } from '../../embeddings/openrouter.js';
16
+
17
+ /**
18
+ * REPL options
19
+ */
20
+ export interface REPLOptions {
21
+ projectRoot: string;
22
+ version: string;
23
+ noTUI?: boolean;
24
+ }
25
+
26
+ /**
27
+ * K0ntext REPL Shell
28
+ */
29
+ export class REPLShell {
30
+ private session: REPLSessionManager;
31
+ private parser: REPLCommandParser;
32
+ private updateChecker: UpdateChecker;
33
+ private projectRoot: string;
34
+ private version: string;
35
+ private readline: readline.Interface;
36
+ private isActive: boolean = false;
37
+ private noTUI: boolean;
38
+
39
+ constructor(options: REPLOptions) {
40
+ this.projectRoot = options.projectRoot;
41
+ this.version = options.version;
42
+ this.noTUI = options.noTUI || false;
43
+
44
+ this.session = new REPLSessionManager(this.projectRoot);
45
+ this.parser = new REPLCommandParser();
46
+ this.updateChecker = new UpdateChecker(options.version);
47
+
48
+ // Create readline interface
49
+ this.readline = readline.createInterface({
50
+ input: process.stdin,
51
+ output: process.stdout,
52
+ prompt: this.getPrompt()
53
+ });
54
+
55
+ this.setupEventHandlers();
56
+ this.registerCommands();
57
+ }
58
+
59
+ /**
60
+ * Get the command prompt
61
+ */
62
+ private getPrompt(): string {
63
+ const isInit = this.session.isInitialized();
64
+ const symbol = isInit ? '█' : '?';
65
+ return K0NTEXT_THEME.primary(`k0ntext${symbol} `);
66
+ }
67
+
68
+ /**
69
+ * Setup event handlers
70
+ */
71
+ private setupEventHandlers(): void {
72
+ this.readline.on('line', async (input) => {
73
+ if (!this.isActive) return;
74
+
75
+ const trimmed = input.trim();
76
+
77
+ if (!trimmed) {
78
+ this.readline.prompt();
79
+ return;
80
+ }
81
+
82
+ // Handle exit commands
83
+ if (trimmed.toLowerCase() === 'exit' || trimmed.toLowerCase() === 'quit') {
84
+ await this.stop();
85
+ return;
86
+ }
87
+
88
+ // Parse and execute command
89
+ const parsed = this.parser.parse(trimmed);
90
+ if (parsed) {
91
+ const startTime = Date.now();
92
+ const result = await this.parser.execute(parsed);
93
+ const duration = Date.now() - startTime;
94
+
95
+ this.session.addCommand(trimmed, result.output, duration);
96
+
97
+ if (result.output) {
98
+ console.log('');
99
+ console.log(result.output);
100
+ }
101
+
102
+ if (result.error) {
103
+ console.log('');
104
+ console.log(K0NTEXT_THEME.error(`✖ ${result.error}`));
105
+ }
106
+ } else {
107
+ console.log(K0NTEXT_THEME.warning('\n⚠ Invalid command. Type "help" for available commands.'));
108
+ }
109
+
110
+ this.readline.prompt();
111
+ });
112
+
113
+ this.readline.on('SIGINT', async () => {
114
+ console.log('');
115
+ console.log(K0NTEXT_THEME.warning('\n⚠ Use "exit" to quit the REPL.'));
116
+ this.readline.prompt();
117
+ });
118
+
119
+ this.readline.on('close', async () => {
120
+ await this.stop();
121
+ });
122
+ }
123
+
124
+ /**
125
+ * Register REPL-specific commands
126
+ */
127
+ private registerCommands(): void {
128
+ // Stats command
129
+ this.parser.registerCommand({
130
+ name: 'stats',
131
+ description: 'Show database and session statistics',
132
+ usage: 'stats',
133
+ examples: ['stats'],
134
+ handler: async () => {
135
+ const db = new DatabaseClient(this.projectRoot);
136
+ const dbStats = db.getStats();
137
+ const sessionStats = this.session.getStats();
138
+ const duration = this.session.getDuration();
139
+
140
+ const output = [
141
+ '',
142
+ K0NTEXT_THEME.header('━━━ Database Statistics ━━━'),
143
+ ` ${K0NTEXT_THEME.cyan('•')} Context Items: ${dbStats.items}`,
144
+ ` ${K0NTEXT_THEME.cyan('•')} Relations: ${dbStats.relations}`,
145
+ ` ${K0NTEXT_THEME.cyan('•')} Git Commits: ${dbStats.commits}`,
146
+ ` ${K0NTEXT_THEME.cyan('•')} Embeddings: ${dbStats.embeddings}`,
147
+ ` ${K0NTEXT_THEME.cyan('•')} Tool Configs: ${dbStats.toolConfigs}`,
148
+ ` ${K0NTEXT_THEME.cyan('•')} Database Path: ${dbStats.path || '.k0ntext.db'}`,
149
+ '',
150
+ K0NTEXT_THEME.header('━━━ Session Statistics ━━━'),
151
+ ` ${K0NTEXT_THEME.cyan('•')} Duration: ${duration.human}`,
152
+ ` ${K0NTEXT_THEME.cyan('•')} Commands Run: ${sessionStats.commandsExecuted}`,
153
+ ` ${K0NTEXT_THEME.cyan('•')} Searches: ${sessionStats.searchesPerformed}`,
154
+ ` ${K0NTEXT_THEME.cyan('•')} Files Indexed: ${sessionStats.filesIndexed}`,
155
+ ` ${K0NTEXT_THEME.cyan('•')} Embeddings: ${sessionStats.embeddingsGenerated}`,
156
+ ''
157
+ ];
158
+
159
+ db.close();
160
+
161
+ return { success: true, output: output.join('\n') };
162
+ }
163
+ });
164
+
165
+ // Index command
166
+ this.parser.registerCommand({
167
+ name: 'index',
168
+ description: 'Index codebase into database',
169
+ usage: 'index [options]',
170
+ examples: ['index', 'index --code', 'index --all'],
171
+ handler: async (args, flags) => {
172
+ const analyzer = createIntelligentAnalyzer(this.projectRoot);
173
+ const db = new DatabaseClient(this.projectRoot);
174
+
175
+ let indexedCount = 0;
176
+
177
+ try {
178
+ const [docs, code, tools] = await Promise.all([
179
+ analyzer.discoverDocs(),
180
+ analyzer.discoverCode(),
181
+ analyzer.discoverToolConfigs()
182
+ ]);
183
+
184
+ const docsCount = docs.length;
185
+ const codeCount = Math.min(code.length, 500); // Limit for now
186
+ const toolsCount = tools.length;
187
+
188
+ // Index docs
189
+ for (const doc of docs) {
190
+ const content = require('fs').readFileSync(doc.path, 'utf-8').slice(0, 50000);
191
+ db.upsertItem({
192
+ type: 'doc',
193
+ name: require('path').basename(doc.relativePath),
194
+ content,
195
+ filePath: doc.relativePath,
196
+ metadata: { size: doc.size }
197
+ });
198
+ indexedCount++;
199
+ }
200
+
201
+ // Index code
202
+ for (const codeFile of code.slice(0, 500)) {
203
+ const content = require('fs').existsSync(codeFile.path)
204
+ ? require('fs').readFileSync(codeFile.path, 'utf-8').slice(0, 20000)
205
+ : '';
206
+ if (content) {
207
+ db.upsertItem({
208
+ type: 'code',
209
+ name: require('path').basename(codeFile.relativePath),
210
+ content,
211
+ filePath: codeFile.relativePath,
212
+ metadata: { size: codeFile.size }
213
+ });
214
+ indexedCount++;
215
+ }
216
+ }
217
+
218
+ // Index tools
219
+ for (const tool of tools) {
220
+ const content = require('fs').existsSync(tool.path)
221
+ ? require('fs').readFileSync(tool.path, 'utf-8').slice(0, 50000)
222
+ : '';
223
+ if (content) {
224
+ db.upsertItem({
225
+ type: 'tool_config',
226
+ name: `${tool.tool}:${require('path').basename(tool.relativePath)}`,
227
+ content,
228
+ filePath: tool.relativePath,
229
+ metadata: { tool: tool.tool, size: tool.size }
230
+ });
231
+ indexedCount++;
232
+ }
233
+ }
234
+
235
+ this.session.updateStats({
236
+ filesIndexed: this.session.getStats().filesIndexed + indexedCount
237
+ });
238
+
239
+ const output = [
240
+ '',
241
+ K0NTEXT_THEME.success('✓ Indexing complete'),
242
+ ` ${K0NTEXT_THEME.cyan('•')} Documents: ${docsCount}`,
243
+ ` ${K0NTEXT_THEME.cyan('•')} Code Files: ${codeCount}`,
244
+ ` ${K0NTEXT_THEME.cyan('•')} Tool Configs: ${toolsCount}`,
245
+ ` ${K0NTEXT_THEME.cyan('•')} Total Indexed: ${indexedCount}`,
246
+ ''
247
+ ];
248
+
249
+ db.close();
250
+
251
+ return { success: true, output: output.join('\n') };
252
+ } catch (error) {
253
+ db.close();
254
+ return {
255
+ success: false,
256
+ error: error instanceof Error ? error.message : String(error)
257
+ };
258
+ }
259
+ }
260
+ });
261
+
262
+ // Search command
263
+ this.parser.registerCommand({
264
+ name: 'search',
265
+ description: 'Search indexed content',
266
+ usage: 'search <query>',
267
+ examples: ['search auth', 'search "user login"'],
268
+ completions: () => [],
269
+ handler: async (args) => {
270
+ const query = args.join(' ');
271
+ if (!query) {
272
+ return {
273
+ success: false,
274
+ error: 'Please provide a search query'
275
+ };
276
+ }
277
+
278
+ const db = new DatabaseClient(this.projectRoot);
279
+ const results = db.searchText(query);
280
+
281
+ this.session.updateStats({
282
+ searchesPerformed: this.session.getStats().searchesPerformed + 1
283
+ });
284
+
285
+ if (results.length === 0) {
286
+ db.close();
287
+ return {
288
+ success: true,
289
+ output: K0NTEXT_THEME.dim('\nNo results found.')
290
+ };
291
+ }
292
+
293
+ const output = [
294
+ '',
295
+ K0NTEXT_THEME.header(`━━━ Search Results: "${query}" ━━━`),
296
+ ''
297
+ ];
298
+
299
+ for (let i = 0; i < Math.min(results.length, 10); i++) {
300
+ const result = results[i];
301
+ output.push(` ${K0NTEXT_THEME.primary(`${i + 1}.`)} ${result.name} [${result.type}]`);
302
+ if (result.filePath) {
303
+ output.push(` ${K0NTEXT_THEME.dim(result.filePath)}`);
304
+ }
305
+ }
306
+
307
+ if (results.length > 10) {
308
+ output.push(` ${K0NTEXT_THEME.dim(`... and ${results.length - 10} more`)}`);
309
+ }
310
+
311
+ output.push('');
312
+
313
+ db.close();
314
+
315
+ return { success: true, output: output.join('\n') };
316
+ }
317
+ });
318
+
319
+ // Init command (re-run wizard)
320
+ this.parser.registerCommand({
321
+ name: 'init',
322
+ description: 'Re-run initialization wizard',
323
+ usage: 'init',
324
+ examples: ['init'],
325
+ handler: async () => {
326
+ const wizard = new InitWizard(this.projectRoot);
327
+ const config = await wizard.run();
328
+
329
+ if (config) {
330
+ this.session.setInitialized(config.apiKey, config.projectType);
331
+ this.session.updateConfig({
332
+ apiKey: config.apiKey,
333
+ projectType: config.projectType,
334
+ aiTools: config.aiTools,
335
+ features: config.features
336
+ });
337
+
338
+ wizard.showSuccess(config);
339
+ }
340
+
341
+ return { success: true };
342
+ }
343
+ });
344
+
345
+ // Update command
346
+ this.parser.registerCommand({
347
+ name: 'update',
348
+ description: 'Check for k0ntext updates',
349
+ usage: 'update',
350
+ examples: ['update'],
351
+ handler: async () => {
352
+ const hasUpdate = await this.updateChecker.checkAndPrompt();
353
+ return {
354
+ success: true,
355
+ output: hasUpdate ? '' : K0NTEXT_THEME.success('\n✓ Already on latest version')
356
+ };
357
+ }
358
+ });
359
+
360
+ // Drift command
361
+ this.parser.registerCommand({
362
+ name: 'drift',
363
+ description: 'Check for documentation drift',
364
+ usage: 'drift',
365
+ examples: ['drift'],
366
+ handler: async () => {
367
+ const db = new DatabaseClient(this.projectRoot);
368
+ const items = db.getAllItems();
369
+
370
+ const now = new Date();
371
+ const driftDays = 7;
372
+ const driftThreshold = new Date(now.getTime() - driftDays * 24 * 60 * 60 * 1000);
373
+
374
+ const drifted = items.filter(item => {
375
+ if (!item.updatedAt) return false;
376
+ const updated = new Date(item.updatedAt);
377
+ return updated < driftThreshold;
378
+ });
379
+
380
+ db.close();
381
+
382
+ const output = [
383
+ '',
384
+ K0NTEXT_THEME.header('━━━ Documentation Drift Check ━━━'),
385
+ ''
386
+ ];
387
+
388
+ if (drifted.length === 0) {
389
+ output.push(K0NTEXT_THEME.success('✓ All context files are up to date'));
390
+ } else {
391
+ output.push(K0NTEXT_THEME.warning(`⚠ Found ${drifted.length} files that may be out of sync:`));
392
+ output.push('');
393
+
394
+ for (const item of drifted.slice(0, 10)) {
395
+ output.push(` ${K0NTEXT_THEME.primary('•')} ${item.name} ${K0NTEXT_THEME.dim(`(${item.updatedAt})`)}`);
396
+ }
397
+
398
+ if (drifted.length > 10) {
399
+ output.push(` ${K0NTEXT_THEME.dim(`... and ${drifted.length - 10} more`)}`);
400
+ }
401
+
402
+ output.push('');
403
+ output.push(K0NTEXT_THEME.info('Run "index" to update your context.'));
404
+ }
405
+
406
+ output.push('');
407
+
408
+ return { success: true, output: output.join('\n') };
409
+ }
410
+ });
411
+
412
+ // Config command
413
+ this.parser.registerCommand({
414
+ name: 'config',
415
+ description: 'View or set configuration',
416
+ usage: 'config [get|set|list] [key] [value]',
417
+ examples: ['config list', 'config get projectType', 'config set projectType webapp'],
418
+ handler: async (args) => {
419
+ const action = args[0] || 'list';
420
+ const state = this.session.getState();
421
+
422
+ if (action === 'list') {
423
+ const output = [
424
+ '',
425
+ K0NTEXT_THEME.header('━━━ Configuration ━━━'),
426
+ '',
427
+ ` ${K0NTEXT_THEME.cyan('projectType:')} ${state.config.projectType || 'not set'}`,
428
+ ` ${K0NTEXT_THEME.cyan('apiKey:')} ${state.config.apiKey ? '✓ set' : '○ not set'}`,
429
+ ` ${K0NTEXT_THEME.cyan('aiTools:')} ${state.config.aiTools.join(', ') || 'none'}`,
430
+ ` ${K0NTEXT_THEME.cyan('features:')} ${state.config.features.join(', ') || 'none'}`,
431
+ ` ${K0NTEXT_THEME.cyan('autoUpdate:')} ${state.config.autoUpdate}`,
432
+ ''
433
+ ];
434
+ return { success: true, output: output.join('\n') };
435
+ }
436
+
437
+ if (action === 'get') {
438
+ const key = args[1];
439
+ if (!key) return { success: false, error: 'Please specify a key' };
440
+ const value = (state.config as any)[key];
441
+ return { success: true, output: `${key}: ${value || 'not set'}` };
442
+ }
443
+
444
+ if (action === 'set') {
445
+ const key = args[1];
446
+ const value = args.slice(2).join(' ');
447
+ if (!key || !value) return { success: false, error: 'Usage: config set <key> <value>' };
448
+
449
+ this.session.updateConfig({ [key]: value });
450
+ return { success: true, output: K0NTEXT_THEME.success(`✓ Set ${key} = ${value}`) };
451
+ }
452
+
453
+ return { success: false, error: 'Unknown action. Use: get, set, or list' };
454
+ }
455
+ });
456
+ }
457
+
458
+ /**
459
+ * Start the REPL
460
+ */
461
+ async start(): Promise<void> {
462
+ this.isActive = true;
463
+
464
+ // Show banner
465
+ this.showBanner();
466
+
467
+ // Check for updates
468
+ if (this.session.getState().config.autoUpdate) {
469
+ await this.updateChecker.showNotification({ showIfCurrent: false, checkInterval: 24 * 60 * 60 * 1000 });
470
+ }
471
+
472
+ // Check if initialized
473
+ if (!this.session.isInitialized()) {
474
+ console.log('');
475
+ console.log(K0NTEXT_THEME.info('🔧 First-time setup detected. Running initialization wizard...\n'));
476
+
477
+ const wizard = new InitWizard(this.projectRoot);
478
+ const config = await wizard.run();
479
+
480
+ if (config) {
481
+ this.session.setInitialized(config.apiKey, config.projectType);
482
+ this.session.updateConfig({
483
+ apiKey: config.apiKey,
484
+ projectType: config.projectType,
485
+ aiTools: config.aiTools,
486
+ features: config.features
487
+ });
488
+
489
+ wizard.showSuccess(config);
490
+ } else {
491
+ console.log(K0NTEXT_THEME.info('\n✓ Skipping initialization. You can run "init" later.\n'));
492
+ }
493
+ }
494
+
495
+ // Show project stats if initialized
496
+ if (this.session.isInitialized()) {
497
+ const analyzer = createIntelligentAnalyzer(this.projectRoot);
498
+ const analysis = await analyzer.analyze();
499
+
500
+ console.log('');
501
+ console.log(K0NTEXT_THEME.header('━━━ Project Overview ━━━'));
502
+ console.log(` ${K0NTEXT_THEME.primary('Type:')} ${this.session.getState().config.projectType}`);
503
+ console.log(` ${K0NTEXT_THEME.primary('Docs:')} ${analysis.existingContext.files.filter(f => f.type === 'doc').length}`);
504
+ console.log(` ${K0NTEXT_THEME.primary('Code:')} ${analysis.existingContext.files.filter(f => f.type === 'code').length}`);
505
+ console.log(` ${K0NTEXT_THEME.primary('Tech:')} ${analysis.techStack.languages.slice(0, 3).join(', ')}`);
506
+ }
507
+
508
+ console.log('');
509
+ console.log(K0NTEXT_THEME.dim('Type "help" for available commands, "exit" to quit.'));
510
+
511
+ // Start readline
512
+ this.readline.prompt();
513
+ }
514
+
515
+ /**
516
+ * Stop the REPL
517
+ */
518
+ async stop(): Promise<void> {
519
+ this.isActive = false;
520
+ this.session.end();
521
+ this.readline.close();
522
+ console.log('');
523
+ console.log(K0NTEXT_THEME.success('✓ Session saved. Goodbye!'));
524
+ }
525
+
526
+ /**
527
+ * Show welcome banner
528
+ */
529
+ private showBanner(): void {
530
+ const { supportsColor } = K0NTEXT_THEME.detectCapabilities();
531
+
532
+ if (!supportsColor) {
533
+ console.log(`\n K0ntext v${this.version}\n`);
534
+ return;
535
+ }
536
+
537
+ console.log('');
538
+ console.log(K0NTEXT_THEME.box(
539
+ `K0NTEXT v${this.version}`,
540
+ `${K0NTEXT_THEME.dim('Interactive shell for AI context engineering')}
541
+ ${K0NTEXT_THEME.dim('Type "help" for commands, "exit" to quit')}`,
542
+ 'primary'
543
+ ));
544
+ console.log('');
545
+ }
546
+ }
547
+
548
+ /**
549
+ * Create and start a REPL shell
550
+ */
551
+ export async function startREPL(projectRoot: string = process.cwd(), version: string, noTUI = false): Promise<void> {
552
+ const shell = new REPLShell({ projectRoot, version, noTUI });
553
+ await shell.start();
554
+ }