dragon-cli 0.2.0__py3-none-any.whl

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.
@@ -0,0 +1,197 @@
1
+ """Cross-platform installer for llama-cpp-python.
2
+
3
+ Compiling llama-cpp-python from source requires a C/C++ toolchain (Visual
4
+ Studio Build Tools on Windows, gcc/build-essential on Linux, Xcode command
5
+ line tools on macOS). This detects the current OS and GPU hardware, then
6
+ installs the appropriate prebuilt wheel (CPU, CUDA, or ROCm) for optimal
7
+ performance.
8
+ """
9
+
10
+ import os
11
+ import platform
12
+ import shutil
13
+ import subprocess
14
+ import sys
15
+
16
+
17
+ WHEEL_INDEX_URLS = {
18
+ "cpu": "https://abetlen.github.io/llama-cpp-python/whl/cpu",
19
+ "cuda": "https://abetlen.github.io/llama-cpp-python/whl/cu124",
20
+ "rocm": "https://abetlen.github.io/llama-cpp-python/whl/rocm",
21
+ "mps": "https://abetlen.github.io/llama-cpp-python/whl/metal",
22
+ }
23
+
24
+
25
+ def detect_gpu():
26
+ """Detect available GPU and return GPU type.
27
+
28
+ Prefer system commands such as nvidia-smi and rocm-smi because they work
29
+ even in stripped-down virtual environments where torch or CUDA libraries
30
+ may not be installed yet.
31
+ """
32
+ gpu_type = "cpu"
33
+
34
+ nvidia_smi = shutil.which("nvidia-smi")
35
+ if nvidia_smi:
36
+ result = subprocess.run(
37
+ [nvidia_smi, "--query-gpu=name,memory.total", "--format=csv,noheader"],
38
+ capture_output=True,
39
+ text=True,
40
+ check=False,
41
+ )
42
+ if result.returncode == 0:
43
+ first_line = next((line.strip() for line in result.stdout.splitlines() if line.strip()), "")
44
+ if first_line:
45
+ gpu_name = first_line.split(",")[0].strip()
46
+ print(f"[dragon-cli] Detected NVIDIA GPU via nvidia-smi: {gpu_name}")
47
+ gpu_type = "cuda"
48
+ return gpu_type
49
+
50
+ if platform.system() == "Linux":
51
+ rocminfo = shutil.which("rocminfo")
52
+ if rocminfo:
53
+ result = subprocess.run(
54
+ [rocminfo],
55
+ capture_output=True,
56
+ text=True,
57
+ timeout=5,
58
+ check=False,
59
+ )
60
+ if result.returncode == 0 and "Agent" in result.stdout:
61
+ print("[dragon-cli] Detected AMD GPU via ROCm")
62
+ return "rocm"
63
+
64
+ rocm_smi = shutil.which("rocm-smi")
65
+ if rocm_smi:
66
+ result = subprocess.run(
67
+ [rocm_smi, "--showproductname"],
68
+ capture_output=True,
69
+ text=True,
70
+ timeout=5,
71
+ check=False,
72
+ )
73
+ if result.returncode == 0:
74
+ print("[dragon-cli] Detected AMD GPU via rocm-smi")
75
+ return "rocm"
76
+
77
+ if platform.system() == "Darwin":
78
+ system_profiler = shutil.which("system_profiler")
79
+ if system_profiler:
80
+ result = subprocess.run(
81
+ [system_profiler, "SPDisplaysDataType"],
82
+ capture_output=True,
83
+ text=True,
84
+ timeout=10,
85
+ check=False,
86
+ )
87
+ if result.returncode == 0 and "Apple" in result.stdout:
88
+ print("[dragon-cli] Detected Apple Silicon GPU via system_profiler")
89
+ return "mps"
90
+
91
+ print("[dragon-cli] No dedicated GPU detected, using CPU-only mode")
92
+ return gpu_type
93
+
94
+
95
+ def prompt_backend_choice(gpu_type: str) -> str:
96
+ """Ask the user in English which backend to use.
97
+
98
+ CPU remains the default backend for safety, even if a GPU is detected.
99
+ """
100
+ if gpu_type == "cpu":
101
+ print("[dragon-cli] No dedicated GPU detected. Defaulting to CPU backend.")
102
+ return "cpu"
103
+
104
+ print("[dragon-cli] GPU detected. Do you want to use a GPU-enabled wheel?")
105
+ print("[dragon-cli] Default backend: CPU")
106
+ print("[dragon-cli] Available backends: cpu, cuda, rocm, mps")
107
+
108
+ if not sys.stdin.isatty():
109
+ print("[dragon-cli] Non-interactive shell detected. Keeping CPU as the default backend.")
110
+ return "cpu"
111
+
112
+ answer = input("[dragon-cli] Choose backend [cpu]: ").strip().lower()
113
+ if answer in {"cpu", "cuda", "rocm", "mps"}:
114
+ return answer
115
+ if answer == "":
116
+ return "cpu"
117
+
118
+ print("[dragon-cli] Invalid choice. Keeping CPU as the default backend.")
119
+ return "cpu"
120
+
121
+
122
+ def get_install_command(backend: str):
123
+ """Install llama-cpp-python using a prebuilt wheel backend.
124
+
125
+ CPU is the safe default; GPU backends can be selected later by the user.
126
+ """
127
+ index_url = WHEEL_INDEX_URLS.get(backend, WHEEL_INDEX_URLS["cpu"])
128
+ print(f"[dragon-cli] Installing llama-cpp-python from prebuilt {backend.upper()} wheels ({index_url})...")
129
+
130
+ result = subprocess.run([
131
+ sys.executable, "-m", "pip", "install", "llama-cpp-python",
132
+ "--prefer-binary", "--extra-index-url", index_url,
133
+ ])
134
+
135
+ return result
136
+
137
+
138
+ TROUBLESHOOTING = {
139
+ "Windows": (
140
+ "Installation failed. Make sure you have Visual Studio Build Tools\n"
141
+ "with C++ workload installed:\n"
142
+ "https://visualstudio.microsoft.com/visual-cpp-build-tools/\n"
143
+ "Also ensure CMake is installed and in PATH."
144
+ ),
145
+ "Linux": (
146
+ "Installation failed. Install build tools and CMake:\n"
147
+ " Ubuntu/Debian: sudo apt install build-essential cmake\n"
148
+ " Fedora/RHEL: sudo dnf install gcc gcc-c++ make cmake\n"
149
+ "\nFor GPU support, also install:\n"
150
+ " NVIDIA CUDA: https://developer.nvidia.com/cuda-downloads\n"
151
+ " AMD ROCm: https://rocm.docs.amd.com/"
152
+ ),
153
+ "Darwin": (
154
+ "Installation failed. Install Xcode command line tools:\n"
155
+ " xcode-select --install\n"
156
+ "\nFor Apple Silicon, ensure you're using Python from python.org or Homebrew."
157
+ ),
158
+ }
159
+
160
+
161
+ def main() -> None:
162
+ system = platform.system()
163
+ print(f"[dragon-cli] Detected {system} ({platform.machine()}), Python {platform.python_version()}")
164
+ print()
165
+
166
+ gpu_type = detect_gpu()
167
+ print()
168
+
169
+ backend = prompt_backend_choice(gpu_type)
170
+ print()
171
+
172
+ result = get_install_command(backend)
173
+
174
+ if result.returncode == 0:
175
+ print()
176
+ print("[dragon-cli] ✓ llama-cpp-python installed successfully!")
177
+
178
+ if backend != "cpu":
179
+ print(f"[dragon-cli] GPU backend selected ({backend.upper()})")
180
+ print("[dragon-cli] GPU-enabled wheel installed successfully")
181
+ else:
182
+ print("[dragon-cli] CPU backend selected by default")
183
+ print("[dragon-cli] You can choose a GPU wheel later if needed")
184
+
185
+ return
186
+
187
+ print("\n[dragon-cli] ✗ Installation failed.")
188
+ print(TROUBLESHOOTING.get(
189
+ system,
190
+ "Install a C/C++ toolchain and CMake for your platform, then retry: "
191
+ "pip install llama-cpp-python",
192
+ ))
193
+ sys.exit(1)
194
+
195
+
196
+ if __name__ == "__main__":
197
+ main()
@@ -0,0 +1,3 @@
1
+ from .conversations import Conversation, ConversationStore, Message
2
+
3
+ __all__ = ["Conversation", "ConversationStore", "Message"]
@@ -0,0 +1,134 @@
1
+ """High-level conversation persistence API used by the chat layer."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from dataclasses import dataclass
7
+ from datetime import datetime, timezone
8
+ from pathlib import Path
9
+
10
+ from .database import connect
11
+
12
+
13
+ def _now() -> str:
14
+ return datetime.now(timezone.utc).isoformat()
15
+
16
+
17
+ @dataclass
18
+ class Conversation:
19
+ id: int
20
+ title: str
21
+ model_name: str | None
22
+ created_at: str
23
+ updated_at: str
24
+ message_count: int = 0
25
+
26
+
27
+ @dataclass
28
+ class Message:
29
+ role: str
30
+ content: str
31
+ created_at: str
32
+
33
+
34
+ class ConversationStore:
35
+ """CRUD + search/export over the conversations/messages SQLite tables."""
36
+
37
+ def __init__(self, db_path: Path):
38
+ self.conn = connect(db_path)
39
+
40
+ def create(self, title: str, model_name: str | None = None) -> int:
41
+ now = _now()
42
+ cur = self.conn.execute(
43
+ "INSERT INTO conversations (title, model_name, created_at, updated_at) VALUES (?, ?, ?, ?)",
44
+ (title, model_name, now, now),
45
+ )
46
+ self.conn.commit()
47
+ return cur.lastrowid
48
+
49
+ def list(self) -> list[Conversation]:
50
+ rows = self.conn.execute(
51
+ """
52
+ SELECT c.id, c.title, c.model_name, c.created_at, c.updated_at,
53
+ COUNT(m.id) AS message_count
54
+ FROM conversations c
55
+ LEFT JOIN messages m ON m.conversation_id = c.id
56
+ GROUP BY c.id
57
+ ORDER BY c.updated_at DESC
58
+ """
59
+ ).fetchall()
60
+ return [Conversation(**dict(r)) for r in rows]
61
+
62
+ def get(self, conv_id: int) -> tuple[Conversation, list[Message]] | None:
63
+ row = self.conn.execute("SELECT * FROM conversations WHERE id = ?", (conv_id,)).fetchone()
64
+ if row is None:
65
+ return None
66
+ msg_rows = self.conn.execute(
67
+ "SELECT role, content, created_at FROM messages WHERE conversation_id = ? ORDER BY id",
68
+ (conv_id,),
69
+ ).fetchall()
70
+ messages = [Message(**dict(m)) for m in msg_rows]
71
+ conv = Conversation(
72
+ id=row["id"], title=row["title"], model_name=row["model_name"],
73
+ created_at=row["created_at"], updated_at=row["updated_at"],
74
+ message_count=len(messages),
75
+ )
76
+ return conv, messages
77
+
78
+ def append_message(self, conv_id: int, role: str, content: str) -> None:
79
+ now = _now()
80
+ self.conn.execute(
81
+ "INSERT INTO messages (conversation_id, role, content, created_at) VALUES (?, ?, ?, ?)",
82
+ (conv_id, role, content, now),
83
+ )
84
+ self.conn.execute("UPDATE conversations SET updated_at = ? WHERE id = ?", (now, conv_id))
85
+ self.conn.commit()
86
+
87
+ def rename(self, conv_id: int, title: str) -> bool:
88
+ cur = self.conn.execute(
89
+ "UPDATE conversations SET title = ?, updated_at = ? WHERE id = ?", (title, _now(), conv_id)
90
+ )
91
+ self.conn.commit()
92
+ return cur.rowcount > 0
93
+
94
+ def delete(self, conv_id: int) -> bool:
95
+ cur = self.conn.execute("DELETE FROM conversations WHERE id = ?", (conv_id,))
96
+ self.conn.commit()
97
+ return cur.rowcount > 0
98
+
99
+ def search(self, text: str) -> list[tuple[int, str, str, str]]:
100
+ rows = self.conn.execute(
101
+ """
102
+ SELECT c.id, c.title, m.role, m.content
103
+ FROM messages m
104
+ JOIN conversations c ON c.id = m.conversation_id
105
+ WHERE m.content LIKE ?
106
+ ORDER BY m.created_at DESC
107
+ """,
108
+ (f"%{text}%",),
109
+ ).fetchall()
110
+ return [(r["id"], r["title"], r["role"], r["content"]) for r in rows]
111
+
112
+ def export(self, conv_id: int, path: Path, fmt: str = "md") -> Path | None:
113
+ found = self.get(conv_id)
114
+ if found is None:
115
+ return None
116
+ conv, messages = found
117
+ if fmt == "json":
118
+ payload = {
119
+ "id": conv.id, "title": conv.title, "model_name": conv.model_name,
120
+ "created_at": conv.created_at, "updated_at": conv.updated_at,
121
+ "messages": [{"role": m.role, "content": m.content, "created_at": m.created_at} for m in messages],
122
+ }
123
+ path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
124
+ else:
125
+ lines = [f"# {conv.title}", f"_model: {conv.model_name or 'unknown'} · updated: {conv.updated_at}_", ""]
126
+ for m in messages:
127
+ if m.role == "system":
128
+ continue
129
+ lines.append(f"**{m.role}:** {m.content}\n")
130
+ path.write_text("\n".join(lines), encoding="utf-8")
131
+ return path
132
+
133
+ def close(self) -> None:
134
+ self.conn.close()
@@ -0,0 +1,32 @@
1
+ """SQLite connection and schema management for conversation storage."""
2
+
3
+ import sqlite3
4
+ from pathlib import Path
5
+
6
+ SCHEMA = """
7
+ CREATE TABLE IF NOT EXISTS conversations (
8
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
9
+ title TEXT NOT NULL,
10
+ model_name TEXT,
11
+ created_at TEXT NOT NULL,
12
+ updated_at TEXT NOT NULL
13
+ );
14
+
15
+ CREATE TABLE IF NOT EXISTS messages (
16
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
17
+ conversation_id INTEGER NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
18
+ role TEXT NOT NULL,
19
+ content TEXT NOT NULL,
20
+ created_at TEXT NOT NULL
21
+ );
22
+
23
+ CREATE INDEX IF NOT EXISTS idx_messages_conversation ON messages(conversation_id);
24
+ """
25
+
26
+
27
+ def connect(db_path: Path) -> sqlite3.Connection:
28
+ conn = sqlite3.connect(str(db_path))
29
+ conn.row_factory = sqlite3.Row
30
+ conn.execute("PRAGMA foreign_keys = ON")
31
+ conn.executescript(SCHEMA)
32
+ return conn
dragon_cli/web.py ADDED
@@ -0,0 +1,112 @@
1
+ """HTML landing page and favicon for the --serve HTTP server."""
2
+
3
+ # A dragon emoji rendered as SVG text works as a favicon with zero image
4
+ # assets to ship, and doubles as the landing page's hero icon.
5
+ FAVICON_SVG = (
6
+ '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">'
7
+ '<text x="32" y="50" font-size="52" text-anchor="middle">\U0001F409</text>'
8
+ "</svg>"
9
+ )
10
+
11
+ _INDEX_TEMPLATE = """<!doctype html>
12
+ <html lang="en">
13
+ <head>
14
+ <meta charset="utf-8">
15
+ <meta name="viewport" content="width=device-width, initial-scale=1">
16
+ <title>Dragon CLI — __MODEL_NAME__</title>
17
+ <link rel="icon" type="image/svg+xml" href="/favicon.ico">
18
+ <style>
19
+ :root { color-scheme: dark; }
20
+ * { box-sizing: border-box; }
21
+ body {
22
+ margin: 0; padding: 2.5rem 1.5rem 4rem;
23
+ background: #0b0f14; color: #d7e0ea;
24
+ font-family: -apple-system, Segoe UI, Helvetica, Arial, sans-serif;
25
+ line-height: 1.55;
26
+ }
27
+ main { max-width: 720px; margin: 0 auto; }
28
+ header { display: flex; align-items: center; gap: 1rem; margin-bottom: 0.5rem; }
29
+ .dragon { font-size: 3rem; line-height: 1; }
30
+ h1 { margin: 0; font-size: 1.6rem; color: #eef4fa; }
31
+ .subtitle { margin: 0.15rem 0 0; color: #7fe0d1; font-size: 0.95rem; }
32
+ .card {
33
+ background: #121924; border: 1px solid #212c3a; border-radius: 10px;
34
+ padding: 1rem 1.25rem; margin: 1.75rem 0;
35
+ }
36
+ .card h2 { margin-top: 0; font-size: 1.05rem; color: #7fe0d1; }
37
+ .kv { display: grid; grid-template-columns: auto 1fr; gap: 0.35rem 1rem; font-size: 0.92rem; }
38
+ .kv dt { color: #8a97a8; }
39
+ .kv dd { margin: 0; font-family: ui-monospace, Consolas, monospace; word-break: break-all; }
40
+ pre {
41
+ background: #0d1420; border: 1px solid #1c2634; border-radius: 8px;
42
+ padding: 0.9rem 1rem; overflow-x: auto; font-size: 0.85rem;
43
+ color: #c7e8ff;
44
+ }
45
+ code { font-family: ui-monospace, Consolas, monospace; }
46
+ a { color: #67c9ff; }
47
+ footer { margin-top: 2.5rem; color: #62707f; font-size: 0.85rem; }
48
+ </style>
49
+ </head>
50
+ <body>
51
+ <main>
52
+ <header>
53
+ <span class="dragon">\U0001F409</span>
54
+ <div>
55
+ <h1>Dragon CLI</h1>
56
+ <p class="subtitle">local model server — OpenAI-compatible API</p>
57
+ </div>
58
+ </header>
59
+
60
+ <div class="card">
61
+ <dl class="kv">
62
+ <dt>Model</dt><dd>__MODEL_NAME__</dd>
63
+ <dt>Base URL</dt><dd>__BASE_URL__</dd>
64
+ </dl>
65
+ </div>
66
+
67
+ <div class="card">
68
+ <h2>Interactive API docs</h2>
69
+ <p><a href="/docs">Swagger UI</a> &middot; <a href="/redoc">ReDoc</a> &middot;
70
+ <a href="/openapi.json">OpenAPI schema</a></p>
71
+ </div>
72
+
73
+ <div class="card">
74
+ <h2>List models</h2>
75
+ <pre><code>curl __BASE_URL__/v1/models</code></pre>
76
+ </div>
77
+
78
+ <div class="card">
79
+ <h2>Chat completion</h2>
80
+ <pre><code>curl __BASE_URL__/v1/chat/completions \\
81
+ -H "Content-Type: application/json" \\
82
+ -d '{"messages": [{"role": "user", "content": "Hello!"}]}'</code></pre>
83
+ <p>Add <code>"stream": true</code> to the JSON body for server-sent-events streaming.</p>
84
+ </div>
85
+
86
+ <div class="card">
87
+ <h2>Python (OpenAI SDK)</h2>
88
+ <pre><code>from openai import OpenAI
89
+
90
+ client = OpenAI(base_url="__BASE_URL_SLASH_V1__", api_key="not-needed")
91
+ resp = client.chat.completions.create(
92
+ model="__MODEL_NAME__",
93
+ messages=[{"role": "user", "content": "Hello!"}],
94
+ )
95
+ print(resp.choices[0].message.content)</code></pre>
96
+ </div>
97
+
98
+ <footer>No MCP tool-calling on this endpoint — model only. Started with <code>dragon-cli --serve</code>.</footer>
99
+ </main>
100
+ </body>
101
+ </html>
102
+ """
103
+
104
+
105
+ def render_index(model_name: str, base_url: str) -> str:
106
+ base = base_url.rstrip("/")
107
+ return (
108
+ _INDEX_TEMPLATE
109
+ .replace("__MODEL_NAME__", model_name)
110
+ .replace("__BASE_URL_SLASH_V1__", f"{base}/v1")
111
+ .replace("__BASE_URL__", base)
112
+ )
@@ -0,0 +1,147 @@
1
+ Metadata-Version: 2.4
2
+ Name: dragon-cli
3
+ Version: 0.2.0
4
+ Summary: CLI to chat with local GGUF LLMs, with MCP tool-calling and SQLite conversation history.
5
+ Author: JPEDROPS092
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://jpedrops092.github.io/dragon-cli-ai-local/
8
+ Project-URL: Repository, https://github.com/JPEDROPS092/dragon-cli-ai-local
9
+ Project-URL: Issues, https://github.com/JPEDROPS092/dragon-cli-ai-local/issues
10
+ Requires-Python: >=3.10
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: rich
14
+ Requires-Dist: huggingface_hub
15
+ Requires-Dist: psutil
16
+ Requires-Dist: mcp
17
+ Requires-Dist: fastapi
18
+ Requires-Dist: starlette
19
+ Requires-Dist: uvicorn
20
+ Requires-Dist: fastapi
21
+ Provides-Extra: llm
22
+ Requires-Dist: llama-cpp-python; extra == "llm"
23
+ Dynamic: license-file
24
+
25
+ # Dragon CLI
26
+
27
+ ![1785346956957](image/README/1785346956957.png)
28
+
29
+ [![License: MIT](https://img.shields.io/github/license/JPEDROPS092/dragon-cli-ai-local)](LICENSE)
30
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue)](https://www.python.org/)
31
+ [![Docs](<https://img.shields.io/badge/docs-GitHub%20Pages-7fe0d1>)](https://jpedrops092.github.io/dragon-cli-ai-local/)
32
+ [![GitHub stars](https://img.shields.io/github/stars/JPEDROPS092/dragon-cli-ai-local?style=social)](https://github.com/JPEDROPS092/dragon-cli-ai-local/stargazers)
33
+ [![GitHub issues](https://img.shields.io/github/issues/JPEDROPS092/dragon-cli-ai-local)](https://github.com/JPEDROPS092/dragon-cli-ai-local/issues)
34
+
35
+ Chat with local GGUF LLMs (100% CPU, via `llama-cpp-python`), with optional
36
+ MCP (Model Context Protocol) tool-calling and a SQLite-backed conversation
37
+ history.
38
+
39
+ ## Install
40
+
41
+ ```bash
42
+ pip install -e .
43
+ dragon-cli-install-llm
44
+ ```
45
+
46
+ This exposes a `dragon-cli` command. `llama-cpp-python` is intentionally kept
47
+ out of the core dependencies, since it ships as C++ source on PyPI for many
48
+ platforms — a plain `pip install` would try to compile it locally and fail
49
+ without a C/C++ toolchain (Visual Studio Build Tools on Windows, gcc on
50
+ Linux, Xcode command line tools on macOS).
51
+
52
+ `dragon-cli-install-llm` detects your OS/architecture (Windows, Linux, or
53
+ macOS) and installs a prebuilt CPU wheel instead, so a compiler usually isn't
54
+ needed at all. If no prebuilt wheel is available for your exact
55
+ platform/Python combination, it prints the toolchain to install and the
56
+ fallback command to run.
57
+
58
+ If you already have a working C/C++ toolchain and would rather build from
59
+ source yourself, skip the installer and use the extra instead:
60
+ `pip install -e ".[llm]"`.
61
+
62
+ You can also run it without installing:
63
+
64
+ ```bash
65
+ python dragon_cli.py
66
+ # or
67
+ python -m dragon_cli
68
+ ```
69
+
70
+ ## Usage
71
+
72
+ ```bash
73
+ dragon-cli # model menu + chat
74
+ dragon-cli --model 2 # skip the model menu
75
+ dragon-cli --list # just list the model catalog
76
+ dragon-cli --no-mcp # disable MCP connectors
77
+ dragon-cli --resume 3 # resume saved conversation #3
78
+ dragon-cli --list-conversations # list saved conversations and exit
79
+ dragon-cli --db path/to/other.db # use a different conversation database
80
+ dragon-cli --serve # serve the model over HTTP instead of chatting
81
+ dragon-cli --serve --port 8080 # custom port (default: 8000)
82
+ dragon-cli --serve --host 0.0.0.0 # expose beyond localhost (e.g. to your LAN) — opt-in only
83
+ ```
84
+
85
+ ## HTTP server (`--serve`)
86
+
87
+ `dragon-cli --serve` loads a model and exposes it over a minimal
88
+ OpenAI-compatible HTTP API instead of starting the interactive chat. It's
89
+ built on FastAPI, so interactive docs come for free:
90
+
91
+ - `GET /` — landing page with usage examples
92
+ - `GET /docs` — Swagger UI
93
+ - `GET /redoc` — ReDoc
94
+ - `GET /openapi.json` — raw OpenAPI schema
95
+ - `GET /v1/models` — lists the currently loaded model
96
+ - `POST /v1/chat/completions` — same shape as OpenAI's endpoint, streaming
97
+ (`"stream": true`, server-sent events) and non-streaming both supported,
98
+ request body validated (422 with a clear error on malformed input)
99
+
100
+ Point any OpenAI-compatible client/SDK at `http://127.0.0.1:8000/v1` (adjust
101
+ host/port to match `--host`/`--port`) and it works without extra glue code.
102
+ This mode does **not** expose MCP tool-calling — it's the model only,
103
+ kept intentionally simple and predictable. It also binds to `127.0.0.1` by
104
+ default (only reachable from your own machine); pass `--host 0.0.0.0`
105
+ explicitly if you want it reachable from other devices on your network.
106
+
107
+ ## In-chat commands
108
+
109
+ | Command | Description |
110
+ | -------------------------- | ----------------------------------- |
111
+ | `/help` | show available commands |
112
+ | `/models` | list the model catalog |
113
+ | `/tools` | list connected MCP tools |
114
+ | `/mcp` | show MCP server connection status |
115
+ | `/clear` | clear the current in-memory history |
116
+ | `/new [title]` | start a new conversation |
117
+ | `/list` | list saved conversations |
118
+ | `/resume <id>` | resume a saved conversation |
119
+ | `/rename <id> <title>` | rename a saved conversation |
120
+ | `/delete <id>` | delete a saved conversation |
121
+ | `/search <text>` | search saved messages for text |
122
+ | `/export <id> [md\|json]` | export a conversation to a file |
123
+ | `/exit`, `/quit` | end the program |
124
+
125
+ Every message is autosaved to SQLite (`dragon_cli.db` by default) as soon as
126
+ it's sent, so nothing is lost if the process crashes.
127
+
128
+ ## MCP connectors
129
+
130
+ On first run, an example `mcp_servers.json` is created in the working
131
+ directory. Edit it to point at your MCP servers and restart to connect.
132
+
133
+ ## Project layout
134
+
135
+ ```
136
+ src/dragon_cli/
137
+ ├── cli.py # argparse entry point
138
+ ├── app.py # model selection + orchestration
139
+ ├── setup_llm.py # cross-platform llama-cpp-python installer
140
+ ├── server.py # OpenAI-compatible HTTP server (--serve)
141
+ ├── banner.py # ASCII banner
142
+ ├── config.py # shared paths/colors
143
+ ├── models/ # model catalog, download, loading
144
+ ├── mcp/ # MCP server manager + tool-call parsing
145
+ ├── storage/ # SQLite conversation store
146
+ └── chat/ # chat session, streaming, slash commands
147
+ ```
@@ -0,0 +1,34 @@
1
+ dragon_cli/__init__.py,sha256=tSPFdYJBXGD8urB1CBZJO6WSwA-itm6EMxXp-oUf90o,87
2
+ dragon_cli/__main__.py,sha256=MSmt_5Xg84uHqzTN38JwgseJK8rsJn_11A8WD99VtEo,61
3
+ dragon_cli/_cuda.py,sha256=q2EDpKtIE1WCs1NwKqWTWzLHhw-Kc7GGCJ4A9vdX1A8,1343
4
+ dragon_cli/app.py,sha256=2xmllRuySzep1MkExQaIWhaZFlU2WXhaT6fR7j3eUbY,4232
5
+ dragon_cli/banner.py,sha256=uKDhnNTkuNZVSnmafHaMbVS0urmK6oPIpeole52me4k,3968
6
+ dragon_cli/cli.py,sha256=MenxJaLekcbBnuXOp7Rj4nT6kP-OYgGXnDj9JBmW93I,6430
7
+ dragon_cli/config.py,sha256=-4QYyPh2WOcaOyJf6XLXGsuG5KzrUeD5mcT4VygInrg,700
8
+ dragon_cli/hardware.py,sha256=_buxSPWLc38bOPzsxR8tiOYZTslOoj6QaAZ2cUPcmzs,13054
9
+ dragon_cli/menu.py,sha256=2R23yLYeYiNaPKiBzdya8pXlX_oghkNXEJ-jFO00_kM,47271
10
+ dragon_cli/model_hub.py,sha256=0NRR0wXJH2d9-YpAHctNEuR8cIdEUYcxEd-c5Z1LlEg,8171
11
+ dragon_cli/recommendation.py,sha256=9bhw2lV_LGY0dk1UNUr4Jh1UID7HvWJMorNZSTiEc2M,10845
12
+ dragon_cli/runtime.py,sha256=ZdmM5eZrPeMeydkZaAZFwWw2omlS3aG5iEJSZJ6Uw1o,8395
13
+ dragon_cli/server.py,sha256=mp235Ehwhbn2uv5leMSZR2a6UZ85-gWFyiwU_lenzVM,5666
14
+ dragon_cli/setup_llm.py,sha256=LVj-XFsYEW_Jje5bj2aPWDMXX8mJTI1LrtLU8Zb5FsA,6645
15
+ dragon_cli/web.py,sha256=KG4Ji89ex2aK_BAMettZfjKfxdXgsYyVeeno0niD_8s,3730
16
+ dragon_cli/chat/__init__.py,sha256=0gvbs-QAH1xwE_GXz-dEBzI-UFjgkL60PsfrvHkGHSk,148
17
+ dragon_cli/chat/commands.py,sha256=ec_V0HIgXnqsYGmjX7MYOqIsA8u72xDO1FplUe-NnWA,5753
18
+ dragon_cli/chat/session.py,sha256=HSiqPTl7fEKWyPtw6uP7A381Izt_ZhIkrIPcmgkcUCg,3840
19
+ dragon_cli/chat/streaming.py,sha256=P-sJ1VmRCirxpKX8crIQdtVyy_06psdqgQM9sQVmXl4,555
20
+ dragon_cli/mcp/__init__.py,sha256=kWwXepBx7phIfRl9qfm5TB2L6mhnwWjCVoLimy5KMdU,243
21
+ dragon_cli/mcp/manager.py,sha256=s-L037Go6VbHGdOW65wCZtyWYfoYH3luaswc1VYcjGc,6041
22
+ dragon_cli/mcp/tool_call.py,sha256=nCvkUvMo3-wJXO628UZEMtzkjt65wDwYioWAEtg9agc,433
23
+ dragon_cli/models/__init__.py,sha256=HtQADqyoNjL9dbk0mGoMu3AnLGoZIWYfFdwgqP5g7mE,280
24
+ dragon_cli/models/catalog.py,sha256=F5Jn2BdK1C2LAAtQcEpR1m3_2NpxBqms2Rb4cZ--Lv8,7701
25
+ dragon_cli/models/loader.py,sha256=KybLGUDKMEHwrNHgRSB9kN2rt3FzIdVSQuSLwI3tBDQ,6951
26
+ dragon_cli/storage/__init__.py,sha256=11JtsPqCen-9xkUKXUxeVqDtAYwUhXiTmNrDMfyVLWc,128
27
+ dragon_cli/storage/conversations.py,sha256=5aGfrzFYQlAtT8EiaOgMw2La_OQfR909fgoibzBwulc,4671
28
+ dragon_cli/storage/database.py,sha256=7Mc74FFDsYveOj3P0ZMT4sNfMH_hb93FFeSd-S_HXP8,932
29
+ dragon_cli-0.2.0.dist-info/licenses/LICENSE,sha256=6Fd0LBu1a_4jvyuo71J5dwl16ToLI1nYBckfIogXjcE,1068
30
+ dragon_cli-0.2.0.dist-info/METADATA,sha256=8wOg1XeMEJtjlET8nLuns_54OHtvqLWXNfpAao3cEeY,6409
31
+ dragon_cli-0.2.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
32
+ dragon_cli-0.2.0.dist-info/entry_points.txt,sha256=oDjJQZyVKWMIp8mCw08qcIlJSPTpQhyIWvmgjfk0ktQ,102
33
+ dragon_cli-0.2.0.dist-info/top_level.txt,sha256=_6Q26X07zb3vGpQJ4xnpjUdWI4h2FEN7o9nTBvUDz3Q,11
34
+ dragon_cli-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ dragon-cli = dragon_cli.cli:main
3
+ dragon-cli-install-llm = dragon_cli.setup_llm:main