contaigents
Version:
Modular AI Content Ecosystem with Audio Generation
562 lines (561 loc) ⢠22.4 kB
JavaScript
import { Command } from "commander";
import { Server } from "./server.js";
import open from 'open';
import { AudioGenerator } from "./services/audioGenerator.js";
import { ChatService } from "./services/chatService.js";
import { readFile } from 'fs/promises';
import path from 'path';
import dotenv from 'dotenv';
import readline from 'readline';
import { testToolArchitecture } from './tests/toolTest.js';
import { demonstrateChatWithTools } from './tests/chatWithToolsDemo.js';
import { testFileWriteTool } from './tests/fileWriteToolTest.js';
import { testCombinedTools } from './tests/combinedToolsTest.js';
import { testToolCallLimits } from './tests/toolCallLimitTest.js';
import { debugChatServiceWrite } from './tests/debugChatServiceWrite.js';
import { manualWriteTest } from './tests/manualWriteTest.js';
import { testAudioProjectTool } from './tests/audioProjectToolTest.js';
import { testHybridMusicGeneration } from './tests/hybridMusicGenerationTest.js';
import { testFFmpegTool } from './tests/ffmpegToolTest.js';
import { testFFmpegGuardrails } from './tests/ffmpegGuardrailsTest.js';
import { createTestCommandToolCommand } from './commands/test-command-tool.js';
import { createConfigureCommand } from './commands/configure.js';
import { createTextToImageCommand } from './commands/text-to-image.js';
// Load environment variables from parent directory
dotenv.config({ path: path.join('..', '.env') });
const program = new Command();
program
.version("0.1.0-alpha.15")
.description("Contaigents - AI Content Ecosystem");
// Default action when no command is provided
program.action(async () => {
const server = new Server();
server.start();
const url = "http://localhost:2668";
console.log(`Opening editor at ${url}`);
await open(url);
});
// Keep the develop command as an alias
program
.command("develop")
.description("Start development server with file editing capabilities")
.action(async () => {
const server = new Server();
server.start();
const url = "http://localhost:2668/files";
console.log(`Opening editor at ${url}`);
await open(url);
});
// Audio generation command
program
.command("audio")
.description("Generate audio from text using AI providers")
.option("-t, --text <text>", "Text to convert to audio (direct input)")
.option("-f, --file <file>", "Path to file containing text to convert")
.option("-o, --output <output>", "Output file path (default: audio_output)")
.option("-p, --provider <provider>", "AI provider to use", "gemini")
.option("-v, --voice <voice>", "Voice to use for generation", "Zephyr")
.option("-m, --model <model>", "Model to use for generation")
.option("--api-key <apiKey>", "API key for the provider (or set GEMINI_API_KEY env var)")
.option("--output-dir <dir>", "Output directory (default: current directory)")
.action(async (options) => {
try {
// Validate input
if (!options.text && !options.file) {
console.error("ā Error: Either --text or --file must be provided");
process.exit(1);
}
if (options.text && options.file) {
console.error("ā Error: Cannot use both --text and --file options");
process.exit(1);
}
// Get API key from options or environment
const apiKey = options.apiKey || process.env.GEMINI_API_KEY;
if (!apiKey) {
console.error("ā Error: API key is required. Use --api-key option or set GEMINI_API_KEY environment variable");
process.exit(1);
}
// Get text content
let textContent;
if (options.file) {
try {
const filePath = path.resolve(options.file);
textContent = await readFile(filePath, 'utf-8');
console.log(`š Read text from file: ${filePath}`);
}
catch (error) {
console.error(`ā Error reading file: ${error.message}`);
process.exit(1);
}
}
else {
textContent = options.text;
}
if (!textContent.trim()) {
console.error("ā Error: Text content is empty");
process.exit(1);
}
console.log(`š Text length: ${textContent.length} characters`);
// Initialize audio generator
const audioGenerator = new AudioGenerator({
provider: options.provider,
apiKey: apiKey,
voice: options.voice,
model: options.model
});
console.log(`šļø Available voices: ${audioGenerator.getAvailableVoices().join(', ')}`);
// Generate audio
console.log("š Generating audio...");
const audioBuffer = await audioGenerator.generateAudio(textContent);
// Determine output path
const outputDir = options.outputDir || process.cwd();
const outputName = options.output || 'audio_output';
const outputPath = path.resolve(outputDir, outputName);
// Save audio file
const savedPath = await audioGenerator.saveAudioToFile(outputPath, audioBuffer);
console.log(`ā
Audio generation completed!`);
console.log(`š File saved to: ${savedPath}`);
console.log(`š File URL: file://${savedPath}`);
}
catch (error) {
console.error(`ā Error generating audio: ${error.message}`);
process.exit(1);
}
});
// Chat command
program
.command("chat")
.description("Start an interactive chat session with AI")
.option("-p, --provider <provider>", "AI provider to use (openai, anthropic, google, etc.)")
.option("-m, --model <model>", "Model to use for generation")
.option("-t, --temperature <temperature>", "Temperature for response generation (0.0-1.0)", parseFloat)
.option("--max-tokens <maxTokens>", "Maximum tokens for response", parseInt)
.option("--system-prompt <systemPrompt>", "Custom system prompt for the chat session")
.option("--non-interactive", "Run in non-interactive mode (for testing)")
.option("--debug", "Enable debug logging (shows prompts, responses, and internal operations)")
.option("--debug-file <file>", "Save debug logs to file (default: chat-debug.log)")
.action(async (options) => {
try {
console.log("š¤ Starting Contaigents Chat...");
// Set up debug logging
const debugFile = options.debug ? (options.debugFile || 'chat-debug.log') : undefined;
const chatService = new ChatService(process.cwd(), options.debug, debugFile);
if (options.debug) {
console.log(`š Debug logging enabled${debugFile ? ` (saving to ${debugFile})` : ''}`);
}
// Create a new chat session
const session = await chatService.createSession({
provider: options.provider,
model: options.model,
temperature: options.temperature,
maxTokens: options.maxTokens,
systemPrompt: options.systemPrompt
});
console.log(`š¬ Chat session created: ${session.id}`);
console.log("Type 'exit', 'quit', or press Ctrl+C to end the chat.\n");
if (options.nonInteractive) {
console.log("Running in non-interactive mode for testing");
// In non-interactive mode, read from stdin if available
let stdinData = '';
// Check if there's input from stdin (piped input)
if (!process.stdin.isTTY) {
process.stdin.setEncoding('utf8');
for await (const chunk of process.stdin) {
stdinData += chunk;
}
if (stdinData.trim()) {
console.log(`š Processing input: ${stdinData.trim()}`);
try {
const response = await chatService.sendMessage(session.id, stdinData.trim(), {
provider: options.provider,
model: options.model,
temperature: options.temperature,
maxTokens: options.maxTokens
});
console.log(`š¤ Assistant: ${response.content}`);
}
catch (error) {
console.error(`ā Error: ${error.message}`);
}
}
else {
console.log("No input provided via stdin");
}
}
else {
console.log("No piped input detected");
}
return;
}
// Set up readline interface for interactive chat
console.log("š” Tip: For multiline input, paste your content and press Enter twice to submit.\n");
// Enable raw mode to better handle pasted content
if (process.stdin.isTTY) {
process.stdin.setRawMode(false); // Keep cooked mode for better compatibility
}
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
prompt: 'š¤ You: ',
terminal: true
});
// Handle graceful shutdown
const cleanup = async () => {
console.log("\nš Chat session ended.");
// Save debug logs if enabled
if (options.debug) {
const debugLogger = chatService.getDebugLogger();
const dumpFile = `chat-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 chat 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 chatService.sendMessage(session.id, fullInput, {
provider: options.provider,
model: options.model,
temperature: options.temperature,
maxTokens: options.maxTokens
});
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 chatService.sendMessage(session.id, fullInput, {
provider: options.provider,
model: options.model,
temperature: options.temperature,
maxTokens: options.maxTokens
});
console.log(`š¤ Assistant: ${response.content}\n`);
}
catch (error) {
console.error(`ā Error: ${error.message}\n`);
}
finally {
isProcessing = false;
}
}
rl.prompt();
}
}, 1000);
rl.prompt();
return;
}
// Handle exit command
if (userInput.toLowerCase() === 'exit' || userInput.toLowerCase() === 'quit') {
cleanup().catch(console.error);
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 chatService.sendMessage(session.id, userInput, {
provider: options.provider,
model: options.model,
temperature: options.temperature,
maxTokens: options.maxTokens
});
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);
});
}
catch (error) {
console.error(`ā Error starting chat: ${error.message}`);
process.exit(1);
}
});
// Test tools command
program
.command("test-tools")
.description("Test the tool architecture functionality")
.action(async () => {
try {
await testToolArchitecture();
}
catch (error) {
console.error(`ā Tool test failed: ${error.message}`);
process.exit(1);
}
});
// Demo chat with tools command
program
.command("demo-chat-tools")
.description("Demonstrate chat service with tool integration")
.action(async () => {
try {
await demonstrateChatWithTools();
}
catch (error) {
console.error(`ā Chat tools demo failed: ${error.message}`);
process.exit(1);
}
});
// Test audio project tool command
program
.command("test-audio-project")
.description("Test the AudioProjectTool functionality")
.action(async () => {
try {
await testAudioProjectTool();
}
catch (error) {
console.error(`ā AudioProjectTool test failed: ${error.message}`);
process.exit(1);
}
});
// Test hybrid music generation command
program
.command("test-hybrid-music")
.description("Test the hybrid music generation functionality (Lyria 2 + MusicGen)")
.action(async () => {
try {
await testHybridMusicGeneration();
}
catch (error) {
console.error(`ā Hybrid music generation test failed: ${error.message}`);
process.exit(1);
}
});
// Test FFmpeg tool command
program
.command("test-ffmpeg")
.description("Test the FFmpeg tool functionality")
.action(async () => {
try {
await testFFmpegTool();
}
catch (error) {
console.error(`ā FFmpeg tool test failed: ${error.message}`);
process.exit(1);
}
});
// Test FFmpeg guardrails command
program
.command("test-ffmpeg-guardrails")
.description("Test the enhanced FFmpeg tool guardrails and aspect ratio intelligence")
.action(async () => {
try {
await testFFmpegGuardrails();
}
catch (error) {
console.error(`ā FFmpeg guardrails test failed: ${error.message}`);
process.exit(1);
}
});
// Test Speech-to-Text tool command
program
.command("test-speech-to-text")
.description("Test the Speech-to-Text tool with multiple providers")
.action(async () => {
try {
const { SpeechToTextTool } = await import('./services/tools/SpeechToTextTool.js');
const tool = new SpeechToTextTool(process.cwd());
console.log('š¤ Testing Speech-to-Text Tool...\n');
console.log('š Tool Information:');
console.log(`Name: ${tool.getName()}`);
console.log(`Description: ${tool.getDescription()}`);
console.log('\nšÆ Agent Guidance:');
console.log(tool.getAgentGuidance());
console.log('\nā
Speech-to-Text tool is available and ready!');
console.log('\nš” Usage Tips:');
console.log(' - Local Whisper (Node.js): npm install @xenova/transformers');
console.log(' - No Python required - pure JavaScript implementation!');
console.log(' - Set OPENAI_API_KEY for cloud transcription');
console.log(' - Set GEMINI_API_KEY for Google AI transcription');
console.log(' - Use "auto" provider for smart fallback selection');
}
catch (error) {
console.error(`ā Speech-to-Text test failed: ${error.message}`);
process.exit(1);
}
});
// Test conversation flow command
program
.command("test-conversation-flow")
.description("Test improved conversation flow with tool calls")
.action(async () => {
try {
const { testImprovedConversationFlow } = await import('./tests/conversationFlowTest.js');
await testImprovedConversationFlow();
}
catch (error) {
console.error(`ā Conversation flow test failed: ${error.message}`);
process.exit(1);
}
});
// Test file write tool command
program
.command("test-file-write")
.description("Test the file writing tool functionality")
.action(async () => {
try {
await testFileWriteTool();
}
catch (error) {
console.error(`ā File write tool test failed: ${error.message}`);
process.exit(1);
}
});
// Test combined tools command
program
.command("test-combined-tools")
.description("Test the combined read/write tool workflow")
.action(async () => {
try {
await testCombinedTools();
}
catch (error) {
console.error(`ā Combined tools test failed: ${error.message}`);
process.exit(1);
}
});
// Test tool call limits command
program
.command("test-tool-limits")
.description("Test the limits and performance of tool call processing")
.action(async () => {
try {
await testToolCallLimits();
}
catch (error) {
console.error(`ā Tool call limits test failed: ${error.message}`);
process.exit(1);
}
});
// Debug ChatService write issue command
program
.command("debug-chat-write")
.description("Debug ChatService file writing issues")
.action(async () => {
try {
await debugChatServiceWrite();
}
catch (error) {
console.error(`ā Debug chat write failed: ${error.message}`);
process.exit(1);
}
});
// Manual write test command
program
.command("manual-write-test")
.description("Manually test writing to README.md file")
.action(async () => {
try {
await manualWriteTest();
}
catch (error) {
console.error(`ā Manual write test failed: ${error.message}`);
process.exit(1);
}
});
// Test command execution tool
program.addCommand(createTestCommandToolCommand());
// Configure command
program.addCommand(createConfigureCommand());
// Text-to-image command
program.addCommand(createTextToImageCommand());
program.parse(process.argv);