@lzhzzzzwill/cofos 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,112 @@
1
+ # COFOS CLI
2
+
3
+ 一个本地 LLM 聊天 CLI:3D 立体欢迎界面 + Hugging Face 模型对话。
4
+
5
+ ```bash
6
+ npx cofos
7
+ ```
8
+
9
+ ```
10
+ ╭─────────────────────────────────────────────────────────╮
11
+ │ ██████╗ ██████╗ ███████╗ ██████╗ ███████╗ │
12
+ │ ██╔════╝ ██╔═══██╗ ██╔════╝ ██╔═══██╗ ██╔════╝ │
13
+ │ ██║ ██║ ██║ ███████╗ ██║ ██║ ███████╗ │
14
+ │ ██║ ██║ ██║ ██╔════╝ ██║ ██║ ╚════██║ │
15
+ │ ╚██████╗ ╚██████╔╝ ██║ ╚██████╔╝ ███████║ │
16
+ │ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝ ╚══════╝ │
17
+ │ ░░░░░░░ ░░░░░░░ ░░░ ░░░░░░░ ░░░░░░░ │ ← 3D shadow
18
+ │ Welcome to COFOS CLI — local LLM chat │
19
+ │ Type /help for commands · /exit to quit│
20
+ ╰─────────────────────────────────────────────────────────╯
21
+ cofos > What is a MOF material?
22
+ [Model streams response...]
23
+ cofos > Tell me more about its applications.
24
+ ```
25
+
26
+ ## 快速开始
27
+
28
+ ```bash
29
+ # 直接运行
30
+ npx cofos
31
+
32
+ # 或本地
33
+ git clone <repo> && cd demo && npm link
34
+ cofos
35
+ ```
36
+
37
+ 首次运行会自动安装 Python 依赖并下载模型(约 30-60 秒)。
38
+
39
+ ## 命令
40
+
41
+ | 命令 | 说明 |
42
+ |------|------|
43
+ | `/help` | 显示帮助 |
44
+ | `/exit` | 退出 |
45
+ | `/clear` | 清屏重新显示欢迎界面 |
46
+ | `/info` | 显示模型和版本信息 |
47
+ | `/model <id>` | 切换 Hugging Face 模型 |
48
+
49
+ ## 项目结构
50
+
51
+ ```
52
+ ├── package.json # npm 包配置,bin → cofos
53
+ ├── requirements.txt # Python 依赖:transformers + torch
54
+ ├── .gitignore
55
+ ├── bin/
56
+ │ └── cofos.js # Node.js CLI:3D 前端 + 子进程管理
57
+ └── scripts/
58
+ └── chat.py # Python 推理:加载模型、生成回复
59
+ ```
60
+
61
+ ## 架构概览
62
+
63
+ ```
64
+ 终端 Node.js (cofos.js) Python (chat.py)
65
+ │ │ │
66
+ │ cofos │ │
67
+ │────────────────────────────────>│ │
68
+ │ │ spawn python3 scripts/chat.py │
69
+ │ │──────────────────────────────────────>│
70
+ │ │ │ 加载模型
71
+ │ │ stdout ← ===READY=== │
72
+ │ │<──────────────────────────────────────│
73
+ │ 显示 3D 欢迎界面 + 提示符 │ │
74
+ │<───────────────────────────────│ │
75
+ │ │ │
76
+ │ 用户输入 "What is MOF?" │ │
77
+ │────────────────────────────────>│ │
78
+ │ │ stdin → "What is MOF?" │
79
+ │ │──────────────────────────────────────>│
80
+ │ │ │ 生成 token
81
+ │ │ stdout ← 流式 token │
82
+ │ 流式显示回复 │<──────────────────────────────────────│
83
+ │<───────────────────────────────│ │
84
+ │ │ stdout ← \n===DONE===\n │
85
+ │ │<──────────────────────────────────────│
86
+ │ 显示 "cofos >" 等待下一条 │ │
87
+ │<───────────────────────────────│ │
88
+ ```
89
+
90
+ ## 技术栈
91
+
92
+ | 层 | 技术 | 职责 |
93
+ |----|------|------|
94
+ | CLI 前端 | Node.js (ESM) | 终端 UI、子进程管理、管道通信 |
95
+ | 模型后端 | Python + transformers | 模型加载、token 生成、流式输出 |
96
+ | 模型 | SmolLM2-360M-Instruct (HF) | 对话推理 |
97
+ | 通信 | stdin/stdout 管道 | 进程间消息传递 |
98
+ | 分發 | npm | 包管理、全局命令、npx |
99
+
100
+ ## 依赖
101
+
102
+ - Node.js ≥ 18
103
+ - Python 3.8+
104
+ - 首次运行需联网(下载模型,~360MB)
105
+
106
+ ## 开发
107
+
108
+ ```bash
109
+ npm link # 注册全局命令
110
+ cofos # 启动
111
+ npm run start # 或直接 node ./bin/cofos.js
112
+ ```
package/bin/cofos.js ADDED
@@ -0,0 +1,183 @@
1
+ #!/usr/bin/env node
2
+ import { spawn, spawnSync } from "node:child_process";
3
+ import { createInterface } from "node:readline";
4
+ import { existsSync } from "node:fs";
5
+ import { dirname, join, resolve } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ const __dirname = dirname(fileURLToPath(import.meta.url));
9
+ const root = resolve(__dirname, "..");
10
+ const chatScript = join(root, "scripts", "chat.py");
11
+ const reqFile = join(root, "requirements.txt");
12
+
13
+ /* ─── ANSI ─── */
14
+ const R = "\x1b[0m", B = "\x1b[1m", D = "\x1b[2m";
15
+ const C = "\x1b[96m", W = "\x1b[97m", G = "\x1b[90m";
16
+
17
+ /* ─── 3D COFOS art ─── */
18
+ const C_ = [" ██████╗ ","██╔════╝ ","██║ ","██║ ","╚██████╗ "," ╚═════╝ "];
19
+ const O_ = [" ██████╗ ","██╔═══██╗","██║ ██║","██║ ██║","╚██████╔╝"," ╚═════╝ "];
20
+ const F_ = ["███████╗ ","██╔════╝ ","███████╗ ","██╔════╝ ","██║ ","╚═╝ "];
21
+ const S_ = [" ██████╗ ","██╔════╝ ","███████╗ ","╚════██║ ","███████║ ","╚══════╝ "];
22
+ const ART = C_.map((_, i) => [C_[i], O_[i], F_[i], O_[i], S_[i]].join(" "));
23
+ const ART_W = ART[0].length;
24
+
25
+ function shadow() {
26
+ let s = " ";
27
+ for (const ch of ART[ART.length - 1]) s += ch !== " " ? "░" : " ";
28
+ return G + D + s.slice(0, -2) + " " + R;
29
+ }
30
+
31
+ function vis(t) { return t.replace(/\x1b\[[0-9;]*m/g, "").length; }
32
+ function fw() { return Math.max((process.stdout.columns || 100) - 2, 50); }
33
+
34
+ function top(w) { return "╭" + "─".repeat(w) + "╮"; }
35
+ function bot(w) { return "╰" + "─".repeat(w) + "╯"; }
36
+ function sep(w) { return "├" + "─".repeat(w) + "┤"; }
37
+ function emp(w) { return "│" + " ".repeat(w) + "│"; }
38
+ function cnt(t, w) {
39
+ const n = vis(t);
40
+ if (n >= w - 1) return "│" + t.slice(0, w) + "│";
41
+ const s = w - n, l = Math.floor(s / 2), r = s - l;
42
+ return "│" + " ".repeat(l) + t + " ".repeat(r) + "│";
43
+ }
44
+
45
+ function render() {
46
+ console.clear();
47
+ const w = fw();
48
+ const l = [];
49
+ l.push(top(w)); l.push(emp(w));
50
+ if (w >= ART_W + 2) {
51
+ for (const r of ART) l.push(cnt(C + B + r + R, w));
52
+ l.push(cnt(shadow(), w));
53
+ } else {
54
+ l.push(cnt(C + B + " ██████╗ ██████╗ ███████╗ ██████╗ ███████╗ " + R, w));
55
+ l.push(cnt(G + D + " ░░░░░░╝ ░░░░░░╝ ░░░░░░░╝ ░░░░░░╝ ░░░░░░░╝ " + R, w));
56
+ }
57
+ l.push(emp(w));
58
+ l.push(sep(w));
59
+ l.push(emp(w));
60
+ l.push(cnt(W + B + "Welcome to COFOS CLI — local LLM chat" + R, w));
61
+ l.push(emp(w));
62
+ l.push(cnt(G + "Type /help for commands · /exit to quit" + R, w));
63
+ l.push(emp(w));
64
+ l.push(bot(w));
65
+ console.log(l.join("\n"));
66
+ }
67
+
68
+ /* ─── helpers ─── */
69
+
70
+ function cmdExist(c) { return spawnSync(c, ["--version"], { encoding: "utf-8", stdio: "pipe" }).status === 0; }
71
+ function findPy() { return cmdExist("python3") ? "python3" : cmdExist("python") ? "python" : null; }
72
+
73
+ function ensureDeps(py) {
74
+ console.log("[cofos] Checking Python dependencies …");
75
+ if (spawnSync(py, ["-c", "import transformers, torch; print('ok')"], { encoding: "utf-8", stdio: "pipe" }).status === 0) return;
76
+ console.log("[cofos] Installing Python dependencies (first run, may take a while) …");
77
+ const r = spawnSync(py, ["-m", "pip", "install", "-r", reqFile], { encoding: "utf-8", stdio: "inherit" });
78
+ if (r.status !== 0) { console.error("[cofos] Failed to install Python dependencies."); process.exit(1); }
79
+ }
80
+
81
+ /* ─── main ─── */
82
+
83
+ function main() {
84
+ if (process.argv.includes("-h") || process.argv.includes("--help")) {
85
+ console.log("\n COFOS CLI — local LLM chat\n Usage: cofos\n"); process.exit(0);
86
+ }
87
+
88
+ render();
89
+
90
+ const py = findPy();
91
+ if (!py) { console.error("[cofos] Python not found."); process.exit(1); }
92
+ if (!existsSync(chatScript)) { console.error("[cofos] Missing script:", chatScript); process.exit(1); }
93
+ ensureDeps(py);
94
+
95
+ console.log(" Loading model … (30–60 s on first run)\n");
96
+
97
+ const proc = spawn(py, [chatScript], { stdio: ["pipe", "pipe", "inherit"], encoding: "utf-8" });
98
+
99
+ let buf = "", ready = false, generating = false, closed = false;
100
+ const inputQ = [];
101
+ const rl = createInterface({ input: process.stdin });
102
+ rl.on("line", (l) => { inputQ.push(l); });
103
+
104
+ proc.stdout.on("data", (chunk) => {
105
+ try {
106
+ buf += chunk;
107
+ if (!ready) {
108
+ const i = buf.indexOf("===READY===");
109
+ if (i !== -1) {
110
+ ready = true;
111
+ buf = buf.slice(i + 10);
112
+ if (buf[0] === "\n") buf = buf.slice(1);
113
+ drain();
114
+ }
115
+ return;
116
+ }
117
+ if (!generating) return;
118
+ const i = buf.indexOf("\n===DONE===");
119
+ if (i !== -1) {
120
+ process.stdout.write(buf.slice(0, i));
121
+ buf = buf.slice(i + 11);
122
+ if (buf[0] === "\n") buf = buf.slice(1);
123
+ generating = false;
124
+ console.log("");
125
+ drain();
126
+ } else {
127
+ const k = 10;
128
+ if (buf.length > k) { process.stdout.write(buf.slice(0, -k)); buf = buf.slice(-k); }
129
+ }
130
+ } catch (_) {}
131
+ });
132
+
133
+ proc.on("error", (e) => { console.error("[cofos] Python error:", e.message); process.exit(1); });
134
+ proc.on("exit", (c) => {
135
+ closed = true;
136
+ if (c && c !== 0) console.error("[cofos] Python exited with code", c);
137
+ try { rl.close(); } catch (_) {}
138
+ process.exit(c ?? 0);
139
+ });
140
+
141
+ function drain() {
142
+ while (inputQ.length) {
143
+ const line = inputQ.shift();
144
+ if (handle(line)) return;
145
+ }
146
+ ask();
147
+ }
148
+
149
+ function ask() {
150
+ if (closed) return;
151
+ try { rl.question(C + " cofos " + R, handle); } catch (_) {}
152
+ }
153
+
154
+ function handle(input) {
155
+ input = input.trim();
156
+ if (!input) { ask(); return; }
157
+
158
+ if (/^\/(exit|quit)$/.test(input)) { proc.stdin.write("/exit\n"); proc.stdin.end(); return; }
159
+ if (/^\/(help)$/.test(input)) {
160
+ console.log("\n Commands:\n /help Show help\n /exit Exit\n /clear Clear screen\n /info Model info\n /model Switch model\n");
161
+ ask(); return;
162
+ }
163
+ if (/^\/(clear)$/.test(input)) { render(); console.log(""); ask(); return; }
164
+ if (/^\/(info)$/.test(input)) {
165
+ console.log("\n COFOS CLI — v1.0.0");
166
+ console.log(" Model: HuggingFaceTB/SmolLM2-360M-Instruct");
167
+ console.log(" Backend: transformers + PyTorch\n");
168
+ ask(); return;
169
+ }
170
+ if (/^\/model\s/.test(input)) {
171
+ const m = input.slice(7).trim();
172
+ console.log("[cofos] Switching model to", m);
173
+ proc.stdin.write(input + "\n");
174
+ generating = true;
175
+ return;
176
+ }
177
+
178
+ generating = true;
179
+ proc.stdin.write(input + "\n");
180
+ }
181
+ }
182
+
183
+ main();
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@lzhzzzzwill/cofos",
3
+ "version": "1.0.0",
4
+ "description": "A local LLM chat CLI with a 3D stereoscopic welcome screen.",
5
+ "type": "module",
6
+ "bin": {
7
+ "cofos": "bin/cofos.js"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "scripts",
12
+ "requirements.txt"
13
+ ],
14
+ "scripts": {
15
+ "start": "node ./bin/cofos.js",
16
+ "test": "node ./bin/cofos.js --help"
17
+ },
18
+ "keywords": [
19
+ "cli",
20
+ "cofos",
21
+ "llm",
22
+ "chat",
23
+ "huggingface"
24
+ ],
25
+ "license": "MIT",
26
+ "engines": {
27
+ "node": ">=18"
28
+ }
29
+ }
@@ -0,0 +1,2 @@
1
+ transformers>=4.40.0
2
+ torch
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env python3
2
+ """Persistent chat: loads an LLM once, reads prompts from stdin, streams to stdout."""
3
+ import sys
4
+ from threading import Thread
5
+
6
+ from transformers import (
7
+ AutoModelForCausalLM,
8
+ AutoTokenizer,
9
+ TextIteratorStreamer,
10
+ )
11
+
12
+ MODEL_ID = "HuggingFaceTB/SmolLM2-360M-Instruct"
13
+
14
+
15
+ def main() -> None:
16
+ model_id = sys.argv[1] if len(sys.argv) > 1 else MODEL_ID
17
+
18
+ print(f"[chat] Loading {model_id} …", file=sys.stderr, flush=True)
19
+ print(f"[chat] 30–60 s on first run (model download).", file=sys.stderr, flush=True)
20
+
21
+ tokenizer = AutoTokenizer.from_pretrained(model_id)
22
+ if tokenizer.pad_token_id is None:
23
+ tokenizer.pad_token_id = tokenizer.eos_token_id
24
+
25
+ model = AutoModelForCausalLM.from_pretrained(
26
+ model_id, dtype="float32", low_cpu_mem_usage=True,
27
+ )
28
+ model.eval()
29
+
30
+ print("[chat] Ready.", file=sys.stderr, flush=True)
31
+ print("===READY===", flush=True)
32
+
33
+ messages: list[dict] = []
34
+
35
+ for raw in sys.stdin:
36
+ line = raw.strip("\r\n")
37
+ if not line or line == "/exit":
38
+ break
39
+ if line.startswith("/model "):
40
+ mid = line[len("/model "):].strip()
41
+ tokenizer = AutoTokenizer.from_pretrained(mid)
42
+ if tokenizer.pad_token_id is None:
43
+ tokenizer.pad_token_id = tokenizer.eos_token_id
44
+ model = AutoModelForCausalLM.from_pretrained(
45
+ mid, dtype="float32", low_cpu_mem_usage=True,
46
+ )
47
+ model.eval()
48
+ print("===DONE===", flush=True)
49
+ continue
50
+
51
+ messages.append({"role": "user", "content": line})
52
+ prompt = tokenizer.apply_chat_template(
53
+ messages, tokenize=False, add_generation_prompt=True,
54
+ )
55
+ inputs = tokenizer(prompt, return_tensors="pt")
56
+
57
+ streamer = TextIteratorStreamer(
58
+ tokenizer, skip_prompt=True, skip_special_tokens=True,
59
+ )
60
+
61
+ gen_kw = dict(
62
+ input_ids=inputs.input_ids,
63
+ attention_mask=inputs.attention_mask,
64
+ streamer=streamer,
65
+ max_new_tokens=512,
66
+ do_sample=True,
67
+ temperature=0.7,
68
+ top_p=0.9,
69
+ repetition_penalty=1.05,
70
+ pad_token_id=tokenizer.eos_token_id,
71
+ )
72
+
73
+ Thread(target=model.generate, kwargs=gen_kw).start()
74
+
75
+ reply = ""
76
+ for tok in streamer:
77
+ sys.stdout.write(tok)
78
+ sys.stdout.flush()
79
+ reply += tok
80
+
81
+ messages.append({"role": "assistant", "content": reply})
82
+ print("\n===DONE===", flush=True)
83
+
84
+
85
+ if __name__ == "__main__":
86
+ try:
87
+ main()
88
+ except KeyboardInterrupt:
89
+ pass
90
+ except Exception as e:
91
+ print(f"[chat] Error: {e}", file=sys.stderr)
92
+ sys.exit(1)