Download chat.mjs from teddyoas/Asnip: direct link, hf CLI and curl.
- Browser
- Download file 8.81 kB
-
https://huggingface.co/teddyoas/Asnip/resolve/main/chat.mjs
- Command line
-
hf download hf://teddyoas/Asnip/chat.mjs
-
curl -L -o chat.mjs https://huggingface.co/teddyoas/Asnip/resolve/main/chat.mjs
8.81 kB
| // Chat — Phantom conversational CLI | |
| // Zero deps. Run: node chat.mjs | |
| import { createRequire } from "module"; | |
| const $r = createRequire(import.meta.url); | |
| import fs from "fs"; | |
| import { resolve } from "path"; | |
| const R = "\x1b[0m"; | |
| const _B = "\x1b[1m"; | |
| const _D = "\x1b[2m"; | |
| function c(n) { | |
| const colors = { green: "32", cyan: "36", dim: "2", yellow: "33", magenta: "35", red: "31", blue: "34" }; | |
| return colors[n] ? `\x1b[${colors[n]}m` : ""; | |
| } | |
| // ── Provider (from phantom.mjs) ── | |
| export function createProvider() { | |
| const PROVIDERS = { | |
| openai: { url: "https://opencode.ai/zen/v1", keyEnv: "OPENCODE_ZEN_API_KEY", defaultModel: "nemotron-3-ultra-free", chatPath: "/chat/completions", fmt: o => ({ model: o.model, messages: o.messages, temperature: 0.7, max_tokens: 16384 }), parse: d => d.choices?.[0]?.message?.content?.trim() || "...", auth: k => ({ Authorization: `Bearer ${k}` }) }, | |
| anthropic: { url: "https://api.anthropic.com/v1", keyEnv: "ANTHROPIC_API_KEY", defaultModel: "claude-sonnet-4-20250514", chatPath: "/messages", fmt: o => ({ model: o.model, messages: o.messages, max_tokens: 4096 }), parse: d => d.content?.[0]?.text || "...", auth: k => ({ "x-api-key": k, "anthropic-version": "2023-06-01" }) }, | |
| openrouter:{ url: "https://openrouter.ai/api/v1", keyEnv: "OPENROUTER_API_KEY", defaultModel: "anthropic/claude-sonnet-4", chatPath: "/chat/completions", fmt: o => ({ model: o.model, messages: o.messages, temperature: 0.7 }), parse: d => d.choices?.[0]?.message?.content?.trim() || "...", auth: k => ({ Authorization: `Bearer ${k}` }) }, | |
| ollama: { url: process.env.OLLAMA_HOST || "http://localhost:11434", keyEnv: "", defaultModel: "llama3", chatPath: "/api/chat", fmt: o => ({ model: o.model, messages: o.messages, stream: false }), parse: d => d.message?.content?.trim() || "...", auth: () => ({}) }, | |
| opencode: { url: "https://opencode.ai/zen/v1", keyEnv: "OPENCODE_ZEN_API_KEY", defaultModel: "nemotron-3-ultra-free", chatPath: "/chat/completions", fmt: o => ({ model: o.model, messages: o.messages, temperature: 0.7, max_tokens: 256 }), parse: d => d.choices?.[0]?.message?.content?.trim() || "...", auth: k => ({ Authorization: `Bearer ${k}` }) }, | |
| }; | |
| const providerName = process.env.PHANTOM_LLM_PROVIDER || "openai"; | |
| function getProvider() { return PROVIDERS[providerName] || PROVIDERS.openai; } | |
| function getKey(p) { | |
| if (!p.keyEnv) return ""; | |
| const k = process.env[p.keyEnv]; | |
| if (k) return k; | |
| // Check project root config (for USB portability) | |
| try { | |
| const projectRoot = resolve(new URL(".", import.meta.url).pathname, ".."); | |
| const projectConfigPath = resolve(projectRoot, "config.json"); | |
| const cfg = JSON.parse(fs.readFileSync(projectConfigPath, "utf8")); | |
| if (cfg[p.keyEnv]) return cfg[p.keyEnv]; | |
| } catch {} | |
| // Check user config | |
| try { | |
| const cfg = JSON.parse(fs.readFileSync(resolve(process.env.HOME || "/root", ".config", "phantom", "config.json"), "utf8")); | |
| return cfg[p.keyEnv] || ""; | |
| } catch { return ""; } | |
| } | |
| return { | |
| get provider() { return providerName; }, | |
| get hasLLM() { const p = getProvider(); return !!(p.keyEnv ? getKey(p) : true); }, | |
| async chat(messages, opts = {}) { | |
| const p = getProvider(); | |
| const key = getKey(p); | |
| if (p.keyEnv && !key) return `[${providerName}] No API key. Set ${p.keyEnv} env var.`; | |
| const model = opts.model || p.defaultModel; | |
| try { | |
| let url = `${p.url}${p.chatPath.replace("{model}", model)}`; | |
| const h = { "Content-Type": "application/json", ...p.auth(key) }; | |
| const body = JSON.stringify(p.fmt({ model, messages })); | |
| const r = await fetch(url, { method: "POST", headers: h, body, signal: AbortSignal.timeout(60000) }); | |
| if (!r.ok) { const t = await r.text().catch(() => ""); return `[${r.status}] ${t.substring(0, 200)}`; } | |
| const d = await r.json(); | |
| return p.parse(d) || "..."; | |
| } catch (e) { return `[err] ${e.message}`; } | |
| } | |
| }; | |
| } | |
| // ── Spinner ── | |
| function spinner(start) { | |
| const frames = ["⠋","⠙","⠹","⠸","⠼","⠴","⠦","⠧","⠇","⠏"]; | |
| let i = 0, timer; | |
| const fmt = ts => { const s = Math.floor((Date.now() - ts) / 1000); return s > 60 ? `${Math.floor(s/60)}m ${s%60}s` : `${s}s`; }; | |
| return { | |
| start() { timer = setInterval(() => { process.stdout.write(`\r${c("dim")}${frames[i]} ${fmt(start)}${R}`); i = (i + 1) % frames.length; }, 120); }, | |
| stop() { clearInterval(timer); process.stdout.write("\r\x1b[K\n"); }, | |
| }; | |
| } | |
| // ── CLI Chat ── | |
| export async function runChat(llm) { | |
| const version = "0.1.0"; | |
| const history = []; | |
| const maxHistory = 20; | |
| // Use provided LLM or create one | |
| const provider = llm || createProvider(); | |
| if (!provider.hasLLM) { | |
| console.error(`${c("red")}No LLM provider configured.${R}`); | |
| console.error(`${c("dim")}Set PHANTOM_LLM_PROVIDER and the corresponding API key env var.${R}`); | |
| process.exit(1); | |
| } | |
| console.log(`${c("green")}${_B}PHANTOM Chat${R} ${c("dim")}v${version} · ${provider.provider}${R}`); | |
| console.log(`${c("dim")}Type /help for commands, /exit to quit${R}\n`); | |
| const rl = $r("readline").createInterface({ input: process.stdin, output: process.stdout, terminal: true }); | |
| function ask() { | |
| rl.question(`${c("green")}>> ${R}`, async (input) => { | |
| const trimmed = input.trim(); | |
| if (!trimmed) { ask(); return; } | |
| // Commands | |
| if (trimmed.startsWith("/")) { | |
| const cmd = trimmed.slice(1).toLowerCase(); | |
| if (cmd === "exit" || cmd === "quit" || cmd === "q") { rl.close(); return; } | |
| if (cmd === "help") { | |
| console.log(`\n${c("dim")}Commands:${R}`); | |
| console.log(` ${c("green")}/exit${R} Exit chat`); | |
| console.log(` ${c("green")}/clear${R} Clear conversation history`); | |
| console.log(` ${c("green")}/model <n>${R} Show/set model`); | |
| console.log(` ${c("green")}/help${R} This help\n`); | |
| ask(); | |
| return; | |
| } | |
| if (cmd === "clear") { history.length = 0; console.log(`${c("dim")}History cleared${R}\n`); ask(); return; } | |
| if (cmd.startsWith("model ")) { | |
| // Could set model, but for now just show current | |
| console.log(`${c("dim")}Model: ${provider.provider}${R}\n`); | |
| ask(); | |
| return; | |
| } | |
| console.log(`${c("yellow")}Unknown: ${trimmed}${R}\n`); | |
| ask(); | |
| return; | |
| } | |
| // Show user input | |
| process.stdout.write(`${c("green")}>${R} ${trimmed}\n`); | |
| // Build context | |
| const messages = [ | |
| { role: "system", content: "You are Phantom, a helpful AI assistant. You are concise, knowledgeable, and direct. Answer clearly and accurately." }, | |
| ...history.slice(-maxHistory), | |
| { role: "user", content: trimmed }, | |
| ]; | |
| // Streaming spinner - DISABLED | |
| // const s = spinner(Date.now()); | |
| // s.start(); | |
| // Streaming spinner | |
| const s = spinner(Date.now()); | |
| s.start(); | |
| const start = Date.now(); | |
| console.error("DEBUG chat.mjs: BEFORE provider.chat"); | |
| let response; | |
| try { | |
| response = await provider.chat(messages); | |
| } catch (e) { | |
| console.error("DEBUG chat.mjs: EXCEPTION in provider.chat:", e.message); | |
| response = "[error] " + e.message; | |
| } | |
| console.error("DEBUG chat.mjs: AFTER provider.chat, response:", response?.substring(0, 50)); | |
| s.stop(); | |
| if (response.startsWith("[") && response.includes("] ")) { | |
| console.log(`${c("red")}${response}${R}\n`); | |
| ask(); | |
| return; | |
| } | |
| // Show response | |
| process.stdout.write("\n"); | |
| process.stdout.write(response + "\n"); | |
| process.stdout.write("\n"); | |
| console.error("DEBUG chat.mjs: wrote response:", response.substring(0, 50)); | |
| const elapsed = Math.floor((Date.now() - start) / 1000); | |
| console.log(`${c("dim")}✓ ${elapsed}s${R}\n`); | |
| // Store in history | |
| history.push({ role: "user", content: trimmed }); | |
| history.push({ role: "assistant", content: response }); | |
| ask(); | |
| }); | |
| } | |
| rl.on("close", () => { | |
| console.log(`\n${c("dim")}Bye.${R}`); | |
| process.exit(0); | |
| }); | |
| ask(); | |
| } | |
| // ── Entry ── | |
| // Only run if executed directly (not imported) | |
| if (import.meta.url === `file://${process.argv[1]}`) { | |
| const llm = createProvider(); | |
| if (!llm.hasLLM) { | |
| console.error(`${c("red")}No LLM provider configured.${R}`); | |
| console.error(`${c("dim")}Set PHANTOM_LLM_PROVIDER and the corresponding API key env var.${R}`); | |
| process.exit(1); | |
| } | |
| runChat(llm).catch(e => { console.error(`${c("red")}${e.message}${R}`); process.exit(1); }); | |
| } | |