vigthoria-cli 1.0.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.
- package/README.md +413 -0
- package/dist/commands/auth.d.ts +24 -0
- package/dist/commands/auth.d.ts.map +1 -0
- package/dist/commands/auth.js +194 -0
- package/dist/commands/auth.js.map +1 -0
- package/dist/commands/chat.d.ts +64 -0
- package/dist/commands/chat.d.ts.map +1 -0
- package/dist/commands/chat.js +596 -0
- package/dist/commands/chat.js.map +1 -0
- package/dist/commands/config.d.ts +25 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +291 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/edit.d.ts +28 -0
- package/dist/commands/edit.d.ts.map +1 -0
- package/dist/commands/edit.js +257 -0
- package/dist/commands/edit.js.map +1 -0
- package/dist/commands/explain.d.ts +21 -0
- package/dist/commands/explain.d.ts.map +1 -0
- package/dist/commands/explain.js +98 -0
- package/dist/commands/explain.js.map +1 -0
- package/dist/commands/generate.d.ts +25 -0
- package/dist/commands/generate.d.ts.map +1 -0
- package/dist/commands/generate.js +155 -0
- package/dist/commands/generate.js.map +1 -0
- package/dist/commands/review.d.ts +24 -0
- package/dist/commands/review.d.ts.map +1 -0
- package/dist/commands/review.js +153 -0
- package/dist/commands/review.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +205 -0
- package/dist/index.js.map +1 -0
- package/dist/utils/api.d.ts +88 -0
- package/dist/utils/api.d.ts.map +1 -0
- package/dist/utils/api.js +431 -0
- package/dist/utils/api.js.map +1 -0
- package/dist/utils/config.d.ts +57 -0
- package/dist/utils/config.d.ts.map +1 -0
- package/dist/utils/config.js +167 -0
- package/dist/utils/config.js.map +1 -0
- package/dist/utils/files.d.ts +31 -0
- package/dist/utils/files.d.ts.map +1 -0
- package/dist/utils/files.js +217 -0
- package/dist/utils/files.js.map +1 -0
- package/dist/utils/logger.d.ts +23 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +104 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/session.d.ts +61 -0
- package/dist/utils/session.d.ts.map +1 -0
- package/dist/utils/session.js +172 -0
- package/dist/utils/session.js.map +1 -0
- package/dist/utils/tools.d.ts +145 -0
- package/dist/utils/tools.d.ts.map +1 -0
- package/dist/utils/tools.js +781 -0
- package/dist/utils/tools.js.map +1 -0
- package/install.sh +248 -0
- package/package.json +52 -0
- package/src/commands/auth.ts +225 -0
- package/src/commands/chat.ts +690 -0
- package/src/commands/config.ts +297 -0
- package/src/commands/edit.ts +310 -0
- package/src/commands/explain.ts +115 -0
- package/src/commands/generate.ts +177 -0
- package/src/commands/review.ts +186 -0
- package/src/index.ts +221 -0
- package/src/types/marked-terminal.d.ts +31 -0
- package/src/utils/api.ts +531 -0
- package/src/utils/config.ts +224 -0
- package/src/utils/files.ts +212 -0
- package/src/utils/logger.ts +125 -0
- package/src/utils/session.ts +167 -0
- package/src/utils/tools.ts +933 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,690 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive Chat Command for Vigthoria CLI
|
|
3
|
+
*
|
|
4
|
+
* Now with Claude Code-like agentic capabilities!
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
import ora from 'ora';
|
|
9
|
+
import * as readline from 'readline';
|
|
10
|
+
import { Marked } from 'marked';
|
|
11
|
+
import { markedTerminal } from 'marked-terminal';
|
|
12
|
+
import { Config } from '../utils/config.js';
|
|
13
|
+
import { Logger } from '../utils/logger.js';
|
|
14
|
+
import { APIClient, ChatMessage } from '../utils/api.js';
|
|
15
|
+
import { FileUtils } from '../utils/files.js';
|
|
16
|
+
import { AgenticTools, ToolCall, ToolResult } from '../utils/tools.js';
|
|
17
|
+
import { SessionManager, Session } from '../utils/session.js';
|
|
18
|
+
|
|
19
|
+
interface ChatOptions {
|
|
20
|
+
model: string;
|
|
21
|
+
project: string;
|
|
22
|
+
agent?: boolean;
|
|
23
|
+
autoApprove?: boolean;
|
|
24
|
+
resume?: boolean;
|
|
25
|
+
stream?: boolean;
|
|
26
|
+
local?: boolean; // Skip authentication for local testing
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class ChatCommand {
|
|
30
|
+
private config: Config;
|
|
31
|
+
private logger: Logger;
|
|
32
|
+
private api: APIClient;
|
|
33
|
+
private messages: ChatMessage[] = [];
|
|
34
|
+
private fileUtils: FileUtils;
|
|
35
|
+
private marked: Marked;
|
|
36
|
+
private tools: AgenticTools | null = null;
|
|
37
|
+
private agentMode: boolean = false;
|
|
38
|
+
private rl: readline.Interface | null = null;
|
|
39
|
+
private sessionManager: SessionManager;
|
|
40
|
+
private currentSession: Session | null = null;
|
|
41
|
+
private streamMode: boolean = true;
|
|
42
|
+
private localMode: boolean = false;
|
|
43
|
+
|
|
44
|
+
constructor(config: Config, logger: Logger) {
|
|
45
|
+
this.config = config;
|
|
46
|
+
this.logger = logger;
|
|
47
|
+
this.api = new APIClient(config, logger);
|
|
48
|
+
this.fileUtils = new FileUtils(process.cwd(), config.get('project').ignorePatterns);
|
|
49
|
+
this.sessionManager = new SessionManager();
|
|
50
|
+
|
|
51
|
+
// Setup marked for terminal rendering
|
|
52
|
+
this.marked = new Marked();
|
|
53
|
+
this.marked.use(markedTerminal() as any);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async run(options: ChatOptions): Promise<void> {
|
|
57
|
+
// Check authentication (skip for local testing mode)
|
|
58
|
+
if (!options.local && !this.config.isAuthenticated()) {
|
|
59
|
+
this.logger.error('Not authenticated. Run: vigthoria login');
|
|
60
|
+
this.logger.info('Tip: Use --local flag to test with local Ollama models');
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!options.local && !this.config.hasValidSubscription()) {
|
|
65
|
+
this.logger.warn('Your subscription has expired. Some features may be limited.');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (options.local) {
|
|
69
|
+
this.logger.info('Local mode: Using Ollama for AI responses');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Setup agentic tools if enabled
|
|
73
|
+
this.agentMode = options.agent || false;
|
|
74
|
+
this.streamMode = options.stream !== false; // Default to true
|
|
75
|
+
this.localMode = options.local || false;
|
|
76
|
+
|
|
77
|
+
if (this.agentMode) {
|
|
78
|
+
this.tools = new AgenticTools(
|
|
79
|
+
this.logger,
|
|
80
|
+
options.project,
|
|
81
|
+
this.askPermission.bind(this),
|
|
82
|
+
options.autoApprove || false
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Try to resume previous session or create new one
|
|
87
|
+
if (options.resume) {
|
|
88
|
+
this.currentSession = this.sessionManager.getLatest(options.project);
|
|
89
|
+
if (this.currentSession) {
|
|
90
|
+
this.messages = [...this.currentSession.messages];
|
|
91
|
+
this.logger.success(`Resumed session: ${this.currentSession.id}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!this.currentSession) {
|
|
96
|
+
this.currentSession = this.sessionManager.create(
|
|
97
|
+
options.project,
|
|
98
|
+
options.model,
|
|
99
|
+
this.agentMode
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Get project context
|
|
104
|
+
const projectContext = await this.fileUtils.getProjectContext();
|
|
105
|
+
|
|
106
|
+
// Setup system message
|
|
107
|
+
const systemMessage: ChatMessage = {
|
|
108
|
+
role: 'system',
|
|
109
|
+
content: this.buildSystemPrompt(projectContext, options),
|
|
110
|
+
};
|
|
111
|
+
this.messages.push(systemMessage);
|
|
112
|
+
|
|
113
|
+
this.printWelcome(options);
|
|
114
|
+
|
|
115
|
+
// Start REPL
|
|
116
|
+
await this.startRepl(options);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private buildSystemPrompt(
|
|
120
|
+
projectContext: { type: string; files: string[]; dependencies: Record<string, string> },
|
|
121
|
+
options: ChatOptions
|
|
122
|
+
): string {
|
|
123
|
+
let prompt = `You are Vigthoria, a premium AI coding assistant. You help developers write, understand, and improve code.
|
|
124
|
+
|
|
125
|
+
Project Context:
|
|
126
|
+
- Type: ${projectContext.type}
|
|
127
|
+
- Root: ${options.project}
|
|
128
|
+
- Key files: ${projectContext.files.slice(0, 10).join(', ')}
|
|
129
|
+
${projectContext.type === 'node' ? `- Dependencies: ${Object.keys(projectContext.dependencies).slice(0, 15).join(', ')}` : ''}
|
|
130
|
+
|
|
131
|
+
Guidelines:
|
|
132
|
+
- Provide complete, production-ready code
|
|
133
|
+
- Use modern best practices (2024-2026 standards)
|
|
134
|
+
- Include proper error handling
|
|
135
|
+
- Explain your reasoning when helpful
|
|
136
|
+
- Be concise but thorough
|
|
137
|
+
|
|
138
|
+
Special Commands (user may use these):
|
|
139
|
+
- /file <path> - Read and include a file in context
|
|
140
|
+
- /edit <path> - Switch to file editing mode
|
|
141
|
+
- /diff - Show pending changes
|
|
142
|
+
- /apply - Apply pending changes
|
|
143
|
+
- /clear - Clear conversation history
|
|
144
|
+
- /model <name> - Switch AI model
|
|
145
|
+
- /agent - Toggle agentic mode (Claude Code-like autonomous actions)
|
|
146
|
+
- /help - Show available commands`;
|
|
147
|
+
|
|
148
|
+
// Add tool instructions if in agent mode
|
|
149
|
+
if (this.agentMode && this.tools) {
|
|
150
|
+
prompt += `\n\n${AgenticTools.getToolsForPrompt()}`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return prompt;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private printWelcome(options: ChatOptions): void {
|
|
157
|
+
const sub = this.config.get('subscription');
|
|
158
|
+
|
|
159
|
+
console.log();
|
|
160
|
+
this.logger.box(
|
|
161
|
+
`Model: ${chalk.cyan(options.model)}\n` +
|
|
162
|
+
`Plan: ${chalk.green(sub.plan || 'free')}\n` +
|
|
163
|
+
`Project: ${chalk.gray(options.project)}\n` +
|
|
164
|
+
`Agent Mode: ${this.agentMode ? chalk.green('ON ✓') : chalk.gray('OFF')}`,
|
|
165
|
+
'Vigthoria Chat'
|
|
166
|
+
);
|
|
167
|
+
console.log();
|
|
168
|
+
if (this.agentMode) {
|
|
169
|
+
console.log(chalk.yellow('🤖 Agent Mode: AI can read files, edit code, and run commands autonomously.'));
|
|
170
|
+
}
|
|
171
|
+
console.log(chalk.gray('Type your message or /help for commands. Press Ctrl+C to exit.'));
|
|
172
|
+
console.log();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private async startRepl(options: ChatOptions): Promise<void> {
|
|
176
|
+
this.rl = readline.createInterface({
|
|
177
|
+
input: process.stdin,
|
|
178
|
+
output: process.stdout,
|
|
179
|
+
prompt: chalk.cyan('you › '),
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
let currentModel = options.model;
|
|
183
|
+
let pendingChanges: { file: string; content: string } | null = null;
|
|
184
|
+
|
|
185
|
+
this.rl.prompt();
|
|
186
|
+
|
|
187
|
+
for await (const line of this.rl) {
|
|
188
|
+
const input = line.trim();
|
|
189
|
+
|
|
190
|
+
if (!input) {
|
|
191
|
+
this.rl.prompt();
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Handle special commands
|
|
196
|
+
if (input.startsWith('/')) {
|
|
197
|
+
const [cmd, ...args] = input.slice(1).split(' ');
|
|
198
|
+
|
|
199
|
+
switch (cmd) {
|
|
200
|
+
case 'help':
|
|
201
|
+
this.printHelp();
|
|
202
|
+
break;
|
|
203
|
+
|
|
204
|
+
case 'clear':
|
|
205
|
+
this.messages = [this.messages[0]]; // Keep system message
|
|
206
|
+
this.logger.success('Conversation cleared');
|
|
207
|
+
break;
|
|
208
|
+
|
|
209
|
+
case 'model':
|
|
210
|
+
if (args[0]) {
|
|
211
|
+
currentModel = args[0];
|
|
212
|
+
this.logger.success(`Model switched to: ${currentModel}`);
|
|
213
|
+
} else {
|
|
214
|
+
this.printModels();
|
|
215
|
+
}
|
|
216
|
+
break;
|
|
217
|
+
|
|
218
|
+
case 'agent':
|
|
219
|
+
this.agentMode = !this.agentMode;
|
|
220
|
+
if (this.agentMode && !this.tools) {
|
|
221
|
+
this.tools = new AgenticTools(
|
|
222
|
+
this.logger,
|
|
223
|
+
options.project,
|
|
224
|
+
this.askPermission.bind(this),
|
|
225
|
+
options.autoApprove || false
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
// Rebuild system prompt with/without tools
|
|
229
|
+
this.messages[0] = {
|
|
230
|
+
role: 'system',
|
|
231
|
+
content: this.buildSystemPrompt(
|
|
232
|
+
await this.fileUtils.getProjectContext(),
|
|
233
|
+
{ ...options, agent: this.agentMode }
|
|
234
|
+
),
|
|
235
|
+
};
|
|
236
|
+
this.logger.success(`Agent mode: ${this.agentMode ? chalk.green('ON') : chalk.red('OFF')}`);
|
|
237
|
+
if (this.agentMode) {
|
|
238
|
+
console.log(chalk.yellow(' AI can now read files, edit code, and run commands.'));
|
|
239
|
+
}
|
|
240
|
+
break;
|
|
241
|
+
|
|
242
|
+
case 'approve':
|
|
243
|
+
if (this.tools) {
|
|
244
|
+
options.autoApprove = !options.autoApprove;
|
|
245
|
+
this.tools = new AgenticTools(
|
|
246
|
+
this.logger,
|
|
247
|
+
options.project,
|
|
248
|
+
this.askPermission.bind(this),
|
|
249
|
+
options.autoApprove
|
|
250
|
+
);
|
|
251
|
+
this.logger.success(`Auto-approve: ${options.autoApprove ? chalk.green('ON') : chalk.red('OFF')}`);
|
|
252
|
+
if (options.autoApprove) {
|
|
253
|
+
console.log(chalk.red(' ⚠️ AI actions will be executed without confirmation!'));
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
break;
|
|
257
|
+
|
|
258
|
+
case 'file':
|
|
259
|
+
if (args[0]) {
|
|
260
|
+
await this.addFileToContext(args[0]);
|
|
261
|
+
} else {
|
|
262
|
+
this.logger.error('Usage: /file <path>');
|
|
263
|
+
}
|
|
264
|
+
break;
|
|
265
|
+
|
|
266
|
+
case 'edit':
|
|
267
|
+
if (args[0]) {
|
|
268
|
+
pendingChanges = await this.startEditMode(args[0], currentModel);
|
|
269
|
+
} else {
|
|
270
|
+
this.logger.error('Usage: /edit <path>');
|
|
271
|
+
}
|
|
272
|
+
break;
|
|
273
|
+
|
|
274
|
+
case 'diff':
|
|
275
|
+
if (pendingChanges) {
|
|
276
|
+
this.showPendingDiff(pendingChanges);
|
|
277
|
+
} else {
|
|
278
|
+
this.logger.info('No pending changes');
|
|
279
|
+
}
|
|
280
|
+
break;
|
|
281
|
+
|
|
282
|
+
case 'apply':
|
|
283
|
+
if (pendingChanges) {
|
|
284
|
+
this.applyChanges(pendingChanges);
|
|
285
|
+
pendingChanges = null;
|
|
286
|
+
} else {
|
|
287
|
+
this.logger.info('No pending changes to apply');
|
|
288
|
+
}
|
|
289
|
+
break;
|
|
290
|
+
|
|
291
|
+
case 'sessions':
|
|
292
|
+
this.listSessions();
|
|
293
|
+
break;
|
|
294
|
+
|
|
295
|
+
case 'history':
|
|
296
|
+
this.showHistory();
|
|
297
|
+
break;
|
|
298
|
+
|
|
299
|
+
case 'save':
|
|
300
|
+
if (this.currentSession) {
|
|
301
|
+
this.sessionManager.save(this.currentSession);
|
|
302
|
+
this.logger.success(`Session saved: ${this.currentSession.id}`);
|
|
303
|
+
}
|
|
304
|
+
break;
|
|
305
|
+
|
|
306
|
+
case 'new':
|
|
307
|
+
// Start new session
|
|
308
|
+
this.currentSession = this.sessionManager.create(
|
|
309
|
+
options.project,
|
|
310
|
+
currentModel,
|
|
311
|
+
this.agentMode
|
|
312
|
+
);
|
|
313
|
+
this.messages = [this.messages[0]]; // Keep only system message
|
|
314
|
+
this.logger.success(`New session: ${this.currentSession.id}`);
|
|
315
|
+
break;
|
|
316
|
+
|
|
317
|
+
case 'compact':
|
|
318
|
+
// Compact context by summarizing older messages
|
|
319
|
+
await this.compactContext(currentModel);
|
|
320
|
+
break;
|
|
321
|
+
|
|
322
|
+
case 'undo':
|
|
323
|
+
// Undo last file operation
|
|
324
|
+
if (this.tools) {
|
|
325
|
+
const undoResult = await this.tools.undo();
|
|
326
|
+
if (undoResult.success) {
|
|
327
|
+
this.logger.success(undoResult.output || 'Undo completed');
|
|
328
|
+
if (undoResult.metadata?.remainingUndos !== undefined) {
|
|
329
|
+
console.log(chalk.gray(` ${undoResult.metadata.remainingUndos} more undo(s) available`));
|
|
330
|
+
}
|
|
331
|
+
} else {
|
|
332
|
+
this.logger.error(undoResult.error || 'Nothing to undo');
|
|
333
|
+
}
|
|
334
|
+
} else {
|
|
335
|
+
this.logger.info('Undo is only available in agent mode. Use /agent to enable.');
|
|
336
|
+
}
|
|
337
|
+
break;
|
|
338
|
+
|
|
339
|
+
case 'exit':
|
|
340
|
+
case 'quit':
|
|
341
|
+
// Auto-save session on exit
|
|
342
|
+
if (this.currentSession && this.messages.length > 1) {
|
|
343
|
+
this.sessionManager.save(this.currentSession);
|
|
344
|
+
console.log(chalk.gray(`Session saved: ${this.currentSession.id}`));
|
|
345
|
+
}
|
|
346
|
+
console.log(chalk.cyan('\nGoodbye! 👋\n'));
|
|
347
|
+
this.rl!.close();
|
|
348
|
+
return;
|
|
349
|
+
|
|
350
|
+
default:
|
|
351
|
+
this.logger.warn(`Unknown command: /${cmd}`);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
this.rl!.prompt();
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Regular chat message
|
|
359
|
+
await this.chat(input, currentModel);
|
|
360
|
+
|
|
361
|
+
// Save message to session
|
|
362
|
+
if (this.currentSession) {
|
|
363
|
+
this.currentSession.messages = [...this.messages];
|
|
364
|
+
this.sessionManager.save(this.currentSession);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
this.rl!.prompt();
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
private async chat(userInput: string, model: string): Promise<void> {
|
|
372
|
+
// Add user message (skip if empty - used for tool continuation)
|
|
373
|
+
if (userInput) {
|
|
374
|
+
this.messages.push({ role: 'user', content: userInput });
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const spinner = ora({
|
|
378
|
+
text: chalk.gray('Thinking...'),
|
|
379
|
+
spinner: 'dots',
|
|
380
|
+
}).start();
|
|
381
|
+
|
|
382
|
+
try {
|
|
383
|
+
const response = await this.api.chat(this.messages, model, this.localMode);
|
|
384
|
+
|
|
385
|
+
spinner.stop();
|
|
386
|
+
|
|
387
|
+
// Add assistant message
|
|
388
|
+
this.messages.push({ role: 'assistant', content: response.message });
|
|
389
|
+
|
|
390
|
+
// Render markdown response
|
|
391
|
+
console.log();
|
|
392
|
+
console.log(chalk.cyan('vigthoria ›'));
|
|
393
|
+
console.log(this.marked.parse(response.message));
|
|
394
|
+
|
|
395
|
+
// Show token usage
|
|
396
|
+
if (response.usage) {
|
|
397
|
+
console.log(chalk.gray(`[${response.usage.total_tokens} tokens]`));
|
|
398
|
+
}
|
|
399
|
+
console.log();
|
|
400
|
+
|
|
401
|
+
// In agent mode, check for and execute tool calls
|
|
402
|
+
if (this.agentMode && this.tools) {
|
|
403
|
+
const toolCalls = AgenticTools.parseToolCalls(response.message);
|
|
404
|
+
|
|
405
|
+
if (toolCalls.length > 0) {
|
|
406
|
+
await this.executeToolCalls(toolCalls, model);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
} catch (error) {
|
|
411
|
+
spinner.stop();
|
|
412
|
+
this.logger.error('Failed to get response:', (error as Error).message);
|
|
413
|
+
// Remove failed user message
|
|
414
|
+
this.messages.pop();
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Execute tool calls from AI response (Claude Code-like)
|
|
420
|
+
*/
|
|
421
|
+
private async executeToolCalls(calls: ToolCall[], model: string): Promise<void> {
|
|
422
|
+
const results: { tool: string; result: ToolResult }[] = [];
|
|
423
|
+
|
|
424
|
+
for (const call of calls) {
|
|
425
|
+
console.log(chalk.yellow(`\n⚙ Executing: ${call.tool}`));
|
|
426
|
+
|
|
427
|
+
const result = await this.tools!.execute(call);
|
|
428
|
+
results.push({ tool: call.tool, result });
|
|
429
|
+
|
|
430
|
+
if (result.success) {
|
|
431
|
+
this.logger.success(`${call.tool}: Success`);
|
|
432
|
+
if (result.output) {
|
|
433
|
+
console.log(chalk.gray(this.truncateOutput(result.output)));
|
|
434
|
+
}
|
|
435
|
+
} else {
|
|
436
|
+
this.logger.error(`${call.tool}: ${result.error}`);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Send tool results back to AI for continuation
|
|
441
|
+
const toolResultsMessage: ChatMessage = {
|
|
442
|
+
role: 'user',
|
|
443
|
+
content: this.formatToolResults(results),
|
|
444
|
+
};
|
|
445
|
+
this.messages.push(toolResultsMessage);
|
|
446
|
+
|
|
447
|
+
// Get AI's follow-up response
|
|
448
|
+
console.log();
|
|
449
|
+
await this.chat('', model);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Format tool results for AI context
|
|
454
|
+
*/
|
|
455
|
+
private formatToolResults(
|
|
456
|
+
results: { tool: string; result: ToolResult }[]
|
|
457
|
+
): string {
|
|
458
|
+
let msg = 'Tool execution results:\n\n';
|
|
459
|
+
|
|
460
|
+
for (const { tool, result } of results) {
|
|
461
|
+
msg += `## ${tool}\n`;
|
|
462
|
+
msg += `Status: ${result.success ? 'Success' : 'Failed'}\n`;
|
|
463
|
+
if (result.output) {
|
|
464
|
+
msg += `Output:\n\`\`\`\n${this.truncateOutput(result.output)}\n\`\`\`\n`;
|
|
465
|
+
}
|
|
466
|
+
if (result.error) {
|
|
467
|
+
msg += `Error: ${result.error}\n`;
|
|
468
|
+
}
|
|
469
|
+
msg += '\n';
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
msg += 'Continue with your analysis or next steps.';
|
|
473
|
+
return msg;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Truncate long output for context management
|
|
478
|
+
*/
|
|
479
|
+
private truncateOutput(output: string, maxLines: number = 50): string {
|
|
480
|
+
const lines = output.split('\n');
|
|
481
|
+
if (lines.length <= maxLines) {
|
|
482
|
+
return output;
|
|
483
|
+
}
|
|
484
|
+
return lines.slice(0, maxLines).join('\n') + `\n... (${lines.length - maxLines} more lines)`;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Ask user for permission to execute dangerous action
|
|
489
|
+
*/
|
|
490
|
+
private async askPermission(action: string): Promise<boolean> {
|
|
491
|
+
return new Promise((resolve) => {
|
|
492
|
+
console.log('\n' + action);
|
|
493
|
+
|
|
494
|
+
if (!this.rl) {
|
|
495
|
+
resolve(false);
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
this.rl.question(chalk.yellow('Allow? [y/N] '), (answer) => {
|
|
500
|
+
const allowed = answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes';
|
|
501
|
+
resolve(allowed);
|
|
502
|
+
});
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
private async addFileToContext(filePath: string): Promise<void> {
|
|
507
|
+
const file = this.fileUtils.readFile(filePath);
|
|
508
|
+
|
|
509
|
+
if (!file) {
|
|
510
|
+
this.logger.error(`File not found: ${filePath}`);
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Add file content to messages
|
|
515
|
+
const fileMessage: ChatMessage = {
|
|
516
|
+
role: 'user',
|
|
517
|
+
content: `Here is the content of ${file.relativePath} (${file.language}, ${file.lines} lines):\n\n\`\`\`${file.language}\n${file.content}\n\`\`\``,
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
this.messages.push(fileMessage);
|
|
521
|
+
this.logger.success(`Added ${file.relativePath} to context (${file.lines} lines)`);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
private async startEditMode(
|
|
525
|
+
filePath: string,
|
|
526
|
+
model: string
|
|
527
|
+
): Promise<{ file: string; content: string } | null> {
|
|
528
|
+
const file = this.fileUtils.readFile(filePath);
|
|
529
|
+
|
|
530
|
+
if (!file) {
|
|
531
|
+
this.logger.error(`File not found: ${filePath}`);
|
|
532
|
+
return null;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
console.log();
|
|
536
|
+
this.logger.section(`Editing: ${file.relativePath}`);
|
|
537
|
+
console.log(chalk.gray('What changes would you like to make?'));
|
|
538
|
+
console.log();
|
|
539
|
+
|
|
540
|
+
// This would enter a sub-REPL for editing
|
|
541
|
+
// For now, return the file info
|
|
542
|
+
return { file: file.path, content: file.content };
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
private showPendingDiff(changes: { file: string; content: string }): void {
|
|
546
|
+
const file = this.fileUtils.readFile(changes.file);
|
|
547
|
+
if (!file) return;
|
|
548
|
+
|
|
549
|
+
const diff = this.fileUtils.createDiff(file.content, changes.content);
|
|
550
|
+
|
|
551
|
+
this.logger.section('Pending Changes');
|
|
552
|
+
this.logger.diff(diff.added, diff.removed);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
private applyChanges(changes: { file: string; content: string }): void {
|
|
556
|
+
// Backup first
|
|
557
|
+
const backup = this.fileUtils.backupFile(changes.file);
|
|
558
|
+
if (backup) {
|
|
559
|
+
this.logger.info(`Backup created: ${backup}`);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Apply
|
|
563
|
+
if (this.fileUtils.writeFile(changes.file, changes.content)) {
|
|
564
|
+
this.logger.success(`Changes applied to ${changes.file}`);
|
|
565
|
+
} else {
|
|
566
|
+
this.logger.error('Failed to apply changes');
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
private printHelp(): void {
|
|
571
|
+
console.log();
|
|
572
|
+
this.logger.section('Available Commands');
|
|
573
|
+
console.log(chalk.cyan('/file <path>') + ' - Add file to conversation context');
|
|
574
|
+
console.log(chalk.cyan('/edit <path>') + ' - Start editing a file');
|
|
575
|
+
console.log(chalk.cyan('/diff') + ' - Show pending changes');
|
|
576
|
+
console.log(chalk.cyan('/apply') + ' - Apply pending changes');
|
|
577
|
+
console.log(chalk.cyan('/model <name>') + ' - Switch AI model');
|
|
578
|
+
console.log(chalk.cyan('/agent') + ' - Toggle agentic mode (Claude Code-like)');
|
|
579
|
+
console.log(chalk.cyan('/approve') + ' - Toggle auto-approve for agent actions');
|
|
580
|
+
console.log(chalk.cyan('/undo') + ' - Undo last file operation (agent mode)');
|
|
581
|
+
console.log(chalk.cyan('/clear') + ' - Clear conversation history');
|
|
582
|
+
console.log(chalk.cyan('/compact') + ' - Compact context (summarize older messages)');
|
|
583
|
+
console.log(chalk.cyan('/sessions') + ' - List saved sessions');
|
|
584
|
+
console.log(chalk.cyan('/history') + ' - Show conversation history');
|
|
585
|
+
console.log(chalk.cyan('/save') + ' - Save current session');
|
|
586
|
+
console.log(chalk.cyan('/new') + ' - Start new session');
|
|
587
|
+
console.log(chalk.cyan('/help') + ' - Show this help');
|
|
588
|
+
console.log(chalk.cyan('/exit') + ' - Exit Vigthoria (auto-saves)');
|
|
589
|
+
console.log();
|
|
590
|
+
|
|
591
|
+
if (this.agentMode) {
|
|
592
|
+
console.log(chalk.yellow('Agent Mode Tools:'));
|
|
593
|
+
console.log(chalk.gray(' read_file, write_file, edit_file, bash, grep, list_dir, glob, git'));
|
|
594
|
+
console.log();
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
private listSessions(): void {
|
|
599
|
+
const sessions = this.sessionManager.list();
|
|
600
|
+
|
|
601
|
+
console.log();
|
|
602
|
+
this.logger.section('Saved Sessions');
|
|
603
|
+
|
|
604
|
+
if (sessions.length === 0) {
|
|
605
|
+
console.log(chalk.gray(' No saved sessions'));
|
|
606
|
+
} else {
|
|
607
|
+
sessions.slice(0, 10).forEach(s => {
|
|
608
|
+
const current = this.currentSession?.id === s.id ? chalk.green(' (current)') : '';
|
|
609
|
+
const agent = s.agentMode ? chalk.yellow(' [agent]') : '';
|
|
610
|
+
console.log(chalk.cyan(s.id) + agent + current);
|
|
611
|
+
console.log(chalk.gray(` ${s.project} - ${new Date(s.updatedAt).toLocaleString()}`));
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
console.log();
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
private showHistory(): void {
|
|
618
|
+
console.log();
|
|
619
|
+
this.logger.section('Conversation History');
|
|
620
|
+
|
|
621
|
+
const userMessages = this.messages.filter(m => m.role !== 'system');
|
|
622
|
+
|
|
623
|
+
if (userMessages.length === 0) {
|
|
624
|
+
console.log(chalk.gray(' No messages yet'));
|
|
625
|
+
} else {
|
|
626
|
+
userMessages.forEach((m, i) => {
|
|
627
|
+
const role = m.role === 'user' ? chalk.cyan('you') : chalk.green('vigthoria');
|
|
628
|
+
const preview = m.content.substring(0, 60).replace(/\n/g, ' ');
|
|
629
|
+
console.log(`${i + 1}. ${role}: ${chalk.gray(preview)}...`);
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
console.log();
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
private async compactContext(model: string): Promise<void> {
|
|
636
|
+
// If we have too many messages, summarize older ones
|
|
637
|
+
if (this.messages.length < 10) {
|
|
638
|
+
this.logger.info('Context is already compact');
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const spinner = ora({
|
|
643
|
+
text: chalk.gray('Compacting context...'),
|
|
644
|
+
spinner: 'dots',
|
|
645
|
+
}).start();
|
|
646
|
+
|
|
647
|
+
try {
|
|
648
|
+
// Keep system message and last 4 messages
|
|
649
|
+
const systemMessage = this.messages[0];
|
|
650
|
+
const recentMessages = this.messages.slice(-4);
|
|
651
|
+
const olderMessages = this.messages.slice(1, -4);
|
|
652
|
+
|
|
653
|
+
if (olderMessages.length === 0) {
|
|
654
|
+
spinner.stop();
|
|
655
|
+
this.logger.info('Nothing to compact');
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Ask AI to summarize older conversation
|
|
660
|
+
const summaryResponse = await this.api.chat([
|
|
661
|
+
{ role: 'system', content: 'Summarize this conversation in a concise way, preserving key context and decisions.' },
|
|
662
|
+
...olderMessages,
|
|
663
|
+
], model, this.localMode);
|
|
664
|
+
|
|
665
|
+
// Create compacted context
|
|
666
|
+
this.messages = [
|
|
667
|
+
systemMessage,
|
|
668
|
+
{ role: 'system', content: `[Previous conversation summary]: ${summaryResponse.message}` },
|
|
669
|
+
...recentMessages,
|
|
670
|
+
];
|
|
671
|
+
|
|
672
|
+
spinner.stop();
|
|
673
|
+
this.logger.success(`Compacted ${olderMessages.length} messages into summary`);
|
|
674
|
+
} catch (error) {
|
|
675
|
+
spinner.stop();
|
|
676
|
+
this.logger.error('Failed to compact context:', (error as Error).message);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
private printModels(): void {
|
|
681
|
+
const models = this.config.getAvailableModels();
|
|
682
|
+
|
|
683
|
+
console.log();
|
|
684
|
+
this.logger.section('Available Models');
|
|
685
|
+
models.forEach(m => {
|
|
686
|
+
console.log(chalk.cyan(m.id.padEnd(20)) + chalk.gray(m.description));
|
|
687
|
+
});
|
|
688
|
+
console.log();
|
|
689
|
+
}
|
|
690
|
+
}
|