nova-terminal-assistant 0.1.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.
Potentially problematic release.
This version of nova-terminal-assistant might be problematic. Click here for more details.
- package/README.md +358 -0
- package/bin/nova +38 -0
- package/bin/nova.js +12 -0
- package/package.json +67 -0
- package/src/cli/commands/SmartCompletion.ts +458 -0
- package/src/cli/index.ts +5 -0
- package/src/cli/startup/IFlowRepl.ts +212 -0
- package/src/cli/startup/InkBasedRepl.ts +1056 -0
- package/src/cli/startup/InteractiveRepl.ts +2833 -0
- package/src/cli/startup/NovaApp.ts +1861 -0
- package/src/cli/startup/index.ts +4 -0
- package/src/cli/startup/parseArgs.ts +293 -0
- package/src/cli/test-modules.ts +27 -0
- package/src/cli/ui/IFlowDropdown.ts +425 -0
- package/src/cli/ui/ModernReplUI.ts +276 -0
- package/src/cli/ui/SimpleSelector2.ts +215 -0
- package/src/cli/ui/components/ConfirmDialog.ts +176 -0
- package/src/cli/ui/components/ErrorPanel.ts +364 -0
- package/src/cli/ui/components/InkAppRunner.tsx +67 -0
- package/src/cli/ui/components/InkComponents.tsx +613 -0
- package/src/cli/ui/components/NovaInkApp.tsx +312 -0
- package/src/cli/ui/components/ProgressBar.ts +177 -0
- package/src/cli/ui/components/ProgressIndicator.ts +298 -0
- package/src/cli/ui/components/QuickActions.ts +396 -0
- package/src/cli/ui/components/SimpleErrorPanel.ts +231 -0
- package/src/cli/ui/components/StatusBar.ts +194 -0
- package/src/cli/ui/components/ThinkingBlockRenderer.ts +401 -0
- package/src/cli/ui/components/index.ts +27 -0
- package/src/cli/ui/ink-prototype.tsx +347 -0
- package/src/cli/utils/CliUI.ts +336 -0
- package/src/cli/utils/CompletionHelper.ts +388 -0
- package/src/cli/utils/EnhancedCompleter.test.ts +226 -0
- package/src/cli/utils/EnhancedCompleter.ts +513 -0
- package/src/cli/utils/ErrorEnhancer.ts +429 -0
- package/src/cli/utils/OutputFormatter.ts +193 -0
- package/src/cli/utils/index.ts +9 -0
- package/src/core/agents/AgentOrchestrator.ts +515 -0
- package/src/core/agents/index.ts +17 -0
- package/src/core/audit/AuditLogger.ts +509 -0
- package/src/core/audit/index.ts +11 -0
- package/src/core/auth/AuthManager.d.ts.map +1 -0
- package/src/core/auth/AuthManager.ts +138 -0
- package/src/core/auth/index.d.ts.map +1 -0
- package/src/core/auth/index.ts +2 -0
- package/src/core/config/ConfigManager.d.ts.map +1 -0
- package/src/core/config/ConfigManager.test.ts +183 -0
- package/src/core/config/ConfigManager.ts +1219 -0
- package/src/core/config/index.d.ts.map +1 -0
- package/src/core/config/index.ts +1 -0
- package/src/core/context/ContextBuilder.d.ts.map +1 -0
- package/src/core/context/ContextBuilder.ts +171 -0
- package/src/core/context/ContextCompressor.d.ts.map +1 -0
- package/src/core/context/ContextCompressor.ts +642 -0
- package/src/core/context/LayeredMemoryManager.ts +657 -0
- package/src/core/context/MemoryDiscovery.d.ts.map +1 -0
- package/src/core/context/MemoryDiscovery.ts +175 -0
- package/src/core/context/defaultSystemPrompt.d.ts.map +1 -0
- package/src/core/context/defaultSystemPrompt.ts +35 -0
- package/src/core/context/index.d.ts.map +1 -0
- package/src/core/context/index.ts +22 -0
- package/src/core/extensions/SkillGenerator.ts +421 -0
- package/src/core/extensions/SkillInstaller.d.ts.map +1 -0
- package/src/core/extensions/SkillInstaller.ts +257 -0
- package/src/core/extensions/SkillRegistry.d.ts.map +1 -0
- package/src/core/extensions/SkillRegistry.ts +361 -0
- package/src/core/extensions/SkillValidator.ts +525 -0
- package/src/core/extensions/index.ts +15 -0
- package/src/core/index.d.ts.map +1 -0
- package/src/core/index.ts +42 -0
- package/src/core/mcp/McpManager.d.ts.map +1 -0
- package/src/core/mcp/McpManager.ts +632 -0
- package/src/core/mcp/index.d.ts.map +1 -0
- package/src/core/mcp/index.ts +2 -0
- package/src/core/model/ModelClient.d.ts.map +1 -0
- package/src/core/model/ModelClient.ts +217 -0
- package/src/core/model/ModelConnectionTester.ts +363 -0
- package/src/core/model/ModelValidator.ts +348 -0
- package/src/core/model/index.d.ts.map +1 -0
- package/src/core/model/index.ts +6 -0
- package/src/core/model/providers/AnthropicProvider.d.ts.map +1 -0
- package/src/core/model/providers/AnthropicProvider.ts +279 -0
- package/src/core/model/providers/CodingPlanProvider.d.ts.map +1 -0
- package/src/core/model/providers/CodingPlanProvider.ts +210 -0
- package/src/core/model/providers/OllamaCloudProvider.d.ts.map +1 -0
- package/src/core/model/providers/OllamaCloudProvider.ts +405 -0
- package/src/core/model/providers/OllamaManager.d.ts.map +1 -0
- package/src/core/model/providers/OllamaManager.ts +201 -0
- package/src/core/model/providers/OllamaProvider.d.ts.map +1 -0
- package/src/core/model/providers/OllamaProvider.ts +73 -0
- package/src/core/model/providers/OpenAICompatibleProvider.d.ts.map +1 -0
- package/src/core/model/providers/OpenAICompatibleProvider.ts +327 -0
- package/src/core/model/providers/OpenAIProvider.d.ts.map +1 -0
- package/src/core/model/providers/OpenAIProvider.ts +29 -0
- package/src/core/model/providers/index.d.ts.map +1 -0
- package/src/core/model/providers/index.ts +12 -0
- package/src/core/model/types.d.ts.map +1 -0
- package/src/core/model/types.ts +77 -0
- package/src/core/security/ApprovalManager.d.ts.map +1 -0
- package/src/core/security/ApprovalManager.ts +174 -0
- package/src/core/security/FileFilter.d.ts.map +1 -0
- package/src/core/security/FileFilter.ts +141 -0
- package/src/core/security/HookExecutor.d.ts.map +1 -0
- package/src/core/security/HookExecutor.ts +178 -0
- package/src/core/security/SandboxExecutor.ts +447 -0
- package/src/core/security/index.d.ts.map +1 -0
- package/src/core/security/index.ts +8 -0
- package/src/core/session/AgentLoop.d.ts.map +1 -0
- package/src/core/session/AgentLoop.ts +501 -0
- package/src/core/session/SessionManager.d.ts.map +1 -0
- package/src/core/session/SessionManager.test.ts +183 -0
- package/src/core/session/SessionManager.ts +460 -0
- package/src/core/session/index.d.ts.map +1 -0
- package/src/core/session/index.ts +3 -0
- package/src/core/telemetry/Telemetry.d.ts.map +1 -0
- package/src/core/telemetry/Telemetry.ts +90 -0
- package/src/core/telemetry/TelemetryService.ts +531 -0
- package/src/core/telemetry/index.d.ts.map +1 -0
- package/src/core/telemetry/index.ts +12 -0
- package/src/core/testing/AutoFixer.ts +385 -0
- package/src/core/testing/ErrorAnalyzer.ts +499 -0
- package/src/core/testing/TestRunner.ts +265 -0
- package/src/core/testing/agent-cli-tests.ts +538 -0
- package/src/core/testing/index.ts +11 -0
- package/src/core/tools/ToolRegistry.d.ts.map +1 -0
- package/src/core/tools/ToolRegistry.test.ts +206 -0
- package/src/core/tools/ToolRegistry.ts +260 -0
- package/src/core/tools/impl/EditFileTool.d.ts.map +1 -0
- package/src/core/tools/impl/EditFileTool.ts +97 -0
- package/src/core/tools/impl/ListDirectoryTool.d.ts.map +1 -0
- package/src/core/tools/impl/ListDirectoryTool.ts +142 -0
- package/src/core/tools/impl/MemoryTool.d.ts.map +1 -0
- package/src/core/tools/impl/MemoryTool.ts +102 -0
- package/src/core/tools/impl/ReadFileTool.d.ts.map +1 -0
- package/src/core/tools/impl/ReadFileTool.ts +58 -0
- package/src/core/tools/impl/SearchContentTool.d.ts.map +1 -0
- package/src/core/tools/impl/SearchContentTool.ts +94 -0
- package/src/core/tools/impl/SearchFileTool.d.ts.map +1 -0
- package/src/core/tools/impl/SearchFileTool.ts +61 -0
- package/src/core/tools/impl/ShellTool.d.ts.map +1 -0
- package/src/core/tools/impl/ShellTool.ts +118 -0
- package/src/core/tools/impl/TaskTool.d.ts.map +1 -0
- package/src/core/tools/impl/TaskTool.ts +207 -0
- package/src/core/tools/impl/TodoTool.d.ts.map +1 -0
- package/src/core/tools/impl/TodoTool.ts +122 -0
- package/src/core/tools/impl/WebFetchTool.d.ts.map +1 -0
- package/src/core/tools/impl/WebFetchTool.ts +103 -0
- package/src/core/tools/impl/WebSearchTool.d.ts.map +1 -0
- package/src/core/tools/impl/WebSearchTool.ts +89 -0
- package/src/core/tools/impl/WriteFileTool.d.ts.map +1 -0
- package/src/core/tools/impl/WriteFileTool.ts +49 -0
- package/src/core/tools/impl/index.d.ts.map +1 -0
- package/src/core/tools/impl/index.ts +16 -0
- package/src/core/tools/index.d.ts.map +1 -0
- package/src/core/tools/index.ts +7 -0
- package/src/core/tools/schemas/execution.d.ts.map +1 -0
- package/src/core/tools/schemas/execution.ts +42 -0
- package/src/core/tools/schemas/file.d.ts.map +1 -0
- package/src/core/tools/schemas/file.ts +119 -0
- package/src/core/tools/schemas/index.d.ts.map +1 -0
- package/src/core/tools/schemas/index.ts +11 -0
- package/src/core/tools/schemas/memory.d.ts.map +1 -0
- package/src/core/tools/schemas/memory.ts +52 -0
- package/src/core/tools/schemas/orchestration.d.ts.map +1 -0
- package/src/core/tools/schemas/orchestration.ts +44 -0
- package/src/core/tools/schemas/search.d.ts.map +1 -0
- package/src/core/tools/schemas/search.ts +112 -0
- package/src/core/tools/schemas/todo.d.ts.map +1 -0
- package/src/core/tools/schemas/todo.ts +32 -0
- package/src/core/tools/schemas/web.d.ts.map +1 -0
- package/src/core/tools/schemas/web.ts +86 -0
- package/src/core/types/config.d.ts.map +1 -0
- package/src/core/types/config.ts +200 -0
- package/src/core/types/errors.d.ts.map +1 -0
- package/src/core/types/errors.ts +204 -0
- package/src/core/types/index.d.ts.map +1 -0
- package/src/core/types/index.ts +8 -0
- package/src/core/types/session.d.ts.map +1 -0
- package/src/core/types/session.ts +216 -0
- package/src/core/types/tools.d.ts.map +1 -0
- package/src/core/types/tools.ts +157 -0
- package/src/core/utils/CheckpointManager.d.ts.map +1 -0
- package/src/core/utils/CheckpointManager.ts +327 -0
- package/src/core/utils/Logger.d.ts.map +1 -0
- package/src/core/utils/Logger.ts +98 -0
- package/src/core/utils/RetryManager.ts +471 -0
- package/src/core/utils/TokenCounter.d.ts.map +1 -0
- package/src/core/utils/TokenCounter.ts +414 -0
- package/src/core/utils/VectorMemoryStore.ts +440 -0
- package/src/core/utils/helpers.d.ts.map +1 -0
- package/src/core/utils/helpers.ts +89 -0
- package/src/core/utils/index.d.ts.map +1 -0
- package/src/core/utils/index.ts +19 -0
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// SmartCompletion - Intelligent command suggestion system for Nova CLI
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import type { SessionInfo } from '../core/types/config.js';
|
|
7
|
+
|
|
8
|
+
export interface CommandSuggestion {
|
|
9
|
+
text: string;
|
|
10
|
+
description: string;
|
|
11
|
+
category: 'navigation' | 'session' | 'model' | 'tools' | 'help' | 'mcp' | 'skills';
|
|
12
|
+
icon?: string;
|
|
13
|
+
shortcut?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface CompletionContext {
|
|
17
|
+
input: string;
|
|
18
|
+
session: SessionInfo | null;
|
|
19
|
+
mode: string;
|
|
20
|
+
recentCommands: string[];
|
|
21
|
+
errorHistory: string[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class SmartCompletion {
|
|
25
|
+
private allCommands = new Map<string, CommandSuggestion>();
|
|
26
|
+
private recentCommands: string[] = [];
|
|
27
|
+
private errorHistory: string[] = [];
|
|
28
|
+
private context: CompletionContext;
|
|
29
|
+
|
|
30
|
+
constructor(session: SessionInfo | null) {
|
|
31
|
+
this.context = {
|
|
32
|
+
input: '',
|
|
33
|
+
session,
|
|
34
|
+
mode: session?.mode || 'auto',
|
|
35
|
+
recentCommands: [],
|
|
36
|
+
errorHistory: []
|
|
37
|
+
};
|
|
38
|
+
this.initializeCommands();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Handle user input and return suggestions
|
|
43
|
+
*/
|
|
44
|
+
async handleInput(input: string): Promise<CommandSuggestion[]> {
|
|
45
|
+
this.context.input = input.trim();
|
|
46
|
+
|
|
47
|
+
// Update context with latest info
|
|
48
|
+
if (this.context.session) {
|
|
49
|
+
this.context.mode = this.context.session.mode;
|
|
50
|
+
}
|
|
51
|
+
this.context.recentCommands = this.recentCommands.slice(0, 10);
|
|
52
|
+
|
|
53
|
+
// Get suggestions based on input
|
|
54
|
+
const suggestions = this.getSuggestions(this.context.input);
|
|
55
|
+
|
|
56
|
+
// If we have suggestions and it's a partial match, show dropdown
|
|
57
|
+
if (suggestions.length > 0 && this.context.input.startsWith('/')) {
|
|
58
|
+
await this.showDropdown(suggestions);
|
|
59
|
+
return suggestions;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return suggestions;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Show interactive dropdown menu
|
|
67
|
+
*/
|
|
68
|
+
private async showDropdown(suggestions: CommandSuggestion[]): Promise<void> {
|
|
69
|
+
console.log('\n'); // Clear line before dropdown
|
|
70
|
+
|
|
71
|
+
const width = Math.min(process.stdout.columns || 80, 60);
|
|
72
|
+
const border = '─'.repeat(width);
|
|
73
|
+
|
|
74
|
+
// Header
|
|
75
|
+
console.log(chalk.bgBlue.white.bold(' COMMAND SUGGESTIONS '));
|
|
76
|
+
console.log(chalk.blue(border));
|
|
77
|
+
|
|
78
|
+
// Sort suggestions by relevance
|
|
79
|
+
const sortedSuggestions = this.sortByRelevance(suggestions);
|
|
80
|
+
|
|
81
|
+
// Display suggestions with navigation
|
|
82
|
+
let selectedIndex = 0;
|
|
83
|
+
const renderMenu = () => {
|
|
84
|
+
// Clear previous menu lines
|
|
85
|
+
const menuHeight = Math.min(sortedSuggestions.length + 2, 15);
|
|
86
|
+
process.stdout.write(`\x1b[${menuHeight}A`);
|
|
87
|
+
|
|
88
|
+
// Show header
|
|
89
|
+
console.log(chalk.yellow(`\n Use ↑↓ to navigate, Enter to select, Esc to cancel\n Input: ${chalk.cyan(this.context.input)}\n`));
|
|
90
|
+
|
|
91
|
+
// Show suggestions
|
|
92
|
+
sortedSuggestions.forEach((suggestion, index) => {
|
|
93
|
+
const isSelected = index === selectedIndex;
|
|
94
|
+
const prefix = isSelected ? chalk.green('▶ ') : chalk.gray(' ');
|
|
95
|
+
const keyDisplay = isSelected ? chalk.cyan(`[${suggestion.text}]`) : chalk.gray(suggestion.text);
|
|
96
|
+
const descDisplay = chalk.white(suggestion.description);
|
|
97
|
+
|
|
98
|
+
console.log(`${prefix}${keyDisplay} ${descDisplay}`);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Bottom border
|
|
102
|
+
console.log(chalk.blue('┌' + border + '┐'));
|
|
103
|
+
console.log(chalk.blue('│') + ' '.repeat(width) + chalk.blue('│'));
|
|
104
|
+
console.log(chalk.blue('└' + border + '┘'));
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// Initial render
|
|
108
|
+
renderMenu();
|
|
109
|
+
|
|
110
|
+
// Handle keyboard input
|
|
111
|
+
return new Promise((resolve) => {
|
|
112
|
+
const readline = await import('node:readline');
|
|
113
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
114
|
+
|
|
115
|
+
const handleKeyPress = (key: string) => {
|
|
116
|
+
switch (key) {
|
|
117
|
+
case '\x1b[A': // Up arrow
|
|
118
|
+
selectedIndex = Math.max(0, selectedIndex - 1);
|
|
119
|
+
renderMenu();
|
|
120
|
+
break;
|
|
121
|
+
case '\x1b[B': // Down arrow
|
|
122
|
+
selectedIndex = Math.min(sortedSuggestions.length - 1, selectedIndex + 1);
|
|
123
|
+
renderMenu();
|
|
124
|
+
break;
|
|
125
|
+
case '\r': // Enter
|
|
126
|
+
case '\n':
|
|
127
|
+
rl.close();
|
|
128
|
+
const selected = sortedSuggestions[selectedIndex];
|
|
129
|
+
console.log(`\n✓ Selected: ${chalk.cyan(selected.text)}`);
|
|
130
|
+
this.executeCommand(selected.text);
|
|
131
|
+
resolve();
|
|
132
|
+
break;
|
|
133
|
+
case '\x1b': // Escape
|
|
134
|
+
rl.close();
|
|
135
|
+
console.log(`\nCancelled.`);
|
|
136
|
+
resolve();
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// Set up raw mode for direct key capture
|
|
142
|
+
const wasRaw = process.stdin.isRaw;
|
|
143
|
+
if (process.stdin.isTTY) {
|
|
144
|
+
process.stdin.setRawMode(true);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
process.stdin.on('data', (buffer: Buffer) => {
|
|
148
|
+
const key = buffer.toString();
|
|
149
|
+
handleKeyPress(key);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Execute the selected command
|
|
156
|
+
*/
|
|
157
|
+
private executeCommand(commandText: string): void {
|
|
158
|
+
console.log(chalk.gray(`Executing: ${commandText}`));
|
|
159
|
+
|
|
160
|
+
// Simulate command execution
|
|
161
|
+
setTimeout(() => {
|
|
162
|
+
console.log(chalk.green(`✓ Command executed: ${commandText}`));
|
|
163
|
+
|
|
164
|
+
// Add to recent commands
|
|
165
|
+
this.addRecentCommand(commandText);
|
|
166
|
+
}, 500);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Get command suggestions based on current input and context
|
|
171
|
+
*/
|
|
172
|
+
private getSuggestions(input: string): CommandSuggestion[] {
|
|
173
|
+
if (!input.startsWith('/')) {
|
|
174
|
+
return []; // Only suggest commands that start with /
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const baseInput = input.slice(1).toLowerCase(); // Remove / and lowercase
|
|
178
|
+
const suggestions: CommandSuggestion[] = [];
|
|
179
|
+
|
|
180
|
+
// Filter commands based on input
|
|
181
|
+
for (const [commandName, suggestion] of this.allCommands) {
|
|
182
|
+
const matchesInput = commandName.toLowerCase().startsWith(baseInput) ||
|
|
183
|
+
suggestion.description.toLowerCase().includes(baseInput);
|
|
184
|
+
|
|
185
|
+
// Additional context-based filtering
|
|
186
|
+
const contextMatches = this.getContextualMatches(commandName, suggestion);
|
|
187
|
+
|
|
188
|
+
if (matchesInput || contextMatches) {
|
|
189
|
+
suggestions.push({
|
|
190
|
+
...suggestion,
|
|
191
|
+
text: '/' + commandName
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Sort by relevance
|
|
197
|
+
return this.sortByRelevance(suggestions);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Get contextual matches based on session state and history
|
|
202
|
+
*/
|
|
203
|
+
private getContextualMatches(commandName: string, suggestion: CommandSuggestion): boolean {
|
|
204
|
+
const session = this.context.session;
|
|
205
|
+
if (!session) return false;
|
|
206
|
+
|
|
207
|
+
// Mode-specific suggestions
|
|
208
|
+
if (session.mode === 'plan' && commandName.includes('approve')) {
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (session.mode === 'ask' && commandName.includes('edit')) {
|
|
213
|
+
return false; // Hide editing commands in ask mode
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Recent command suggestions
|
|
217
|
+
if (this.context.recentCommands.includes(commandName)) {
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Error-based suggestions
|
|
222
|
+
if (this.context.errorHistory.length > 0) {
|
|
223
|
+
const lastError = this.context.errorHistory[this.context.errorHistory.length - 1];
|
|
224
|
+
if (lastError.includes('model') && commandName.includes('model')) {
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
if (lastError.includes('config') && commandName.includes('config')) {
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Sort suggestions by relevance
|
|
237
|
+
*/
|
|
238
|
+
private sortByRelevance(suggestions: CommandSuggestion[]): CommandSuggestion[] {
|
|
239
|
+
return suggestions.sort((a, b) => {
|
|
240
|
+
// Priority order: exact match, then startsWith, then contains
|
|
241
|
+
const aExact = a.text.toLowerCase() === this.context.input.toLowerCase();
|
|
242
|
+
const bExact = b.text.toLowerCase() === this.context.input.toLowerCase();
|
|
243
|
+
|
|
244
|
+
if (aExact && !bExact) return -1;
|
|
245
|
+
if (!aExact && bExact) return 1;
|
|
246
|
+
|
|
247
|
+
// Then by recent usage
|
|
248
|
+
const aRecent = this.context.recentCommands.indexOf(a.text.slice(1)) >= 0;
|
|
249
|
+
const bRecent = this.context.recentCommands.indexOf(b.text.slice(1)) >= 0;
|
|
250
|
+
|
|
251
|
+
if (aRecent && !bRecent) return -1;
|
|
252
|
+
if (!aRecent && bRecent) return 1;
|
|
253
|
+
|
|
254
|
+
// Finally alphabetically
|
|
255
|
+
return a.text.localeCompare(b.text);
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Initialize all available commands
|
|
261
|
+
*/
|
|
262
|
+
private initializeCommands(): void {
|
|
263
|
+
// Navigation commands
|
|
264
|
+
this.addCommand('help', {
|
|
265
|
+
description: 'Show detailed help information',
|
|
266
|
+
category: 'help',
|
|
267
|
+
icon: '?',
|
|
268
|
+
shortcut: '/h'
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
this.addCommand('quit', {
|
|
272
|
+
description: 'Exit Nova CLI (session auto-saved)',
|
|
273
|
+
category: 'navigation',
|
|
274
|
+
icon: '✗',
|
|
275
|
+
shortcut: '/exit'
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
this.addCommand('clear', {
|
|
279
|
+
description: 'Clear conversation and start new session',
|
|
280
|
+
category: 'session',
|
|
281
|
+
icon: '🗑️',
|
|
282
|
+
shortcut: '/reset'
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// Session management
|
|
286
|
+
this.addCommand('status', {
|
|
287
|
+
description: 'Show current session statistics and info',
|
|
288
|
+
category: 'session',
|
|
289
|
+
icon: '📊'
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
this.addCommand('history', {
|
|
293
|
+
description: 'Browse and manage previous sessions',
|
|
294
|
+
category: 'session',
|
|
295
|
+
icon: '📚'
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
this.addCommand('compress', {
|
|
299
|
+
description: 'Optimize context window size',
|
|
300
|
+
category: 'session',
|
|
301
|
+
icon: '⚡'
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// Model commands
|
|
305
|
+
this.addCommand('model', {
|
|
306
|
+
description: 'Switch or list available models',
|
|
307
|
+
category: 'model',
|
|
308
|
+
icon: '🤖'
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
this.addCommand('models', {
|
|
312
|
+
description: 'List all available models',
|
|
313
|
+
category: 'model',
|
|
314
|
+
icon: '🤖'
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
// Mode commands
|
|
318
|
+
this.addCommand('mode', {
|
|
319
|
+
description: 'Change interaction mode (AUTO/PLAN/ASK)',
|
|
320
|
+
category: 'session',
|
|
321
|
+
icon: '🔄'
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
this.addCommand('auto', {
|
|
325
|
+
description: 'Switch to AUTO mode (no approval needed)',
|
|
326
|
+
category: 'session',
|
|
327
|
+
icon: '🚀'
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
this.addCommand('plan', {
|
|
331
|
+
description: 'Switch to PLAN mode (confirm before action)',
|
|
332
|
+
category: 'session',
|
|
333
|
+
icon: '📋'
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
this.addCommand('ask', {
|
|
337
|
+
description: 'Switch to ASK mode (read-only questions)',
|
|
338
|
+
category: 'session',
|
|
339
|
+
icon: '❓'
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
// Tool commands
|
|
343
|
+
this.addCommand('tools', {
|
|
344
|
+
description: 'Manage built-in tools and capabilities',
|
|
345
|
+
category: 'tools',
|
|
346
|
+
icon: '🛠️'
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
this.addCommand('skills', {
|
|
350
|
+
description: 'Use or manage AI skills',
|
|
351
|
+
category: 'skills',
|
|
352
|
+
icon: '🧩'
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
this.addCommand('init', {
|
|
356
|
+
description: 'Generate NOVA.md project memory file',
|
|
357
|
+
category: 'tools',
|
|
358
|
+
icon: '📝'
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// MCP commands
|
|
362
|
+
this.addCommand('mcp', {
|
|
363
|
+
description: 'Manage MCP server connections',
|
|
364
|
+
category: 'mcp',
|
|
365
|
+
icon: '🌐'
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
this.addCommand('mcp-status', {
|
|
369
|
+
description: 'Check MCP server connection status',
|
|
370
|
+
category: 'mcp',
|
|
371
|
+
icon: '🌐'
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
// Memory commands
|
|
375
|
+
this.addCommand('memory', {
|
|
376
|
+
description: 'Manage persistent notes and memory',
|
|
377
|
+
category: 'session',
|
|
378
|
+
icon: '💾'
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
this.addCommand('memory-show', {
|
|
382
|
+
description: 'Display all saved memories',
|
|
383
|
+
category: 'session',
|
|
384
|
+
icon: '💾'
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
this.addCommand('memory-add', {
|
|
388
|
+
description: 'Add a new memory note',
|
|
389
|
+
category: 'session',
|
|
390
|
+
icon: '➕'
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
// Quick actions
|
|
394
|
+
this.addCommand('profile', {
|
|
395
|
+
description: 'Show detailed session profile',
|
|
396
|
+
category: 'session',
|
|
397
|
+
icon: '👤'
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
this.addCommand('stats', {
|
|
401
|
+
description: 'Show token usage and performance stats',
|
|
402
|
+
category: 'session',
|
|
403
|
+
icon: '📈'
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
this.addCommand('theme', {
|
|
407
|
+
description: 'Change UI theme (light/dark)',
|
|
408
|
+
category: 'tools',
|
|
409
|
+
icon: '🎨'
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Add a command to the registry
|
|
415
|
+
*/
|
|
416
|
+
private addCommand(name: string, suggestion: Omit<CommandSuggestion, 'text'>): void {
|
|
417
|
+
this.allCommands.set(name, {
|
|
418
|
+
text: name,
|
|
419
|
+
...suggestion
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Add command to recent usage history
|
|
425
|
+
*/
|
|
426
|
+
private addRecentCommand(command: string): void {
|
|
427
|
+
const cmdName = command.startsWith('/') ? command.slice(1) : command;
|
|
428
|
+
this.recentCommands.unshift(cmdName);
|
|
429
|
+
this.recentCommands = this.recentCommands.slice(0, 20); // Keep only 20 most recent
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Record an error for future suggestions
|
|
434
|
+
*/
|
|
435
|
+
recordError(error: string): void {
|
|
436
|
+
this.errorHistory.unshift(error);
|
|
437
|
+
this.errorHistory = this.errorHistory.slice(0, 10); // Keep only 10 most recent errors
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Get all available commands (for testing/debugging)
|
|
442
|
+
*/
|
|
443
|
+
getAllCommands(): CommandSuggestion[] {
|
|
444
|
+
return Array.from(this.allCommands.values()).map(cmd => ({
|
|
445
|
+
...cmd,
|
|
446
|
+
text: '/' + cmd.text
|
|
447
|
+
}));
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Get commands by category
|
|
452
|
+
*/
|
|
453
|
+
getCommandsByCategory(category: CommandSuggestion['category']): CommandSuggestion[] {
|
|
454
|
+
return this.getAllCommands()
|
|
455
|
+
.filter(cmd => cmd.category === category)
|
|
456
|
+
.sort((a, b) => a.text.localeCompare(b.text));
|
|
457
|
+
}
|
|
458
|
+
}
|
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// IFlowRepl - True iFlow CLI style REPL for Nova CLI
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import type { SessionInfo, NovaConfig } from '../core/types/config.js';
|
|
7
|
+
import { IFlowDropdown } from '../ui/IFlowDropdown.js';
|
|
8
|
+
|
|
9
|
+
export interface IFlowReplOptions {
|
|
10
|
+
enableDropdown?: boolean;
|
|
11
|
+
showPrompt?: boolean;
|
|
12
|
+
theme?: 'light' | 'dark';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class IFlowRepl {
|
|
16
|
+
private session: SessionInfo | null = null;
|
|
17
|
+
private config: NovaConfig | null = null;
|
|
18
|
+
private options: IFlowReplOptions;
|
|
19
|
+
private dropdown: IFlowDropdown | null = null;
|
|
20
|
+
|
|
21
|
+
constructor(options: IFlowReplOptions = {}) {
|
|
22
|
+
this.options = {
|
|
23
|
+
enableDropdown: true,
|
|
24
|
+
showPrompt: true,
|
|
25
|
+
theme: 'dark',
|
|
26
|
+
...options
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
if (this.options.enableDropdown) {
|
|
30
|
+
this.dropdown = new IFlowDropdown({
|
|
31
|
+
theme: this.options.theme,
|
|
32
|
+
showIcons: true,
|
|
33
|
+
maxHeight: 12
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Initialize the iFlow-style REPL
|
|
40
|
+
*/
|
|
41
|
+
async start(session: SessionInfo, config: NovaConfig): Promise<void> {
|
|
42
|
+
this.session = session;
|
|
43
|
+
this.config = config;
|
|
44
|
+
|
|
45
|
+
// Clear screen and show header
|
|
46
|
+
console.clear();
|
|
47
|
+
this.showHeader();
|
|
48
|
+
|
|
49
|
+
// Show welcome message
|
|
50
|
+
this.showWelcomeMessage();
|
|
51
|
+
|
|
52
|
+
// Start input loop
|
|
53
|
+
await this.runInputLoop();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Show professional header similar to iFlow
|
|
58
|
+
*/
|
|
59
|
+
private showHeader(): void {
|
|
60
|
+
const width = Math.min(process.stdout.columns || 80, 70);
|
|
61
|
+
const border = '━'.repeat(width);
|
|
62
|
+
const title = ' NOVA CLI · AI-Powered Terminal Assistant ';
|
|
63
|
+
const header = `╭${border}╮\n│${title.padEnd(width)}│\n├${'─'.repeat(width)}┤`;
|
|
64
|
+
|
|
65
|
+
console.log(chalk.bgBlue.black(header));
|
|
66
|
+
|
|
67
|
+
// Model info line
|
|
68
|
+
const modelShort = this.session?.model.split('/').pop() || this.session?.model || 'unknown';
|
|
69
|
+
const modelLine = `│ ${chalk.cyan('Model:')} ${chalk.white(modelShort.padEnd(50))}${chalk.blue('│')}`;
|
|
70
|
+
console.log(chalk.blue(modelLine));
|
|
71
|
+
|
|
72
|
+
// Mode and directory info
|
|
73
|
+
const modeLabel = this.session?.mode === 'auto' ? 'AUTO' :
|
|
74
|
+
this.session?.mode === 'plan' ? 'PLAN' : 'ASK';
|
|
75
|
+
const modeColor = this.session?.mode === 'auto' ? 'green' :
|
|
76
|
+
this.session?.mode === 'plan' ? 'yellow' : 'blue';
|
|
77
|
+
|
|
78
|
+
const modeLine = `│ ${chalk[modeColor](`Mode: ${modeLabel}`).padEnd(56)}${chalk.blue('│')}`;
|
|
79
|
+
const dirLine = `│ ${chalk.cyan('Dir:')} ${chalk.white((this.session?.workingDirectory || '.').padEnd(49))}${chalk.blue('│')}`;
|
|
80
|
+
|
|
81
|
+
console.log(chalk.blue(modeLine));
|
|
82
|
+
console.log(chalk.blue(dirLine));
|
|
83
|
+
|
|
84
|
+
const footer = `╰${'━'.repeat(width)}╯`;
|
|
85
|
+
console.log(chalk.blue(footer));
|
|
86
|
+
console.log('');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Show welcome message
|
|
91
|
+
*/
|
|
92
|
+
private showWelcomeMessage(): void {
|
|
93
|
+
const messages = [
|
|
94
|
+
chalk.cyan('🚀 Welcome to Nova CLI - Your AI-Powered Terminal Assistant'),
|
|
95
|
+
'',
|
|
96
|
+
chalk.yellow('✨ Features:'),
|
|
97
|
+
' • Multiple AI model providers (OpenAI, Anthropic, Ollama, etc.)',
|
|
98
|
+
' • Smart file operations with @file references',
|
|
99
|
+
' • Built-in tools for code analysis and generation',
|
|
100
|
+
' • MCP server integration for extended functionality',
|
|
101
|
+
' • Session persistence and history management',
|
|
102
|
+
'',
|
|
103
|
+
chalk.green('🎯 Quick Start:'),
|
|
104
|
+
' • Type /help for command suggestions',
|
|
105
|
+
' • Use ↑↓ arrows to navigate, Enter to select',
|
|
106
|
+
' • Press ESC to cancel any operation',
|
|
107
|
+
'',
|
|
108
|
+
chalk.blue('💡 Pro Tip:'),
|
|
109
|
+
' Try typing / and see the interactive dropdown!',
|
|
110
|
+
'',
|
|
111
|
+
chalk.gray('Press Ctrl+C at any time to cancel current operation')
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
messages.forEach(msg => console.log(msg));
|
|
115
|
+
console.log('');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Main input loop with iFlow-style handling
|
|
120
|
+
*/
|
|
121
|
+
private async runInputLoop(): Promise<void> {
|
|
122
|
+
while (true) {
|
|
123
|
+
try {
|
|
124
|
+
// Show prompt
|
|
125
|
+
if (this.options.showPrompt) {
|
|
126
|
+
this.showPrompt();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Get user input
|
|
130
|
+
const input = await this.getInput();
|
|
131
|
+
|
|
132
|
+
if (!input) continue;
|
|
133
|
+
|
|
134
|
+
// Handle iFlow-style dropdown if enabled
|
|
135
|
+
if (this.options.enableDropdown && input.startsWith('/')) {
|
|
136
|
+
const result = await this.handleDropdown(input);
|
|
137
|
+
if (result) {
|
|
138
|
+
// Execute the selected command
|
|
139
|
+
await this.executeCommand(result.id);
|
|
140
|
+
}
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Regular command execution
|
|
145
|
+
await this.executeCommand(input);
|
|
146
|
+
|
|
147
|
+
} catch (error) {
|
|
148
|
+
if (error instanceof Error && error.message.includes('SIGINT')) {
|
|
149
|
+
console.log(chalk.yellow('\nUse /quit or Ctrl+D to exit'));
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
console.error(chalk.red(`Error: ${(error as Error).message}`));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Show iFlow-style prompt
|
|
159
|
+
*/
|
|
160
|
+
private showPrompt(): void {
|
|
161
|
+
const promptText = `${chalk.cyan('[NOVA]')} ${chalk.white('> ')}`;
|
|
162
|
+
process.stdout.write(promptText);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Get user input
|
|
167
|
+
*/
|
|
168
|
+
private async getInput(): Promise<string> {
|
|
169
|
+
return new Promise((resolve) => {
|
|
170
|
+
const readline = require('node:readline');
|
|
171
|
+
const rl = readline.createInterface({
|
|
172
|
+
input: process.stdin,
|
|
173
|
+
output: process.stdout
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
rl.question('', (answer: string) => {
|
|
177
|
+
rl.close();
|
|
178
|
+
resolve(answer.trim());
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Handle iFlow-style dropdown
|
|
185
|
+
*/
|
|
186
|
+
private async handleDropdown(input: string): Promise<any> {
|
|
187
|
+
if (!this.dropdown || !this.session) return null;
|
|
188
|
+
|
|
189
|
+
return await this.dropdown.show(input, this.session);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Execute a command
|
|
194
|
+
*/
|
|
195
|
+
private async executeCommand(command: string): Promise<void> {
|
|
196
|
+
console.log(chalk.gray(`Executing: ${command}`));
|
|
197
|
+
|
|
198
|
+
// Simulate command execution delay
|
|
199
|
+
setTimeout(() => {
|
|
200
|
+
console.log(chalk.green(`✓ Command executed: ${command}`));
|
|
201
|
+
}, 300);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Cleanup resources
|
|
206
|
+
*/
|
|
207
|
+
dispose(): void {
|
|
208
|
+
if (this.dropdown) {
|
|
209
|
+
this.dropdown.hide();
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|