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,457 @@
1
+ /**
2
+ * Configuration UI Panel
3
+ *
4
+ * Visual configuration management for k0ntext settings
5
+ */
6
+
7
+ import fs from 'fs';
8
+ import path from 'path';
9
+ import chalk from 'chalk';
10
+ import { K0NTEXT_THEME } from '../theme.js';
11
+ import { input, confirm, select, checkbox } from '@inquirer/prompts';
12
+
13
+ /**
14
+ * Config category
15
+ */
16
+ interface ConfigCategory {
17
+ name: string;
18
+ description: string;
19
+ keys: ConfigKey[];
20
+ }
21
+
22
+ /**
23
+ * Config key definition
24
+ */
25
+ interface ConfigKey {
26
+ name: string;
27
+ type: 'string' | 'number' | 'boolean' | 'array';
28
+ description: string;
29
+ options?: string[];
30
+ validate?: (value: string) => boolean | string;
31
+ defaultValue?: unknown;
32
+ }
33
+
34
+ /**
35
+ * All configuration definitions
36
+ */
37
+ const CONFIG_DEFINITIONS: Record<string, ConfigCategory> = {
38
+ 'Project': {
39
+ name: 'Project',
40
+ description: 'Project-specific settings',
41
+ keys: [
42
+ {
43
+ name: 'projectType',
44
+ type: 'string',
45
+ description: 'Type of project (monorepo, webapp, library, api, cli)',
46
+ options: ['monorepo', 'webapp', 'library', 'api', 'cli', 'unknown'],
47
+ defaultValue: 'unknown'
48
+ },
49
+ {
50
+ name: 'maxFilesPerIndex',
51
+ type: 'number',
52
+ description: 'Maximum code files to index per module (batch indexing)',
53
+ defaultValue: 500
54
+ },
55
+ {
56
+ name: 'indexBatchSize',
57
+ type: 'number',
58
+ description: 'Files per batch during indexing',
59
+ defaultValue: 100
60
+ }
61
+ ]
62
+ },
63
+ 'AI Tools': {
64
+ name: 'AI Tools',
65
+ description: 'AI coding assistant configuration',
66
+ keys: [
67
+ {
68
+ name: 'aiTools',
69
+ type: 'array',
70
+ description: 'Enabled AI tools',
71
+ options: ['claude', 'copilot', 'cline', 'cursor', 'windsurf', 'aider', 'continue', 'gemini'],
72
+ defaultValue: ['claude']
73
+ },
74
+ {
75
+ name: 'openrouterKey',
76
+ type: 'string',
77
+ description: 'OpenRouter API key for intelligent analysis',
78
+ validate: (value) => value.startsWith('sk-or-v1-') || value === '',
79
+ defaultValue: ''
80
+ },
81
+ {
82
+ name: 'generateEmbeddings',
83
+ type: 'boolean',
84
+ description: 'Automatically generate embeddings during indexing',
85
+ defaultValue: true
86
+ }
87
+ ]
88
+ },
89
+ 'Features': {
90
+ name: 'Features',
91
+ description: 'Enabled k0ntext features',
92
+ keys: [
93
+ {
94
+ name: 'features',
95
+ type: 'array',
96
+ description: 'Enabled features',
97
+ options: ['stats', 'search', 'docs', 'drift', 'workflows', 'mcp'],
98
+ defaultValue: ['stats', 'search']
99
+ },
100
+ {
101
+ name: 'autoUpdate',
102
+ type: 'boolean',
103
+ description: 'Check for updates on startup',
104
+ defaultValue: true
105
+ },
106
+ {
107
+ name: 'updateCheckInterval',
108
+ type: 'number',
109
+ description: 'Update check interval (hours)',
110
+ defaultValue: 24
111
+ }
112
+ ]
113
+ },
114
+ 'Display': {
115
+ name: 'Display',
116
+ description: 'UI and theme settings',
117
+ keys: [
118
+ {
119
+ name: 'theme',
120
+ type: 'string',
121
+ description: 'Color theme',
122
+ options: ['default', 'dark', 'light', 'high-contrast'],
123
+ defaultValue: 'default'
124
+ },
125
+ {
126
+ name: 'showTimestamps',
127
+ type: 'boolean',
128
+ description: 'Show timestamps in command output',
129
+ defaultValue: true
130
+ },
131
+ {
132
+ name: 'compactMode',
133
+ type: 'boolean',
134
+ description: 'Use compact output for large result sets',
135
+ defaultValue: false
136
+ }
137
+ ]
138
+ }
139
+ };
140
+
141
+ /**
142
+ * Configuration UI Panel
143
+ */
144
+ export class ConfigPanel {
145
+ private projectRoot: string;
146
+ private configPath: string;
147
+ private sessionConfig: Record<string, unknown>;
148
+
149
+ constructor(projectRoot: string, sessionConfig: Record<string, unknown>) {
150
+ this.projectRoot = projectRoot;
151
+ this.configPath = path.join(projectRoot, '.k0ntext', 'config.json');
152
+ this.sessionConfig = sessionConfig;
153
+ }
154
+
155
+ /**
156
+ * Display current configuration
157
+ */
158
+ displayConfig(): string {
159
+ const lines: string[] = [];
160
+
161
+ lines.push('');
162
+ lines.push(K0NTEXT_THEME.header('━━━ Configuration ━━━'));
163
+ lines.push('');
164
+
165
+ // Group by category
166
+ for (const [categoryId, category] of Object.entries(CONFIG_DEFINITIONS)) {
167
+ lines.push(` ${K0NTEXT_THEME.header(category.name)} - ${K0NTEXT_THEME.dim(category.description)}`);
168
+ lines.push('');
169
+
170
+ for (const key of category.keys) {
171
+ const value = this.getValue(key.name);
172
+ const displayValue = this.formatValue(value, key);
173
+
174
+ lines.push(` ${K0NTEXT_THEME.cyan(key.name.padEnd(25))} ${displayValue}`);
175
+ lines.push(` ${K0NTEXT_THEME.dim(key.description)}`);
176
+ lines.push('');
177
+ }
178
+ }
179
+
180
+ lines.push(` ${K0NTEXT_THEME.dim('Config file:')} ${this.configPath}`);
181
+ lines.push('');
182
+
183
+ return lines.join('\n');
184
+ }
185
+
186
+ /**
187
+ * Interactive configuration editor
188
+ */
189
+ async interactiveConfig(category?: string): Promise<void> {
190
+ const targetCategory = category || await this.selectCategory();
191
+
192
+ if (!targetCategory) {
193
+ return; // User cancelled
194
+ }
195
+
196
+ const configDef = CONFIG_DEFINITIONS[targetCategory];
197
+ if (!configDef) {
198
+ return;
199
+ }
200
+
201
+ console.log('');
202
+ console.log(K0NTEXT_THEME.info(`Editing ${configDef.name} settings...`));
203
+ console.log('');
204
+
205
+ for (const key of configDef.keys) {
206
+ const newValue = await this.promptForKey(key);
207
+ if (newValue !== undefined) {
208
+ this.setValue(key.name, newValue);
209
+ console.log(K0NTEXT_THEME.success(`✓ Set ${key.name} = ${this.formatValue(newValue, key)}`));
210
+ }
211
+ }
212
+
213
+ await this.saveConfig();
214
+ console.log('');
215
+ console.log(K0NTEXT_THEME.success('✓ Configuration saved'));
216
+ }
217
+
218
+ /**
219
+ * Select a category to edit
220
+ */
221
+ private async selectCategory(): Promise<string | undefined> {
222
+ const choices = Object.entries(CONFIG_DEFINITIONS).map(([id, cat]) => ({
223
+ name: `${cat.name} - ${cat.description}`,
224
+ value: id
225
+ }));
226
+
227
+ const { configCategory } = this.sessionConfig as any;
228
+
229
+ const result = await select({
230
+ message: 'Select configuration category:',
231
+ choices: [
232
+ ...choices,
233
+ { name: 'Cancel', value: 'cancel' }
234
+ ],
235
+ default: configCategory || choices[0]?.value
236
+ });
237
+
238
+ return result === 'cancel' ? undefined : result;
239
+ }
240
+
241
+ /**
242
+ * Prompt user for a configuration value
243
+ */
244
+ private async promptForKey(key: ConfigKey): Promise<unknown> {
245
+ const currentValue = this.getValue(key.name);
246
+
247
+ switch (key.type) {
248
+ case 'boolean':
249
+ return await confirm({
250
+ message: key.description,
251
+ default: Boolean(currentValue || key.defaultValue)
252
+ });
253
+
254
+ case 'array':
255
+ if (key.options) {
256
+ const selected = await checkbox({
257
+ message: key.description,
258
+ choices: key.options.map(opt => ({
259
+ name: opt,
260
+ value: opt,
261
+ checked: Array.isArray(currentValue) ? currentValue.includes(opt) : false
262
+ }))
263
+ });
264
+ return selected.length > 0 ? selected : key.defaultValue;
265
+ }
266
+ return currentValue || key.defaultValue;
267
+
268
+ case 'string':
269
+ if (key.options) {
270
+ return await select({
271
+ message: key.description,
272
+ choices: key.options.map(opt => ({ name: opt, value: opt })),
273
+ default: currentValue || key.defaultValue
274
+ });
275
+ }
276
+
277
+ if (key.name === 'openrouterKey') {
278
+ const result = await input({
279
+ message: key.description,
280
+ default: String(currentValue || ''),
281
+ validate: key.validate
282
+ });
283
+ return result;
284
+ }
285
+
286
+ return await input({
287
+ message: key.description,
288
+ default: String(currentValue || key.defaultValue || '')
289
+ });
290
+
291
+ case 'number':
292
+ const inputResult = await input({
293
+ message: key.description,
294
+ default: String(currentValue || key.defaultValue || ''),
295
+ validate: (value) => {
296
+ const num = Number(value);
297
+ return !isNaN(num) && num >= 0;
298
+ }
299
+ });
300
+ return Number(inputResult);
301
+
302
+ default:
303
+ return currentValue;
304
+ }
305
+ }
306
+
307
+ /**
308
+ * Get a configuration value
309
+ */
310
+ getValue(name: string): unknown {
311
+ // Check session config first
312
+ if (name in this.sessionConfig) {
313
+ return this.sessionConfig[name];
314
+ }
315
+
316
+ // Check file config
317
+ if (fs.existsSync(this.configPath)) {
318
+ try {
319
+ const fileConfig = JSON.parse(fs.readFileSync(this.configPath, 'utf-8'));
320
+ return fileConfig[name];
321
+ } catch {
322
+ // Invalid JSON, ignore
323
+ }
324
+ }
325
+
326
+ // Return default
327
+ for (const category of Object.values(CONFIG_DEFINITIONS)) {
328
+ for (const key of category.keys) {
329
+ if (key.name === name) {
330
+ return key.defaultValue;
331
+ }
332
+ }
333
+ }
334
+
335
+ return undefined;
336
+ }
337
+
338
+ /**
339
+ * Set a configuration value
340
+ */
341
+ setValue(name: string, value: unknown): void {
342
+ // Update session config
343
+ this.sessionConfig[name] = value;
344
+ }
345
+
346
+ /**
347
+ * Save configuration to file
348
+ */
349
+ async saveConfig(): Promise<void> {
350
+ const configDir = path.dirname(this.configPath);
351
+
352
+ if (!fs.existsSync(configDir)) {
353
+ fs.mkdirSync(configDir, { recursive: true });
354
+ }
355
+
356
+ // Merge with existing config
357
+ let existingConfig = {};
358
+ if (fs.existsSync(this.configPath)) {
359
+ try {
360
+ existingConfig = JSON.parse(fs.readFileSync(this.configPath, 'utf-8'));
361
+ } catch {
362
+ // Invalid JSON, start fresh
363
+ }
364
+ }
365
+
366
+ const mergedConfig = { ...existingConfig, ...this.sessionConfig };
367
+ fs.writeFileSync(this.configPath, JSON.stringify(mergedConfig, null, 2));
368
+ }
369
+
370
+ /**
371
+ * Format a value for display
372
+ */
373
+ formatValue(value: unknown, key: ConfigKey): string {
374
+ if (value === undefined || value === null) {
375
+ return K0NTEXT_THEME.dim('(not set)');
376
+ }
377
+
378
+ switch (key.type) {
379
+ case 'boolean':
380
+ return value ? K0NTEXT_THEME.success('✓ enabled') : K0NTEXT_THEME.dim('○ disabled');
381
+ case 'array':
382
+ if (!Array.isArray(value)) return K0NTEXT_THEME.dim('(invalid)');
383
+ if (value.length === 0) return K0NTEXT_THEME.dim('(none)');
384
+ return K0NTEXT_THEME.cyan(value.join(', '));
385
+ case 'number':
386
+ return K0NTEXT_THEME.highlight(String(value));
387
+ case 'string':
388
+ if (key.name === 'openrouterKey' && value) {
389
+ return String(value).slice(0, 8) + '...' + K0NTEXT_THEME.success('(set)');
390
+ }
391
+ return K0NTEXT_THEME.cyan(String(value));
392
+ default:
393
+ return String(value);
394
+ }
395
+ }
396
+
397
+ /**
398
+ * Show configuration help
399
+ */
400
+ showConfigHelp(): string {
401
+ const lines = [
402
+ '',
403
+ K0NTEXT_THEME.header('━━━ Configuration Help ━━━'),
404
+ '',
405
+ ' Commands:',
406
+ ' config Show all configuration',
407
+ ' config list Show all configuration (alias)',
408
+ ' config get <key> Get a specific value',
409
+ ' config set <key> <val> Set a value',
410
+ ' config edit Interactive configuration editor',
411
+ '',
412
+ ' Categories:',
413
+ ...Object.entries(CONFIG_DEFINITIONS).map(([id, cat]) => ` ${id.padEnd(15)} - ${cat.description}`),
414
+ '',
415
+ ' Examples:',
416
+ ' config get projectType',
417
+ ' config set projectType monorepo',
418
+ ' config edit Project',
419
+ ''
420
+ ];
421
+
422
+ return lines.join('\n');
423
+ }
424
+
425
+ /**
426
+ * Validate configuration
427
+ */
428
+ validateConfig(): { valid: boolean; errors: string[]; warnings: string[] } {
429
+ const errors: string[] = [];
430
+ const warnings: string[] = [];
431
+
432
+ // Check API key if embeddings are enabled
433
+ const generateEmbeddings = this.getValue('generateEmbeddings') === true;
434
+ const apiKey = this.getValue('openrouterKey') || process.env.OPENROUTER_API_KEY;
435
+
436
+ if (generateEmbeddings && !apiKey) {
437
+ warnings.push('Embeddings are enabled but no API key is set');
438
+ }
439
+
440
+ // Validate numeric ranges
441
+ const maxFiles = this.getValue('maxFilesPerIndex');
442
+ if (typeof maxFiles === 'number' && (maxFiles < 100 || maxFiles > 10000)) {
443
+ errors.push('maxFilesPerIndex should be between 100 and 10000');
444
+ }
445
+
446
+ const batchSize = this.getValue('indexBatchSize');
447
+ if (typeof batchSize === 'number' && (batchSize < 10 || batchSize > 1000)) {
448
+ errors.push('indexBatchSize should be between 10 and 1000');
449
+ }
450
+
451
+ return {
452
+ valid: errors.length === 0,
453
+ errors,
454
+ warnings
455
+ };
456
+ }
457
+ }