Download server.js from adyoi/indigo-chat: direct link, hf CLI and curl.
- Browser
- Download file 14 kB
-
https://huggingface.co/adyoi/indigo-chat/resolve/main/server.js
- Command line
-
hf download hf://adyoi/indigo-chat/server.js
-
curl -L -o server.js https://huggingface.co/adyoi/indigo-chat/resolve/main/server.js
14 kB
| const express = require("express"); | |
| const { spawn, execSync } = require("child_process"); | |
| const fs = require("fs"); | |
| const path = require("path"); | |
| const multer = require("multer"); | |
| const app = express(); | |
| const PORT = process.env.PORT || 3000; | |
| const INDIGO_DIR = path.resolve(__dirname, "..", "Indigo"); | |
| const DATA_DIR = path.join(INDIGO_DIR, "data"); | |
| const RUNS_DIR = path.join(INDIGO_DIR, "runs"); | |
| const CHAT_DIR = path.join(__dirname, "chats"); | |
| const CONFIG_PATH = path.join(__dirname, "config.json"); | |
| function findPython() { | |
| try { | |
| const cfg = loadConfig(); | |
| if (cfg.pythonPath && fs.existsSync(cfg.pythonPath)) return cfg.pythonPath; | |
| } catch (e) {} | |
| try { | |
| return execSync('python -c "import sys; print(sys.executable)"', { encoding: "utf-8", timeout: 5000 }).trim(); | |
| } catch (e) { | |
| return "python"; | |
| } | |
| } | |
| const PYTHON = process.env.PYTHON_PATH || findPython(); | |
| console.log("PYTHON:", PYTHON); | |
| if (!fs.existsSync(CHAT_DIR)) fs.mkdirSync(CHAT_DIR, { recursive: true }); | |
| app.use(express.json({ limit: "10mb" })); | |
| app.use(express.static(path.join(__dirname, "public"))); | |
| const upload = multer({ dest: path.join(__dirname, "uploads") }); | |
| function loadConfig() { | |
| const defaults = { | |
| model: null, | |
| temperature: 0.8, | |
| topK: 40, | |
| topP: 0.95, | |
| maxNew: 200, | |
| guard: true, | |
| device: "auto", | |
| }; | |
| if (fs.existsSync(CONFIG_PATH)) { | |
| return { ...defaults, ...JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8")) }; | |
| } | |
| return defaults; | |
| } | |
| function saveConfig(cfg) { | |
| fs.writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2), "utf-8"); | |
| } | |
| function listCheckpoints() { | |
| const results = []; | |
| if (!fs.existsSync(RUNS_DIR)) return results; | |
| function scanDir(dir, label) { | |
| for (const f of fs.readdirSync(dir)) { | |
| if (f.endsWith(".safetensors") && !f.includes("optimizer")) { | |
| const metaPath = path.join(dir, f.replace(".safetensors", "_meta.json")); | |
| let meta = {}; | |
| if (fs.existsSync(metaPath)) { | |
| meta = JSON.parse(fs.readFileSync(metaPath, "utf-8")); | |
| } | |
| results.push({ | |
| name: `${label}/${f}`, | |
| path: path.join(dir, f), | |
| step: meta.step || 0, | |
| val_loss: meta.val_loss || null, | |
| config: meta.config || {}, | |
| tokenizer: meta.tokenizer || { type: "char" }, | |
| size: fs.statSync(path.join(dir, f)).size, | |
| }); | |
| } | |
| } | |
| } | |
| for (const run of fs.readdirSync(RUNS_DIR)) { | |
| const runDir = path.join(RUNS_DIR, run); | |
| if (!fs.statSync(runDir).isDirectory()) continue; | |
| const ckptDir = path.join(runDir, "ckpt"); | |
| if (fs.existsSync(ckptDir)) scanDir(ckptDir, run); | |
| else scanDir(runDir, run); | |
| } | |
| return results; | |
| } | |
| const TEXT_EXTS = [".txt", ".md", ".py", ".js", ".ts", ".json", ".csv", ".log", ".html", ".css", ".xml", ".yaml", ".yml", ".toml", ".cfg", ".ini", ".sh", ".bat", ".sql", ".r", ".java", ".c", ".cpp", ".h", ".hpp", ".rs", ".go", ".rb", ".php", ".swift", ".kt", ".scala", ".lua", ".pl", ".tex", ".bib"]; | |
| // API: list models | |
| app.get("/api/models", (req, res) => { | |
| res.json(listCheckpoints()); | |
| }); | |
| // API: config | |
| app.get("/api/config", (req, res) => { | |
| res.json(loadConfig()); | |
| }); | |
| app.post("/api/config", (req, res) => { | |
| const cfg = { ...loadConfig(), ...req.body }; | |
| saveConfig(cfg); | |
| res.json(cfg); | |
| }); | |
| // API: MCP server check | |
| app.get("/api/mcp-check", (req, res) => { | |
| const mcpScript = path.join(__dirname, "mcp-server.js"); | |
| if (!fs.existsSync(mcpScript)) return res.json({ ok: false, error: "mcp-server.js not found" }); | |
| const initReq = JSON.stringify({ jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: "2024-11-05", capabilities: {}, clientInfo: { name: "indigo-chat", version: "1.0.0" } } }); | |
| const listReq = JSON.stringify({ jsonrpc: "2.0", id: 2, method: "tools/list", params: {} }); | |
| const input = initReq + "\n" + listReq + "\n"; | |
| const proc = spawn("node", [mcpScript], { cwd: __dirname, timeout: 10000 }); | |
| let stdout = ""; | |
| let stderr = ""; | |
| proc.stdout.on("data", (d) => { stdout += d.toString(); }); | |
| proc.stderr.on("data", (d) => { stderr += d.toString(); }); | |
| proc.stdin.write(input); | |
| proc.stdin.end(); | |
| proc.on("close", () => { | |
| try { | |
| const lines = stdout.trim().split("\n"); | |
| let tools = []; | |
| for (const line of lines) { | |
| try { | |
| const d = JSON.parse(line); | |
| if (d.id === 2 && d.result && d.result.tools) tools = d.result.tools; | |
| } catch (e) {} | |
| } | |
| res.json({ ok: true, tools: tools.map((t) => t.name), count: tools.length }); | |
| } catch (e) { | |
| res.json({ ok: false, error: "Failed to parse response", raw: stdout.substring(0, 300), stderr: stderr.substring(0, 300) }); | |
| } | |
| }); | |
| proc.on("error", (e) => { | |
| res.json({ ok: false, error: e.message }); | |
| }); | |
| }); | |
| // API: chat history | |
| app.get("/api/history", (req, res) => { | |
| const files = fs.readdirSync(CHAT_DIR) | |
| .filter((f) => f.endsWith(".json")) | |
| .map((f) => { | |
| const data = JSON.parse(fs.readFileSync(path.join(CHAT_DIR, f), "utf-8")); | |
| return { id: f.replace(".json", ""), title: data.title || "Untitled", updated: data.updated }; | |
| }) | |
| .sort((a, b) => (b.updated || 0) - (a.updated || 0)); | |
| res.json(files); | |
| }); | |
| app.get("/api/history/:id", (req, res) => { | |
| const fp = path.join(CHAT_DIR, `${req.params.id}.json`); | |
| if (!fs.existsSync(fp)) return res.status(404).json({ error: "not found" }); | |
| const data = JSON.parse(fs.readFileSync(fp, "utf-8")); | |
| res.json(data); | |
| }); | |
| app.post("/api/history", (req, res) => { | |
| const { id, messages, title } = req.body; | |
| const chatId = id || `chat_${Date.now()}`; | |
| const data = { | |
| title: title || messages?.[0]?.content?.substring(0, 50) || "New Chat", | |
| messages, | |
| updated: Date.now(), | |
| }; | |
| fs.writeFileSync(path.join(CHAT_DIR, `${chatId}.json`), JSON.stringify(data, null, 2), "utf-8"); | |
| res.json({ id: chatId }); | |
| }); | |
| app.delete("/api/history/:id", (req, res) => { | |
| const fp = path.join(CHAT_DIR, `${req.params.id}.json`); | |
| if (fs.existsSync(fp)) fs.unlinkSync(fp); | |
| res.json({ ok: true }); | |
| }); | |
| // API: upload file (returns content for text files) | |
| app.post("/api/upload", upload.single("file"), (req, res) => { | |
| if (!req.file) return res.status(400).json({ error: "no file" }); | |
| const cfg = loadConfig(); | |
| const targetDir = path.join(cfg.projectDir || INDIGO_DIR, "uploads"); | |
| if (!fs.existsSync(targetDir)) fs.mkdirSync(targetDir, { recursive: true }); | |
| const target = path.join(targetDir, req.file.originalname); | |
| fs.copyFileSync(req.file.path, target); | |
| fs.unlinkSync(req.file.path); | |
| let content = ""; | |
| try { | |
| const ext = path.extname(req.file.originalname).toLowerCase(); | |
| if (TEXT_EXTS.includes(ext)) { | |
| content = fs.readFileSync(target, "utf-8"); | |
| if (content.length > 8000) content = content.substring(0, 8000) + "\n... (dipotong)"; | |
| } | |
| } catch (e) {} | |
| res.json({ path: target, name: req.file.originalname, content }); | |
| }); | |
| // MCP agent: server calls MCP tools, injects context into prompt | |
| function callMCPServer(jsonl) { | |
| return new Promise((resolve) => { | |
| const mcpScript = path.join(__dirname, "mcp-server.js"); | |
| if (!fs.existsSync(mcpScript)) return resolve(null); | |
| const proc = spawn("node", [mcpScript], { cwd: __dirname, timeout: 10000 }); | |
| let stdout = ""; | |
| proc.stdout.on("data", (d) => { stdout += d.toString(); }); | |
| proc.on("close", () => { | |
| const results = {}; | |
| for (const line of stdout.trim().split("\n")) { | |
| try { | |
| const d = JSON.parse(line); | |
| if (d.id !== undefined && d.result) results[d.id] = d.result; | |
| } catch (e) {} | |
| } | |
| resolve(results); | |
| }); | |
| proc.on("error", () => resolve(null)); | |
| proc.stdin.write(jsonl); | |
| proc.stdin.end(); | |
| }); | |
| } | |
| const FILE_KEYWORDS = ["file", "folder", "directory", "direktori", "berkas", "list", "search", "cari", "baca", "read", "tampilkan", "show", "kode", "code", "isi", "nama file", "file apa", "isi folder", "file mana", "cari file"]; | |
| function needsFileContext(text) { | |
| const t = text.toLowerCase(); | |
| return FILE_KEYWORDS.some((k) => t.includes(k)); | |
| } | |
| function mcpInit() { | |
| return JSON.stringify({ jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: "2024-11-05", capabilities: {}, clientInfo: { name: "indigo-chat", version: "1.0.0" } } }) + "\n"; | |
| } | |
| async function agentGatherContext(prompt) { | |
| if (!needsFileContext(prompt)) return null; | |
| const initLine = mcpInit(); | |
| const p = prompt.toLowerCase(); | |
| const listDir = /(list|isi|sebutkan|tampilkan|show).*(folder|direktori|directory|root|berkas|semua)/.test(p); | |
| const calls = []; | |
| if (listDir) { | |
| calls.push({ id: 2, name: "list_directory", args: {} }); | |
| } else { | |
| // extract potential filenames (e.g. "train.py", "kamus_id.txt") | |
| const fileRefs = p.match(/[a-z0-9_\\-]+\.[a-z0-9]{1,5}/g) || []; | |
| if (fileRefs.length > 0) { | |
| for (let i = 0; i < Math.min(fileRefs.length, 2); i++) { | |
| calls.push({ id: 2 + i, name: "search_files", args: { query: fileRefs[i].replace(/^[^a-z0-9]/g, "") } }); | |
| } | |
| } else { | |
| // fallback: search by a keyword from the prompt | |
| const kw = p.split(/[^a-z0-9]+/).filter((w) => w.length >= 3).pop(); | |
| if (kw) calls.push({ id: 2, name: "search_files", args: { query: kw } }); | |
| } | |
| } | |
| if (calls.length === 0) return null; | |
| const jsonl = initLine + calls.map((c) => JSON.stringify({ jsonrpc: "2.0", id: c.id, method: "tools/call", params: { name: c.name, arguments: c.args } })).join("\n") + "\n"; | |
| const resp = await callMCPServer(jsonl); | |
| if (!resp) return null; | |
| const sections = []; | |
| const readCalls = []; | |
| let nextId = calls.length + 2; | |
| for (const c of calls) { | |
| const result = resp[c.id]; | |
| if (!result) continue; | |
| let text = result.content?.[0]?.text || ""; | |
| if (c.name === "list_directory") { | |
| sections.push(`[Konteks: konten folder]\n${text.substring(0, 3000)}`); | |
| continue; | |
| } | |
| let items = []; | |
| try { items = JSON.parse(text); } catch (e) { continue; } | |
| if (!Array.isArray(items)) continue; | |
| sections.push(`[Hasil pencarian "${c.args.query}"]\n` + items.map((f) => f.path).join("\n").substring(0, 1000)); | |
| // antrekan pembacaan file (batch dalam satu spawn MCP) | |
| for (const item of items.filter((f) => !f.isDir).slice(0, 2)) { | |
| readCalls.push({ id: nextId, path: item.path, name: item.name }); | |
| nextId++; | |
| } | |
| } | |
| // Baca semua file dalam satu sesi MCP (satu spawn, bukan per-file) | |
| if (readCalls.length > 0) { | |
| const readJsonl = initLine + readCalls.map((r) => JSON.stringify({ jsonrpc: "2.0", id: r.id, method: "tools/call", params: { name: "read_file", arguments: { path: r.path, limit: 2000 } } })).join("\n") + "\n"; | |
| const readResp = await callMCPServer(readJsonl); | |
| for (const r of readCalls) { | |
| const rc = readResp?.[r.id]; | |
| if (rc?.content?.[0]) { | |
| sections.push(`[Isi file: ${r.name}]\n${rc.content[0].text.substring(0, 2000)}`); | |
| } | |
| } | |
| } | |
| if (sections.length === 0) return null; | |
| return { context: sections.join("\n\n") }; | |
| } | |
| // API: generate (streaming) | |
| app.post("/api/generate", async (req, res) => { | |
| const { prompt, modelPath, history } = req.body; | |
| const cfg = loadConfig(); | |
| const ckpt = modelPath ? path.resolve(INDIGO_DIR, modelPath) : cfg.model; | |
| if (!ckpt) return res.status(400).json({ error: "no model selected" }); | |
| res.setHeader("Content-Type", "text/event-stream"); | |
| res.setHeader("Cache-Control", "no-cache"); | |
| res.setHeader("Connection", "keep-alive"); | |
| const genScript = path.join(__dirname, "generate.py"); | |
| // agent loop: gather file context via MCP if needed | |
| let agentCtx = null; | |
| if (cfg.agent !== false) { | |
| try { | |
| agentCtx = await agentGatherContext(prompt); | |
| if (agentCtx) res.write(`data: ${JSON.stringify({ type: "agent", context: agentCtx.context })}\n\n`); | |
| } catch (e) { | |
| console.error("[agent error]", e.message); | |
| res.write(`data: ${JSON.stringify({ type: "agent", error: e.message })}\n\n`); | |
| } | |
| } | |
| const finalPrompt = agentCtx ? `Konteks dari MCP:\n${agentCtx.context}\n\nPertanyaan: ${prompt}` : prompt; | |
| const env = { | |
| ...process.env, | |
| INDIGO_DIR: INDIGO_DIR, | |
| CKPT: ckpt, | |
| PROMPT: finalPrompt, | |
| HISTORY: JSON.stringify(history || []), | |
| TEMPERATURE: String(cfg.temperature ?? 0.8), | |
| TOP_K: String(cfg.topK ?? 40), | |
| TOP_P: String(cfg.topP ?? 0.95), | |
| MAX_NEW: String(cfg.maxNew ?? 200), | |
| }; | |
| const proc = spawn(PYTHON, ["-u", genScript], { cwd: __dirname, env }); | |
| let output = ""; | |
| let stderrBuf = ""; | |
| proc.on("error", (e) => { console.error("[generate spawn error]", e.message); }); | |
| proc.stdout.on("data", (d) => { output += d.toString(); }); | |
| proc.stderr.on("data", (d) => { stderrBuf += d.toString(); }); | |
| proc.on("close", () => { | |
| try { | |
| const lines = output.trim().split("\n"); | |
| const lastLine = lines[lines.length - 1]; | |
| const result = JSON.parse(lastLine); | |
| res.write(`data: ${JSON.stringify(result)}\n\n`); | |
| } catch (e) { | |
| res.write(`data: ${JSON.stringify({ error: "parse error", raw: output.substring(0, 500), stderr: stderrBuf.substring(0, 300) })}\n\n`); | |
| } | |
| res.write("data: [DONE]\n\n"); | |
| res.end(); | |
| }); | |
| res.on("close", () => { | |
| if (!proc.killed) proc.kill(); | |
| }); | |
| }); | |
| // API: guard check | |
| app.post("/api/guard", (req, res) => { | |
| const { text } = req.body; | |
| const kamusPath = path.join(DATA_DIR, "kamus_id.txt"); | |
| if (!fs.existsSync(kamusPath)) return res.json({ ratio: 1.0, words: 0, known: 0 }); | |
| const words = new Set(fs.readFileSync(kamusPath, "utf-8").split("\n").map((w) => w.trim().toLowerCase()).filter(Boolean)); | |
| const tokens = text.toLowerCase().split(/\s+/).filter(Boolean); | |
| const known = tokens.filter((t) => words.has(t)).length; | |
| res.json({ ratio: tokens.length ? known / tokens.length : 1.0, words: tokens.length, known }); | |
| }); | |
| app.listen(PORT, () => { | |
| console.log(`indigo-chat running at http://localhost:${PORT}`); | |
| }); | |