/***
@ Base: https://app.khoj.dev/
@ Author: Shannz
@ Note: AI assistant real-time with multi-modal support.
***/
import axios from 'axios';
import { wrapper } from 'axios-cookiejar-support';
import { CookieJar } from 'tough-cookie';
import WebSocket from 'ws';
import fs from 'fs';
import path from 'path';
import mime from 'mime-types';
import FormData from 'form-data';
const CONFIG = {
BASE_URL: "https://app.khoj.dev",
API: {
SESSION: "/api/chat/sessions",
WS: "wss://app.khoj.dev/api/chat/ws",
CONVERT: "/api/content/convert"
},
DEFAULT_COOKIE: 'PASTE_COOKIE_DISINI',
HEADERS: {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"Origin": "https://app.khoj.dev",
"Referer": "https://app.khoj.dev/"
},
CLIENT_PARAMS: {
client: "web",
agent_slug: "khoj"
}
};
const createClient = (cookieStr) => {
const jar = new CookieJar();
if (cookieStr) {
jar.setCookieSync(cookieStr, CONFIG.BASE_URL);
}
return wrapper(axios.create({ jar }));
};
const fileUtils = {
processImages: (imagePaths) => {
const images = [];
for (const filePath of imagePaths) {
if (fs.existsSync(filePath)) {
const fileBuffer = fs.readFileSync(filePath);
const mimeType = mime.lookup(filePath) || 'image/jpeg';
const base64 = fileBuffer.toString('base64');
images.push(`data:${mimeType};base64,${base64}`);
} else {
console.warn(`[Warn] Gambar tidak ditemukan: ${filePath}`);
}
}
return images;
},
processFiles: async (filePaths, client) => {
if (!filePaths || filePaths.length === 0) return [];
try {
const form = new FormData();
let validFiles = 0;
for (const filePath of filePaths) {
if (fs.existsSync(filePath)) {
form.append('files', fs.createReadStream(filePath));
validFiles++;
} else {
console.warn(`[Warn] File tidak ditemukan: ${filePath}`);
}
}
if (validFiles === 0) return [];
const res = await client.post(CONFIG.BASE_URL + CONFIG.API.CONVERT, form, {
headers: {
...CONFIG.HEADERS,
...form.getHeaders()
}
});
return res.data;
} catch (error) {
console.error(`[Error] Gagal convert file: ${error.message}`);
return [];
}
}
};
export const khoj = {
chat: async (query, options = {}) => {
const {
chatId: initialChatId = null,
cookie = CONFIG.DEFAULT_COOKIE,
images = [],
files = []
} = options;
return new Promise(async (resolve, reject) => {
const client = createClient(cookie);
let chatId = initialChatId;
let finalResult = {
chatId: chatId,
query: query,
answer: "",
tools: [],
searchQueries: [],
sources: [],
debug: { images_sent: 0, files_sent: 0 }
};
let safetyTimer;
try {
if (!chatId) {
try {
const sessionRes = await client.post(
`${CONFIG.BASE_URL}${CONFIG.API.SESSION}`,
{},
{
params: CONFIG.CLIENT_PARAMS,
headers: { ...CONFIG.HEADERS, 'Cookie': cookie }
}
);
chatId = sessionRes.data.conversation_id;
finalResult.chatId = chatId;
} catch (err) {
return reject({ status: 'error', message: `Gagal membuat sesi: ${err.message}` });
}
}
const [processedImages, processedFiles] = await Promise.all([
images.length > 0 ? fileUtils.processImages(images) : [],
files.length > 0 ? fileUtils.processFiles(files, client) : []
]);
finalResult.debug.images_sent = processedImages.length;
finalResult.debug.files_sent = processedFiles.length;
const wsUrl = `${CONFIG.API.WS}?client=web`;
const ws = new WebSocket(wsUrl, {
headers: { 'Cookie': cookie, 'Origin': CONFIG.BASE_URL }
});
safetyTimer = setTimeout(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.close();
reject({ status: 'timeout', message: 'No response received within 60s' });
}
}, 60000);
ws.on('open', () => {
const payload = {
"q": query,
"conversation_id": chatId,
"stream": true,
"city": "Malang", "region": "East Java", "country": "ID", "timezone": "Asia/Jakarta"
};
if (processedImages.length > 0) {
payload.images = processedImages;
}
if (processedFiles.length > 0) {
payload.files = processedFiles;
}
ws.send(JSON.stringify(payload));
});
ws.on('message', (rawData) => {
let str = rawData.toString().trim();
str = str.replace(/␃|🔚|␗/g, "").trim();
if (!str) return;
try {
const json = JSON.parse(str);
if (json.type === "metadata") {
finalResult.chatId = json.data.conversationId;
}
else if (json.type === "status") {
if (json.data.includes("Selected Tools:")) {
const toolText = json.data.replace("**Selected Tools:** ", "");
finalResult.tools = toolText.split(", ");
}
if (json.data.includes("Searching the web for")) {
const queries = json.data.split("\n- ").slice(1);
finalResult.searchQueries = queries.map(q => q.trim());
}
}
else if (json.type === "references") {
const onlineContext = json.data.onlineContext;
if (onlineContext) {
Object.values(onlineContext).forEach(val => {
if (val.organic) {
val.organic.forEach(item => {
finalResult.sources.push({ title: item.title, url: item.link });
});
}
});
}
}
else if (json.type === "end_response") {
finalResult.answer = finalResult.answer.trim();
clearTimeout(safetyTimer);
ws.close();
resolve(finalResult);
}
} catch (e) {
if (!str.startsWith('{"type":') && !str.includes('"turnId"')) {
finalResult.answer += str;
}
}
});
ws.on('error', (err) => {
clearTimeout(safetyTimer);
ws.close();
reject({ status: 'error', message: `WebSocket Error: ${err.message}` });
});
ws.on('close', () => {
clearTimeout(safetyTimer);
});
} catch (error) {
clearTimeout(safetyTimer);
reject({ status: 'error', message: `Fatal Error: ${error.message}` });
}
});
}
};
/*(async () => {
try {
console.log("Mengirim request...");
// Contoh: Kirim Text + Gambar + File Code
const result = await khoj.chat("gambar apa ini? dan apa fungsi file ini?", {
images: ['./orang.jpg'],
files: ['./script.py']
});
console.log(result);
} catch (error) {
console.error("Error:", error);
}
})();*/