UNPKG

tidyai-ts

Version:

AI-powered cross-platform file organizer using OpenRouter API

226 lines (196 loc) 7.04 kB
import * as https from "https"; import * as http from "http"; import { URL } from "url"; import { displayInfo, displayWarning, createLogEntry } from "./cli-utils"; // Try to get API key from environment, with better error handling const OPENROUTER_API_KEY = process.env.TIDYAI_API_KEY || process.env.OPENROUTER_API_KEY || ""; const OPENROUTER_API_URL = "https://openrouter.ai/api/v1/chat/completions"; const DEFAULT_MODEL = "openai/gpt-oss-20b:free"; const IS_DEVELOPMENT = process.env.NODE_ENV === "development"; interface ChatMessage { role: "user" | "assistant" | "system"; content: string; } interface ChatCompletionRequest { model: string; messages: ChatMessage[]; } interface ChatCompletionResponse { choices: { message: { content: string; }; }[]; } export class OpenRouterClient { private apiKey: string; constructor() { this.apiKey = OPENROUTER_API_KEY; } async getSuggestions(fileNames: string[]): Promise<Record<string, string>> { // If no API key is set, use default suggestions if (!this.apiKey) { displayWarning( "TIDYAI_API_KEY not set, using default folder suggestions" ); // Create a log entry await createLogEntry( process.cwd(), "TIDYAI_API_KEY not set, using default folder suggestions", ".tidyai/logs" ); return this.getDefaultSuggestions(fileNames); } displayInfo("Sending request to OpenRouter API..."); // Prepare the prompt for the AI const fileList = fileNames.join("\n"); const prompt = `You are an AI assistant that helps organize files. Based on the filenames provided, suggest appropriate folder names to organize them. File names: ${fileList} For each file, suggest one logical folder name based on its content or type. Respond ONLY with a JSON object where keys are the filenames and values are the suggested folder names. Do not include any other text in your response. Example response format: { "document.pdf": "Documents", "photo.jpg": "Images", "script.js": "Scripts" }`; try { const response = await this.makeRequest({ model: DEFAULT_MODEL, messages: [{ role: "user", content: prompt }], }); // Check if the response has the expected structure if (!response || !response.choices || response.choices.length === 0) { displayWarning( "Unexpected API response structure, using default folder suggestions" ); // Create a log entry await createLogEntry( process.cwd(), "Unexpected API response structure, using default folder suggestions", ".tidyai/logs" ); return this.getDefaultSuggestions(fileNames); } const content = response.choices[0].message.content; // Parse the JSON response return JSON.parse(content); } catch (error) { displayWarning( `Error getting suggestions from OpenRouter API: ${ (error as Error).message }` ); // Create a log entry await createLogEntry( process.cwd(), `Error getting suggestions from OpenRouter API: ${ (error as Error).message }`, ".tidyai/logs" ); // Return default folder names if API fails return this.getDefaultSuggestions(fileNames); } } private makeRequest( data: ChatCompletionRequest ): Promise<ChatCompletionResponse> { return new Promise((resolve, reject) => { const postData = JSON.stringify(data); const url = new URL(OPENROUTER_API_URL); // Log request in development mode if (IS_DEVELOPMENT) { console.log("\n=== OPENROUTER API REQUEST ==="); console.log("URL:", OPENROUTER_API_URL); console.log("Method: POST"); console.log("Headers:", { "Authorization": "Bearer [REDACTED]", "Content-Type": "application/json", "Content-Length": Buffer.byteLength(postData), }); console.log("Body:", JSON.stringify(data, null, 2)); console.log("===============================\n"); } const options = { hostname: url.hostname, port: 443, path: url.pathname, method: "POST", headers: { Authorization: `Bearer ${this.apiKey}`, "Content-Type": "application/json", "Content-Length": Buffer.byteLength(postData), }, }; const req = https.request(options, (res) => { let responseBody = ""; res.on("data", (chunk) => { responseBody += chunk; }); res.on("end", () => { try { // Log response in development mode if (IS_DEVELOPMENT) { console.log("\n=== OPENROUTER API RESPONSE ==="); console.log("Status Code:", res.statusCode); console.log("Headers:", res.headers); console.log("Body:", responseBody); console.log("================================\n"); } // Check if the response status indicates an error if (res.statusCode && res.statusCode >= 400) { displayWarning( `API request failed with status ${res.statusCode}: ${responseBody}` ); reject( new Error(`API request failed with status ${res.statusCode}`) ); return; } const response = JSON.parse(responseBody); resolve(response); } catch (error) { displayWarning(`Error parsing API response: ${responseBody}`); reject(error); } }); }); req.on("error", (error) => { // Log error in development mode if (IS_DEVELOPMENT) { console.log("\n=== OPENROUTER API ERROR ==="); console.log("Error:", error); console.log("============================\n"); } reject(error); }); req.write(postData); req.end(); }); } private getDefaultSuggestions(fileNames: string[]): Record<string, string> { const suggestions: Record<string, string> = {}; for (const fileName of fileNames) { // Simple default logic based on file extensions const ext = fileName.split(".").pop()?.toLowerCase() || ""; if (["jpg", "jpeg", "png", "gif", "bmp", "webp"].includes(ext)) { suggestions[fileName] = "Images"; } else if (["pdf", "doc", "docx", "txt", "rtf"].includes(ext)) { suggestions[fileName] = "Documents"; } else if (["mp4", "avi", "mov", "wmv", "flv"].includes(ext)) { suggestions[fileName] = "Videos"; } else if (["mp3", "wav", "ogg", "flac"].includes(ext)) { suggestions[fileName] = "Audio"; } else if ( ["js", "ts", "jsx", "tsx", "html", "css", "scss"].includes(ext) ) { suggestions[fileName] = "Code"; } else { suggestions[fileName] = "Other"; } } return suggestions; } }