getbased-agent-stack 0.3.1__tar.gz → 0.4.1__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.
- {getbased_agent_stack-0.3.1/src/getbased_agent_stack.egg-info → getbased_agent_stack-0.4.1}/PKG-INFO +54 -24
- {getbased_agent_stack-0.3.1 → getbased_agent_stack-0.4.1}/README.md +49 -19
- {getbased_agent_stack-0.3.1 → getbased_agent_stack-0.4.1}/pyproject.toml +10 -5
- {getbased_agent_stack-0.3.1 → getbased_agent_stack-0.4.1}/src/getbased_agent_stack/__init__.py +1 -1
- getbased_agent_stack-0.4.1/src/getbased_agent_stack/cli.py +366 -0
- getbased_agent_stack-0.4.1/src/getbased_agent_stack/env_file.py +102 -0
- getbased_agent_stack-0.4.1/src/getbased_agent_stack/mcp_configs.py +177 -0
- getbased_agent_stack-0.4.1/src/getbased_agent_stack/systemd/getbased-dashboard.service +32 -0
- getbased_agent_stack-0.4.1/src/getbased_agent_stack/systemd/getbased-rag.service +40 -0
- getbased_agent_stack-0.4.1/src/getbased_agent_stack/units.py +198 -0
- {getbased_agent_stack-0.3.1 → getbased_agent_stack-0.4.1/src/getbased_agent_stack.egg-info}/PKG-INFO +54 -24
- {getbased_agent_stack-0.3.1 → getbased_agent_stack-0.4.1}/src/getbased_agent_stack.egg-info/SOURCES.txt +11 -1
- getbased_agent_stack-0.4.1/src/getbased_agent_stack.egg-info/requires.txt +11 -0
- getbased_agent_stack-0.4.1/tests/test_cli.py +310 -0
- getbased_agent_stack-0.4.1/tests/test_env_file.py +106 -0
- getbased_agent_stack-0.4.1/tests/test_mcp_configs.py +125 -0
- getbased_agent_stack-0.4.1/tests/test_systemd_units.py +109 -0
- getbased_agent_stack-0.4.1/tests/test_units.py +296 -0
- getbased_agent_stack-0.3.1/src/getbased_agent_stack/cli.py +0 -72
- getbased_agent_stack-0.3.1/src/getbased_agent_stack.egg-info/requires.txt +0 -11
- {getbased_agent_stack-0.3.1 → getbased_agent_stack-0.4.1}/LICENSE +0 -0
- {getbased_agent_stack-0.3.1 → getbased_agent_stack-0.4.1}/setup.cfg +0 -0
- {getbased_agent_stack-0.3.1 → getbased_agent_stack-0.4.1}/src/getbased_agent_stack.egg-info/dependency_links.txt +0 -0
- {getbased_agent_stack-0.3.1 → getbased_agent_stack-0.4.1}/src/getbased_agent_stack.egg-info/entry_points.txt +0 -0
- {getbased_agent_stack-0.3.1 → getbased_agent_stack-0.4.1}/src/getbased_agent_stack.egg-info/top_level.txt +0 -0
- {getbased_agent_stack-0.3.1 → getbased_agent_stack-0.4.1}/tests/test_integration.py +0 -0
{getbased_agent_stack-0.3.1/src/getbased_agent_stack.egg-info → getbased_agent_stack-0.4.1}/PKG-INFO
RENAMED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: getbased-agent-stack
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Summary: One-command install of the full getbased agent stack — getbased-mcp + getbased-rag + getbased-dashboard
|
|
5
5
|
License-Expression: GPL-3.0-only
|
|
6
6
|
Requires-Python: >=3.10
|
|
7
7
|
Description-Content-Type: text/markdown
|
|
8
8
|
License-File: LICENSE
|
|
9
|
-
Requires-Dist: getbased-mcp>=0.2.
|
|
10
|
-
Requires-Dist: getbased-rag>=0.7.
|
|
11
|
-
Requires-Dist: getbased-dashboard>=0.6.
|
|
9
|
+
Requires-Dist: getbased-mcp>=0.2.3
|
|
10
|
+
Requires-Dist: getbased-rag>=0.7.1
|
|
11
|
+
Requires-Dist: getbased-dashboard>=0.6.1
|
|
12
12
|
Provides-Extra: full
|
|
13
|
-
Requires-Dist: getbased-rag[full]>=0.7.
|
|
13
|
+
Requires-Dist: getbased-rag[full]>=0.7.1; extra == "full"
|
|
14
14
|
Provides-Extra: test
|
|
15
15
|
Requires-Dist: pytest>=8.0; extra == "test"
|
|
16
16
|
Requires-Dist: httpx>=0.27; extra == "test"
|
|
@@ -19,7 +19,7 @@ Dynamic: license-file
|
|
|
19
19
|
|
|
20
20
|
# getbased-agent-stack
|
|
21
21
|
|
|
22
|
-
Meta-package bundling the full [getbased](https://getbased.health) agent stack into one install: the MCP adapter, the RAG engine, the browser dashboard,
|
|
22
|
+
Meta-package bundling the full [getbased](https://getbased.health) agent stack into one install: the MCP adapter, the RAG engine, the browser dashboard, an orchestration CLI (`init` / `install` / `mcp-config`), hardened systemd units for rag + dashboard, and paste-ready configs for Claude Desktop/Code, Cursor, Cline, and Hermes.
|
|
23
23
|
|
|
24
24
|
Part of the [getbased-agents monorepo](https://github.com/elkimek/getbased-agents).
|
|
25
25
|
|
|
@@ -39,36 +39,65 @@ Pulls:
|
|
|
39
39
|
|
|
40
40
|
Total install: ~500 MB (the ML deps dominate). Smaller installs available — `pipx install getbased-mcp` (10 MB, agent only), `pipx install "getbased-rag[full]"` (RAG only), `pipx install getbased-dashboard` (UI + MCP; pulls rag if you want the Knowledge tab working).
|
|
41
41
|
|
|
42
|
-
## Quickstart
|
|
42
|
+
## Quickstart — one command
|
|
43
43
|
|
|
44
44
|
```bash
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
getbased-stack init
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The wizard (~30 seconds):
|
|
49
|
+
|
|
50
|
+
1. Prompts for your `GETBASED_TOKEN` (skip if you don't use sync)
|
|
51
|
+
2. Generates a rag API key if one doesn't exist
|
|
52
|
+
3. Writes `~/.config/getbased/env` (mode 0600) — the shared config file
|
|
53
|
+
4. Installs systemd user units for rag + dashboard, enables them, starts them
|
|
54
|
+
|
|
55
|
+
Then paste one line into your MCP client:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
getbased-stack mcp-config claude-desktop # or: claude-code, cursor, cline, hermes
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The snippet carries only `GETBASED_STACK_MANAGED=1` in its env block. No secrets in client configs — every MCP spawn reads the shared env file and loads the token + rag URL + API key path from there.
|
|
62
|
+
|
|
63
|
+
Open the dashboard:
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
http://127.0.0.1:8323
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Login URL with bearer key:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
getbased-dashboard login-url # prints http://127.0.0.1:8323/?key=...
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Upload docs, create libraries, manage sources, and test the MCP probe from the web UI. Rotate the sync token from the CLI (see below) or by editing `~/.config/getbased/env` directly.
|
|
48
76
|
|
|
49
|
-
|
|
50
|
-
getbased-dashboard serve # serves on 127.0.0.1:8323
|
|
77
|
+
### Surviving reboot on headless hosts
|
|
51
78
|
|
|
52
|
-
|
|
53
|
-
# Create libraries, drag-drop files to ingest (live chunks/sec pill),
|
|
54
|
-
# run the MCP Test button to verify your agent path
|
|
79
|
+
User systemd services stop on logout. On a headless server (no GUI session), they won't come back at boot unless you enable linger once:
|
|
55
80
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
# Claude Desktop, Claude Code, Cursor, Cline, and Hermes.
|
|
81
|
+
```bash
|
|
82
|
+
sudo loginctl enable-linger $USER
|
|
59
83
|
```
|
|
60
84
|
|
|
61
|
-
|
|
85
|
+
`getbased-stack init` prints this reminder when it detects a headless environment. On a laptop with a GUI login, linger is nice-to-have — services start when you log in.
|
|
62
86
|
|
|
63
|
-
|
|
87
|
+
### Other commands
|
|
64
88
|
|
|
65
89
|
```bash
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
90
|
+
getbased-stack status # env file, unit state, linger
|
|
91
|
+
getbased-stack set GETBASED_TOKEN=new # rotate the token
|
|
92
|
+
getbased-stack install # re-apply unit files after package upgrade
|
|
93
|
+
getbased-stack uninstall # stop + disable + remove units
|
|
69
94
|
```
|
|
70
95
|
|
|
71
|
-
|
|
96
|
+
### Migrating from an older install
|
|
97
|
+
|
|
98
|
+
If you have a hand-rolled setup (standalone `lens-rag.service`, hermes-style `~/.hermes/config.yaml` with MCP env), **leave it alone** — `getbased-stack init` only writes new paths and installs new unit names (`getbased-rag.service`, `getbased-dashboard.service`), so it can coexist. The opt-in loader in every binary is gated on `GETBASED_STACK_MANAGED=1`; without that flag set, every binary behaves exactly as before.
|
|
99
|
+
|
|
100
|
+
If you're running the existing Hermes VM deployment on this host, don't run `init` there. Your `~/.hermes/config.yaml` continues to supply env explicitly; nothing from this package touches it.
|
|
72
101
|
|
|
73
102
|
## Architecture
|
|
74
103
|
|
|
@@ -97,6 +126,7 @@ The dashboard is likewise stateless — it proxies rag for Knowledge operations,
|
|
|
97
126
|
|---|---|---|---|---|
|
|
98
127
|
| 0.1.x | ≥0.2.0 | ≥0.1.0 | — | v1 (multi-library) |
|
|
99
128
|
| 0.2.x | ≥0.2.2 | ≥0.6.0 | ≥0.5.0 | v1 (+ streaming ingest, per-library models) |
|
|
129
|
+
| 0.4.x | ≥0.2.3 | ≥0.7.1 | ≥0.6.1 | v1 (+ shared env file, `getbased-stack init`, systemd units) |
|
|
100
130
|
|
|
101
131
|
Bump the meta's major when sibling protocols break; bump siblings freely for normal features.
|
|
102
132
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# getbased-agent-stack
|
|
2
2
|
|
|
3
|
-
Meta-package bundling the full [getbased](https://getbased.health) agent stack into one install: the MCP adapter, the RAG engine, the browser dashboard,
|
|
3
|
+
Meta-package bundling the full [getbased](https://getbased.health) agent stack into one install: the MCP adapter, the RAG engine, the browser dashboard, an orchestration CLI (`init` / `install` / `mcp-config`), hardened systemd units for rag + dashboard, and paste-ready configs for Claude Desktop/Code, Cursor, Cline, and Hermes.
|
|
4
4
|
|
|
5
5
|
Part of the [getbased-agents monorepo](https://github.com/elkimek/getbased-agents).
|
|
6
6
|
|
|
@@ -20,36 +20,65 @@ Pulls:
|
|
|
20
20
|
|
|
21
21
|
Total install: ~500 MB (the ML deps dominate). Smaller installs available — `pipx install getbased-mcp` (10 MB, agent only), `pipx install "getbased-rag[full]"` (RAG only), `pipx install getbased-dashboard` (UI + MCP; pulls rag if you want the Knowledge tab working).
|
|
22
22
|
|
|
23
|
-
## Quickstart
|
|
23
|
+
## Quickstart — one command
|
|
24
24
|
|
|
25
25
|
```bash
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
getbased-stack init
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The wizard (~30 seconds):
|
|
30
|
+
|
|
31
|
+
1. Prompts for your `GETBASED_TOKEN` (skip if you don't use sync)
|
|
32
|
+
2. Generates a rag API key if one doesn't exist
|
|
33
|
+
3. Writes `~/.config/getbased/env` (mode 0600) — the shared config file
|
|
34
|
+
4. Installs systemd user units for rag + dashboard, enables them, starts them
|
|
35
|
+
|
|
36
|
+
Then paste one line into your MCP client:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
getbased-stack mcp-config claude-desktop # or: claude-code, cursor, cline, hermes
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The snippet carries only `GETBASED_STACK_MANAGED=1` in its env block. No secrets in client configs — every MCP spawn reads the shared env file and loads the token + rag URL + API key path from there.
|
|
43
|
+
|
|
44
|
+
Open the dashboard:
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
http://127.0.0.1:8323
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Login URL with bearer key:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
getbased-dashboard login-url # prints http://127.0.0.1:8323/?key=...
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Upload docs, create libraries, manage sources, and test the MCP probe from the web UI. Rotate the sync token from the CLI (see below) or by editing `~/.config/getbased/env` directly.
|
|
29
57
|
|
|
30
|
-
|
|
31
|
-
getbased-dashboard serve # serves on 127.0.0.1:8323
|
|
58
|
+
### Surviving reboot on headless hosts
|
|
32
59
|
|
|
33
|
-
|
|
34
|
-
# Create libraries, drag-drop files to ingest (live chunks/sec pill),
|
|
35
|
-
# run the MCP Test button to verify your agent path
|
|
60
|
+
User systemd services stop on logout. On a headless server (no GUI session), they won't come back at boot unless you enable linger once:
|
|
36
61
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
# Claude Desktop, Claude Code, Cursor, Cline, and Hermes.
|
|
62
|
+
```bash
|
|
63
|
+
sudo loginctl enable-linger $USER
|
|
40
64
|
```
|
|
41
65
|
|
|
42
|
-
|
|
66
|
+
`getbased-stack init` prints this reminder when it detects a headless environment. On a laptop with a GUI login, linger is nice-to-have — services start when you log in.
|
|
43
67
|
|
|
44
|
-
|
|
68
|
+
### Other commands
|
|
45
69
|
|
|
46
70
|
```bash
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
71
|
+
getbased-stack status # env file, unit state, linger
|
|
72
|
+
getbased-stack set GETBASED_TOKEN=new # rotate the token
|
|
73
|
+
getbased-stack install # re-apply unit files after package upgrade
|
|
74
|
+
getbased-stack uninstall # stop + disable + remove units
|
|
50
75
|
```
|
|
51
76
|
|
|
52
|
-
|
|
77
|
+
### Migrating from an older install
|
|
78
|
+
|
|
79
|
+
If you have a hand-rolled setup (standalone `lens-rag.service`, hermes-style `~/.hermes/config.yaml` with MCP env), **leave it alone** — `getbased-stack init` only writes new paths and installs new unit names (`getbased-rag.service`, `getbased-dashboard.service`), so it can coexist. The opt-in loader in every binary is gated on `GETBASED_STACK_MANAGED=1`; without that flag set, every binary behaves exactly as before.
|
|
80
|
+
|
|
81
|
+
If you're running the existing Hermes VM deployment on this host, don't run `init` there. Your `~/.hermes/config.yaml` continues to supply env explicitly; nothing from this package touches it.
|
|
53
82
|
|
|
54
83
|
## Architecture
|
|
55
84
|
|
|
@@ -78,6 +107,7 @@ The dashboard is likewise stateless — it proxies rag for Knowledge operations,
|
|
|
78
107
|
|---|---|---|---|---|
|
|
79
108
|
| 0.1.x | ≥0.2.0 | ≥0.1.0 | — | v1 (multi-library) |
|
|
80
109
|
| 0.2.x | ≥0.2.2 | ≥0.6.0 | ≥0.5.0 | v1 (+ streaming ingest, per-library models) |
|
|
110
|
+
| 0.4.x | ≥0.2.3 | ≥0.7.1 | ≥0.6.1 | v1 (+ shared env file, `getbased-stack init`, systemd units) |
|
|
81
111
|
|
|
82
112
|
Bump the meta's major when sibling protocols break; bump siblings freely for normal features.
|
|
83
113
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "getbased-agent-stack"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.1"
|
|
8
8
|
description = "One-command install of the full getbased agent stack — getbased-mcp + getbased-rag + getbased-dashboard"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "GPL-3.0-only"
|
|
@@ -12,15 +12,15 @@ requires-python = ">=3.10"
|
|
|
12
12
|
# Pulls every sibling package. Bump this meta when a sibling protocol
|
|
13
13
|
# bump requires coordinated release.
|
|
14
14
|
dependencies = [
|
|
15
|
-
"getbased-mcp>=0.2.
|
|
16
|
-
"getbased-rag>=0.7.
|
|
17
|
-
"getbased-dashboard>=0.6.
|
|
15
|
+
"getbased-mcp>=0.2.3",
|
|
16
|
+
"getbased-rag>=0.7.1",
|
|
17
|
+
"getbased-dashboard>=0.6.1",
|
|
18
18
|
]
|
|
19
19
|
|
|
20
20
|
[project.optional-dependencies]
|
|
21
21
|
# `full` includes PDF/DOCX parsers and ONNX acceleration (pre-exported
|
|
22
22
|
# weights — no PyTorch/transformers/optimum dance).
|
|
23
|
-
full = ["getbased-rag[full]>=0.7.
|
|
23
|
+
full = ["getbased-rag[full]>=0.7.1"]
|
|
24
24
|
# For contributors running the integration test harness.
|
|
25
25
|
test = ["pytest>=8.0", "httpx>=0.27", "pytest-timeout>=2.3"]
|
|
26
26
|
|
|
@@ -36,6 +36,11 @@ getbased-stack = "getbased_agent_stack.cli:main"
|
|
|
36
36
|
[tool.setuptools.packages.find]
|
|
37
37
|
where = ["src"]
|
|
38
38
|
|
|
39
|
+
[tool.setuptools.package-data]
|
|
40
|
+
# Ship the user systemd unit files inside the wheel so
|
|
41
|
+
# `getbased-stack install` can locate them via importlib.resources.
|
|
42
|
+
getbased_agent_stack = ["systemd/*.service"]
|
|
43
|
+
|
|
39
44
|
[tool.pytest.ini_options]
|
|
40
45
|
testpaths = ["tests"]
|
|
41
46
|
addopts = "-ra -q"
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
"""getbased-stack — orchestration CLI.
|
|
2
|
+
|
|
3
|
+
Subcommands:
|
|
4
|
+
init — interactive one-time setup: token, API key, systemd units
|
|
5
|
+
install — install/refresh the bundled systemd user units
|
|
6
|
+
uninstall — stop, disable, and remove the systemd units
|
|
7
|
+
status — show env file, units, and linger state
|
|
8
|
+
set KEY=VALUE — upsert a single var in the shared env file
|
|
9
|
+
mcp-config CLIENT — print the MCP client config snippet
|
|
10
|
+
version — print installed package versions
|
|
11
|
+
info / serve / everything else → delegate to the `lens` CLI
|
|
12
|
+
|
|
13
|
+
Deliberate zero-dep: argparse + stdlib only. No typer, no click, no
|
|
14
|
+
python-dotenv. The env file format is simple enough that we own it.
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
import os
|
|
20
|
+
import secrets
|
|
21
|
+
import shutil
|
|
22
|
+
import sys
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
from . import env_file, mcp_configs, units
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ── helpers ───────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _default_api_key_file() -> Path:
|
|
32
|
+
xdg = os.environ.get("XDG_DATA_HOME") or os.path.expanduser("~/.local/share")
|
|
33
|
+
return Path(xdg) / "getbased" / "lens" / "api_key"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _ensure_api_key(path: Path) -> str:
|
|
37
|
+
"""Generate a key if missing; return the key text either way. Mode 0600."""
|
|
38
|
+
if path.exists():
|
|
39
|
+
return path.read_text(encoding="utf-8").strip()
|
|
40
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
41
|
+
key = secrets.token_urlsafe(32)
|
|
42
|
+
path.write_text(key + "\n", encoding="utf-8")
|
|
43
|
+
os.chmod(path, 0o600)
|
|
44
|
+
return key
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _prompt(msg: str, default: str = "", secret: bool = False) -> str:
|
|
48
|
+
"""Non-intrusive readline prompt. Treats EOF/Ctrl-D as 'use default'."""
|
|
49
|
+
suffix = f" [{default}]" if default else ""
|
|
50
|
+
try:
|
|
51
|
+
if secret:
|
|
52
|
+
import getpass
|
|
53
|
+
|
|
54
|
+
answer = getpass.getpass(f"{msg}{suffix}: ")
|
|
55
|
+
else:
|
|
56
|
+
answer = input(f"{msg}{suffix}: ")
|
|
57
|
+
except EOFError:
|
|
58
|
+
return default
|
|
59
|
+
return answer.strip() or default
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _display_value(key: str, value: str) -> str:
|
|
63
|
+
"""Mask only actual secrets (a variable NAMED token/key/secret/password).
|
|
64
|
+
Keys like LENS_API_KEY_FILE that hold a path, not a secret, show verbatim."""
|
|
65
|
+
upper = key.upper()
|
|
66
|
+
# Values that end with _FILE or _PATH are filesystem paths — show them.
|
|
67
|
+
if upper.endswith(("_FILE", "_PATH", "_DIR", "_HOME", "_URL")):
|
|
68
|
+
return value
|
|
69
|
+
if any(upper.endswith("_" + s) or upper == s for s in ("TOKEN", "KEY", "SECRET", "PASSWORD")):
|
|
70
|
+
return "****" + value[-4:] if len(value) > 4 else "****"
|
|
71
|
+
return value
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _yesno(msg: str, default: bool = True) -> bool:
|
|
75
|
+
suffix = "[Y/n]" if default else "[y/N]"
|
|
76
|
+
try:
|
|
77
|
+
ans = input(f"{msg} {suffix}: ").strip().lower()
|
|
78
|
+
except EOFError:
|
|
79
|
+
return default
|
|
80
|
+
if not ans:
|
|
81
|
+
return default
|
|
82
|
+
return ans in ("y", "yes")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# ── subcommand implementations ────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def cmd_init(args: argparse.Namespace) -> int:
|
|
89
|
+
print("getbased-stack init — one-time setup")
|
|
90
|
+
print(
|
|
91
|
+
"Writes ~/.config/getbased/env, (optionally) installs + starts systemd\n"
|
|
92
|
+
"user units for rag and dashboard. Idempotent: safe to re-run."
|
|
93
|
+
)
|
|
94
|
+
print()
|
|
95
|
+
|
|
96
|
+
# 1. token (optional)
|
|
97
|
+
existing = env_file.read_env_file()
|
|
98
|
+
current_token = existing.get("GETBASED_TOKEN", "")
|
|
99
|
+
masked = "****" + current_token[-4:] if current_token else "(unset)"
|
|
100
|
+
print(f"[1/4] getbased sync token (current: {masked})")
|
|
101
|
+
token = _prompt(
|
|
102
|
+
"Paste GETBASED_TOKEN (press Enter to keep current / skip)",
|
|
103
|
+
default=current_token,
|
|
104
|
+
secret=True,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# 2. API key
|
|
108
|
+
key_path = Path(existing.get("LENS_API_KEY_FILE", str(_default_api_key_file())))
|
|
109
|
+
print(f"\n[2/4] rag API key file ({key_path})")
|
|
110
|
+
if key_path.exists():
|
|
111
|
+
print(" existing key found — reusing (init is idempotent).")
|
|
112
|
+
else:
|
|
113
|
+
print(" no key found — one will be generated on first service start.")
|
|
114
|
+
key_value = _ensure_api_key(key_path)
|
|
115
|
+
print(f" key ready (length {len(key_value)} chars, mode 0600)")
|
|
116
|
+
|
|
117
|
+
# 3. write env file
|
|
118
|
+
print("\n[3/4] writing ~/.config/getbased/env")
|
|
119
|
+
merged = {**existing}
|
|
120
|
+
merged["GETBASED_STACK_MANAGED"] = "1"
|
|
121
|
+
if token:
|
|
122
|
+
merged["GETBASED_TOKEN"] = token
|
|
123
|
+
merged["LENS_API_KEY_FILE"] = str(key_path)
|
|
124
|
+
merged.setdefault("LENS_URL", "http://127.0.0.1:8322")
|
|
125
|
+
path = env_file.write_env_file(merged)
|
|
126
|
+
print(f" wrote {path} (mode 0600)")
|
|
127
|
+
|
|
128
|
+
# 4. install units
|
|
129
|
+
print("\n[4/4] install systemd user units?")
|
|
130
|
+
if _yesno("install + start getbased-rag + getbased-dashboard?", default=True):
|
|
131
|
+
mgr = units.UnitManager()
|
|
132
|
+
for line in mgr.install(enable=True, start=True):
|
|
133
|
+
print(f" {line}")
|
|
134
|
+
else:
|
|
135
|
+
print(" skipped — run `getbased-stack install` later to enable.")
|
|
136
|
+
|
|
137
|
+
# 5. linger check
|
|
138
|
+
print("\n── Post-install ──")
|
|
139
|
+
_print_linger_hint(strict=False)
|
|
140
|
+
|
|
141
|
+
# 6. MCP config pointers
|
|
142
|
+
print("\nConfigure your MCP client(s):")
|
|
143
|
+
for client in mcp_configs.SUPPORTED_CLIENTS:
|
|
144
|
+
print(f" getbased-stack mcp-config {client}")
|
|
145
|
+
|
|
146
|
+
return 0
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _print_linger_hint(strict: bool) -> None:
|
|
150
|
+
user = os.environ.get("USER", "")
|
|
151
|
+
if not user:
|
|
152
|
+
return
|
|
153
|
+
try:
|
|
154
|
+
linger_on = units.has_linger(user)
|
|
155
|
+
except FileNotFoundError:
|
|
156
|
+
# loginctl not on PATH (uncommon but possible in minimal containers)
|
|
157
|
+
print(" loginctl not found — cannot check linger status.")
|
|
158
|
+
return
|
|
159
|
+
gui = units.is_gui_session()
|
|
160
|
+
if linger_on:
|
|
161
|
+
print(" linger: enabled ✓ (services will survive logout + reboot)")
|
|
162
|
+
return
|
|
163
|
+
# Not on.
|
|
164
|
+
if gui:
|
|
165
|
+
# Laptop with GUI login — user will be logged in when they use this,
|
|
166
|
+
# so linger is nice-to-have, not blocking.
|
|
167
|
+
print(" linger: off (fine for laptops; services only run while you're logged in)")
|
|
168
|
+
print(f" enable with: sudo loginctl enable-linger {user}")
|
|
169
|
+
return
|
|
170
|
+
# Headless + no linger = services die on logout. This is the "silent
|
|
171
|
+
# breakage after reboot" failure mode.
|
|
172
|
+
print(" ⚠ linger: off AND no GUI session detected (headless).")
|
|
173
|
+
print(f" Without linger, rag + dashboard will stop as soon as this SSH session ends.")
|
|
174
|
+
print(f" Run this once, then re-enable with `systemctl --user start getbased-rag.service`:")
|
|
175
|
+
print(f" sudo loginctl enable-linger {user}")
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def cmd_install(args: argparse.Namespace) -> int:
|
|
179
|
+
mgr = units.UnitManager()
|
|
180
|
+
for line in mgr.install(enable=not args.no_enable, start=not args.no_start):
|
|
181
|
+
print(line)
|
|
182
|
+
_print_linger_hint(strict=False)
|
|
183
|
+
return 0
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def cmd_uninstall(args: argparse.Namespace) -> int:
|
|
187
|
+
mgr = units.UnitManager()
|
|
188
|
+
for line in mgr.uninstall():
|
|
189
|
+
print(line)
|
|
190
|
+
if args.delete_env:
|
|
191
|
+
p = env_file.env_file_path()
|
|
192
|
+
if p.exists():
|
|
193
|
+
p.unlink()
|
|
194
|
+
print(f"removed {p}")
|
|
195
|
+
return 0
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def cmd_status(args: argparse.Namespace) -> int:
|
|
199
|
+
# env file
|
|
200
|
+
path = env_file.env_file_path()
|
|
201
|
+
if path.exists():
|
|
202
|
+
data = env_file.read_env_file(path)
|
|
203
|
+
keys = sorted(data.keys())
|
|
204
|
+
print(f"env file: {path} ({len(keys)} keys)")
|
|
205
|
+
for k in keys:
|
|
206
|
+
print(f" {k}={_display_value(k, data[k])}")
|
|
207
|
+
else:
|
|
208
|
+
print(f"env file: {path} (not present — run `getbased-stack init`)")
|
|
209
|
+
|
|
210
|
+
# systemd units
|
|
211
|
+
print("\nunits:")
|
|
212
|
+
mgr = units.UnitManager()
|
|
213
|
+
try:
|
|
214
|
+
for svc in mgr.status():
|
|
215
|
+
flags = []
|
|
216
|
+
flags.append("installed" if svc["installed"] else "not installed")
|
|
217
|
+
flags.append("enabled" if svc["enabled"] else "not enabled")
|
|
218
|
+
flags.append("active" if svc["active"] else "inactive")
|
|
219
|
+
print(f" {svc['name']}: {', '.join(flags)}")
|
|
220
|
+
except FileNotFoundError:
|
|
221
|
+
print(" systemctl not found — cannot check unit status.")
|
|
222
|
+
|
|
223
|
+
# linger
|
|
224
|
+
print()
|
|
225
|
+
_print_linger_hint(strict=False)
|
|
226
|
+
return 0
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def cmd_set(args: argparse.Namespace) -> int:
|
|
230
|
+
if "=" not in args.assignment:
|
|
231
|
+
print("usage: getbased-stack set KEY=VALUE", file=sys.stderr)
|
|
232
|
+
return 2
|
|
233
|
+
key, _, value = args.assignment.partition("=")
|
|
234
|
+
key = key.strip()
|
|
235
|
+
value = value.strip()
|
|
236
|
+
path = env_file.set_env_var(key, value)
|
|
237
|
+
print(f"{key} updated in {path}")
|
|
238
|
+
# If rag/dashboard are running, they'll pick up the change on next
|
|
239
|
+
# restart — remind the user.
|
|
240
|
+
print("restart services to apply: systemctl --user restart getbased-rag getbased-dashboard")
|
|
241
|
+
return 0
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def cmd_mcp_config(args: argparse.Namespace) -> int:
|
|
245
|
+
try:
|
|
246
|
+
snippet = mcp_configs.emit(args.client)
|
|
247
|
+
except ValueError as e:
|
|
248
|
+
print(str(e), file=sys.stderr)
|
|
249
|
+
return 2
|
|
250
|
+
print(snippet, end="")
|
|
251
|
+
return 0
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def cmd_version(args: argparse.Namespace) -> int:
|
|
255
|
+
try:
|
|
256
|
+
import importlib.metadata as md
|
|
257
|
+
|
|
258
|
+
import getbased_agent_stack
|
|
259
|
+
|
|
260
|
+
print(f"getbased-agent-stack {getbased_agent_stack.__version__}")
|
|
261
|
+
for pkg in ("getbased-mcp", "getbased-rag", "getbased-dashboard"):
|
|
262
|
+
try:
|
|
263
|
+
print(f" {pkg} {md.version(pkg)}")
|
|
264
|
+
except md.PackageNotFoundError:
|
|
265
|
+
print(f" {pkg} (not installed)")
|
|
266
|
+
return 0
|
|
267
|
+
except ImportError as e:
|
|
268
|
+
print(f"Missing dependency: {e}", file=sys.stderr)
|
|
269
|
+
return 1
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _delegate_to_lens(argv: "list[str]") -> int:
|
|
273
|
+
"""Historical behavior: unknown subcommands fall through to `lens`
|
|
274
|
+
so the user can do `getbased-stack serve` / `info` / `ingest` without
|
|
275
|
+
needing to remember a separate binary."""
|
|
276
|
+
try:
|
|
277
|
+
from lens.cli import app as lens_app
|
|
278
|
+
|
|
279
|
+
sys.argv = ["lens"] + argv
|
|
280
|
+
try:
|
|
281
|
+
lens_app()
|
|
282
|
+
return 0
|
|
283
|
+
except SystemExit as e:
|
|
284
|
+
return int(e.code or 0)
|
|
285
|
+
except ImportError:
|
|
286
|
+
print(
|
|
287
|
+
"getbased-rag not installed — install with "
|
|
288
|
+
"`pipx install getbased-agent-stack[full]`",
|
|
289
|
+
file=sys.stderr,
|
|
290
|
+
)
|
|
291
|
+
return 1
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
# ── argparse wiring ───────────────────────────────────────────────────
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
298
|
+
p = argparse.ArgumentParser(
|
|
299
|
+
prog="getbased-stack",
|
|
300
|
+
description=(
|
|
301
|
+
"One-command orchestrator for the getbased agent stack. "
|
|
302
|
+
"Use `init` for first-time setup."
|
|
303
|
+
),
|
|
304
|
+
)
|
|
305
|
+
sub = p.add_subparsers(dest="command")
|
|
306
|
+
|
|
307
|
+
sub.add_parser("init", help="Interactive one-time setup (token, API key, units).")
|
|
308
|
+
|
|
309
|
+
pi = sub.add_parser("install", help="Install + start the systemd user units.")
|
|
310
|
+
pi.add_argument("--no-enable", action="store_true", help="Copy files only; don't enable.")
|
|
311
|
+
pi.add_argument("--no-start", action="store_true", help="Enable but don't start now.")
|
|
312
|
+
|
|
313
|
+
pu = sub.add_parser("uninstall", help="Stop, disable, and remove the systemd units.")
|
|
314
|
+
pu.add_argument(
|
|
315
|
+
"--delete-env",
|
|
316
|
+
action="store_true",
|
|
317
|
+
help="Also delete ~/.config/getbased/env (keeps API key + data).",
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
sub.add_parser("status", help="Show env file, unit state, linger.")
|
|
321
|
+
|
|
322
|
+
ps = sub.add_parser("set", help="Upsert a single key in the shared env file.")
|
|
323
|
+
ps.add_argument("assignment", help="KEY=VALUE")
|
|
324
|
+
|
|
325
|
+
pm = sub.add_parser("mcp-config", help="Print an MCP client config snippet.")
|
|
326
|
+
pm.add_argument(
|
|
327
|
+
"client",
|
|
328
|
+
choices=mcp_configs.SUPPORTED_CLIENTS,
|
|
329
|
+
help="Which client to emit for.",
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
sub.add_parser("version", help="Print installed package versions.")
|
|
333
|
+
|
|
334
|
+
return p
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
COMMANDS = {
|
|
338
|
+
"init": cmd_init,
|
|
339
|
+
"install": cmd_install,
|
|
340
|
+
"uninstall": cmd_uninstall,
|
|
341
|
+
"status": cmd_status,
|
|
342
|
+
"set": cmd_set,
|
|
343
|
+
"mcp-config": cmd_mcp_config,
|
|
344
|
+
"version": cmd_version,
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def main(argv: "list[str] | None" = None) -> int:
|
|
349
|
+
argv = argv if argv is not None else sys.argv[1:]
|
|
350
|
+
|
|
351
|
+
# Fast path: no args → show our help, not lens's.
|
|
352
|
+
if not argv or argv[0] in ("-h", "--help", "help"):
|
|
353
|
+
build_parser().print_help()
|
|
354
|
+
return 0
|
|
355
|
+
|
|
356
|
+
# New commands take priority; everything else falls through to lens
|
|
357
|
+
# (preserves the old thin-wrapper behavior for `serve`, `info`, etc).
|
|
358
|
+
if argv[0] in COMMANDS:
|
|
359
|
+
args = build_parser().parse_args(argv)
|
|
360
|
+
return COMMANDS[args.command](args)
|
|
361
|
+
|
|
362
|
+
return _delegate_to_lens(argv)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
if __name__ == "__main__":
|
|
366
|
+
sys.exit(main())
|