tidyai-ts
Version:
AI-powered cross-platform file organizer using OpenRouter API
226 lines (196 loc) • 7.04 kB
text/typescript
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;
}
}