dragon-cli 0.2.0__tar.gz

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.
Files changed (39) hide show
  1. dragon_cli-0.2.0/LICENSE +21 -0
  2. dragon_cli-0.2.0/PKG-INFO +147 -0
  3. dragon_cli-0.2.0/README.md +123 -0
  4. dragon_cli-0.2.0/pyproject.toml +42 -0
  5. dragon_cli-0.2.0/setup.cfg +4 -0
  6. dragon_cli-0.2.0/src/dragon_cli/__init__.py +5 -0
  7. dragon_cli-0.2.0/src/dragon_cli/__main__.py +4 -0
  8. dragon_cli-0.2.0/src/dragon_cli/_cuda.py +42 -0
  9. dragon_cli-0.2.0/src/dragon_cli/app.py +123 -0
  10. dragon_cli-0.2.0/src/dragon_cli/banner.py +96 -0
  11. dragon_cli-0.2.0/src/dragon_cli/chat/__init__.py +4 -0
  12. dragon_cli-0.2.0/src/dragon_cli/chat/commands.py +175 -0
  13. dragon_cli-0.2.0/src/dragon_cli/chat/session.py +97 -0
  14. dragon_cli-0.2.0/src/dragon_cli/chat/streaming.py +18 -0
  15. dragon_cli-0.2.0/src/dragon_cli/cli.py +147 -0
  16. dragon_cli-0.2.0/src/dragon_cli/config.py +28 -0
  17. dragon_cli-0.2.0/src/dragon_cli/hardware.py +361 -0
  18. dragon_cli-0.2.0/src/dragon_cli/mcp/__init__.py +10 -0
  19. dragon_cli-0.2.0/src/dragon_cli/mcp/manager.py +148 -0
  20. dragon_cli-0.2.0/src/dragon_cli/mcp/tool_call.py +17 -0
  21. dragon_cli-0.2.0/src/dragon_cli/menu.py +1140 -0
  22. dragon_cli-0.2.0/src/dragon_cli/model_hub.py +252 -0
  23. dragon_cli-0.2.0/src/dragon_cli/models/__init__.py +11 -0
  24. dragon_cli-0.2.0/src/dragon_cli/models/catalog.py +205 -0
  25. dragon_cli-0.2.0/src/dragon_cli/models/loader.py +187 -0
  26. dragon_cli-0.2.0/src/dragon_cli/recommendation.py +295 -0
  27. dragon_cli-0.2.0/src/dragon_cli/runtime.py +252 -0
  28. dragon_cli-0.2.0/src/dragon_cli/server.py +144 -0
  29. dragon_cli-0.2.0/src/dragon_cli/setup_llm.py +197 -0
  30. dragon_cli-0.2.0/src/dragon_cli/storage/__init__.py +3 -0
  31. dragon_cli-0.2.0/src/dragon_cli/storage/conversations.py +134 -0
  32. dragon_cli-0.2.0/src/dragon_cli/storage/database.py +32 -0
  33. dragon_cli-0.2.0/src/dragon_cli/web.py +112 -0
  34. dragon_cli-0.2.0/src/dragon_cli.egg-info/PKG-INFO +147 -0
  35. dragon_cli-0.2.0/src/dragon_cli.egg-info/SOURCES.txt +37 -0
  36. dragon_cli-0.2.0/src/dragon_cli.egg-info/dependency_links.txt +1 -0
  37. dragon_cli-0.2.0/src/dragon_cli.egg-info/entry_points.txt +3 -0
  38. dragon_cli-0.2.0/src/dragon_cli.egg-info/requires.txt +11 -0
  39. dragon_cli-0.2.0/src/dragon_cli.egg-info/top_level.txt +1 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 JPEDROPS092
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -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,123 @@
1
+ # Dragon CLI
2
+
3
+ ![1785346956957](image/README/1785346956957.png)
4
+
5
+ [![License: MIT](https://img.shields.io/github/license/JPEDROPS092/dragon-cli-ai-local)](LICENSE)
6
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue)](https://www.python.org/)
7
+ [![Docs](<https://img.shields.io/badge/docs-GitHub%20Pages-7fe0d1>)](https://jpedrops092.github.io/dragon-cli-ai-local/)
8
+ [![GitHub stars](https://img.shields.io/github/stars/JPEDROPS092/dragon-cli-ai-local?style=social)](https://github.com/JPEDROPS092/dragon-cli-ai-local/stargazers)
9
+ [![GitHub issues](https://img.shields.io/github/issues/JPEDROPS092/dragon-cli-ai-local)](https://github.com/JPEDROPS092/dragon-cli-ai-local/issues)
10
+
11
+ Chat with local GGUF LLMs (100% CPU, via `llama-cpp-python`), with optional
12
+ MCP (Model Context Protocol) tool-calling and a SQLite-backed conversation
13
+ history.
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ pip install -e .
19
+ dragon-cli-install-llm
20
+ ```
21
+
22
+ This exposes a `dragon-cli` command. `llama-cpp-python` is intentionally kept
23
+ out of the core dependencies, since it ships as C++ source on PyPI for many
24
+ platforms — a plain `pip install` would try to compile it locally and fail
25
+ without a C/C++ toolchain (Visual Studio Build Tools on Windows, gcc on
26
+ Linux, Xcode command line tools on macOS).
27
+
28
+ `dragon-cli-install-llm` detects your OS/architecture (Windows, Linux, or
29
+ macOS) and installs a prebuilt CPU wheel instead, so a compiler usually isn't
30
+ needed at all. If no prebuilt wheel is available for your exact
31
+ platform/Python combination, it prints the toolchain to install and the
32
+ fallback command to run.
33
+
34
+ If you already have a working C/C++ toolchain and would rather build from
35
+ source yourself, skip the installer and use the extra instead:
36
+ `pip install -e ".[llm]"`.
37
+
38
+ You can also run it without installing:
39
+
40
+ ```bash
41
+ python dragon_cli.py
42
+ # or
43
+ python -m dragon_cli
44
+ ```
45
+
46
+ ## Usage
47
+
48
+ ```bash
49
+ dragon-cli # model menu + chat
50
+ dragon-cli --model 2 # skip the model menu
51
+ dragon-cli --list # just list the model catalog
52
+ dragon-cli --no-mcp # disable MCP connectors
53
+ dragon-cli --resume 3 # resume saved conversation #3
54
+ dragon-cli --list-conversations # list saved conversations and exit
55
+ dragon-cli --db path/to/other.db # use a different conversation database
56
+ dragon-cli --serve # serve the model over HTTP instead of chatting
57
+ dragon-cli --serve --port 8080 # custom port (default: 8000)
58
+ dragon-cli --serve --host 0.0.0.0 # expose beyond localhost (e.g. to your LAN) — opt-in only
59
+ ```
60
+
61
+ ## HTTP server (`--serve`)
62
+
63
+ `dragon-cli --serve` loads a model and exposes it over a minimal
64
+ OpenAI-compatible HTTP API instead of starting the interactive chat. It's
65
+ built on FastAPI, so interactive docs come for free:
66
+
67
+ - `GET /` — landing page with usage examples
68
+ - `GET /docs` — Swagger UI
69
+ - `GET /redoc` — ReDoc
70
+ - `GET /openapi.json` — raw OpenAPI schema
71
+ - `GET /v1/models` — lists the currently loaded model
72
+ - `POST /v1/chat/completions` — same shape as OpenAI's endpoint, streaming
73
+ (`"stream": true`, server-sent events) and non-streaming both supported,
74
+ request body validated (422 with a clear error on malformed input)
75
+
76
+ Point any OpenAI-compatible client/SDK at `http://127.0.0.1:8000/v1` (adjust
77
+ host/port to match `--host`/`--port`) and it works without extra glue code.
78
+ This mode does **not** expose MCP tool-calling — it's the model only,
79
+ kept intentionally simple and predictable. It also binds to `127.0.0.1` by
80
+ default (only reachable from your own machine); pass `--host 0.0.0.0`
81
+ explicitly if you want it reachable from other devices on your network.
82
+
83
+ ## In-chat commands
84
+
85
+ | Command | Description |
86
+ | -------------------------- | ----------------------------------- |
87
+ | `/help` | show available commands |
88
+ | `/models` | list the model catalog |
89
+ | `/tools` | list connected MCP tools |
90
+ | `/mcp` | show MCP server connection status |
91
+ | `/clear` | clear the current in-memory history |
92
+ | `/new [title]` | start a new conversation |
93
+ | `/list` | list saved conversations |
94
+ | `/resume <id>` | resume a saved conversation |
95
+ | `/rename <id> <title>` | rename a saved conversation |
96
+ | `/delete <id>` | delete a saved conversation |
97
+ | `/search <text>` | search saved messages for text |
98
+ | `/export <id> [md\|json]` | export a conversation to a file |
99
+ | `/exit`, `/quit` | end the program |
100
+
101
+ Every message is autosaved to SQLite (`dragon_cli.db` by default) as soon as
102
+ it's sent, so nothing is lost if the process crashes.
103
+
104
+ ## MCP connectors
105
+
106
+ On first run, an example `mcp_servers.json` is created in the working
107
+ directory. Edit it to point at your MCP servers and restart to connect.
108
+
109
+ ## Project layout
110
+
111
+ ```
112
+ src/dragon_cli/
113
+ ├── cli.py # argparse entry point
114
+ ├── app.py # model selection + orchestration
115
+ ├── setup_llm.py # cross-platform llama-cpp-python installer
116
+ ├── server.py # OpenAI-compatible HTTP server (--serve)
117
+ ├── banner.py # ASCII banner
118
+ ├── config.py # shared paths/colors
119
+ ├── models/ # model catalog, download, loading
120
+ ├── mcp/ # MCP server manager + tool-call parsing
121
+ ├── storage/ # SQLite conversation store
122
+ └── chat/ # chat session, streaming, slash commands
123
+ ```
@@ -0,0 +1,42 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "dragon-cli"
7
+ version = "0.2.0"
8
+ description = "CLI to chat with local GGUF LLMs, with MCP tool-calling and SQLite conversation history."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ authors = [{ name = "JPEDROPS092" }]
13
+ dependencies = [
14
+ "rich",
15
+ "huggingface_hub",
16
+ "psutil",
17
+ "mcp",
18
+ "fastapi",
19
+ "starlette",
20
+ "uvicorn",
21
+ "fastapi"
22
+ ]
23
+
24
+ [project.optional-dependencies]
25
+ # llama-cpp-python ships as C++ source on PyPI for many platforms, so pulling
26
+ # it in as a hard dependency makes `pip install -e .` try to compile it and
27
+ # fail without a C/C++ toolchain. Install it separately with
28
+ # `dragon-cli-install-llm` (auto-detects OS/GPU, uses optimal prebuilt wheels),
29
+ # or via this extra if you already have a working compiler: pip install -e ".[llm]"
30
+ llm = ["llama-cpp-python"]
31
+
32
+ [project.urls]
33
+ Homepage = "https://jpedrops092.github.io/dragon-cli-ai-local/"
34
+ Repository = "https://github.com/JPEDROPS092/dragon-cli-ai-local"
35
+ Issues = "https://github.com/JPEDROPS092/dragon-cli-ai-local/issues"
36
+
37
+ [project.scripts]
38
+ dragon-cli = "dragon_cli.cli:main"
39
+ dragon-cli-install-llm = "dragon_cli.setup_llm:main"
40
+
41
+ [tool.setuptools.packages.find]
42
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,5 @@
1
+ __version__ = "0.2.0"
2
+
3
+ from ._cuda import preload_cuda_runtime
4
+
5
+ preload_cuda_runtime()
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -0,0 +1,42 @@
1
+ """Preload CUDA runtime shared libraries shipped via pip wheels.
2
+
3
+ The GPU-enabled llama-cpp-python wheel links against libcudart/libcublas/
4
+ libnvrtc, which the pip ``nvidia-*-cu12`` packages install under
5
+ ``site-packages/nvidia/*/lib`` (not on the system library path). Loading them
6
+ here by absolute path with ``RTLD_GLOBAL`` lets libllama.so resolve them
7
+ without LD_LIBRARY_PATH. No-op when the libs are already resolvable or absent
8
+ (e.g. the CPU-only wheel).
9
+
10
+ Kept dependency-free (stdlib only) so it can run at package import time
11
+ without risking circular imports.
12
+ """
13
+
14
+ import ctypes
15
+ import glob
16
+ import os
17
+ import sys
18
+
19
+ _CUDA_SONAMES = (
20
+ "libcudart.so.12",
21
+ "libcublas.so.12",
22
+ "libcublasLt.so.12",
23
+ "libnvrtc.so.12",
24
+ )
25
+
26
+
27
+ def preload_cuda_runtime() -> None:
28
+ loaded: set[str] = set()
29
+ for entry in sys.path:
30
+ nvidia_dir = os.path.join(entry, "nvidia")
31
+ if not entry or not os.path.isdir(nvidia_dir):
32
+ continue
33
+ for soname in _CUDA_SONAMES:
34
+ if soname in loaded:
35
+ continue
36
+ for match in glob.glob(os.path.join(nvidia_dir, "*", "lib", soname)):
37
+ try:
38
+ ctypes.CDLL(match, mode=ctypes.RTLD_GLOBAL)
39
+ loaded.add(soname)
40
+ break
41
+ except OSError:
42
+ continue
@@ -0,0 +1,123 @@
1
+ """Async orchestration: model selection, MCP startup, and the chat session."""
2
+
3
+ from pathlib import Path
4
+
5
+ from .chat import ChatSession, chat_loop
6
+ from .config import C_RED, C_RESET
7
+ from .mcp import MCPManager
8
+ from .models import MODEL_CATALOG, available_ram_gb, download_model, list_catalog, load_llm
9
+ from .storage import ConversationStore
10
+
11
+
12
+ def _gpu_layers_arg(args) -> int | None:
13
+ """Translate ``--n-gpu-layers`` into a ``load_llm`` override.
14
+
15
+ The CLI default is -1 (auto). Any value >= 0 is an explicit override
16
+ (0 = CPU only, >0 = exact layer count). None means "let the runtime
17
+ auto-decide".
18
+ """
19
+ n = getattr(args, "n_gpu_layers", -1)
20
+ return n if n is not None and n >= 0 else None
21
+
22
+
23
+ def choose_model(args) -> dict | None:
24
+ # Handle --auto flag for automatic model selection
25
+ if getattr(args, 'auto', False):
26
+ from .recommendation import ModelRecommender
27
+ recommender = ModelRecommender()
28
+ best_model = recommender.recommend_best(priority=getattr(args, 'priority', 'balanced'))
29
+
30
+ if best_model:
31
+ print(f"\n{C_RESET}Auto-selected model based on your hardware:")
32
+ print(f" → {best_model['name']}")
33
+ reason = recommender.get_recommendation_reason(best_model)
34
+ print(f" {C_DIM}Why: {reason}{C_RESET}\n")
35
+ return best_model
36
+ else:
37
+ print(f"{C_RED}[error]{C_RESET} No compatible models found for your hardware.")
38
+ # Fall back to manual selection
39
+ pass
40
+
41
+ # Skip the plain catalog print when a model was already chosen upstream
42
+ # (e.g. by the rich interactive menu), so the user doesn't see it twice.
43
+ if not args.model:
44
+ list_catalog()
45
+ if args.model:
46
+ choice = args.model
47
+ else:
48
+ try:
49
+ choice = int(input("\nChoose a model number: ").strip())
50
+ except (ValueError, EOFError, KeyboardInterrupt):
51
+ print("Invalid choice.")
52
+ return None
53
+ if not (1 <= choice <= len(MODEL_CATALOG)):
54
+ print("Number not in the catalog.")
55
+ return None
56
+ model = MODEL_CATALOG[choice - 1]
57
+ ram = available_ram_gb()
58
+ if 0 < ram < model["ram_min_gb"]:
59
+ ok = input(f"{C_RED}[warning]{C_RESET} Model needs ~{model['ram_min_gb']} GB, "
60
+ f"you have {ram:.1f} GB free. Continue? (y/N) ").strip().lower()
61
+ if ok != "y":
62
+ return None
63
+ return model
64
+
65
+
66
+ def print_conversations(store: ConversationStore) -> None:
67
+ conversations = store.list()
68
+ if not conversations:
69
+ print("No conversations yet.")
70
+ return
71
+ for c in conversations:
72
+ print(f" #{c.id} {c.title} ({c.message_count} msgs, updated {c.updated_at})")
73
+
74
+
75
+ async def run(args) -> None:
76
+ if args.serve:
77
+ model = choose_model(args)
78
+ if not model:
79
+ return
80
+ path = download_model(model)
81
+ llm = load_llm(path, model["ctx"], model_info=model,
82
+ gpu_layers=_gpu_layers_arg(args),
83
+ force_cpu=getattr(args, "cpu", False),
84
+ force_gpu=getattr(args, "gpu", False))
85
+ from .server import run_server
86
+ await run_server(llm, model["name"], args.host, args.port)
87
+ return
88
+
89
+ store = ConversationStore(Path(args.db))
90
+
91
+ if args.list_conversations:
92
+ print_conversations(store)
93
+ store.close()
94
+ return
95
+
96
+ model = choose_model(args)
97
+ if not model:
98
+ store.close()
99
+ return
100
+
101
+ path = download_model(model)
102
+ llm = load_llm(path, model["ctx"], model_info=model,
103
+ gpu_layers=_gpu_layers_arg(args),
104
+ force_cpu=getattr(args, "cpu", False),
105
+ force_gpu=getattr(args, "gpu", False))
106
+
107
+ mcp = MCPManager()
108
+ if not args.no_mcp:
109
+ await mcp.start()
110
+
111
+ session = ChatSession(llm, mcp, store, model["name"])
112
+ if args.resume is not None:
113
+ if not session.resume(args.resume):
114
+ print(f"{C_RED}[error]{C_RESET} No conversation with id {args.resume}, starting a new one.")
115
+ session.start_new()
116
+ else:
117
+ session.start_new()
118
+
119
+ try:
120
+ await chat_loop(session)
121
+ finally:
122
+ await mcp.stop()
123
+ store.close()
@@ -0,0 +1,96 @@
1
+ """ASCII dragon banner shown at startup."""
2
+
3
+ BANNER = r'''
4
+ _,_
5
+ /7/Y/^\
6
+ vuVV|C)| __ _
7
+ \|^ / .' Y '>,
8
+ )| \) / _ _ \
9
+ //)|\\ )(_) (_)(|}
10
+ / ^| \ \ { 4A } /
11
+ //^| || \\ \uLuJJ/\l
12
+ >//||| |\\\| |3 p)/
13
+ | """"" 7/>l__ _____ ____ /nnm_n//
14
+ L>_ _-< 7/|_-__,__-)\,__)(". \_>-<_/D
15
+ )D" Y "c) 9) //V \_"-._.__G<D __
16
+ \ || . _/ ( \ "-._"> G<D-_.-"_=)_
17
+ ) __ / / " _/.__ />_c__.-__<"/ ( \
18
+ | | | |(| < "-._"> _.G_.___)\ \7\
19
+ \"=" // | (,"-.__.|\ \<.__.-" ) \ \
20
+ '---' | |,"-.__"| \!"-.__.-".\ \ \
21
+ |_;._/ (_"-.__"'\ \"-.__.-".| \_\
22
+ )(" V \"-.__"'|\ \-.__.-".) \ \
23
+ ( "-.__'"\_\ \.__.-"./ \ l
24
+ ) ".__"">>G\ \__.-"> V )
25
+ AI PROVIDERS "" G<\ \_.-" ./7
26
+ G<\ \ ///
27
+ ___ G<D\ \ /7^
28
+ /".-_"-"G"<\ \__ ///))
29
+ ||,-_ " '.\_"- /// (\
30
+ | (_) _ "/ __7 |X/ V
31
+ /\__""-/ "\ _""c_"GBV
32
+ _)( ""' "--.c__>D'
33
+ / / )E_>"
34
+ _/ ( | \()
35
+ / \ / | |
36
+ /\\| | |
37
+ / '(( | |
38
+ / / )\ \ |
39
+ / y \y |Y |
40
+ / / ( | |
41
+ L ." | | /
42
+ | \( | |
43
+ \_| | |
44
+ | \ { "|
45
+ | || | |
46
+ |x|| \_,/
47
+ } || / \'
48
+ | || |_/
49
+ | (| | }\
50
+ | || } ||
51
+ | || | ||
52
+ | || |\||
53
+ / || | ||
54
+ | || ( |!
55
+ | |/ ) ||
56
+ _/ \ | }|
57
+ _.-"_ ( )| jjs ! ||
58
+ c_"-_-"_ ) | ||
59
+ c,-_-'_--"" { ||
60
+ "C(_/" \ /|
61
+ (! )
62
+ /| \
63
+ / |(
64
+ /7||\\
65
+ ()U cUu"
66
+ '''
67
+
68
+ SUBTITLE = " DRAGON CLI — local LLM + MCP | /help for commands"
69
+
70
+ # Compact mascot icon shown as a splash on the interactive menu. Raw string so
71
+ # the backslashes in the art are preserved verbatim.
72
+ ICON = r""",--.
73
+ { }
74
+ K, }
75
+ / `Y`
76
+ _ / /
77
+ {_'-K.__/
78
+ `/-.__L._
79
+ / ' /`\_}
80
+ / ' / -AI PROVIDERS-
81
+ ____ / ' /
82
+ ,-'~~~~ ~~/ ' /_
83
+ ,' ``~~~%%',
84
+ ( % Y
85
+ { %% I
86
+ { - % `.
87
+ | ', % )
88
+ | | ,..__ __. Y
89
+ | .,_./ Y ' / ^Y J )|
90
+ \ |' / | | ||
91
+ \ L_/ . _ (_,.'(
92
+ \, , ^^""' / | )
93
+ \_ \ /,L] /
94
+ '-_`-, ` ` ./`
95
+ `-(_ )
96
+ ^^\..___,.--`"""
@@ -0,0 +1,4 @@
1
+ from .session import ChatSession, chat_loop
2
+ from .streaming import generate_streaming
3
+
4
+ __all__ = ["ChatSession", "chat_loop", "generate_streaming"]