contaigents
Version:
Modular AI Content Ecosystem with Audio Generation
378 lines (375 loc) ⢠15.5 kB
JavaScript
import { ChatService } from './chatService.js';
import { ConversationPersistence } from './conversation/ConversationPersistence.js';
import readline from 'readline';
export class AgentCLI {
constructor(baseDir) {
this.currentSession = null;
this.currentConversationId = null;
this.chatService = new ChatService(baseDir);
this.persistence = new ConversationPersistence(baseDir);
}
/**
* Start a new conversation or resume an existing one
*/
async startConversation(options = {}) {
// Try to resume existing conversation
if (options.conversationId) {
const persisted = await this.persistence.loadConversation(options.conversationId);
if (persisted) {
console.log(`š Resuming conversation: ${persisted.name || persisted.id}`);
if (persisted.description) {
console.log(`š Description: ${persisted.description}`);
}
if (persisted.tags && persisted.tags.length > 0) {
console.log(`š·ļø Tags: ${persisted.tags.join(', ')}`);
}
console.log(`š¬ Messages: ${persisted.session.messages.length}`);
console.log('');
// Restore session to ChatService
this.currentSession = persisted.session;
this.currentConversationId = persisted.id;
this.chatService.restoreSession(persisted.session);
return persisted.session;
}
else {
console.log(`ā ļø Conversation ${options.conversationId} not found. Starting new conversation.`);
}
}
// Create new conversation
const session = await this.chatService.createSession(options);
this.currentSession = session;
this.currentConversationId = session.id;
// Save initial conversation
if (options.autosave !== false) {
await this.saveCurrentConversation({
name: options.conversationName,
description: options.conversationDescription,
tags: options.tags
});
}
console.log(`š¬ New conversation started: ${session.id}`);
if (options.conversationName) {
console.log(`š Name: ${options.conversationName}`);
}
return session;
}
/**
* Send a message in the current conversation
*/
async sendMessage(message, options) {
if (!this.currentSession) {
throw new Error('No active conversation. Start a conversation first.');
}
const response = await this.chatService.sendMessage(this.currentSession.id, message, options);
// Auto-save after each message
await this.saveCurrentConversation();
return response;
}
/**
* Save the current conversation
*/
async saveCurrentConversation(metadata) {
if (!this.currentSession || !this.currentConversationId) {
return;
}
const persisted = {
id: this.currentConversationId,
session: this.currentSession,
lastAccessed: Date.now(),
...metadata
};
await this.persistence.saveConversation(persisted);
}
/**
* List all saved conversations
*/
async listConversations() {
return await this.persistence.listConversations();
}
/**
* Search conversations
*/
async searchConversations(query) {
return await this.persistence.searchConversations(query);
}
/**
* Delete a conversation
*/
async deleteConversation(conversationId) {
return await this.persistence.deleteConversation(conversationId);
}
/**
* Export a conversation
*/
async exportConversation(conversationId, format = 'json') {
return await this.persistence.exportConversation(conversationId, format);
}
/**
* Get conversation statistics
*/
async getStatistics() {
return await this.persistence.getStatistics();
}
/**
* Start interactive CLI session
*/
async startInteractiveSession(options = {}) {
console.log("š¤ Starting Enhanced Contaigents Agent CLI...");
// Set up debug logging if enabled
if (options.debug) {
const debugFile = options.debugFile || 'agent-cli-debug.log';
console.log(`š Debug logging enabled (saving to ${debugFile})`);
}
// Start or resume conversation
const session = await this.startConversation(options);
console.log("Type 'help' for commands, 'exit' to quit.");
console.log("š” Tip: For multiline input, paste your content and press Enter twice to submit.\n");
// Set up readline interface with multiline support
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
prompt: 'š¤ You: '
});
// Handle graceful shutdown
const cleanup = async () => {
console.log("\nš Saving conversation and exiting...");
await this.saveCurrentConversation();
if (options.debug) {
const debugLogger = this.chatService.getDebugLogger();
const dumpFile = `agent-cli-debug-dump-${Date.now()}.json`;
await debugLogger.saveDumpToFile(dumpFile);
console.log(`š Debug session saved to: ${dumpFile}`);
}
console.log("Goodbye!");
rl.close();
process.exit(0);
};
process.on('SIGINT', () => cleanup().catch(console.error));
process.on('SIGTERM', () => cleanup().catch(console.error));
// Enhanced multiline input handling with paste detection
let multilineBuffer = [];
let isMultilineMode = false;
let pasteTimeout = null;
let rapidInputCount = 0;
let lastInputTime = 0;
let isProcessing = false; // Flag to prevent concurrent processing
// Start the interactive loop
rl.prompt();
rl.on('line', async (input) => {
const currentTime = Date.now();
const timeSinceLastInput = currentTime - lastInputTime;
lastInputTime = currentTime;
// Check if we're already processing a request
if (isProcessing) {
console.log("ā³ Please wait, I'm still processing your previous message...");
rl.prompt();
return;
}
// Detect rapid input (likely paste) - multiple lines within 100ms
if (timeSinceLastInput < 100) {
rapidInputCount++;
}
else {
rapidInputCount = 0;
}
const userInput = input.trim();
// Handle empty line in multiline mode
if (!userInput && isMultilineMode) {
// Empty line - submit multiline input
const fullInput = multilineBuffer.join('\n').trim();
multilineBuffer = [];
isMultilineMode = false;
rapidInputCount = 0;
if (fullInput) {
try {
isProcessing = true;
console.log("š¤ Thinking...");
const response = await this.sendMessage(fullInput, options);
console.log(`š¤ Assistant: ${response.content}\n`);
}
catch (error) {
console.error(`ā Error: ${error.message}\n`);
}
finally {
isProcessing = false;
}
}
rl.prompt();
return;
}
// Handle empty line in normal mode
if (!userInput && !isMultilineMode) {
rl.prompt();
return;
}
// If we're in multiline mode, add to buffer
if (isMultilineMode) {
multilineBuffer.push(input);
// Clear any existing timeout and set a new one
if (pasteTimeout) {
clearTimeout(pasteTimeout);
}
// Auto-submit after 1 second of no input in multiline mode
pasteTimeout = setTimeout(async () => {
if (multilineBuffer.length > 0 && !isProcessing) {
console.log("\nš Auto-submitting multiline input...");
const fullInput = multilineBuffer.join('\n').trim();
multilineBuffer = [];
isMultilineMode = false;
rapidInputCount = 0;
if (fullInput) {
try {
isProcessing = true;
console.log("š¤ Thinking...");
const response = await this.sendMessage(fullInput, options);
console.log(`š¤ Assistant: ${response.content}\n`);
}
catch (error) {
console.error(`ā Error: ${error.message}\n`);
}
finally {
isProcessing = false;
}
}
rl.prompt();
}
}, 1000);
rl.prompt();
return;
}
// Handle special commands (only in single-line mode)
if (userInput.toLowerCase() === 'exit' || userInput.toLowerCase() === 'quit') {
cleanup().catch(console.error);
return;
}
if (userInput.toLowerCase() === 'help') {
this.showHelp();
rl.prompt();
return;
}
if (userInput.toLowerCase().startsWith('save ')) {
const name = userInput.substring(5).trim();
await this.saveCurrentConversation({ name });
console.log(`š¾ Conversation saved as: ${name}\n`);
rl.prompt();
return;
}
if (userInput.toLowerCase() === 'list') {
await this.showConversationList();
rl.prompt();
return;
}
if (userInput.toLowerCase().startsWith('search ')) {
const query = userInput.substring(7).trim();
await this.showSearchResults(query);
rl.prompt();
return;
}
if (userInput.toLowerCase() === 'stats') {
await this.showStatistics();
rl.prompt();
return;
}
// Detect paste operation (rapid input or long single line)
const isPastedContent = rapidInputCount >= 2 || input.length > 200 || input.includes('\n');
if (isPastedContent || userInput.toLowerCase() === 'multiline') {
if (userInput.toLowerCase() === 'multiline') {
console.log("š Entering multiline mode. Type your message and press Enter twice to submit.");
isMultilineMode = true;
rl.prompt();
return;
}
else {
// Handle pasted content
multilineBuffer.push(input);
isMultilineMode = true;
console.log(`š Detected pasted content. Collecting input... (press Enter on empty line to submit)`);
rl.prompt();
return;
}
}
// Process regular single-line message
try {
isProcessing = true;
console.log("š¤ Thinking...");
const response = await this.sendMessage(userInput, options);
console.log(`š¤ Assistant: ${response.content}\n`);
}
catch (error) {
console.error(`ā Error: ${error.message}\n`);
}
finally {
isProcessing = false;
}
rl.prompt();
});
rl.on('close', () => {
cleanup().catch(console.error);
});
}
showHelp() {
console.log(`
š Enhanced Agent CLI Commands:
help - Show this help message
save <name> - Save conversation with a name
list - List all saved conversations
search <query> - Search conversations by content
stats - Show conversation statistics
multiline - Enter multiline input mode
exit, quit - Save and exit
š Input Methods:
⢠Single line: Type your message and press Enter
⢠Multiline: Paste content or type 'multiline', then press Enter twice to submit
⢠Long text: Automatically detected and handled as multiline
š” Tips:
- Conversations are automatically saved after each message
- Use natural language to interact with AI and tools
- The AI can read, write, and search files in your project
- All tool operations are logged and can be debugged
- Paste multiline content directly - it will be detected automatically
`);
}
async showConversationList() {
const conversations = await this.listConversations();
if (conversations.length === 0) {
console.log("š No saved conversations found.\n");
return;
}
console.log("š Saved Conversations:");
conversations.slice(0, 10).forEach((conv, index) => {
const name = conv.name || conv.id.substring(0, 8);
const messageCount = conv.session.messages.length;
const lastAccessed = new Date(conv.lastAccessed).toLocaleDateString();
console.log(` ${index + 1}. ${name} (${messageCount} messages, ${lastAccessed})`);
});
if (conversations.length > 10) {
console.log(` ... and ${conversations.length - 10} more`);
}
console.log('');
}
async showSearchResults(query) {
const results = await this.searchConversations(query);
if (results.length === 0) {
console.log(`š No conversations found matching "${query}"\n`);
return;
}
console.log(`š Found ${results.length} conversation(s) matching "${query}":`);
results.slice(0, 5).forEach((conv, index) => {
const name = conv.name || conv.id.substring(0, 8);
const messageCount = conv.session.messages.length;
console.log(` ${index + 1}. ${name} (${messageCount} messages)`);
});
console.log('');
}
async showStatistics() {
const stats = await this.getStatistics();
console.log(`
š Conversation Statistics:
Total Conversations: ${stats.totalConversations}
Total Messages: ${stats.totalMessages}
Average Messages per Conversation: ${stats.averageMessagesPerConversation}
Oldest Conversation: ${stats.oldestConversation?.toLocaleDateString() || 'N/A'}
Newest Conversation: ${stats.newestConversation?.toLocaleDateString() || 'N/A'}
`);
}
}