jpanasuk commited on
Commit
94e4c89
Β·
1 Parent(s): fe9a48a

v1.0.3: indigo identity (skin+branding+basecamp/model), boot race fix, external-ollama auto-select, alias verification

Browse files
Files changed (7) hide show
  1. README.md +25 -2
  2. SOUL.md +14 -0
  3. basecamp.sh +1 -1
  4. discover.py +47 -8
  5. entrypoint.sh +17 -8
  6. index.html +548 -0
  7. skins/indigo.yaml +43 -0
README.md CHANGED
@@ -17,6 +17,20 @@ library_name: docker
17
 
18
  # πŸ•οΈ Basecamp β€” The First Portable AI Agent Container with Network Auto-Discovery
19
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  **v1.0.2** β€” the stack-gauntlet release (tested against a live 6-service AI
21
  stack):
22
  - **TabbyAPI + SillyTavern now discovered** β€” auth-gated services (401/403)
@@ -80,7 +94,7 @@ install on the host that runs it:
80
 
81
  ```bash
82
  # Option A: pull the prebuilt image (Docker Hub)
83
- docker pull jpanasuk/basecamp:1.0.2
84
 
85
  # Option B: build from source
86
  docker build -t local/basecamp:latest .
@@ -93,7 +107,7 @@ docker run -it --rm \
93
  -p 11436:11434 \
94
  -v basecamp-ollama:/root/.ollama \
95
  -v basecamp-hermes:/root/.hermes \
96
- jpanasuk/basecamp:1.0.2
97
  ```
98
 
99
  Or use the convenience launcher (auto-detects network + GPU, TTY-safe):
@@ -139,6 +153,15 @@ basecamp/
139
  └── supervisord.conf # Ollama only (hermes is exec'd, not supervised)
140
  ```
141
 
 
 
 
 
 
 
 
 
 
142
  ## Development notes
143
 
144
  - Test discovery locally without touching anything:
 
17
 
18
  # πŸ•οΈ Basecamp β€” The First Portable AI Agent Container with Network Auto-Discovery
19
 
20
+ **v1.0.3** β€” the identity + reliability release:
21
+ - **Indigo identity** β€” Basecamp runs its own indigo skin ("Welcome to Basecamp
22
+ Hermes!") so you can tell it apart from a host install at a glance; model
23
+ readout shows `basecamp/<model>` (created via `ollama cp` alias on the
24
+ discovered engine).
25
+ - **Boot race fixed** β€” the bundled-model pull now starts AFTER discovery,
26
+ so the network scan never competes with a multi-GB download. Cold boots
27
+ consistently discover the whole stack (7/7 on the reference stack).
28
+ - **Smarter auto-select** β€” an external Ollama (your stack) is preferred
29
+ over the bundled fallback; alias creation is verified before use, and
30
+ stale `basecamp/` aliases are excluded from model lists.
31
+ - **Separate commands** β€” host `hermes` = your install; `basecamp` =
32
+ the container edition. No more ambiguity about which agent you're in.
33
+
34
  **v1.0.2** β€” the stack-gauntlet release (tested against a live 6-service AI
35
  stack):
36
  - **TabbyAPI + SillyTavern now discovered** β€” auth-gated services (401/403)
 
94
 
95
  ```bash
96
  # Option A: pull the prebuilt image (Docker Hub)
97
+ docker pull jpanasuk/basecamp:1.0.3
98
 
99
  # Option B: build from source
100
  docker build -t local/basecamp:latest .
 
107
  -p 11436:11434 \
108
  -v basecamp-ollama:/root/.ollama \
109
  -v basecamp-hermes:/root/.hermes \
110
+ jpanasuk/basecamp:1.0.3
111
  ```
112
 
113
  Or use the convenience launcher (auto-detects network + GPU, TTY-safe):
 
153
  └── supervisord.conf # Ollama only (hermes is exec'd, not supervised)
154
  ```
155
 
156
+ ## Credits
157
+
158
+ Basecamp is a **packaging of [Hermes Agent](https://hermes-agent.nousresearch.com)** by
159
+ [Nous Research](https://nousresearch.com) β€” all agent intelligence, tools,
160
+ skills, and the gateway are theirs. Basecamp's only contribution is the box:
161
+ the Docker packaging, the network auto-discovery, the safe env-scoped
162
+ wiring, and the `tavern` toolkit. Full credit and gratitude to the Hermes
163
+ team for building the agent that makes this project worth shipping.
164
+
165
  ## Development notes
166
 
167
  - Test discovery locally without touching anything:
SOUL.md ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ You are Hermes Agent running inside Basecamp β€” a portable, self-contained
2
+ agent container that auto-discovers local AI stacks. You are the twin of the
3
+ host Hermes install, but you live in your own box: your config, keys, and
4
+ state stay inside this container and never touch a host install.
5
+
6
+ You identify as Basecamp Hermes. If asked who you are or where you run, say
7
+ you're the Basecamp edition (containerized) β€” distinct from any host-level
8
+ Hermes install. You are helpful, knowledgeable, and direct. You assist with
9
+ the full range of Hermes Agent tasks: answering questions, writing and
10
+ editing code, analyzing information, creative work, and executing actions
11
+ via your tools. You communicate clearly, admit uncertainty when appropriate,
12
+ and prioritize being genuinely useful over being verbose. You may be asked
13
+ to use the `tavern` CLI toolkit to inspect or manage the local AI stack you
14
+ discovered (inference engines, search, MCP tools, UIs).
basecamp.sh CHANGED
@@ -18,7 +18,7 @@
18
  # basecamp bash # drop into shell inside container
19
  # basecamp rediscover # re-scan for services
20
 
21
- IMAGE="${BASECAMP_IMAGE:-local/basecamp:latest}"
22
  CONTAINER="${BASECAMP_CONTAINER:-basecamp}"
23
 
24
  # Interactive flags: only use -it when stdin is a TTY (cron/CI-safe)
 
18
  # basecamp bash # drop into shell inside container
19
  # basecamp rediscover # re-scan for services
20
 
21
+ IMAGE="${BASECAMP_IMAGE:-jpanasuk/basecamp:latest}"
22
  CONTAINER="${BASECAMP_CONTAINER:-basecamp}"
23
 
24
  # Interactive flags: only use -it when stdin is a TTY (cron/CI-safe)
discover.py CHANGED
@@ -436,14 +436,26 @@ def generate_config(services, selected=None, auth_keys=None):
436
  "tabbyapi", "vllm", "litellm", "localai", "llamacpp",
437
  "text-generation-webui")]
438
  if ollamas:
439
- primary = ollamas[0]
 
 
 
 
 
 
 
440
  config["inference"] = {
441
  "type": primary["type"], "url": primary["url"],
442
  "label": primary["label"]}
443
  config["openai_models"] = []
444
- config["ollama_models"] = primary.get("details", {}).get("models", [])
445
- if len(ollamas) > 1:
446
- config["secondary"] = {"type": "ollama", "url": ollamas[1]["url"], "label": ollamas[1]["label"]}
 
 
 
 
 
447
  elif openai_engines:
448
  primary = openai_engines[0]
449
  config["inference"] = {
@@ -847,16 +859,43 @@ def write_basecamp_env(config):
847
  api_key = config.get("api_keys", {}).get(primary.get("url", ""), "") or "ollama"
848
 
849
  models = config.get("openai_models") or config.get("ollama_models") or ["llama3.1:8b"]
850
- default_model = models[0] if models else "llama3.1:8b"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
851
 
852
  env_path = Path(os.environ.get("BASECAMP_ENV_FILE", "/opt/basecamp/basecamp.env"))
853
  env_path.parent.mkdir(parents=True, exist_ok=True)
854
  env_path.write_text(
855
  f'export OPENAI_BASE_URL="{base_url}"\n'
856
  f'export OPENAI_API_KEY="{api_key}"\n'
857
- # bare model id β€” the base URL already routes to the discovered
858
- # engine; an "openai/" prefix is sent literally by hermes and 404s
859
- # against ollama's /v1 (model 'openai/llama3.1:8b' not found).
860
  f'export HERMES_MODEL="{default_model}"\n'
861
  )
862
  try:
 
436
  "tabbyapi", "vllm", "litellm", "localai", "llamacpp",
437
  "text-generation-webui")]
438
  if ollamas:
439
+ # Prefer an EXTERNAL ollama (the user's stack) over basecamp's own
440
+ # bundled one (127.0.0.1 / localhost) β€” the bundled ollama is the
441
+ # fallback for when nothing else exists. Discovery order is racy
442
+ # (parallel probes), so pick deterministically.
443
+ external = [s for s in ollamas
444
+ if not str(s["url"]).replace("http://", "").replace("https://", "").startswith(("127.", "localhost", "::1"))]
445
+ ordered = external + [s for s in ollamas if s not in external]
446
+ primary = ordered[0]
447
  config["inference"] = {
448
  "type": primary["type"], "url": primary["url"],
449
  "label": primary["label"]}
450
  config["openai_models"] = []
451
+ # Exclude pre-existing basecamp/ aliases from the model list β€”
452
+ # they're created by write_basecamp_env, not user models.
453
+ config["ollama_models"] = [
454
+ m for m in primary.get("details", {}).get("models", [])
455
+ if not str(m).startswith("basecamp/")
456
+ ]
457
+ if len(ordered) > 1:
458
+ config["secondary"] = {"type": "ollama", "url": ordered[1]["url"], "label": ordered[1]["label"]}
459
  elif openai_engines:
460
  primary = openai_engines[0]
461
  config["inference"] = {
 
859
  api_key = config.get("api_keys", {}).get(primary.get("url", ""), "") or "ollama"
860
 
861
  models = config.get("openai_models") or config.get("ollama_models") or ["llama3.1:8b"]
862
+ # Never pick a pre-namespaced basecamp/ alias as the base model β€” it would
863
+ # get double-prefixed below (basecamp/basecamp/...). Prefer a bare id.
864
+ bare_models = [m for m in models if not str(m).startswith("basecamp/")]
865
+ default_model = (bare_models[0] if bare_models else models[0]) if models else "llama3.1:8b"
866
+
867
+ # Namespace the model as basecamp/<model> so the hermes status/model
868
+ # readout clearly identifies this as the Basecamp instance. Only for
869
+ # ollama engines (they support aliasing via /api/copy); openai-compatible
870
+ # engines get the bare id. Best-effort: if the alias already exists or the
871
+ # server rejects it, fall back to the bare model id.
872
+ if primary.get("type") == "ollama":
873
+ ns_model = f"basecamp/{default_model}"
874
+ root_url = primary["url"].rstrip("/")
875
+ existing = http_get(f"{root_url}/api/tags", timeout=2) or ""
876
+ if f'"{ns_model}"' in existing:
877
+ default_model = ns_model
878
+ else:
879
+ http_post(
880
+ f"{root_url}/api/copy",
881
+ data={"source": default_model, "destination": ns_model},
882
+ timeout=5,
883
+ )
884
+ # VERIFY the alias actually exists before using it β€” /api/copy can
885
+ # return 404-with-body (source missing) which http_post surfaces as
886
+ # a non-None string, so a truthiness check would wrongly pass.
887
+ after = http_get(f"{root_url}/api/tags", timeout=2) or ""
888
+ if f'"{ns_model}"' in after:
889
+ default_model = ns_model
890
 
891
  env_path = Path(os.environ.get("BASECAMP_ENV_FILE", "/opt/basecamp/basecamp.env"))
892
  env_path.parent.mkdir(parents=True, exist_ok=True)
893
  env_path.write_text(
894
  f'export OPENAI_BASE_URL="{base_url}"\n'
895
  f'export OPENAI_API_KEY="{api_key}"\n'
896
+ # namespaced model id (basecamp/<model>) when the engine supports it β€”
897
+ # the readout then shows Basecamp's identity; falls back to the bare
898
+ # id for engines that can't alias (tabbyapi/vllm/etc).
899
  f'export HERMES_MODEL="{default_model}"\n'
900
  )
901
  try:
entrypoint.sh CHANGED
@@ -21,14 +21,6 @@ for i in $(seq 1 60); do
21
  sleep 1
22
  done
23
 
24
- # Pull default model in background
25
- if [ -n "$BASECAMP_MODEL" ]; then
26
- if ! ollama list 2>/dev/null | grep -q "$BASECAMP_MODEL"; then
27
- echo "Pulling $BASECAMP_MODEL in background..."
28
- ollama pull "$BASECAMP_MODEL" > /dev/null 2>&1 &
29
- fi
30
- fi
31
-
32
  # First run: show connect screen. Everything written lives inside basecamp's
33
  # own state (the mounted volume) β€” never touches a host hermes install.
34
  CONFIG_FILE="/root/.hermes/basecamp_config.json"
@@ -93,6 +85,23 @@ if [ -n "${OPENAI_BASE_URL:-}" ]; then
93
  rm -f "${HERMES_HOME:-/root/.hermes}/context_length_cache.yaml" 2>/dev/null || true
94
  fi
95
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  echo ""
97
 
98
  # Handle arguments
 
21
  sleep 1
22
  done
23
 
 
 
 
 
 
 
 
 
24
  # First run: show connect screen. Everything written lives inside basecamp's
25
  # own state (the mounted volume) β€” never touches a host hermes install.
26
  CONFIG_FILE="/root/.hermes/basecamp_config.json"
 
85
  rm -f "${HERMES_HOME:-/root/.hermes}/context_length_cache.yaml" 2>/dev/null || true
86
  fi
87
 
88
+ # Basecamp's own skin β€” install into basecamp's HERMES_HOME and activate it.
89
+ # (Writes only inside the container's own state, never a host install.)
90
+ if [ -d /opt/basecamp/skins ] && [ -f /opt/basecamp/skins/indigo.yaml ]; then
91
+ mkdir -p "${HERMES_HOME:-/root/.hermes}/skins"
92
+ cp /opt/basecamp/skins/indigo.yaml "${HERMES_HOME:-/root/.hermes}/skins/indigo.yaml"
93
+ hermes config set display.skin indigo > /dev/null 2>&1 || true
94
+ fi
95
+
96
+ # Pull default model in background β€” AFTER discovery so the network scan
97
+ # isn't competing with a multi-GB download (missed-service boot race).
98
+ if [ -n "$BASECAMP_MODEL" ]; then
99
+ if ! ollama list 2>/dev/null | grep -q "$BASECAMP_MODEL"; then
100
+ echo "Pulling $BASECAMP_MODEL in background..."
101
+ ollama pull "$BASECAMP_MODEL" > /dev/null 2>&1 &
102
+ fi
103
+ fi
104
+
105
  echo ""
106
 
107
  # Handle arguments
index.html ADDED
@@ -0,0 +1,548 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Basecamp β€” Portable AI Agent with Auto-Discovery</title>
7
+ <style>
8
+ :root {
9
+ --navy: #0B1F3A;
10
+ --blue: #1F5FA8;
11
+ --accent: #E8730C;
12
+ --light: #EEF3F8;
13
+ --gray: #3E4C59;
14
+ --green: #1E8E5A;
15
+ --cream: #FFF7EC;
16
+ --tag: #FFD9A8;
17
+ --purple: #6C3FA8;
18
+ --teal: #0E8E8E;
19
+ --red: #C0392B;
20
+ --dark: #0a0e1a;
21
+ }
22
+ * { margin: 0; padding: 0; box-sizing: border-box; }
23
+ body {
24
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
25
+ background: #f5f7fa;
26
+ color: var(--gray);
27
+ line-height: 1.6;
28
+ -webkit-font-smoothing: antialiased;
29
+ }
30
+
31
+ /* ---- BANNER ---- */
32
+ .banner {
33
+ background: linear-gradient(135deg, var(--dark) 0%, var(--navy) 50%, #152845 100%);
34
+ color: white;
35
+ text-align: center;
36
+ padding: 50px 24px 40px;
37
+ }
38
+ .banner .logo {
39
+ font-size: 2.8rem;
40
+ font-weight: 900;
41
+ letter-spacing: 2px;
42
+ margin-bottom: 8px;
43
+ background: linear-gradient(90deg, var(--accent), var(--tag));
44
+ -webkit-background-clip: text;
45
+ -webkit-text-fill-color: transparent;
46
+ background-clip: text;
47
+ }
48
+ .banner .tagline {
49
+ font-size: 1.2rem;
50
+ color: var(--tag);
51
+ max-width: 700px;
52
+ margin: 0 auto 16px;
53
+ line-height: 1.5;
54
+ }
55
+ .banner .contact {
56
+ font-size: 0.85rem;
57
+ color: var(--tag);
58
+ opacity: 0.7;
59
+ }
60
+ .banner .links {
61
+ margin-top: 18px;
62
+ display: flex;
63
+ gap: 12px;
64
+ justify-content: center;
65
+ flex-wrap: wrap;
66
+ }
67
+ .banner .links a {
68
+ display: inline-block;
69
+ padding: 10px 20px;
70
+ background: var(--accent);
71
+ color: white;
72
+ text-decoration: none;
73
+ border-radius: 6px;
74
+ font-weight: 700;
75
+ font-size: 0.9rem;
76
+ transition: transform 0.15s, box-shadow 0.15s;
77
+ }
78
+ .banner .links a:hover {
79
+ transform: translateY(-2px);
80
+ box-shadow: 0 4px 12px rgba(232,115,12,0.3);
81
+ }
82
+ .banner .links a.secondary {
83
+ background: transparent;
84
+ border: 1.5px solid var(--blue);
85
+ color: var(--tag);
86
+ }
87
+
88
+ /* ---- KICKER ---- */
89
+ .kicker-wrap { padding: 0 24px; }
90
+ .kicker {
91
+ background: var(--light);
92
+ border: 1.5px solid var(--blue);
93
+ text-align: center;
94
+ padding: 14px 20px;
95
+ margin: -18px auto 0;
96
+ max-width: 820px;
97
+ border-radius: 6px;
98
+ position: relative;
99
+ z-index: 1;
100
+ }
101
+ .kicker p {
102
+ font-size: 1.1rem;
103
+ font-weight: 700;
104
+ color: var(--navy);
105
+ }
106
+ .kicker code {
107
+ background: var(--navy);
108
+ color: var(--tag);
109
+ padding: 3px 8px;
110
+ border-radius: 4px;
111
+ font-size: 0.9rem;
112
+ font-family: 'Courier New', monospace;
113
+ }
114
+
115
+ /* ---- MAIN ---- */
116
+ .container {
117
+ max-width: 820px;
118
+ margin: 0 auto;
119
+ padding: 28px 24px 60px;
120
+ }
121
+
122
+ .section { margin-bottom: 32px; }
123
+ .section h2 {
124
+ font-size: 1.3rem;
125
+ font-weight: 800;
126
+ color: var(--navy);
127
+ margin-bottom: 4px;
128
+ }
129
+ .section h2::before {
130
+ content: "\25B6";
131
+ color: var(--accent);
132
+ margin-right: 8px;
133
+ font-size: 0.85rem;
134
+ }
135
+ .section hr {
136
+ border: none;
137
+ border-top: 1.5px solid var(--blue);
138
+ margin-bottom: 12px;
139
+ }
140
+ .section p {
141
+ font-size: 0.95rem;
142
+ margin-bottom: 10px;
143
+ }
144
+ .section ul {
145
+ list-style: none;
146
+ padding: 0;
147
+ }
148
+ .section ul li {
149
+ font-size: 0.93rem;
150
+ padding: 5px 0 5px 22px;
151
+ position: relative;
152
+ }
153
+ .section ul li::before {
154
+ content: "\2022";
155
+ position: absolute;
156
+ left: 4px;
157
+ top: 0;
158
+ color: var(--blue);
159
+ font-weight: bold;
160
+ }
161
+ .section ul li b { color: var(--navy); }
162
+
163
+ /* ---- CODE BLOCK ---- */
164
+ .code-block {
165
+ background: var(--dark);
166
+ color: #c8d6e5;
167
+ padding: 16px 20px;
168
+ border-radius: 6px;
169
+ font-family: 'Courier New', monospace;
170
+ font-size: 0.88rem;
171
+ margin: 12px 0;
172
+ overflow-x: auto;
173
+ border-left: 3px solid var(--accent);
174
+ white-space: pre;
175
+ line-height: 1.5;
176
+ }
177
+ .code-block .comment { color: #5a7a9a; }
178
+ .code-block .cmd { color: var(--accent); font-weight: bold; }
179
+ .code-block .flag { color: var(--teal); }
180
+ .code-block .str { color: var(--green); }
181
+ .code-block .prompt { color: var(--tag); }
182
+
183
+ /* ---- CONNECT SCREEN MOCK ---- */
184
+ .connect-mock {
185
+ background: #0d1117;
186
+ color: #c9d1d9;
187
+ border-radius: 8px;
188
+ padding: 20px 24px;
189
+ font-family: 'Courier New', monospace;
190
+ font-size: 0.85rem;
191
+ margin: 14px 0;
192
+ border: 1px solid #1f2937;
193
+ line-height: 1.65;
194
+ }
195
+ .connect-mock .header { color: var(--accent); font-weight: bold; }
196
+ .connect-mock .svc { color: var(--blue); }
197
+ .connect-mock .svc-icon { color: var(--green); font-weight: bold; }
198
+ .connect-mock .label { color: #8b949e; }
199
+ .connect-mock .prompt { color: var(--tag); }
200
+ .connect-mock .selected { color: var(--accent); }
201
+
202
+ /* ---- DISCOVERY GRID ---- */
203
+ .disc-grid {
204
+ display: grid;
205
+ grid-template-columns: 1fr;
206
+ gap: 10px;
207
+ margin-top: 14px;
208
+ }
209
+ @media (min-width: 640px) {
210
+ .disc-grid { grid-template-columns: 1fr 1fr; }
211
+ }
212
+ .disc-card {
213
+ background: white;
214
+ border: 1px solid #d1d9e0;
215
+ border-radius: 6px;
216
+ padding: 14px 16px;
217
+ font-size: 0.88rem;
218
+ }
219
+ .disc-card .icon {
220
+ display: inline-block;
221
+ font-weight: bold;
222
+ font-size: 0.75rem;
223
+ padding: 2px 8px;
224
+ border-radius: 3px;
225
+ margin-right: 8px;
226
+ color: white;
227
+ }
228
+ .disc-card .icon.llm { background: var(--blue); }
229
+ .disc-card .icon.exl { background: var(--purple); }
230
+ .disc-card .icon.src { background: var(--teal); }
231
+ .disc-card .icon.mcp { background: var(--accent); }
232
+ .disc-card .icon.ui { background: var(--green); }
233
+ .disc-card .icon.cht { background: var(--red); }
234
+ .disc-card b { color: var(--navy); }
235
+ .disc-card .auth {
236
+ font-size: 0.78rem;
237
+ color: var(--accent);
238
+ margin-left: 4px;
239
+ }
240
+
241
+ /* ---- ARCHITECTURE ---- */
242
+ .arch {
243
+ background: #0d1117;
244
+ color: #8b949e;
245
+ border-radius: 8px;
246
+ padding: 20px;
247
+ font-family: 'Courier New', monospace;
248
+ font-size: 0.82rem;
249
+ margin: 14px 0;
250
+ line-height: 1.5;
251
+ overflow-x: auto;
252
+ white-space: pre;
253
+ }
254
+ .arch .box { color: var(--blue); }
255
+ .arch .highlight { color: var(--accent); font-weight: bold; }
256
+ .arch .arrow { color: var(--green); }
257
+
258
+ /* ---- COMPARISON ---- */
259
+ .compare-table {
260
+ width: 100%;
261
+ border-collapse: collapse;
262
+ margin-top: 14px;
263
+ font-size: 0.9rem;
264
+ }
265
+ .compare-table th {
266
+ background: var(--navy);
267
+ color: white;
268
+ padding: 10px 14px;
269
+ text-align: left;
270
+ font-weight: 700;
271
+ }
272
+ .compare-table td {
273
+ padding: 10px 14px;
274
+ border-bottom: 1px solid #e0e4e8;
275
+ }
276
+ .compare-table tr:nth-child(even) td { background: var(--light); }
277
+ .compare-table .yes { color: var(--green); font-weight: bold; }
278
+ .compare-table .no { color: var(--red); }
279
+
280
+ /* ---- FOOTER ---- */
281
+ .footer {
282
+ text-align: center;
283
+ padding: 30px 24px;
284
+ background: var(--navy);
285
+ color: var(--tag);
286
+ font-size: 0.85rem;
287
+ }
288
+ .footer a { color: var(--tag); text-decoration: none; }
289
+ .footer a:hover { text-decoration: underline; }
290
+ .footer .links { margin-top: 10px; }
291
+ .footer .links a {
292
+ margin: 0 8px;
293
+ }
294
+
295
+ /* ---- BADGE ---- */
296
+ .badge {
297
+ display: inline-block;
298
+ padding: 3px 10px;
299
+ background: var(--green);
300
+ color: white;
301
+ border-radius: 12px;
302
+ font-size: 0.75rem;
303
+ font-weight: 700;
304
+ margin-left: 8px;
305
+ vertical-align: middle;
306
+ }
307
+ </style>
308
+ </head>
309
+ <body>
310
+
311
+ <div class="banner">
312
+ <div class="logo">BASECAMP</div>
313
+ <div class="tagline">The first portable AI agent container that auto-discovers your local AI stack β€” Hermes Agent + Ollama in one box that finds your whole stack and just works.</div>
314
+ <div class="contact">No hardcoded URLs. No config files. Just <code style="color:var(--tag);">docker run</code>.</div>
315
+ <div class="links">
316
+ <a href="https://hub.docker.com/r/jpanasuk/basecamp">Get the image</a>
317
+ <a href="https://github.com/jpanasuk-netizen" class="secondary">GitHub</a>
318
+ </div>
319
+ </div>
320
+
321
+ <div class="kicker-wrap">
322
+ <div class="kicker">
323
+ <p>One command to connect: <code>docker run -it --rm --network your-ai-network local/basecamp</code></p>
324
+ </div>
325
+ </div>
326
+
327
+ <div class="container">
328
+
329
+ <!-- PROBLEM -->
330
+ <div class="section">
331
+ <h2>The Problem</h2>
332
+ <hr>
333
+ <p>You've got local AI services running β€” Ollama, maybe TabbyAPI or vLLM, maybe SearXNG. They work. But to actually <b>use</b> them together, you're either:</p>
334
+ <ul>
335
+ <li>Opening 5 browser tabs and copy-pasting between them</li>
336
+ <li>Hardcoding API URLs and keys into config files</li>
337
+ <li>Writing glue code every time you add a service</li>
338
+ </ul>
339
+ <p style="margin-top:8px;color:var(--accent);font-weight:600;">Nothing ties your stack together. Until now.</p>
340
+ </div>
341
+
342
+ <!-- SOLUTION -->
343
+ <div class="section">
344
+ <h2>What Basecamp Does <span class="badge">Auto-Discovery</span></h2>
345
+ <hr>
346
+ <p>Basecamp is a Docker image containing <b>Hermes Agent</b> + <b>Ollama</b>. When you run it on a Docker network, it:</p>
347
+ <ul>
348
+ <li><b>Scans</b> for AI services using DNS probes, port scanning, and Docker network inspection</li>
349
+ <li><b>Identifies</b> service types by fingerprinting HTTP responses (Ollama, TabbyAPI, vLLM, SearXNG, MCPO, and more)</li>
350
+ <li><b>Shows a connect screen</b> where you pick which services to use</li>
351
+ <li><b>Prompts for API keys</b> when a service needs auth β€” no hardcoded secrets</li>
352
+ <li><b>Generates a <code>tavern</code> CLI</b> dynamically from what it found</li>
353
+ <li><b>Drops you into Hermes Agent</b> connected to your entire stack</li>
354
+ </ul>
355
+ </div>
356
+
357
+ <!-- CONNECT SCREEN -->
358
+ <div class="section">
359
+ <h2>The Connect Screen</h2>
360
+ <hr>
361
+ <p>First run, basecamp scans your network and shows you what it found:</p>
362
+ <div class="connect-mock">
363
+ <span class="header">============================================================</span>
364
+ <span class="header"> BASECAMP β€” AI Agent + Local Inference</span>
365
+ <span class="header"> Connect Screen</span>
366
+ <span class="header">============================================================</span>
367
+
368
+ Discovered 5 service(s):
369
+
370
+ [ 0] <span class="svc-icon">LLM</span> <span class="svc">Ollama (GGUF inference)</span> [1 model(s)]
371
+ <span class="label">-> http://ollama:11434</span>
372
+ [ 1] <span class="svc-icon">EXL</span> <span class="svc">TabbyAPI / OpenAI-compatible (EXL3/EXL2)</span> [1 model(s)] [needs auth]
373
+ <span class="label">-> http://tabbyapi:5000</span>
374
+ [ 2] <span class="svc-icon">SRC</span> <span class="svc">SearXNG (private search)</span>
375
+ <span class="label">-> http://searxng:8080</span>
376
+ [ 3] <span class="svc-icon">MCP</span> <span class="svc">MCPO (MCP OpenAPI proxy)</span>
377
+ <span class="label">-> http://mcpo:8000</span>
378
+ [ 4] <span class="svc-icon">UI </span> <span class="svc">Open WebUI (LLM workspace)</span>
379
+ <span class="label">-> http://open-webui:8080</span>
380
+
381
+ <span class="prompt">PRIMARY inference engine:
382
+ 1. Ollama (GGUF inference) (http://ollama:11434)
383
+ 2. TabbyAPI / OpenAI-compatible (http://tabbyapi:5000)
384
+ 0. Use basecamp's own Ollama
385
+ Choice [1]:</span> <span class="selected">2</span>
386
+
387
+ <span class="prompt">TabbyAPI requires authentication (bearer).
388
+ Enter API key (or press Enter to skip):</span> <span class="selected">your-key-here</span>
389
+
390
+ <span class="prompt">Save and continue? [Y/n]:</span> <span class="selected">Y</span>
391
+ </div>
392
+ <p style="font-size:0.85rem;color:#8b949e;">Subsequent runs skip this β€” config is saved to a persistent volume. Run <code>tavern rediscover</code> to re-scan.</p>
393
+ </div>
394
+
395
+ <!-- QUICK START -->
396
+ <div class="section">
397
+ <h2>Quick Start</h2>
398
+ <hr>
399
+ <div class="code-block"><span class="comment"># Pull the image (Docker Hub)</span>
400
+ <span class="cmd">docker pull</span> <span class="str">jpanasuk/basecamp:1.0.2</span>
401
+
402
+ <span class="comment"># Run against your AI network</span>
403
+ <span class="cmd">docker run</span> <span class="flag">-it --rm</span> \
404
+ <span class="flag">--network</span> <span class="str">your-ai-network</span> \
405
+ <span class="flag">--gpus all</span> \
406
+ <span class="flag">-v</span> <span class="str">basecamp-ollama:/root/.ollama</span> \
407
+ <span class="flag">-v</span> <span class="str">basecamp-hermes:/root/.hermes</span> \
408
+ <span class="str">jpanasuk/basecamp:1.0.2</span>
409
+
410
+ <span class="comment"># First run: connect screen appears
411
+ # Pick services, enter API keys if needed
412
+ # Then you're in Hermes Agent with tavern CLI</span>
413
+
414
+ <span class="prompt">tavern status</span> <span class="comment"># show connected services</span>
415
+ <span class="prompt">tavern chat "hello"</span> <span class="comment"># chat with EXL3 model (streaming)</span>
416
+ <span class="prompt">tavern search "AI news"</span> <span class="comment"># search via SearXNG</span>
417
+ <span class="prompt">tavern mcp</span> <span class="comment"># list MCP tools</span>
418
+ <span class="prompt">tavern models</span> <span class="comment"># list available models</span></div>
419
+ </div>
420
+
421
+ <!-- DISCOVERY TABLE -->
422
+ <div class="section">
423
+ <h2>What It Discovers</h2>
424
+ <hr>
425
+ <div class="disc-grid">
426
+ <div class="disc-card"><span class="icon llm">LLM</span><b>Ollama</b><br>GGUF inference<br><span style="font-size:0.8rem;color:#8b949e;">Auto-discovered, no auth</span></div>
427
+ <div class="disc-card"><span class="icon exl">EXL</span><b>TabbyAPI</b><br>EXL3/EXL2 inference<span class="auth">needs API key</span><br><span style="font-size:0.8rem;color:#8b949e;">Prompted during connect</span></div>
428
+ <div class="disc-card"><span class="icon llm">LLM</span><b>vLLM</b><br>High-throughput inference<br><span style="font-size:0.8rem;color:#8b949e;">Auto-discovered, no auth</span></div>
429
+ <div class="disc-card"><span class="icon llm">PXY</span><b>LiteLLM</b><br>Multi-provider proxy<span class="auth">needs API key</span><br><span style="font-size:0.8rem;color:#8b949e;">Prompted during connect</span></div>
430
+ <div class="disc-card"><span class="icon llm">LLM</span><b>LocalAI</b><br>OpenAI replacement<br><span style="font-size:0.8rem;color:#8b949e;">Auto-discovered, no auth</span></div>
431
+ <div class="disc-card"><span class="icon llm">LLM</span><b>llama.cpp</b><br>Lightweight server<br><span style="font-size:0.8rem;color:#8b949e;">Auto-discovered, no auth</span></div>
432
+ <div class="disc-card"><span class="icon src">SRC</span><b>SearXNG</b><br>Private metasearch<br><span style="font-size:0.8rem;color:#8b949e;">Auto-discovered, no auth</span></div>
433
+ <div class="disc-card"><span class="icon mcp">MCP</span><b>MCPO</b><br>MCP OpenAPI proxy<span class="auth">needs API key</span><br><span style="font-size:0.8rem;color:#8b949e;">Prompted during connect</span></div>
434
+ <div class="disc-card"><span class="icon ui">UI</span><b>Open WebUI</b><br>LLM workspace<br><span style="font-size:0.8rem;color:#8b949e;">Auto-discovered, no auth</span></div>
435
+ <div class="disc-card"><span class="icon cht">CHT</span><b>SillyTavern</b><br>Character chat<span class="auth">needs basic auth</span><br><span style="font-size:0.8rem;color:#8b949e;">Tries default, prompts if fails</span></div>
436
+ </div>
437
+ </div>
438
+
439
+ <!-- WHY DIFFERENT -->
440
+ <div class="section">
441
+ <h2>Why It's Different</h2>
442
+ <hr>
443
+ <table class="compare-table">
444
+ <tr>
445
+ <th>Feature</th>
446
+ <th>Other tools</th>
447
+ <th>Basecamp</th>
448
+ </tr>
449
+ <tr>
450
+ <td>Service discovery</td>
451
+ <td class="no">Manual config</td>
452
+ <td class="yes">Auto-discover</td>
453
+ </tr>
454
+ <tr>
455
+ <td>API keys</td>
456
+ <td class="no">Hardcoded in config</td>
457
+ <td class="yes">Prompted at connect</td>
458
+ </tr>
459
+ <tr>
460
+ <td>Works with any stack</td>
461
+ <td class="no">Tied to one app</td>
462
+ <td class="yes">Stack-agnostic</td>
463
+ </tr>
464
+ <tr>
465
+ <td>Agent included</td>
466
+ <td class="no">BYO agent</td>
467
+ <td class="yes">Hermes Agent built-in</td>
468
+ </tr>
469
+ <tr>
470
+ <td>Streaming chat</td>
471
+ <td class="no">Usually no</td>
472
+ <td class="yes">Yes, by default</td>
473
+ </tr>
474
+ <tr>
475
+ <td>Re-scan after changes</td>
476
+ <td class="no">Edit config again</td>
477
+ <td class="yes">tavern rediscover</td>
478
+ </tr>
479
+ <tr>
480
+ <td>Secrets in image</td>
481
+ <td class="no">Often yes</td>
482
+ <td class="yes">Zero hardcoded keys</td>
483
+ </tr>
484
+ </table>
485
+ </div>
486
+
487
+ <!-- ARCHITECTURE -->
488
+ <div class="section">
489
+ <h2>Architecture</h2>
490
+ <hr>
491
+ <div class="arch"><span class="highlight">β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”</span>
492
+ <span class="highlight">β”‚ basecamp container β”‚</span>
493
+ <span class="highlight">β”‚</span> <span class="box">β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”</span> <span class="box">β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”</span> <span class="highlight">β”‚</span>
494
+ <span class="highlight">β”‚</span> <span class="box">β”‚ Ollama β”‚</span> <span class="box">β”‚ Hermes Agent β”‚</span> <span class="highlight">β”‚</span>
495
+ <span class="highlight">β”‚</span> <span class="box">β”‚ (GGUF) β”‚</span> <span class="box">β”‚ (CLI AI assistant) β”‚</span> <span class="highlight">β”‚</span>
496
+ <span class="highlight">β”‚</span> <span class="box">β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜</span> <span class="box">β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜</span> <span class="highlight">β”‚</span>
497
+ <span class="highlight">β”‚</span> <span class="box">β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”</span> <span class="highlight">β”‚</span>
498
+ <span class="highlight">β”‚</span> <span class="box">β”‚ discover.py β”‚</span> <span class="highlight">β”‚</span>
499
+ <span class="highlight">β”‚</span> <span class="box">β”‚ - scans network for AI services β”‚</span> <span class="highlight">β”‚</span>
500
+ <span class="highlight">β”‚</span> <span class="box">β”‚ - connect screen TUI β”‚</span> <span class="highlight">β”‚</span>
501
+ <span class="highlight">β”‚</span> <span class="box">β”‚ - generates tavern CLI dynamically β”‚</span> <span class="highlight">β”‚</span>
502
+ <span class="highlight">β”‚</span> <span class="box">β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜</span> <span class="highlight">β”‚</span>
503
+ <span class="highlight">β”‚</span> <span class="box">β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”</span> <span class="highlight">β”‚</span>
504
+ <span class="highlight">β”‚</span> <span class="box">β”‚ tavern (auto-generated) β”‚</span> <span class="highlight">β”‚</span>
505
+ <span class="highlight">β”‚</span> <span class="box">β”‚ - chat, search, mcp, models, status β”‚</span> <span class="highlight">β”‚</span>
506
+ <span class="highlight">β”‚</span> <span class="box">β”‚ - streaming responses β”‚</span> <span class="highlight">β”‚</span>
507
+ <span class="highlight">β”‚</span> <span class="box">β”‚ - rediscover on demand β”‚</span> <span class="highlight">β”‚</span>
508
+ <span class="highlight">β”‚</span> <span class="box">β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜</span> <span class="highlight">β”‚</span>
509
+ <span class="highlight">β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜</span>
510
+ <span class="arrow">β”‚</span> <span class="arrow">β”‚</span> <span class="arrow">β”‚</span>
511
+ <span class="arrow">β–Ό</span> <span class="arrow">β–Ό</span> <span class="arrow">β–Ό</span>
512
+ <span class="box">β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”</span> <span class="box">β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”</span> <span class="box">β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”</span>
513
+ <span class="box">β”‚ TabbyAPI β”‚</span> <span class="box">β”‚ Ollama β”‚</span> <span class="box">β”‚ SearXNG β”‚</span>
514
+ <span class="box">β”‚ (EXL3) β”‚</span> <span class="box">β”‚ (GGUF) β”‚</span> <span class="box">β”‚ (search) β”‚</span>
515
+ <span class="box">β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜</span> <span class="box">β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜</span> <span class="box">β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜</span>
516
+ <span class="arrow">β”‚</span> <span class="arrow">β”‚</span>
517
+ <span class="arrow">β–Ό</span> <span class="arrow">β–Ό</span>
518
+ <span class="box">β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”</span> <span class="box">β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”</span>
519
+ <span class="box">β”‚ MCPO β”‚</span> <span class="box">β”‚Open WebUIβ”‚</span>
520
+ <span class="box">β”‚ (tools) β”‚</span> <span class="box">β”‚ (UI) β”‚</span>
521
+ <span class="box">β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜</span> <span class="box">β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜</span></div>
522
+ </div>
523
+
524
+ <!-- RELATED -->
525
+ <div class="section">
526
+ <h2>Related Projects</h2>
527
+ <hr>
528
+ <ul>
529
+ <li><b>Tabby Tavern Stack</b> β€” the full 6-service local AI infrastructure (TabbyAPI, SillyTavern, Open WebUI, Ollama, SearXNG, MCPO) β†’ <a href="https://huggingface.co/jpanasuk/tabby-tavern-stack" style="color:var(--blue);">huggingface.co/jpanasuk/tabby-tavern-stack</a></li>
530
+ <li><b>Basecamp</b> β€” this project, the portable agent container β†’ <a href="https://huggingface.co/jpanasuk/basecamp" style="color:var(--blue);">huggingface.co/jpanasuk/basecamp</a></li>
531
+ </ul>
532
+ </div>
533
+
534
+ </div>
535
+
536
+ <div class="footer">
537
+ <div><b>Jeremy Panasuk</b> β€” enterprise data architect turned local AI systems engineer</div>
538
+ <div class="links">
539
+ <a href="https://huggingface.co/jpanasuk">HuggingFace</a>
540
+ <a href="https://github.com/jpanasuk-netizen">GitHub</a>
541
+ <a href="https://www.linkedin.com/in/jeremy-p-34203322">LinkedIn</a>
542
+ </div>
543
+ <div style="margin-top:8px;opacity:0.6;font-size:0.78rem;">MIT License Β· No secrets shipped Β· Built on Hermes Agent + Ollama</div>
544
+ <div style="margin-top:4px;opacity:0.6;font-size:0.78rem;">Agent by <a href="https://nousresearch.com" style="color:inherit;">Nous Research</a> β€” this box just wraps it. All props to the Hermes team. πŸ™</div>
545
+ </div>
546
+
547
+ </body>
548
+ </html>
skins/indigo.yaml ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: indigo
2
+ description: Basecamp indigo β€” deep violet and electric blue
3
+
4
+ branding:
5
+ agent_name: "Basecamp Hermes"
6
+ welcome: "Welcome to Basecamp Hermes!"
7
+ goodbye: "Goodbye from Basecamp! βš•"
8
+ response_label: " βš• Basecamp Hermes "
9
+ prompt_symbol: "❯"
10
+
11
+ colors:
12
+ # Base surface
13
+ background: "#12101f"
14
+
15
+ # Brand accent β€” indigo/violet
16
+ ui_accent: "#7c6cf0"
17
+ banner_accent: "#7c6cf0"
18
+ ui_primary: "#8b7cf8"
19
+
20
+ # Text hierarchy
21
+ banner_title: "#a5b4fc"
22
+ banner_text: "#e0e0f0"
23
+ ui_text: "#e0e0f0"
24
+ banner_dim: "#6f6a99"
25
+ banner_border: "#3b3670"
26
+ ui_border: "#3b3670"
27
+
28
+ # Semantic status
29
+ ui_ok: "#4ade80"
30
+ ui_warn: "#fbbf24"
31
+ ui_error: "#f87171"
32
+ ui_label: "#a5b4fc"
33
+
34
+ # CLI / TUI chrome
35
+ prompt: "#e0e0f0"
36
+ input_rule: "#7c6cf0"
37
+ response_border: "#a5b4fc"
38
+ status_bar_bg: "#0c0a17"
39
+ status_bar_text: "#e0e0f0"
40
+ status_bar_good: "#4ade80"
41
+ status_bar_warn: "#fbbf24"
42
+ status_bar_critical: "#f87171"
43
+ session_label: "#a5b4fc"