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,373 @@
1
+ /**
2
+ * REPL Command Parser
3
+ *
4
+ * Parses and executes commands in the REPL shell
5
+ */
6
+
7
+ import { ProjectType } from './session.js';
8
+
9
+ /**
10
+ * Parsed command
11
+ */
12
+ export interface ParsedCommand {
13
+ raw: string;
14
+ name: string;
15
+ args: string[];
16
+ flags: Record<string, boolean | string>;
17
+ }
18
+
19
+ /**
20
+ * Command result
21
+ */
22
+ export interface CommandResult {
23
+ success: boolean;
24
+ output?: string;
25
+ error?: string;
26
+ data?: unknown;
27
+ }
28
+
29
+ /**
30
+ * Command definition
31
+ */
32
+ export interface CommandDefinition {
33
+ name: string;
34
+ aliases?: string[];
35
+ description: string;
36
+ usage: string;
37
+ examples: string[];
38
+ handler: (args: string[], flags: Record<string, boolean | string>) => Promise<CommandResult>;
39
+ completions?: (args: string[]) => string[];
40
+ }
41
+
42
+ /**
43
+ * REPL Command Parser
44
+ */
45
+ export class REPLCommandParser {
46
+ private commands: Map<string, CommandDefinition>;
47
+ private aliases: Map<string, string>;
48
+
49
+ constructor() {
50
+ this.commands = new Map();
51
+ this.aliases = new Map();
52
+ this.registerDefaultCommands();
53
+ }
54
+
55
+ /**
56
+ * Register a command
57
+ */
58
+ registerCommand(def: CommandDefinition): void {
59
+ this.commands.set(def.name, def);
60
+
61
+ // Register aliases
62
+ if (def.aliases) {
63
+ for (const alias of def.aliases) {
64
+ this.aliases.set(alias, def.name);
65
+ }
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Unregister a command
71
+ */
72
+ unregisterCommand(name: string): void {
73
+ const def = this.commands.get(name);
74
+ if (def) {
75
+ this.commands.delete(name);
76
+
77
+ // Remove aliases
78
+ if (def.aliases) {
79
+ for (const alias of def.aliases) {
80
+ this.aliases.delete(alias);
81
+ }
82
+ }
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Parse a command string
88
+ */
89
+ parse(input: string): ParsedCommand | null {
90
+ const trimmed = input.trim();
91
+ if (!trimmed) {
92
+ return null;
93
+ }
94
+
95
+ // Split into parts, respecting quotes
96
+ const parts: string[] = [];
97
+ let current = '';
98
+ let inQuotes = false;
99
+ let quoteChar = '';
100
+
101
+ for (let i = 0; i < trimmed.length; i++) {
102
+ const char = trimmed[i];
103
+
104
+ if ((char === '"' || char === "'") && (i === 0 || trimmed[i - 1] !== '\\')) {
105
+ if (!inQuotes) {
106
+ inQuotes = true;
107
+ quoteChar = char;
108
+ } else if (char === quoteChar) {
109
+ inQuotes = false;
110
+ quoteChar = '';
111
+ }
112
+ } else if (char === ' ' && !inQuotes) {
113
+ if (current) {
114
+ parts.push(current);
115
+ current = '';
116
+ }
117
+ } else {
118
+ current += char;
119
+ }
120
+ }
121
+
122
+ if (current) {
123
+ parts.push(current);
124
+ }
125
+
126
+ if (parts.length === 0) {
127
+ return null;
128
+ }
129
+
130
+ // Parse flags
131
+ const args: string[] = [];
132
+ const flags: Record<string, boolean | string> = {};
133
+
134
+ for (const part of parts.slice(1)) {
135
+ if (part.startsWith('--')) {
136
+ const flagParts = part.slice(2).split('=');
137
+ const flagName = flagParts[0];
138
+ if (flagParts.length > 1) {
139
+ flags[flagName] = flagParts.slice(1).join('=');
140
+ } else {
141
+ flags[flagName] = true;
142
+ }
143
+ } else if (part.startsWith('-')) {
144
+ flags[part.slice(1)] = true;
145
+ } else {
146
+ args.push(part);
147
+ }
148
+ }
149
+
150
+ // Resolve command name (handle aliases)
151
+ let name = parts[0].toLowerCase();
152
+ if (this.aliases.has(name)) {
153
+ name = this.aliases.get(name)!;
154
+ }
155
+
156
+ return {
157
+ raw: trimmed,
158
+ name,
159
+ args,
160
+ flags
161
+ };
162
+ }
163
+
164
+ /**
165
+ * Execute a parsed command
166
+ */
167
+ async execute(parsed: ParsedCommand): Promise<CommandResult> {
168
+ const def = this.commands.get(parsed.name);
169
+
170
+ if (!def) {
171
+ return {
172
+ success: false,
173
+ error: `Unknown command: ${parsed.name}. Type 'help' for available commands.`
174
+ };
175
+ }
176
+
177
+ try {
178
+ return await def.handler(parsed.args, parsed.flags);
179
+ } catch (error) {
180
+ return {
181
+ success: false,
182
+ error: error instanceof Error ? error.message : String(error)
183
+ };
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Get command completions
189
+ */
190
+ getCompletions(input: string, cursor: number): string[] {
191
+ const parsed = this.parse(input);
192
+ if (!parsed) {
193
+ return Array.from(this.commands.keys());
194
+ }
195
+
196
+ const def = this.commands.get(parsed.name);
197
+ if (def && def.completions) {
198
+ return def.completions(parsed.args);
199
+ }
200
+
201
+ return [];
202
+ }
203
+
204
+ /**
205
+ * Get all command names
206
+ */
207
+ getCommandNames(): string[] {
208
+ return Array.from(this.commands.keys());
209
+ }
210
+
211
+ /**
212
+ * Get command definition
213
+ */
214
+ getCommand(name: string): CommandDefinition | undefined {
215
+ // Resolve alias
216
+ const resolvedName = this.aliases.get(name) || name;
217
+ return this.commands.get(resolvedName);
218
+ }
219
+
220
+ /**
221
+ * Get help text for a command
222
+ */
223
+ getHelp(name?: string): string {
224
+ if (name) {
225
+ const def = this.getCommand(name);
226
+ if (!def) {
227
+ return `Unknown command: ${name}`;
228
+ }
229
+
230
+ return `
231
+ ${def.name} - ${def.description}
232
+
233
+ Usage:
234
+ ${def.usage}
235
+
236
+ Examples:
237
+ ${def.examples.map(e => ` ${e}`).join('\n')}
238
+ `.trim();
239
+ }
240
+
241
+ // Show all commands
242
+ const lines = [
243
+ '\nAvailable Commands:',
244
+ ''
245
+ ];
246
+
247
+ const categories = this.groupCommandsByCategory();
248
+
249
+ for (const [category, commands] of Object.entries(categories)) {
250
+ lines.push(` ${category}:`);
251
+ for (const cmd of commands) {
252
+ const def = this.getCommand(cmd);
253
+ if (def) {
254
+ lines.push(` ${cmd.padEnd(15)} - ${def.description}`);
255
+ } else {
256
+ lines.push(` ${cmd.padEnd(15)} - (no description)`);
257
+ }
258
+ }
259
+ lines.push('');
260
+ }
261
+
262
+ lines.push(' Use "help <command>" for more information on a specific command.');
263
+ lines.push('');
264
+
265
+ return lines.join('\n');
266
+ }
267
+
268
+ /**
269
+ * Group commands by category
270
+ */
271
+ private groupCommandsByCategory(): Record<string, string[]> {
272
+ const categories: Record<string, string[]> = {
273
+ 'Core': ['help', 'exit', 'clear', 'config'],
274
+ 'Database': ['stats', 'index', 'search'],
275
+ 'Initialization': ['init', 'generate'],
276
+ 'Monitoring': ['watch', 'drift'],
277
+ 'MCP': ['mcp', 'sync']
278
+ };
279
+
280
+ // Add all commands to categories
281
+ for (const name of this.getCommandNames()) {
282
+ let categorized = false;
283
+ for (const [_, cmds] of Object.entries(categories)) {
284
+ if (cmds.includes(name)) {
285
+ categorized = true;
286
+ break;
287
+ }
288
+ }
289
+ if (!categorized) {
290
+ if (!categories['Other']) {
291
+ categories['Other'] = [];
292
+ }
293
+ categories['Other'].push(name);
294
+ }
295
+ }
296
+
297
+ return categories;
298
+ }
299
+
300
+ /**
301
+ * Register default commands
302
+ */
303
+ private registerDefaultCommands(): void {
304
+ // Help command
305
+ this.registerCommand({
306
+ name: 'help',
307
+ aliases: ['?', 'h'],
308
+ description: 'Show help information',
309
+ usage: 'help [command]',
310
+ examples: [
311
+ 'help',
312
+ 'help index',
313
+ 'help stats'
314
+ ],
315
+ handler: async (args) => {
316
+ const commandName = args[0];
317
+ return {
318
+ success: true,
319
+ output: this.getHelp(commandName)
320
+ };
321
+ }
322
+ });
323
+
324
+ // Exit command
325
+ this.registerCommand({
326
+ name: 'exit',
327
+ aliases: ['quit', 'q'],
328
+ description: 'Exit the REPL shell',
329
+ usage: 'exit',
330
+ examples: ['exit', 'quit'],
331
+ handler: async () => {
332
+ return {
333
+ success: true,
334
+ output: 'Goodbye!'
335
+ };
336
+ }
337
+ });
338
+
339
+ // Clear command
340
+ this.registerCommand({
341
+ name: 'clear',
342
+ aliases: ['cls'],
343
+ description: 'Clear the screen',
344
+ usage: 'clear',
345
+ examples: ['clear'],
346
+ handler: async () => {
347
+ console.clear();
348
+ return {
349
+ success: true,
350
+ output: ''
351
+ };
352
+ }
353
+ });
354
+
355
+ // Config command
356
+ this.registerCommand({
357
+ name: 'config',
358
+ description: 'Manage configuration',
359
+ usage: 'config [get|set|list] [key] [value]',
360
+ examples: [
361
+ 'config list',
362
+ 'config get projectType',
363
+ 'config set projectType webapp'
364
+ ],
365
+ handler: async (args) => {
366
+ return {
367
+ success: true,
368
+ output: 'Config command - use get|set|list'
369
+ };
370
+ }
371
+ });
372
+ }
373
+ }
@@ -0,0 +1,269 @@
1
+ /**
2
+ * REPL Session Manager
3
+ *
4
+ * Manages REPL session lifecycle, state persistence, and activity tracking
5
+ */
6
+
7
+ import fs from 'fs';
8
+ import path from 'path';
9
+
10
+ /**
11
+ * Generate a simple UUID v4
12
+ */
13
+ function generateUUID(): string {
14
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
15
+ const r = Math.random() * 16 | 0;
16
+ const v = c === 'x' ? r : (r & 0x3 | 0x8);
17
+ return v.toString(16);
18
+ });
19
+ }
20
+
21
+ /**
22
+ * Session state
23
+ */
24
+ export interface SessionState {
25
+ sessionId: string;
26
+ startTime: string;
27
+ lastActivity: string;
28
+ projectRoot: string;
29
+ config: SessionConfig;
30
+ history: CommandHistoryEntry[];
31
+ stats: SessionStats;
32
+ }
33
+
34
+ /**
35
+ * Session configuration
36
+ */
37
+ export interface SessionConfig {
38
+ apiKey?: string;
39
+ projectType?: ProjectType;
40
+ aiTools: string[];
41
+ features: string[];
42
+ autoUpdate: boolean;
43
+ theme: string;
44
+ }
45
+
46
+ /**
47
+ * Project type
48
+ */
49
+ export type ProjectType = 'monorepo' | 'webapp' | 'library' | 'api' | 'cli' | 'unknown';
50
+
51
+ /**
52
+ * Command history entry
53
+ */
54
+ export interface CommandHistoryEntry {
55
+ command: string;
56
+ timestamp: string;
57
+ result?: string;
58
+ duration?: number;
59
+ }
60
+
61
+ /**
62
+ * Session statistics
63
+ */
64
+ export interface SessionStats {
65
+ commandsExecuted: number;
66
+ searchesPerformed: number;
67
+ filesIndexed: number;
68
+ embeddingsGenerated: number;
69
+ errorsEncountered: number;
70
+ }
71
+
72
+ /**
73
+ * REPL Session Manager
74
+ */
75
+ export class REPLSessionManager {
76
+ private state: SessionState;
77
+ private statePath: string;
78
+ private projectRoot: string;
79
+
80
+ constructor(projectRoot: string) {
81
+ this.projectRoot = projectRoot;
82
+ const k0ntextDir = path.join(projectRoot, '.k0ntext');
83
+ this.statePath = path.join(k0ntextDir, 'session.json');
84
+
85
+ // Ensure .k0ntext directory exists
86
+ if (!fs.existsSync(k0ntextDir)) {
87
+ fs.mkdirSync(k0ntextDir, { recursive: true });
88
+ }
89
+
90
+ // Try to load existing session or create new
91
+ this.state = this.load() || this.createNewSession();
92
+ }
93
+
94
+ /**
95
+ * Create a new session
96
+ */
97
+ private createNewSession(): SessionState {
98
+ return {
99
+ sessionId: generateUUID(),
100
+ startTime: new Date().toISOString(),
101
+ lastActivity: new Date().toISOString(),
102
+ projectRoot: this.projectRoot,
103
+ config: {
104
+ aiTools: [],
105
+ autoUpdate: true,
106
+ theme: 'default',
107
+ features: []
108
+ },
109
+ history: [],
110
+ stats: {
111
+ commandsExecuted: 0,
112
+ searchesPerformed: 0,
113
+ filesIndexed: 0,
114
+ embeddingsGenerated: 0,
115
+ errorsEncountered: 0
116
+ }
117
+ };
118
+ }
119
+
120
+ /**
121
+ * Load session from disk
122
+ */
123
+ private load(): SessionState | null {
124
+ try {
125
+ if (fs.existsSync(this.statePath)) {
126
+ const content = fs.readFileSync(this.statePath, 'utf-8');
127
+ return JSON.parse(content) as SessionState;
128
+ }
129
+ } catch {
130
+ // If load fails, return null to create new session
131
+ }
132
+ return null;
133
+ }
134
+
135
+ /**
136
+ * Save session to disk
137
+ */
138
+ save(): void {
139
+ try {
140
+ this.state.lastActivity = new Date().toISOString();
141
+ fs.writeFileSync(this.statePath, JSON.stringify(this.state, null, 2));
142
+ } catch (error) {
143
+ console.error('Failed to save session:', error);
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Get session state
149
+ */
150
+ getState(): SessionState {
151
+ return { ...this.state };
152
+ }
153
+
154
+ /**
155
+ * Update session config
156
+ */
157
+ updateConfig(config: Partial<SessionConfig>): void {
158
+ this.state.config = { ...this.state.config, ...config };
159
+ this.save();
160
+ }
161
+
162
+ /**
163
+ * Add command to history
164
+ */
165
+ addCommand(command: string, result?: string, duration?: number): void {
166
+ const entry: CommandHistoryEntry = {
167
+ command,
168
+ timestamp: new Date().toISOString(),
169
+ result,
170
+ duration
171
+ };
172
+
173
+ this.state.history.push(entry);
174
+
175
+ // Keep only last 100 commands
176
+ if (this.state.history.length > 100) {
177
+ this.state.history = this.state.history.slice(-100);
178
+ }
179
+
180
+ this.state.stats.commandsExecuted++;
181
+ this.save();
182
+ }
183
+
184
+ /**
185
+ * Get command history
186
+ */
187
+ getHistory(limit?: number): CommandHistoryEntry[] {
188
+ const history = this.state.history.slice().reverse();
189
+ return limit ? history.slice(0, limit) : history;
190
+ }
191
+
192
+ /**
193
+ * Update session stats
194
+ */
195
+ updateStats(stats: Partial<SessionStats>): void {
196
+ this.state.stats = { ...this.state.stats, ...stats };
197
+ this.save();
198
+ }
199
+
200
+ /**
201
+ * Get session stats
202
+ */
203
+ getStats(): SessionStats {
204
+ return { ...this.state.stats };
205
+ }
206
+
207
+ /**
208
+ * Get session duration
209
+ */
210
+ getDuration(): { ms: number; human: string } {
211
+ const start = new Date(this.state.startTime);
212
+ const now = new Date();
213
+ const diff = now.getTime() - start.getTime();
214
+
215
+ const seconds = Math.floor(diff / 1000);
216
+ const minutes = Math.floor(seconds / 60);
217
+ const hours = Math.floor(minutes / 60);
218
+ const days = Math.floor(hours / 24);
219
+
220
+ let human = '';
221
+ if (days > 0) human += `${days}d `;
222
+ if (hours % 24 > 0) human += `${hours % 24}h `;
223
+ if (minutes % 60 > 0) human += `${minutes % 60}m `;
224
+ if (seconds % 60 > 0 || human === '') human += `${seconds % 60}s`;
225
+
226
+ return { ms: diff, human: human.trim() };
227
+ }
228
+
229
+ /**
230
+ * Check if session is initialized (has API key and project type)
231
+ */
232
+ isInitialized(): boolean {
233
+ return !!(
234
+ this.state.config.apiKey ||
235
+ process.env.OPENROUTER_API_KEY
236
+ ) && !!this.state.config.projectType;
237
+ }
238
+
239
+ /**
240
+ * Set initialization status
241
+ */
242
+ setInitialized(apiKey: string, projectType: ProjectType): void {
243
+ this.state.config.apiKey = apiKey;
244
+ this.state.config.projectType = projectType;
245
+ this.save();
246
+ }
247
+
248
+ /**
249
+ * Get OpenRouter API key
250
+ */
251
+ getApiKey(): string | undefined {
252
+ return this.state.config.apiKey || process.env.OPENROUTER_API_KEY;
253
+ }
254
+
255
+ /**
256
+ * Clear session (for reset/reinit)
257
+ */
258
+ clear(): void {
259
+ this.state = this.createNewSession();
260
+ this.save();
261
+ }
262
+
263
+ /**
264
+ * End session
265
+ */
266
+ end(): void {
267
+ this.save();
268
+ }
269
+ }