getbased-agent-stack 0.2.0__py3-none-any.whl → 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,4 +5,4 @@ plus a small CLI that proxies to the real binaries. Everything
5
5
  interesting lives in the sibling repos.
6
6
  """
7
7
 
8
- __version__ = "0.1.0"
8
+ __version__ = "0.4.0"
@@ -1,72 +1,366 @@
1
- """Thin CLI wrapper that proxies to getbased-rag's `lens` and getbased-mcp's
2
- runner. Kept minimal so users can still call `lens` / `getbased-mcp`
3
- directly — this exists only for discoverability ("what commands do I have
4
- after `pipx install getbased-agent-stack`?")."""
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
+ """
5
16
  from __future__ import annotations
6
17
 
18
+ import argparse
19
+ import os
20
+ import secrets
21
+ import shutil
7
22
  import sys
23
+ from pathlib import Path
8
24
 
25
+ from . import env_file, mcp_configs, units
9
26
 
10
- HELP = """\
11
- getbased-stack — thin wrapper over the two binaries installed by this package.
12
27
 
13
- Real commands (use directly, they're also on your PATH):
14
- lens — getbased-rag CLI (serve, ingest, stats, ...)
15
- getbased-mcp — getbased-mcp stdio server (spawned by agents)
28
+ # ── helpers ───────────────────────────────────────────────────────────
16
29
 
17
- This wrapper only exists for discoverability:
18
- getbased-stack serve → lens serve
19
- getbased-stack info → lens info
20
- getbased-stack version → print the installed package versions
21
30
 
22
- Quick start:
23
- 1. `getbased-stack serve &` start the RAG server
24
- 2. `lens ingest /path/to/papers` index your docs
25
- 3. configure your MCP agent (Claude Code, Hermes) — see the README
26
- """
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"
27
34
 
28
35
 
29
- def main() -> int:
30
- import getbased_agent_stack
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
31
45
 
32
- argv = sys.argv[1:]
33
- if not argv or argv[0] in ("-h", "--help", "help"):
34
- print(HELP)
35
- return 0
36
46
 
37
- cmd, rest = argv[0], argv[1:]
38
- if cmd == "version":
39
- try:
40
- import getbased_mcp # noqa: F401
41
- import lens # noqa: F401
42
- import importlib.metadata as md
43
- print(f"getbased-agent-stack {getbased_agent_stack.__version__}")
44
- try:
45
- print(f" getbased-mcp {md.version('getbased-mcp')}")
46
- except md.PackageNotFoundError:
47
- print(" getbased-mcp (not installed)")
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"):
48
262
  try:
49
- print(f" getbased-rag {md.version('getbased-rag')}")
263
+ print(f" {pkg} {md.version(pkg)}")
50
264
  except md.PackageNotFoundError:
51
- print(" getbased-rag (not installed)")
52
- return 0
53
- except ImportError as e:
54
- print(f"Missing dependency: {e}", file=sys.stderr)
55
- return 1
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
56
270
 
57
- # Delegate to the lens CLI for everything else.
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."""
58
276
  try:
59
277
  from lens.cli import app as lens_app
60
278
 
61
279
  sys.argv = ["lens"] + argv
62
- lens_app()
63
- return 0
64
- except SystemExit as e:
65
- return int(e.code or 0)
280
+ try:
281
+ lens_app()
282
+ return 0
283
+ except SystemExit as e:
284
+ return int(e.code or 0)
66
285
  except ImportError:
67
- print("getbased-rag not installed — install with `pipx install getbased-agent-stack[full]`", file=sys.stderr)
286
+ print(
287
+ "getbased-rag not installed — install with "
288
+ "`pipx install getbased-agent-stack[full]`",
289
+ file=sys.stderr,
290
+ )
68
291
  return 1
69
292
 
70
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
+
71
365
  if __name__ == "__main__":
72
366
  sys.exit(main())
@@ -0,0 +1,102 @@
1
+ """Shell-style env file I/O for ~/.config/getbased/env.
2
+
3
+ The same file format the opt-in loaders in mcp/rag/dashboard read. Kept
4
+ intentionally tolerant — duplicate keys take last-wins, blanks and `#`
5
+ comments survive round-trips, unknown junk is preserved (we only rewrite
6
+ keys we explicitly care about). This lets a user hand-edit the file
7
+ without `getbased-stack init` clobbering their comments.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import os
12
+ from pathlib import Path
13
+ from typing import Iterable
14
+
15
+
16
+ ENV_PATH_COMMENT = """\
17
+ # getbased shared env — read by mcp, rag, and dashboard when
18
+ # GETBASED_STACK_MANAGED=1. Written by `getbased-stack init` and
19
+ # `getbased-stack set`. Mode 0600.
20
+ #
21
+ # Lines are KEY=VALUE. Comments start with `#`. Quoted values are unwrapped.
22
+ # Explicit env vars always win — this file only provides defaults.
23
+ """
24
+
25
+
26
+ def env_file_path() -> Path:
27
+ """Return the XDG-correct shared env file path. Honors $XDG_CONFIG_HOME."""
28
+ base = os.environ.get("XDG_CONFIG_HOME") or os.path.expanduser("~/.config")
29
+ return Path(base) / "getbased" / "env"
30
+
31
+
32
+ def parse_env_text(text: str) -> "dict[str, str]":
33
+ """Parse shell-style KEY=VALUE text. Last occurrence of a key wins.
34
+ Intentionally permissive — malformed lines are silently skipped."""
35
+ out: dict[str, str] = {}
36
+ for raw in text.splitlines():
37
+ line = raw.strip()
38
+ if not line or line.startswith("#") or "=" not in line:
39
+ continue
40
+ key, _, val = line.partition("=")
41
+ key = key.strip()
42
+ val = val.strip()
43
+ if len(val) >= 2 and val[0] == val[-1] and val[0] in ("'", '"'):
44
+ val = val[1:-1]
45
+ if key:
46
+ out[key] = val
47
+ return out
48
+
49
+
50
+ def read_env_file(path: Path | None = None) -> "dict[str, str]":
51
+ """Read the shared env file, returning {} on missing file."""
52
+ p = path or env_file_path()
53
+ try:
54
+ return parse_env_text(p.read_text(encoding="utf-8"))
55
+ except OSError:
56
+ return {}
57
+
58
+
59
+ def write_env_file(
60
+ values: "dict[str, str]",
61
+ path: Path | None = None,
62
+ header: str = ENV_PATH_COMMENT,
63
+ ) -> Path:
64
+ """Write values to the shared env file at mode 0600.
65
+
66
+ Creates parent dir if missing. Overwrites — call read_env_file first +
67
+ merge if you want to preserve existing keys. Values are never quoted;
68
+ reader handles quoting-or-not symmetrically.
69
+
70
+ Caveat: systemd's `EnvironmentFile=` parser has stricter quoting rules
71
+ than our Python loader — values containing whitespace or shell
72
+ metacharacters should be avoided. Our default values (tokens,
73
+ URL-safe paths) are already safe; users passing arbitrary values via
74
+ `getbased-stack set FOO="has spaces"` may see systemd-loaded services
75
+ and directly-invoked CLIs parse the value differently.
76
+ """
77
+ p = path or env_file_path()
78
+ p.parent.mkdir(parents=True, exist_ok=True)
79
+ lines = [header.rstrip() + "\n\n"] if header else []
80
+ for k, v in values.items():
81
+ # Only alphanumeric + underscore are safe as env keys; anything else
82
+ # is a programming bug upstream, fail loud.
83
+ if not k or not all(c.isalnum() or c == "_" for c in k):
84
+ raise ValueError(f"invalid env key: {k!r}")
85
+ lines.append(f"{k}={v}\n")
86
+ p.write_text("".join(lines), encoding="utf-8")
87
+ os.chmod(p, 0o600)
88
+ return p
89
+
90
+
91
+ def set_env_var(key: str, value: str, path: Path | None = None) -> Path:
92
+ """Idempotent single-key upsert. Preserves other keys."""
93
+ current = read_env_file(path)
94
+ current[key] = value
95
+ return write_env_file(current, path)
96
+
97
+
98
+ def unset_env_var(key: str, path: Path | None = None) -> Path:
99
+ """Idempotent single-key remove. No-op if absent."""
100
+ current = read_env_file(path)
101
+ current.pop(key, None)
102
+ return write_env_file(current, path)
@@ -0,0 +1,177 @@
1
+ """MCP client config snippets.
2
+
3
+ Each client has its own config shape (JSON vs YAML, where to place the
4
+ mcpServers key). All generated snippets point at the absolute path to
5
+ `getbased-mcp` resolved via shutil.which, and carry only one env var:
6
+ GETBASED_STACK_MANAGED=1. Everything else lives in the shared env file,
7
+ read by the MCP process at startup.
8
+
9
+ The stock Hermes snippet is the one exception — it supports richer
10
+ configuration natively (enabled_tools, tool aliases), so we emit those.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import shutil
16
+ from typing import Callable
17
+
18
+
19
+ SUPPORTED_CLIENTS = ("claude-desktop", "claude-code", "cursor", "cline", "hermes")
20
+
21
+ # Single-token list for the Hermes snippet. Matches the tool set the MCP
22
+ # server exposes as of getbased-mcp 0.2.2. Keep alphabetical for diff-friendliness.
23
+ HERMES_ENABLED_TOOLS = [
24
+ "getbased_lens_config",
25
+ "getbased_list_profiles",
26
+ "getbased_read_profile",
27
+ "knowledge_activate_library",
28
+ "knowledge_list_libraries",
29
+ "knowledge_search",
30
+ "knowledge_stats",
31
+ ]
32
+
33
+
34
+ def _resolve_mcp_binary(resolver: Callable[[str], "str | None"] = shutil.which) -> str:
35
+ """Absolute path to the `getbased-mcp` binary, or a bare-name fallback.
36
+ GUI-launched MCP clients (Claude Desktop, Cursor) don't inherit the
37
+ shell PATH, so a bare binary name often fails to resolve. We emit the
38
+ absolute path when we can; otherwise bare name — the caller tells the
39
+ user which case they got via `_resolver_warning`.
40
+ """
41
+ resolved = resolver("getbased-mcp")
42
+ return resolved or "getbased-mcp"
43
+
44
+
45
+ def _resolver_warning(command: str) -> "str | None":
46
+ """Return a warning string if the command isn't an absolute path,
47
+ else None. Used by emitters to tell the user when the snippet needs
48
+ hand-editing to work from a GUI-launched client."""
49
+ if command.startswith("/") or (len(command) > 1 and command[1] == ":"):
50
+ return None
51
+ return (
52
+ "WARNING: `getbased-mcp` wasn't found on PATH when this snippet was\n"
53
+ "generated. GUI-launched MCP clients don't inherit your shell PATH;\n"
54
+ "replace the `command` below with an absolute path (run\n"
55
+ "`which getbased-mcp` in a shell where it works, paste that result)."
56
+ )
57
+
58
+
59
+ def _json_block(command: str) -> "dict":
60
+ return {
61
+ "mcpServers": {
62
+ "getbased": {
63
+ "command": command,
64
+ "env": {"GETBASED_STACK_MANAGED": "1"},
65
+ }
66
+ }
67
+ }
68
+
69
+
70
+ def _prepend_warning(banner: str, warning: "str | None") -> str:
71
+ if not warning:
72
+ return banner
73
+ return "// " + warning.replace("\n", "\n// ") + "\n" + banner
74
+
75
+
76
+ def emit_claude_desktop(resolver: Callable[[str], "str | None"] = shutil.which) -> str:
77
+ cmd = _resolve_mcp_binary(resolver)
78
+ payload = _json_block(cmd)
79
+ banner = (
80
+ "// Paste into ~/Library/Application Support/Claude/claude_desktop_config.json\n"
81
+ "// (macOS) or %APPDATA%\\Claude\\claude_desktop_config.json (Windows).\n"
82
+ "// Merge with existing mcpServers if present.\n"
83
+ )
84
+ banner = _prepend_warning(banner, _resolver_warning(cmd))
85
+ return banner + json.dumps(payload, indent=2) + "\n"
86
+
87
+
88
+ def emit_claude_code(resolver: Callable[[str], "str | None"] = shutil.which) -> str:
89
+ cmd = _resolve_mcp_binary(resolver)
90
+ payload = _json_block(cmd)
91
+ banner = (
92
+ "// Paste into ~/.claude/settings.json (user scope) or <project>/.mcp.json\n"
93
+ "// (project scope). Merge with existing mcpServers if present.\n"
94
+ )
95
+ banner = _prepend_warning(banner, _resolver_warning(cmd))
96
+ return banner + json.dumps(payload, indent=2) + "\n"
97
+
98
+
99
+ def emit_cursor(resolver: Callable[[str], "str | None"] = shutil.which) -> str:
100
+ cmd = _resolve_mcp_binary(resolver)
101
+ payload = _json_block(cmd)
102
+ banner = "// Paste into ~/.cursor/mcp.json (merge with existing mcpServers).\n"
103
+ banner = _prepend_warning(banner, _resolver_warning(cmd))
104
+ return banner + json.dumps(payload, indent=2) + "\n"
105
+
106
+
107
+ def emit_cline(resolver: Callable[[str], "str | None"] = shutil.which) -> str:
108
+ cmd = _resolve_mcp_binary(resolver)
109
+ payload = _json_block(cmd)
110
+ banner = (
111
+ "// Cline MCP settings. Paste into the Cline extension settings panel\n"
112
+ "// under 'MCP Servers' (Cursor/VSCode).\n"
113
+ )
114
+ banner = _prepend_warning(banner, _resolver_warning(cmd))
115
+ return banner + json.dumps(payload, indent=2) + "\n"
116
+
117
+
118
+ def emit_hermes(resolver: Callable[[str], "str | None"] = shutil.which) -> str:
119
+ """Hermes uses YAML — we emit by hand (no yaml dep) since the shape
120
+ is trivial. Includes enabled_tools for parity with the existing
121
+ examples/hermes-mcp.yaml snippet.
122
+
123
+ The emitted snippet carries GETBASED_STACK_MANAGED=1 so MCP reads the
124
+ stack's shared env file. If you'd rather keep Hermes's config.yaml as
125
+ the single source of env (e.g. an existing Hermes VM that already has
126
+ GETBASED_TOKEN + LENS_API_KEY_FILE set explicitly), drop the env block
127
+ entirely. Both modes work — setdefault semantics mean explicit env
128
+ always wins over the shared file — but committing to one keeps
129
+ future debugging simpler.
130
+ """
131
+ cmd = _resolve_mcp_binary(resolver)
132
+ warning = _resolver_warning(cmd)
133
+ lines = [
134
+ "# Hermes Agent MCP configuration snippet for ~/.hermes/config.yaml",
135
+ "# See https://github.com/hermes-agent/hermes-agent for the full config schema.",
136
+ "# The getbased stack's shared env file carries GETBASED_TOKEN + rag URL +",
137
+ "# api key path; only the opt-in flag belongs in Hermes's config.",
138
+ "# (If your Hermes config already sets GETBASED_TOKEN / LENS_* explicitly,",
139
+ "# drop the env block entirely — the Python loader honors existing env.)",
140
+ ]
141
+ if warning:
142
+ lines.append("#")
143
+ for wline in warning.splitlines():
144
+ lines.append(f"# {wline}")
145
+ lines.extend(
146
+ [
147
+ "",
148
+ "mcp_servers:",
149
+ " getbased:",
150
+ f" command: {cmd}",
151
+ " env:",
152
+ ' GETBASED_STACK_MANAGED: "1"',
153
+ " enabled_tools:",
154
+ ]
155
+ )
156
+ for tool in HERMES_ENABLED_TOOLS:
157
+ lines.append(f" - {tool}")
158
+ return "\n".join(lines) + "\n"
159
+
160
+
161
+ def emit(client: str, resolver: Callable[[str], "str | None"] = shutil.which) -> str:
162
+ """Dispatch to the right emitter. Raises ValueError on unknown client."""
163
+ match client:
164
+ case "claude-desktop":
165
+ return emit_claude_desktop(resolver)
166
+ case "claude-code":
167
+ return emit_claude_code(resolver)
168
+ case "cursor":
169
+ return emit_cursor(resolver)
170
+ case "cline":
171
+ return emit_cline(resolver)
172
+ case "hermes":
173
+ return emit_hermes(resolver)
174
+ case _:
175
+ raise ValueError(
176
+ f"unknown client {client!r}. Supported: {', '.join(SUPPORTED_CLIENTS)}"
177
+ )
@@ -0,0 +1,36 @@
1
+ [Unit]
2
+ Description=getbased-dashboard (web UI for rag + mcp management)
3
+ Documentation=https://github.com/elkimek/getbased-agents/tree/main/packages/dashboard
4
+ After=network.target getbased-rag.service
5
+ Wants=getbased-rag.service
6
+
7
+ [Service]
8
+ Type=exec
9
+ # Same shared env file as rag + mcp — one token, one key path, one flag.
10
+ EnvironmentFile=-%h/.config/getbased/env
11
+ EnvironmentFile=-/etc/default/getbased-dashboard
12
+ Environment=GETBASED_STACK_MANAGED=1
13
+ Environment=DASHBOARD_HOST=127.0.0.1
14
+ Environment=DASHBOARD_PORT=8323
15
+ ExecStart=%h/.local/bin/getbased-dashboard serve
16
+ Restart=always
17
+ RestartSec=5s
18
+
19
+ # Hardening — the dashboard is a localhost-only HTTP server.
20
+ NoNewPrivileges=true
21
+ ProtectSystem=strict
22
+ ProtectHome=read-only
23
+ # Needs write access for session state, activity log tailing, and writing
24
+ # the shared env file when the user edits the token in the MCP tab.
25
+ ReadWritePaths=%h/.local/state/getbased %h/.config/getbased
26
+ PrivateTmp=true
27
+ ProtectKernelTunables=true
28
+ ProtectKernelModules=true
29
+ ProtectControlGroups=true
30
+ RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
31
+ RestrictNamespaces=true
32
+ LockPersonality=true
33
+ RestrictRealtime=true
34
+
35
+ [Install]
36
+ WantedBy=default.target
@@ -0,0 +1,41 @@
1
+ [Unit]
2
+ Description=getbased-rag (local RAG knowledge server)
3
+ Documentation=https://github.com/elkimek/getbased-agents/tree/main/packages/rag
4
+ After=network.target
5
+
6
+ [Service]
7
+ Type=exec
8
+ # Shared env file written by `getbased-stack init` — carries GETBASED_TOKEN,
9
+ # LENS_API_KEY_FILE, LENS_URL (and the GETBASED_STACK_MANAGED=1 opt-in
10
+ # flag, which must be set for the Python env loader to pick up this file).
11
+ # Optional: /etc/default/getbased-rag for system-wide overrides.
12
+ EnvironmentFile=-%h/.config/getbased/env
13
+ EnvironmentFile=-/etc/default/getbased-rag
14
+ Environment=GETBASED_STACK_MANAGED=1
15
+ Environment=LENS_HOST=127.0.0.1
16
+ Environment=LENS_PORT=8322
17
+ ExecStart=%h/.local/bin/lens serve
18
+ # Restart=always (not on-failure) so a clean SIGTERM — e.g. operator stop
19
+ # during troubleshooting — still brings the service back at the next boot.
20
+ # Matches the failure mode that kept lens-rag.service dead for hours on
21
+ # the Hermes VM until someone noticed.
22
+ Restart=always
23
+ RestartSec=5s
24
+
25
+ # Hardening — the server only needs its data dir + a network socket.
26
+ NoNewPrivileges=true
27
+ ProtectSystem=strict
28
+ ProtectHome=read-only
29
+ ReadWritePaths=%h/.local/share/getbased
30
+ PrivateTmp=true
31
+ ProtectKernelTunables=true
32
+ ProtectKernelModules=true
33
+ ProtectControlGroups=true
34
+ RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
35
+ RestrictNamespaces=true
36
+ LockPersonality=true
37
+ MemoryDenyWriteExecute=false # ONNX Runtime needs W^X exception
38
+ RestrictRealtime=true
39
+
40
+ [Install]
41
+ WantedBy=default.target
@@ -0,0 +1,198 @@
1
+ """Systemd user-unit install/uninstall/status helpers.
2
+
3
+ Tests drive this through a `UnitManager` class whose shell-out boundary
4
+ (`run_systemctl`, `run_loginctl`) can be monkeypatched. The default
5
+ `shell` implementation uses subprocess; tests swap it for a fake that
6
+ records calls without touching the real systemd.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ import shutil
12
+ import subprocess
13
+ from dataclasses import dataclass
14
+ from importlib import resources
15
+ from pathlib import Path
16
+ from typing import Callable, Iterable
17
+
18
+ # Units this package ships. Order matters for start: rag first (dashboard
19
+ # Wants=+After= rag), so systemctl picks the right order naturally anyway.
20
+ SERVICE_NAMES = ("getbased-rag.service", "getbased-dashboard.service")
21
+
22
+
23
+ def _default_unit_dir() -> Path:
24
+ base = os.environ.get("XDG_CONFIG_HOME") or os.path.expanduser("~/.config")
25
+ return Path(base) / "systemd" / "user"
26
+
27
+
28
+ def bundled_units() -> "list[tuple[str, str]]":
29
+ """Return the (name, text) pairs for every service this package ships.
30
+ Read via importlib.resources — unit files live inside the package tree
31
+ so it works the same from wheel, sdist, and editable installs."""
32
+ results = []
33
+ for name in SERVICE_NAMES:
34
+ ref = resources.files("getbased_agent_stack") / "systemd" / name
35
+ results.append((name, ref.read_text(encoding="utf-8")))
36
+ return results
37
+
38
+
39
+ @dataclass
40
+ class CommandResult:
41
+ returncode: int
42
+ stdout: str
43
+ stderr: str
44
+
45
+
46
+ def _real_shell(cmd: "list[str]") -> CommandResult:
47
+ proc = subprocess.run(cmd, capture_output=True, text=True)
48
+ return CommandResult(proc.returncode, proc.stdout, proc.stderr)
49
+
50
+
51
+ class UnitManager:
52
+ """Orchestrates install/uninstall/status of the bundled service units.
53
+
54
+ The shell boundary is injected so tests can assert on calls without
55
+ touching real systemctl. Production code constructs with defaults."""
56
+
57
+ def __init__(
58
+ self,
59
+ unit_dir: Path | None = None,
60
+ shell: Callable[["list[str]"], CommandResult] | None = None,
61
+ ) -> None:
62
+ self.unit_dir = unit_dir or _default_unit_dir()
63
+ # Resolve at call time (not class definition) so tests can
64
+ # monkeypatch `units._real_shell` and have new instances pick
65
+ # it up without needing to wire the shell through every call site.
66
+ self._shell = shell if shell is not None else _real_shell
67
+
68
+ # ── file operations ────────────────────────────────────────────
69
+
70
+ def install_files(self) -> "list[Path]":
71
+ """Copy bundled unit texts into the user unit dir. Overwrites to
72
+ keep install idempotent across upgrades."""
73
+ self.unit_dir.mkdir(parents=True, exist_ok=True)
74
+ written: "list[Path]" = []
75
+ for name, text in bundled_units():
76
+ dest = self.unit_dir / name
77
+ dest.write_text(text, encoding="utf-8")
78
+ written.append(dest)
79
+ return written
80
+
81
+ def remove_files(self) -> "list[Path]":
82
+ """Delete unit files from the user unit dir. No-op per file if
83
+ absent — idempotent."""
84
+ removed: "list[Path]" = []
85
+ for name in SERVICE_NAMES:
86
+ p = self.unit_dir / name
87
+ if p.exists():
88
+ p.unlink()
89
+ removed.append(p)
90
+ return removed
91
+
92
+ # ── systemctl orchestration ────────────────────────────────────
93
+
94
+ def daemon_reload(self) -> CommandResult:
95
+ return self._shell(["systemctl", "--user", "daemon-reload"])
96
+
97
+ def enable(self, now: bool = True) -> CommandResult:
98
+ args = ["systemctl", "--user", "enable"]
99
+ if now:
100
+ args.append("--now")
101
+ args.extend(SERVICE_NAMES)
102
+ return self._shell(args)
103
+
104
+ def disable(self, now: bool = True) -> CommandResult:
105
+ args = ["systemctl", "--user", "disable"]
106
+ if now:
107
+ args.append("--now")
108
+ args.extend(SERVICE_NAMES)
109
+ return self._shell(args)
110
+
111
+ def is_active(self, service: str) -> bool:
112
+ r = self._shell(["systemctl", "--user", "is-active", service])
113
+ return r.returncode == 0 and r.stdout.strip() == "active"
114
+
115
+ def is_enabled(self, service: str) -> bool:
116
+ r = self._shell(["systemctl", "--user", "is-enabled", service])
117
+ return r.returncode == 0 and r.stdout.strip() in ("enabled", "alias", "static")
118
+
119
+ # ── high-level ops ─────────────────────────────────────────────
120
+
121
+ def install(self, enable: bool = True, start: bool = True) -> "list[str]":
122
+ """Run the full install sequence. Returns a log of human-readable
123
+ steps for the caller to print."""
124
+ log: "list[str]" = []
125
+ written = self.install_files()
126
+ for p in written:
127
+ log.append(f"wrote {p}")
128
+ r = self.daemon_reload()
129
+ if r.returncode != 0:
130
+ log.append(f"daemon-reload FAILED: {r.stderr.strip()}")
131
+ return log
132
+ if enable:
133
+ r = self.enable(now=start)
134
+ if r.returncode != 0:
135
+ log.append(f"enable FAILED: {r.stderr.strip()}")
136
+ else:
137
+ log.append("enabled " + ", ".join(SERVICE_NAMES))
138
+ if start:
139
+ log.append("started " + ", ".join(SERVICE_NAMES))
140
+ return log
141
+
142
+ def uninstall(self) -> "list[str]":
143
+ log: "list[str]" = []
144
+ r = self.disable(now=True)
145
+ if r.returncode != 0:
146
+ # Already-disabled units return non-zero on `disable --now`.
147
+ # Treat as advisory, keep going.
148
+ log.append(f"disable: {r.stderr.strip() or r.stdout.strip()}")
149
+ removed = self.remove_files()
150
+ for p in removed:
151
+ log.append(f"removed {p}")
152
+ if removed:
153
+ r = self.daemon_reload()
154
+ if r.returncode != 0:
155
+ log.append(f"daemon-reload FAILED: {r.stderr.strip()}")
156
+ return log
157
+
158
+ def status(self) -> "list[dict]":
159
+ """Return one dict per service with {name, installed, enabled, active}.
160
+ Cheap: 2 systemctl calls per service."""
161
+ out: "list[dict]" = []
162
+ for name in SERVICE_NAMES:
163
+ out.append(
164
+ {
165
+ "name": name,
166
+ "installed": (self.unit_dir / name).exists(),
167
+ "enabled": self.is_enabled(name),
168
+ "active": self.is_active(name),
169
+ }
170
+ )
171
+ return out
172
+
173
+
174
+ # ── Linger detection (separate from UnitManager — independent concern) ──
175
+
176
+
177
+ def has_linger(user: str | None = None, shell: Callable = _real_shell) -> bool:
178
+ """True when `loginctl show-user --property=Linger` reports `yes`.
179
+
180
+ Linger keeps user services running when the user isn't logged in —
181
+ required for rag/dashboard to come back after a headless reboot.
182
+ """
183
+ user = user or os.environ.get("USER", "")
184
+ if not user:
185
+ return False
186
+ r = shell(["loginctl", "show-user", user, "--property=Linger"])
187
+ return r.returncode == 0 and r.stdout.strip().lower() == "linger=yes"
188
+
189
+
190
+ def is_gui_session() -> bool:
191
+ """Heuristic: does the current session have a display?
192
+ If not, we treat this as a headless host and require linger.
193
+ """
194
+ return bool(
195
+ os.environ.get("DISPLAY")
196
+ or os.environ.get("WAYLAND_DISPLAY")
197
+ or os.environ.get("XDG_SESSION_TYPE") in ("x11", "wayland")
198
+ )
@@ -1,16 +1,16 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: getbased-agent-stack
3
- Version: 0.2.0
3
+ Version: 0.4.0
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.2
10
- Requires-Dist: getbased-rag>=0.6.1
11
- Requires-Dist: getbased-dashboard>=0.5.0
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.6.1; extra == "full"
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, a thin discovery CLI, a hardened systemd unit, and example configs for Claude Code + Hermes.
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
- # 1. Start the RAG server — local Qdrant DB + MiniLM embedder
46
- lens serve # blocks; serves on 127.0.0.1:8322
47
- lens key # prints the bearer token
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
- # 2. Start the dashboard in another terminal
50
- getbased-dashboard serve # serves on 127.0.0.1:8323
77
+ ### Surviving reboot on headless hosts
51
78
 
52
- # 3. Open http://127.0.0.1:8323 in your browser, paste the lens key
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
- # 4. Wire the MCP into your AI client
57
- # The dashboard's MCP tab generates paste-ready config blocks for
58
- # Claude Desktop, Claude Code, Cursor, Cline, and Hermes.
81
+ ```bash
82
+ sudo loginctl enable-linger $USER
59
83
  ```
60
84
 
61
- Both the RAG server and the getbased PWA talk to the same `lens` instance point the PWA at `http://127.0.0.1:8322` under **Settings AI Knowledge Base External server** and the same corpus feeds the browser chat, the dashboard, and any MCP-connected agent.
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
- ## Running as a systemd service
87
+ ### Other commands
64
88
 
65
89
  ```bash
66
- cp systemd/getbased-rag.service ~/.config/systemd/user/
67
- systemctl --user daemon-reload
68
- systemctl --user enable --now getbased-rag
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
- The unit is hardened (`ProtectSystem=strict`, `NoNewPrivileges`, `RestrictAddressFamilies`, etc.); run `systemd-analyze security getbased-rag` to see the score.
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
 
@@ -0,0 +1,13 @@
1
+ getbased_agent_stack/__init__.py,sha256=MgsNcLFuwotjltI3UtO_1ltIdoPW345EA-AyfV4S_-Y,281
2
+ getbased_agent_stack/cli.py,sha256=jkk1o8DaXLooeU12vb9DZRRaeUDHeirOYxQEt8exJOc,12610
3
+ getbased_agent_stack/env_file.py,sha256=lg_LmTspdIbmVTpsxEO4Szdv7wXtvrlCJp5lFN9xCTg,3792
4
+ getbased_agent_stack/mcp_configs.py,sha256=1du4ChGuFv1sTR5RQ9wJgloKC6vnu8iZ0KNl8H-2MKE,6937
5
+ getbased_agent_stack/units.py,sha256=40yvr1DbHiYkpvwKc3RCArhTD4cwwbchKZHbIrDAUNs,7577
6
+ getbased_agent_stack/systemd/getbased-dashboard.service,sha256=ndnJlLD3eu-kWrhi9wfxHksunWNI0-BYAhzcuocPvJg,1173
7
+ getbased_agent_stack/systemd/getbased-rag.service,sha256=wAkL2cb3kB-AzsqMsN7rO3CwzpgNJDpzH08I3pgSU-o,1468
8
+ getbased_agent_stack-0.4.0.dist-info/licenses/LICENSE,sha256=K-IjLWkez1gJQMrlqA5zgyw8vh19mDzk4hKM9Dslmts,1024
9
+ getbased_agent_stack-0.4.0.dist-info/METADATA,sha256=QS4voLGPSuUtMXoxKzVFXQewJAQ_cw8AR1trCyZ4UTE,6831
10
+ getbased_agent_stack-0.4.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
11
+ getbased_agent_stack-0.4.0.dist-info/entry_points.txt,sha256=oIyVEEt9t6HuGncV_3y_6d0fjkY2PDOVQ2OEpyq2Q04,65
12
+ getbased_agent_stack-0.4.0.dist-info/top_level.txt,sha256=NH7AGQBXKHxl7hzWSYDXOaIXCg_9GV3hIQbj8YR8XZQ,21
13
+ getbased_agent_stack-0.4.0.dist-info/RECORD,,
@@ -1,8 +0,0 @@
1
- getbased_agent_stack/__init__.py,sha256=LSo9NAptb9YiMiamzJLzIZTali0QS0bT8JU_975ddv4,281
2
- getbased_agent_stack/cli.py,sha256=IASj51V3gl0v9eebjuDwXfjZplqCBYlMWGCQwdGlNn4,2445
3
- getbased_agent_stack-0.2.0.dist-info/licenses/LICENSE,sha256=K-IjLWkez1gJQMrlqA5zgyw8vh19mDzk4hKM9Dslmts,1024
4
- getbased_agent_stack-0.2.0.dist-info/METADATA,sha256=4JFkTXngtk0Yhymmb6ZlUkzhQYzUzTFfVBxq9zhHBbo,5572
5
- getbased_agent_stack-0.2.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
6
- getbased_agent_stack-0.2.0.dist-info/entry_points.txt,sha256=oIyVEEt9t6HuGncV_3y_6d0fjkY2PDOVQ2OEpyq2Q04,65
7
- getbased_agent_stack-0.2.0.dist-info/top_level.txt,sha256=NH7AGQBXKHxl7hzWSYDXOaIXCg_9GV3hIQbj8YR8XZQ,21
8
- getbased_agent_stack-0.2.0.dist-info/RECORD,,