Instructions to use jpanasuk/basecamp with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- HERMES
How to use jpanasuk/basecamp with HERMES:
# No code snippets available yet for this library. # To use this model, check the repository files and the library's documentation. # Want to help? PRs adding snippets are welcome at: # https://github.com/huggingface/huggingface.js
- Notebooks
- Google Colab
- Kaggle
v1.0.3: expanded discovery (30+ service probes: koboldcpp, lmstudio, sglang, librechat, dify, flowise, n8n, qdrant, milvus, comfyui, whisper, openrouter...), tavern wire wiring audit, stack skill injection into container hermes
Browse files- basecamp.sh +1 -0
- discover.py +507 -6
- entrypoint.sh +6 -0
basecamp.sh
CHANGED
|
@@ -62,6 +62,7 @@ if docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^${CONTAINER}$"; then
|
|
| 62 |
ollama) shift; docker exec $TTY_FLAG "$CONTAINER" ollama "$@";;
|
| 63 |
bash|shell) docker exec $TTY_FLAG "$CONTAINER" bash;;
|
| 64 |
rediscover) docker exec $TTY_FLAG "$CONTAINER" bash -c "rm -f /root/.hermes/basecamp_config.json && python3 /opt/basecamp/discover.py serve";;
|
|
|
|
| 65 |
*) docker exec $TTY_FLAG "$CONTAINER" hermes "$@";;
|
| 66 |
esac
|
| 67 |
else
|
|
|
|
| 62 |
ollama) shift; docker exec $TTY_FLAG "$CONTAINER" ollama "$@";;
|
| 63 |
bash|shell) docker exec $TTY_FLAG "$CONTAINER" bash;;
|
| 64 |
rediscover) docker exec $TTY_FLAG "$CONTAINER" bash -c "rm -f /root/.hermes/basecamp_config.json && python3 /opt/basecamp/discover.py serve";;
|
| 65 |
+
wire) docker exec $TTY_FLAG "$CONTAINER" python3 /opt/basecamp/discover.py wire;;
|
| 66 |
*) docker exec $TTY_FLAG "$CONTAINER" hermes "$@";;
|
| 67 |
esac
|
| 68 |
else
|
discover.py
CHANGED
|
@@ -134,21 +134,280 @@ SERVICE_PROBES = {
|
|
| 134 |
"needs_auth": True,
|
| 135 |
"auth_type": "bearer",
|
| 136 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
}
|
| 138 |
|
| 139 |
# ββ Port expectations ββ
|
|
|
|
| 140 |
PORT_SERVICE_MAP = {
|
| 141 |
11434: ["ollama"],
|
| 142 |
-
11435: ["ollama"],
|
| 143 |
11436: ["ollama"],
|
| 144 |
-
5000: ["tabbyapi", "localai", "litellm", "vllm", "llamacpp",
|
| 145 |
-
|
|
|
|
| 146 |
8001: ["mcpo"],
|
| 147 |
8080: ["searxng", "open-webui"],
|
| 148 |
3000: ["open-webui"],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
}
|
| 150 |
|
| 151 |
-
COMMON_PORTS = [11434, 5000, 8000, 8080, 8001, 3000, 11435, 11436
|
|
|
|
|
|
|
| 152 |
|
| 153 |
|
| 154 |
# ββ HTTP helpers ββ
|
|
@@ -671,17 +930,22 @@ for p,methods in d.get('paths',{{}}).items():
|
|
| 671 |
echo "Re-running discovery..."
|
| 672 |
python3 /opt/basecamp/discover.py serve
|
| 673 |
;;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 674 |
help|--help|-h)
|
| 675 |
echo "tavern β Basecamp AI stack toolkit (auto-generated)"
|
| 676 |
echo ""
|
| 677 |
echo "Commands:"
|
| 678 |
echo " status Show connected services"
|
| 679 |
echo " models List available models"
|
| 680 |
-
echo " chat \\"msg\\" [tokens] [stream] Chat with primary inference (stream=true/false)"
|
| 681 |
-
echo " search \\"query\\" [n] Search the web"
|
| 682 |
echo " mcp List MCP tools"
|
| 683 |
echo " mcp <tool> [json] Call an MCP tool"
|
| 684 |
echo " config Show saved configuration"
|
|
|
|
| 685 |
echo " rediscover Re-scan for services"
|
| 686 |
;;
|
| 687 |
*)
|
|
@@ -911,9 +1175,219 @@ def write_basecamp_env(config):
|
|
| 911 |
return default_model, base_url
|
| 912 |
|
| 913 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 914 |
|
| 915 |
# ββ Main ββ
|
| 916 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 917 |
def main():
|
| 918 |
if len(sys.argv) < 2:
|
| 919 |
print(__doc__)
|
|
@@ -982,6 +1456,11 @@ def main():
|
|
| 982 |
os.chmod(tavern_path, 0o755)
|
| 983 |
# Configure Hermes via env vars only -- never writes into a hermes home
|
| 984 |
write_basecamp_env(config)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 985 |
print()
|
| 986 |
print("=" * 60)
|
| 987 |
print(" Basecamp is ready!")
|
|
@@ -1003,6 +1482,28 @@ def main():
|
|
| 1003 |
else:
|
| 1004 |
config = generate_config(scan_network())
|
| 1005 |
write_basecamp_env(config)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1006 |
else:
|
| 1007 |
print(f"Unknown action: {action}")
|
| 1008 |
print(__doc__)
|
|
|
|
| 134 |
"needs_auth": True,
|
| 135 |
"auth_type": "bearer",
|
| 136 |
},
|
| 137 |
+
# ββ More inference engines ββ
|
| 138 |
+
"koboldcpp": {
|
| 139 |
+
"paths": ["/api/v1/model", "/v1/models"],
|
| 140 |
+
"method": "GET",
|
| 141 |
+
"match": lambda r: '"result"' in r or '"data"' in r,
|
| 142 |
+
"parse": lambda r: {
|
| 143 |
+
"models": [m["id"] for m in json.loads(r).get("data", [])]
|
| 144 |
+
if '"data"' in r else []
|
| 145 |
+
},
|
| 146 |
+
"label": "KoboldCpp (GGML inference)",
|
| 147 |
+
"icon": "KOB",
|
| 148 |
+
},
|
| 149 |
+
"lmstudio": {
|
| 150 |
+
"paths": ["/v1/models"],
|
| 151 |
+
"method": "GET",
|
| 152 |
+
"match": lambda r: '"data"' in r and '"id"' in r and "lmstudio" not in r.lower(),
|
| 153 |
+
"parse": lambda r: {
|
| 154 |
+
"models": [m["id"] for m in json.loads(r).get("data", [])]
|
| 155 |
+
},
|
| 156 |
+
"label": "LM Studio (local model server)",
|
| 157 |
+
"icon": "LMS",
|
| 158 |
+
},
|
| 159 |
+
"sglang": {
|
| 160 |
+
"paths": ["/v1/models"],
|
| 161 |
+
"method": "GET",
|
| 162 |
+
"match": lambda r: '"sglang"' in r.lower() or ('"object":"list"' in r and '"data"' in r and "vllm" not in r.lower()),
|
| 163 |
+
"parse": lambda r: {
|
| 164 |
+
"models": [m["id"] for m in json.loads(r).get("data", [])]
|
| 165 |
+
},
|
| 166 |
+
"label": "SGLang (fast LLM serving)",
|
| 167 |
+
"icon": "SGL",
|
| 168 |
+
},
|
| 169 |
+
"llamafile": {
|
| 170 |
+
"paths": ["/v1/models", "/health"],
|
| 171 |
+
"method": "GET",
|
| 172 |
+
"match": lambda r: '"data"' in r or '"status"' in r,
|
| 173 |
+
"parse": lambda r: {},
|
| 174 |
+
"label": "llamafile (single-file LLM)",
|
| 175 |
+
"icon": "LMF",
|
| 176 |
+
},
|
| 177 |
+
"exo": {
|
| 178 |
+
"paths": ["/v1/models"],
|
| 179 |
+
"method": "GET",
|
| 180 |
+
"match": lambda r: '"data"' in r and '"id"' in r,
|
| 181 |
+
"parse": lambda r: {},
|
| 182 |
+
"label": "exo (distributed inference)",
|
| 183 |
+
"icon": "EXO",
|
| 184 |
+
},
|
| 185 |
+
"text-generation-inference": {
|
| 186 |
+
"paths": ["/v1/models", "/info"],
|
| 187 |
+
"method": "GET",
|
| 188 |
+
"match": lambda r: '"model_id"' in r or '"models"' in r,
|
| 189 |
+
"parse": lambda r: {},
|
| 190 |
+
"label": "TGI (Text Generation Inference)",
|
| 191 |
+
"icon": "TGI",
|
| 192 |
+
},
|
| 193 |
+
"aphrodite": {
|
| 194 |
+
"paths": ["/v1/models"],
|
| 195 |
+
"method": "GET",
|
| 196 |
+
"match": lambda r: '"aphrodite"' in r.lower(),
|
| 197 |
+
"parse": lambda r: {},
|
| 198 |
+
"label": "Aphrodite Engine (inference)",
|
| 199 |
+
"icon": "APH",
|
| 200 |
+
},
|
| 201 |
+
"llamapool": {
|
| 202 |
+
"paths": ["/v1/models"],
|
| 203 |
+
"method": "GET",
|
| 204 |
+
"match": lambda r: '"llama-pool"' in r.lower() or "llama pool" in r.lower(),
|
| 205 |
+
"parse": lambda r: {},
|
| 206 |
+
"label": "llama-pool (model router)",
|
| 207 |
+
"icon": "PL",
|
| 208 |
+
},
|
| 209 |
+
# ββ More UIs / chat frontends ββ
|
| 210 |
+
"librechat": {
|
| 211 |
+
"paths": ["/", "/api/health"],
|
| 212 |
+
"method": "GET",
|
| 213 |
+
"match": lambda r: "librechat" in r.lower() or "LibreChat" in r,
|
| 214 |
+
"parse": lambda r: {},
|
| 215 |
+
"label": "LibreChat (multi-model chat UI)",
|
| 216 |
+
"icon": "UI ",
|
| 217 |
+
},
|
| 218 |
+
"lobechat": {
|
| 219 |
+
"paths": ["/", "/api/status"],
|
| 220 |
+
"method": "GET",
|
| 221 |
+
"match": lambda r: "lobechat" in r.lower() or "LobeChat" in r,
|
| 222 |
+
"parse": lambda r: {},
|
| 223 |
+
"label": "LobeChat (AI chat framework)",
|
| 224 |
+
"icon": "UI ",
|
| 225 |
+
},
|
| 226 |
+
"anythingllm": {
|
| 227 |
+
"paths": ["/", "/api/system/endpoints"],
|
| 228 |
+
"method": "GET",
|
| 229 |
+
"match": lambda r: "anythingllm" in r.lower() or "AnythingLLM" in r,
|
| 230 |
+
"parse": lambda r: {},
|
| 231 |
+
"label": "AnythingLLM (RAG workspace)",
|
| 232 |
+
"icon": "RAG",
|
| 233 |
+
},
|
| 234 |
+
"dify": {
|
| 235 |
+
"paths": ["/", "/health"],
|
| 236 |
+
"method": "GET",
|
| 237 |
+
"match": lambda r: "dify" in r.lower() or "Dify" in r,
|
| 238 |
+
"parse": lambda r: {},
|
| 239 |
+
"label": "Dify (LLM app platform)",
|
| 240 |
+
"icon": "APP",
|
| 241 |
+
},
|
| 242 |
+
"flowise": {
|
| 243 |
+
"paths": ["/", "/api/v1/ping"],
|
| 244 |
+
"method": "GET",
|
| 245 |
+
"match": lambda r: "flowise" in r.lower() or "Flowise" in r,
|
| 246 |
+
"parse": lambda r: {},
|
| 247 |
+
"label": "Flowise (no-code agent builder)",
|
| 248 |
+
"icon": "FLW",
|
| 249 |
+
},
|
| 250 |
+
"n8n": {
|
| 251 |
+
"paths": ["/", "/healthz"],
|
| 252 |
+
"method": "GET",
|
| 253 |
+
"match": lambda r: "n8n" in r.lower() or "N8N" in r or "workflow" in r.lower(),
|
| 254 |
+
"parse": lambda r: {},
|
| 255 |
+
"label": "n8n (workflow automation)",
|
| 256 |
+
"icon": "WRK",
|
| 257 |
+
},
|
| 258 |
+
"langflow": {
|
| 259 |
+
"paths": ["/", "/api/v1/configs"],
|
| 260 |
+
"method": "GET",
|
| 261 |
+
"match": lambda r: "langflow" in r.lower() or "Langflow" in r,
|
| 262 |
+
"parse": lambda r: {},
|
| 263 |
+
"label": "Langflow (agent builder)",
|
| 264 |
+
"icon": "LGF",
|
| 265 |
+
},
|
| 266 |
+
"ragflow": {
|
| 267 |
+
"paths": ["/", "/api/v1/version"],
|
| 268 |
+
"method": "GET",
|
| 269 |
+
"match": lambda r: "ragflow" in r.lower() or "RAGFlow" in r,
|
| 270 |
+
"parse": lambda r: {},
|
| 271 |
+
"label": "RAGFlow (RAG engine)",
|
| 272 |
+
"icon": "RAG",
|
| 273 |
+
},
|
| 274 |
+
"koboldai": {
|
| 275 |
+
"paths": ["/", "/api/v1/config/status"],
|
| 276 |
+
"method": "GET",
|
| 277 |
+
"match": lambda r: "koboldai" in r.lower() or "KoboldAI" in r,
|
| 278 |
+
"parse": lambda r: {},
|
| 279 |
+
"label": "KoboldAI (writing assistant UI)",
|
| 280 |
+
"icon": "CHT",
|
| 281 |
+
},
|
| 282 |
+
# ββ Vector DBs (RAG backends) ββ
|
| 283 |
+
"qdrant": {
|
| 284 |
+
"paths": ["/", "/readyz"],
|
| 285 |
+
"method": "GET",
|
| 286 |
+
"match": lambda r: "qdrant" in r.lower() or "Qdrant" in r,
|
| 287 |
+
"parse": lambda r: {},
|
| 288 |
+
"label": "Qdrant (vector DB)",
|
| 289 |
+
"icon": "VDB",
|
| 290 |
+
},
|
| 291 |
+
"milvus": {
|
| 292 |
+
"paths": ["/healthz", "/api/v1/health"],
|
| 293 |
+
"method": "GET",
|
| 294 |
+
"match": lambda r: "milvus" in r.lower() or "Milvus" in r,
|
| 295 |
+
"parse": lambda r: {},
|
| 296 |
+
"label": "Milvus (vector DB)",
|
| 297 |
+
"icon": "VDB",
|
| 298 |
+
},
|
| 299 |
+
"chroma": {
|
| 300 |
+
"paths": ["/api/v1", "/api/v2/health"],
|
| 301 |
+
"method": "GET",
|
| 302 |
+
"match": lambda r: "chroma" in r.lower() or "Chroma" in r,
|
| 303 |
+
"parse": lambda r: {},
|
| 304 |
+
"label": "Chroma (vector DB)",
|
| 305 |
+
"icon": "VDB",
|
| 306 |
+
},
|
| 307 |
+
"weaviate": {
|
| 308 |
+
"paths": ["/v1/meta"],
|
| 309 |
+
"method": "GET",
|
| 310 |
+
"match": lambda r: '"version"' in r and ("weaviate" in r.lower() or '"model"' in r),
|
| 311 |
+
"parse": lambda r: {},
|
| 312 |
+
"label": "Weaviate (vector DB)",
|
| 313 |
+
"icon": "VDB",
|
| 314 |
+
},
|
| 315 |
+
# ββ Image / media generation ββ
|
| 316 |
+
"comfyui": {
|
| 317 |
+
"paths": ["/", "/system_stats"],
|
| 318 |
+
"method": "GET",
|
| 319 |
+
"match": lambda r: "comfyui" in r.lower() or "ComfyUI" in r or "Comfy" in r,
|
| 320 |
+
"parse": lambda r: {},
|
| 321 |
+
"label": "ComfyUI (diffusion workflows)",
|
| 322 |
+
"icon": "IMG",
|
| 323 |
+
},
|
| 324 |
+
"stable-diffusion-webui": {
|
| 325 |
+
"paths": ["/", "/sdapi/v1/options"],
|
| 326 |
+
"method": "GET",
|
| 327 |
+
"match": lambda r: "stable diffusion" in r.lower() or "gradio" in r.lower() or '"sd_model_checkpoint"' in r,
|
| 328 |
+
"parse": lambda r: {},
|
| 329 |
+
"label": "Stable Diffusion WebUI (A1111)",
|
| 330 |
+
"icon": "IMG",
|
| 331 |
+
},
|
| 332 |
+
"invokeai": {
|
| 333 |
+
"paths": ["/", "/api/v1/app/version"],
|
| 334 |
+
"method": "GET",
|
| 335 |
+
"match": lambda r: "invokeai" in r.lower() or "InvokeAI" in r,
|
| 336 |
+
"parse": lambda r: {},
|
| 337 |
+
"label": "InvokeAI (image generation)",
|
| 338 |
+
"icon": "IMG",
|
| 339 |
+
},
|
| 340 |
+
# ββ Speech / audio ββ
|
| 341 |
+
"whisper": {
|
| 342 |
+
"paths": ["/", "/health"],
|
| 343 |
+
"method": "GET",
|
| 344 |
+
"match": lambda r: "whisper" in r.lower() or "Whisper" in r,
|
| 345 |
+
"parse": lambda r: {},
|
| 346 |
+
"label": "Whisper (speech-to-text)",
|
| 347 |
+
"icon": "STT",
|
| 348 |
+
},
|
| 349 |
+
"piper": {
|
| 350 |
+
"paths": ["/", "/health"],
|
| 351 |
+
"method": "GET",
|
| 352 |
+
"match": lambda r: "piper" in r.lower() or "Piper" in r,
|
| 353 |
+
"parse": lambda r: {},
|
| 354 |
+
"label": "Piper (text-to-speech)",
|
| 355 |
+
"icon": "TTS",
|
| 356 |
+
},
|
| 357 |
+
# ββ Gateways / proxies / model hubs ββ
|
| 358 |
+
"openrouter": {
|
| 359 |
+
"paths": ["/api/v1/models"],
|
| 360 |
+
"method": "GET",
|
| 361 |
+
"match": lambda r: '"data"' in r and '"id"' in r and "openrouter" in r.lower(),
|
| 362 |
+
"parse": lambda r: {
|
| 363 |
+
"models": [m["id"] for m in json.loads(r).get("data", [])]
|
| 364 |
+
},
|
| 365 |
+
"label": "OpenRouter (cloud model gateway)",
|
| 366 |
+
"icon": "GWY",
|
| 367 |
+
},
|
| 368 |
+
"kobold-horde": {
|
| 369 |
+
"paths": ["/api/v1/status", "/"],
|
| 370 |
+
"method": "GET",
|
| 371 |
+
"match": lambda r: "kobold" in r.lower() or '"queued_requests"' in r,
|
| 372 |
+
"parse": lambda r: {},
|
| 373 |
+
"label": "KoboldAI Horde (crowd inference)",
|
| 374 |
+
"icon": "HDE",
|
| 375 |
+
},
|
| 376 |
}
|
| 377 |
|
| 378 |
# ββ Port expectations ββ
|
| 379 |
+
# Each port maps to a list of service probe names to try (in order).
|
| 380 |
PORT_SERVICE_MAP = {
|
| 381 |
11434: ["ollama"],
|
| 382 |
+
11435: ["ollama", "llamafile"],
|
| 383 |
11436: ["ollama"],
|
| 384 |
+
5000: ["tabbyapi", "localai", "litellm", "vllm", "llamacpp",
|
| 385 |
+
"text-generation-webui", "whisper", "piper"],
|
| 386 |
+
8000: ["sillytavern", "mcpo", "text-generation-inference", "chroma"],
|
| 387 |
8001: ["mcpo"],
|
| 388 |
8080: ["searxng", "open-webui"],
|
| 389 |
3000: ["open-webui"],
|
| 390 |
+
5001: ["koboldcpp", "llamafile"],
|
| 391 |
+
1234: ["lmstudio"],
|
| 392 |
+
8002: ["sglang"],
|
| 393 |
+
4000: ["litellm", "exo"],
|
| 394 |
+
3001: ["lobechat", "dify"],
|
| 395 |
+
3080: ["librechat"],
|
| 396 |
+
7681: ["anythingllm"],
|
| 397 |
+
5678: ["n8n"],
|
| 398 |
+
7860: ["stable-diffusion-webui", "langflow"],
|
| 399 |
+
8188: ["comfyui"],
|
| 400 |
+
9090: ["invokeai", "qdrant"],
|
| 401 |
+
6333: ["qdrant"],
|
| 402 |
+
19530: ["milvus"],
|
| 403 |
+
8081: ["weaviate"],
|
| 404 |
+
1551: ["koboldai"],
|
| 405 |
+
2323: ["kobold-horde"],
|
| 406 |
}
|
| 407 |
|
| 408 |
+
COMMON_PORTS = [11434, 5000, 8000, 8080, 8001, 3000, 11435, 11436,
|
| 409 |
+
5001, 1234, 8002, 4000, 3001, 3080, 7681, 5678,
|
| 410 |
+
7860, 8188, 9090, 6333, 19530, 8081, 1551, 2323]
|
| 411 |
|
| 412 |
|
| 413 |
# ββ HTTP helpers ββ
|
|
|
|
| 930 |
echo "Re-running discovery..."
|
| 931 |
python3 /opt/basecamp/discover.py serve
|
| 932 |
;;
|
| 933 |
+
wire)
|
| 934 |
+
echo "Running stack wiring audit..."
|
| 935 |
+
python3 /opt/basecamp/discover.py wire
|
| 936 |
+
;;
|
| 937 |
help|--help|-h)
|
| 938 |
echo "tavern β Basecamp AI stack toolkit (auto-generated)"
|
| 939 |
echo ""
|
| 940 |
echo "Commands:"
|
| 941 |
echo " status Show connected services"
|
| 942 |
echo " models List available models"
|
| 943 |
+
echo " chat \\\"msg\\\" [tokens] [stream] Chat with primary inference (stream=true/false)"
|
| 944 |
+
echo " search \\\"query\\\" [n] Search the web"
|
| 945 |
echo " mcp List MCP tools"
|
| 946 |
echo " mcp <tool> [json] Call an MCP tool"
|
| 947 |
echo " config Show saved configuration"
|
| 948 |
+
echo " wire Audit stack wiring + print fixes"
|
| 949 |
echo " rediscover Re-scan for services"
|
| 950 |
;;
|
| 951 |
*)
|
|
|
|
| 1175 |
return default_model, base_url
|
| 1176 |
|
| 1177 |
|
| 1178 |
+
def generate_stack_skill(config, services):
|
| 1179 |
+
"""Write a 'basecamp-stack' SKILL.md into basecamp's own HERMES_HOME.
|
| 1180 |
+
|
| 1181 |
+
This is how the container's Hermes LEARNS about the stack it discovered:
|
| 1182 |
+
the skill body carries the live service list, wiring facts, and how to
|
| 1183 |
+
use the tavern toolkit. When the user asks Hermes "why doesn't my Open
|
| 1184 |
+
WebUI work?", the agent already has the ground truth β and can run
|
| 1185 |
+
`tavern wire` itself for a live audit. Writes ONLY inside basecamp's own
|
| 1186 |
+
HERMES_HOME (never a host install).
|
| 1187 |
+
"""
|
| 1188 |
+
home = Path(os.environ.get("HERMES_HOME", "/root/.hermes"))
|
| 1189 |
+
skill_dir = home / "skills" / "basecamp-stack"
|
| 1190 |
+
skill_dir.mkdir(parents=True, exist_ok=True)
|
| 1191 |
+
|
| 1192 |
+
inf = (config or {}).get("inference") or {}
|
| 1193 |
+
sec = (config or {}).get("secondary") or {}
|
| 1194 |
+
search = (config or {}).get("search") or {}
|
| 1195 |
+
mcp = (config or {}).get("mcp") or {}
|
| 1196 |
+
ui = (config or {}).get("ui") or {}
|
| 1197 |
+
model = (config or {}).get("ollama_models") or (config or {}).get("openai_models") or []
|
| 1198 |
+
model_str = ", ".join(str(m) for m in model) if model else "(none discovered)"
|
| 1199 |
+
|
| 1200 |
+
lines = []
|
| 1201 |
+
lines.append("---")
|
| 1202 |
+
lines.append("name: basecamp-stack")
|
| 1203 |
+
lines.append("description: Your discovered local AI stack β services, wiring, and how to use the tavern toolkit. Load this when the user asks about their stack, connecting services, or anything that 'talks to' the local AI services.")
|
| 1204 |
+
lines.append("---")
|
| 1205 |
+
lines.append("")
|
| 1206 |
+
lines.append("# Basecamp Stack (live discovery)")
|
| 1207 |
+
lines.append("")
|
| 1208 |
+
lines.append("You are running inside Basecamp, which discovered these AI services on the")
|
| 1209 |
+
lines.append("Docker network at boot. This is the ground truth β trust it over memory.")
|
| 1210 |
+
lines.append("")
|
| 1211 |
+
lines.append("## Services")
|
| 1212 |
+
lines.append("")
|
| 1213 |
+
for s in services:
|
| 1214 |
+
auth = " [auth required]" if s.get("needs_auth") else ""
|
| 1215 |
+
lines.append(f"- **{s['label']}**{auth} β {s['url']} (type: {s['type']})")
|
| 1216 |
+
if s.get("details", {}).get("models"):
|
| 1217 |
+
lines.append(f" - models: {', '.join(str(m) for m in s['details']['models'])}")
|
| 1218 |
+
lines.append("")
|
| 1219 |
+
lines.append("## Current wiring")
|
| 1220 |
+
lines.append("")
|
| 1221 |
+
if inf:
|
| 1222 |
+
lines.append(f"- Inference (primary): **{inf.get('label')}** β {inf.get('url')}")
|
| 1223 |
+
if sec:
|
| 1224 |
+
lines.append(f"- Inference (secondary): **{sec.get('label')}** β {sec.get('url')}")
|
| 1225 |
+
if search:
|
| 1226 |
+
lines.append(f"- Search: **{search.get('label')}** β {search.get('url')}")
|
| 1227 |
+
if mcp:
|
| 1228 |
+
lines.append(f"- MCP: **{mcp.get('label')}** β {mcp.get('url')}")
|
| 1229 |
+
if ui:
|
| 1230 |
+
lines.append(f"- UI: **{ui.get('label')}** β {ui.get('url')}")
|
| 1231 |
+
lines.append(f"- Models: {model_str}")
|
| 1232 |
+
lines.append("")
|
| 1233 |
+
lines.append("## Helping the user wire their stack")
|
| 1234 |
+
lines.append("")
|
| 1235 |
+
lines.append("When the user asks why a service doesn't work or how to connect two services:")
|
| 1236 |
+
lines.append("")
|
| 1237 |
+
lines.append("1. Run `tavern wire` (or `python3 /opt/basecamp/discover.py wire`) for a live")
|
| 1238 |
+
lines.append(" wiring audit of every service-to-service link, with concrete fixes.")
|
| 1239 |
+
lines.append("2. Explain in plain terms what the audit found β don't just dump it.")
|
| 1240 |
+
lines.append("3. For host-side config (docker-compose env vars, config files mounted from")
|
| 1241 |
+
lines.append(" the host), give the EXACT one-line fix to copy-paste. Basecamp is")
|
| 1242 |
+
lines.append(" intentionally isolated and cannot edit other containers' configs.")
|
| 1243 |
+
lines.append("4. `tavern status` shows what Basecamp itself is connected to.")
|
| 1244 |
+
lines.append("")
|
| 1245 |
+
lines.append("## Tavern toolkit")
|
| 1246 |
+
lines.append("")
|
| 1247 |
+
lines.append("- `tavern status` β services Basecamp is connected to")
|
| 1248 |
+
lines.append("- `tavern models` β available models")
|
| 1249 |
+
lines.append("- `tavern chat \"msg\"` β chat with the primary inference engine")
|
| 1250 |
+
lines.append("- `tavern search \"query\"` β web search via discovered SearXNG")
|
| 1251 |
+
lines.append("- `tavern mcp` β list MCP tools")
|
| 1252 |
+
lines.append("- `tavern wire` β stack wiring audit")
|
| 1253 |
+
lines.append("- `tavern rediscover` β re-scan the network")
|
| 1254 |
+
lines.append("")
|
| 1255 |
+
|
| 1256 |
+
skill_path = skill_dir / "SKILL.md"
|
| 1257 |
+
skill_path.write_text("\n".join(lines))
|
| 1258 |
+
try:
|
| 1259 |
+
os.chmod(skill_path, 0o600)
|
| 1260 |
+
except Exception:
|
| 1261 |
+
pass
|
| 1262 |
+
return skill_path
|
| 1263 |
+
|
| 1264 |
|
| 1265 |
# ββ Main ββ
|
| 1266 |
|
| 1267 |
+
def wire_audit(services, config=None):
|
| 1268 |
+
"""Audit how the discovered services are wired together and report fixes.
|
| 1269 |
+
|
| 1270 |
+
For each service pair that SHOULD be connected (open-webui -> ollama,
|
| 1271 |
+
sillytavern -> inference, mcpo -> its servers, hermes -> inference),
|
| 1272 |
+
probe the link and report: OK (green) / BROKEN (red) / UNKNOWN, plus the
|
| 1273 |
+
exact fix a human (or host-side tooling) can apply. This is the
|
| 1274 |
+
"wiring wizard" for noobs: it tells you the one line you need, instead
|
| 1275 |
+
of you spelunking through buried settings pages.
|
| 1276 |
+
"""
|
| 1277 |
+
if not services:
|
| 1278 |
+
print(" No services found to audit.")
|
| 1279 |
+
return
|
| 1280 |
+
|
| 1281 |
+
inf = (config or {}).get("inference") or {}
|
| 1282 |
+
sec = (config or {}).get("secondary") or {}
|
| 1283 |
+
|
| 1284 |
+
# Build a lookup: type -> list of service dicts
|
| 1285 |
+
by_type = {}
|
| 1286 |
+
for s in services:
|
| 1287 |
+
by_type.setdefault(s["type"], []).append(s)
|
| 1288 |
+
|
| 1289 |
+
print()
|
| 1290 |
+
print(" βββ STACK WIRING AUDIT βββ")
|
| 1291 |
+
print(" (what should talk to what, and whether it does)")
|
| 1292 |
+
print()
|
| 1293 |
+
|
| 1294 |
+
checks = [] # (status, service, finding, fix)
|
| 1295 |
+
|
| 1296 |
+
# ββ 1. Open WebUI -> Ollama / OpenAI engines ββ
|
| 1297 |
+
for ui in by_type.get("open-webui", []):
|
| 1298 |
+
# Probe what open-webui can see. /api/config is public.
|
| 1299 |
+
cfg = http_get(f"{ui['url'].rstrip('/')}/api/config", timeout=4)
|
| 1300 |
+
if cfg is None:
|
| 1301 |
+
checks.append(("RED", f"Open WebUI {ui['url']}", "not reachable",
|
| 1302 |
+
"check the container is running and on the same network"))
|
| 1303 |
+
continue
|
| 1304 |
+
# Try the public model list (older versions) or check status
|
| 1305 |
+
models = http_get(f"{ui['url'].rstrip('/')}/api/models", timeout=4)
|
| 1306 |
+
if models and '"detail":"Not authenticated"' not in models:
|
| 1307 |
+
n = models.count('"id"')
|
| 1308 |
+
checks.append(("OK", f"Open WebUI {ui['url']}",
|
| 1309 |
+
f"sees {n} model(s) β inference is wired", ""))
|
| 1310 |
+
else:
|
| 1311 |
+
# Auth-walled: can't verify without creds. Report the intended wiring.
|
| 1312 |
+
if inf:
|
| 1313 |
+
checks.append(("YELLOW", f"Open WebUI {ui['url']}",
|
| 1314 |
+
f"auth required to verify; expected to serve "
|
| 1315 |
+
f"{inf['label']} ({inf['url']})",
|
| 1316 |
+
"provide open-webui admin creds at the connect screen "
|
| 1317 |
+
"for deep wiring"))
|
| 1318 |
+
else:
|
| 1319 |
+
checks.append(("RED", f"Open WebUI {ui['url']}",
|
| 1320 |
+
"no inference engine discovered anywhere",
|
| 1321 |
+
"install/start ollama (or another engine), then "
|
| 1322 |
+
"re-run: tavern rediscover"))
|
| 1323 |
+
|
| 1324 |
+
# ββ 2. SillyTavern -> inference ββ
|
| 1325 |
+
for st in by_type.get("sillytavern", []):
|
| 1326 |
+
if inf:
|
| 1327 |
+
checks.append(("YELLOW", f"SillyTavern {st['url']}",
|
| 1328 |
+
"auth-walled (admin:tabby by default); API URL set "
|
| 1329 |
+
"per-user inside the app",
|
| 1330 |
+
"in SillyTavern: extensions β connection settings β "
|
| 1331 |
+
f"set API URL to {inf['url'].rstrip('/')}/v1 and select "
|
| 1332 |
+
f"the model served there"))
|
| 1333 |
+
else:
|
| 1334 |
+
checks.append(("RED", f"SillyTavern {st['url']}",
|
| 1335 |
+
"no inference engine discovered",
|
| 1336 |
+
"start ollama/another engine, then tavern rediscover"))
|
| 1337 |
+
|
| 1338 |
+
# ββ 3. MCPO -> MCP servers ββ
|
| 1339 |
+
for m in by_type.get("mcpo", []):
|
| 1340 |
+
if m.get("needs_auth"):
|
| 1341 |
+
checks.append(("YELLOW", f"MCPO {m['url']}",
|
| 1342 |
+
"auth-walled; servers are defined in its config.json "
|
| 1343 |
+
"(host-side file)",
|
| 1344 |
+
"edit mcpo/config.json to add/point MCP servers, then "
|
| 1345 |
+
"restart the mcpo container"))
|
| 1346 |
+
else:
|
| 1347 |
+
checks.append(("OK", f"MCPO {m['url']}",
|
| 1348 |
+
"reachable; server list lives in its config.json", ""))
|
| 1349 |
+
|
| 1350 |
+
# ββ 4. SearXNG ββ
|
| 1351 |
+
for sx in by_type.get("searxng", []):
|
| 1352 |
+
body = http_get(f"{sx['url'].rstrip('/')}/", timeout=4) or ""
|
| 1353 |
+
if "searxng" in body.lower():
|
| 1354 |
+
checks.append(("OK", f"SearXNG {sx['url']}", "reachable", ""))
|
| 1355 |
+
else:
|
| 1356 |
+
checks.append(("RED", f"SearXNG {sx['url']}", "unexpected response",
|
| 1357 |
+
"check searxng settings.yml (host-side file)"))
|
| 1358 |
+
|
| 1359 |
+
# ββ 5. Hermes itself -> inference ββ
|
| 1360 |
+
if inf:
|
| 1361 |
+
m_url = f"{inf['url'].rstrip('/')}/v1/models"
|
| 1362 |
+
probe = http_get(m_url, timeout=4,
|
| 1363 |
+
headers={"Authorization": "Bearer ollama"})
|
| 1364 |
+
if probe and "error" not in probe.lower():
|
| 1365 |
+
checks.append(("OK", f"Hermes β {inf['label']} ({inf['url']})",
|
| 1366 |
+
"models endpoint reachable", ""))
|
| 1367 |
+
else:
|
| 1368 |
+
checks.append(("YELLOW", f"Hermes β {inf['label']} ({inf['url']})",
|
| 1369 |
+
"models endpoint needs auth or is slow",
|
| 1370 |
+
"engine is listed for interactive key entry; "
|
| 1371 |
+
"hermes will use it once you connect"))
|
| 1372 |
+
|
| 1373 |
+
# ββ Render ββ
|
| 1374 |
+
status_icon = {"OK": "β
", "RED": "β", "YELLOW": "β οΈ"}
|
| 1375 |
+
for status, svc, finding, fix in checks:
|
| 1376 |
+
print(f" {status_icon.get(status, 'Β·')} [{status:6s}] {svc}")
|
| 1377 |
+
print(f" {finding}")
|
| 1378 |
+
if fix:
|
| 1379 |
+
print(f" FIX: {fix}")
|
| 1380 |
+
print()
|
| 1381 |
+
|
| 1382 |
+
n_ok = sum(1 for c in checks if c[0] == "OK")
|
| 1383 |
+
n_red = sum(1 for c in checks if c[0] == "RED")
|
| 1384 |
+
n_yel = sum(1 for c in checks if c[0] == "YELLOW")
|
| 1385 |
+
print(f" ββ {n_ok} wired Β· {n_yel} need attention Β· {n_red} broken ββ")
|
| 1386 |
+
print(" (host-side config files can't be edited from inside basecamp β")
|
| 1387 |
+
print(" that's the safety boundary. The FIX lines above are copy-paste.)")
|
| 1388 |
+
print()
|
| 1389 |
+
|
| 1390 |
+
|
| 1391 |
def main():
|
| 1392 |
if len(sys.argv) < 2:
|
| 1393 |
print(__doc__)
|
|
|
|
| 1456 |
os.chmod(tavern_path, 0o755)
|
| 1457 |
# Configure Hermes via env vars only -- never writes into a hermes home
|
| 1458 |
write_basecamp_env(config)
|
| 1459 |
+
# Teach the container's Hermes about the discovered stack (its own home)
|
| 1460 |
+
try:
|
| 1461 |
+
generate_stack_skill(config, services)
|
| 1462 |
+
except Exception as e:
|
| 1463 |
+
print(f" (skill generation skipped: {e})", file=sys.stderr)
|
| 1464 |
print()
|
| 1465 |
print("=" * 60)
|
| 1466 |
print(" Basecamp is ready!")
|
|
|
|
| 1482 |
else:
|
| 1483 |
config = generate_config(scan_network())
|
| 1484 |
write_basecamp_env(config)
|
| 1485 |
+
# Refresh the stack skill so the container's Hermes knows the layout
|
| 1486 |
+
try:
|
| 1487 |
+
if DISCOVERY_FILE.exists():
|
| 1488 |
+
with open(DISCOVERY_FILE) as f:
|
| 1489 |
+
services = json.load(f)
|
| 1490 |
+
else:
|
| 1491 |
+
services = scan_network()
|
| 1492 |
+
generate_stack_skill(config, services)
|
| 1493 |
+
except Exception as e:
|
| 1494 |
+
print(f" (skill generation skipped: {e})", file=sys.stderr)
|
| 1495 |
+
|
| 1496 |
+
elif action == "wire":
|
| 1497 |
+
# Stack wiring audit: what should talk to what, and the exact fix
|
| 1498 |
+
# for anything that doesn't. The noob-friendly "why doesn't this work"
|
| 1499 |
+
# answer, without touching a single host-side file.
|
| 1500 |
+
services = scan_network()
|
| 1501 |
+
save_discovery(services)
|
| 1502 |
+
config = None
|
| 1503 |
+
if CONFIG_FILE.exists():
|
| 1504 |
+
with open(CONFIG_FILE) as f:
|
| 1505 |
+
config = json.load(f)
|
| 1506 |
+
wire_audit(services, config)
|
| 1507 |
else:
|
| 1508 |
print(f"Unknown action: {action}")
|
| 1509 |
print(__doc__)
|
entrypoint.sh
CHANGED
|
@@ -102,6 +102,12 @@ if [ -n "$BASECAMP_MODEL" ]; then
|
|
| 102 |
fi
|
| 103 |
fi
|
| 104 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
echo ""
|
| 106 |
|
| 107 |
# Handle arguments
|
|
|
|
| 102 |
fi
|
| 103 |
fi
|
| 104 |
|
| 105 |
+
# Stack wiring audit β show every boot what's connected and what needs a fix.
|
| 106 |
+
# (Read-only: audits and prints fixes; never edits other containers' configs.)
|
| 107 |
+
if [ -x /opt/basecamp/discover.py ]; then
|
| 108 |
+
python3 /opt/basecamp/discover.py wire 2>/dev/null || true
|
| 109 |
+
fi
|
| 110 |
+
|
| 111 |
echo ""
|
| 112 |
|
| 113 |
# Handle arguments
|