getbased-rag 0.6.1__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.
@@ -0,0 +1,243 @@
1
+ Metadata-Version: 2.4
2
+ Name: getbased-rag
3
+ Version: 0.6.1
4
+ Summary: getbased-rag — standalone RAG knowledge server (formerly the Electron-bundled Lens)
5
+ License-Expression: GPL-3.0-only
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Requires-Dist: fastapi>=0.110
10
+ Requires-Dist: uvicorn[standard]>=0.29
11
+ Requires-Dist: qdrant-client>=1.9
12
+ Requires-Dist: sentence-transformers>=2.6
13
+ Requires-Dist: typer>=0.12
14
+ Requires-Dist: pydantic>=2.0
15
+ Requires-Dist: pyyaml>=6.0
16
+ Requires-Dist: rich>=13.0
17
+ Requires-Dist: python-multipart>=0.0.6
18
+ Provides-Extra: pdf
19
+ Requires-Dist: pypdf>=3.9; extra == "pdf"
20
+ Provides-Extra: docx
21
+ Requires-Dist: python-docx>=1.0; extra == "docx"
22
+ Provides-Extra: onnx
23
+ Requires-Dist: onnxruntime>=1.17; extra == "onnx"
24
+ Requires-Dist: optimum[onnxruntime]>=1.17; extra == "onnx"
25
+ Requires-Dist: transformers>=5.0; extra == "onnx"
26
+ Provides-Extra: full
27
+ Requires-Dist: pypdf>=3.9; extra == "full"
28
+ Requires-Dist: python-docx>=1.0; extra == "full"
29
+ Requires-Dist: onnxruntime>=1.17; extra == "full"
30
+ Requires-Dist: optimum[onnxruntime]>=1.17; extra == "full"
31
+ Requires-Dist: transformers>=5.0; extra == "full"
32
+ Provides-Extra: test
33
+ Requires-Dist: pytest>=8.0; extra == "test"
34
+ Requires-Dist: httpx>=0.27; extra == "test"
35
+ Dynamic: license-file
36
+
37
+ # getbased-rag
38
+
39
+ > **Installing for the first time?** The [getbased-agent-stack](https://github.com/elkimek/getbased-agents/tree/main/packages/stack) meta-package bundles this server with the MCP that Claude Code / Hermes / OpenClaw talk to, plus [getbased-dashboard](https://github.com/elkimek/getbased-agents/tree/main/packages/dashboard) for a browser UI. One command and you're up.
40
+
41
+ A standalone RAG knowledge server — the backend that used to ship inside the getbased Electron desktop app, now just Python. Point any client (the getbased PWA's *External server* lens backend, the dashboard, or your own) at it.
42
+
43
+ - **Stack**: FastAPI + Uvicorn · Qdrant (embedded local mode) · sentence-transformers / ONNX Runtime
44
+ - **Default port**: 8322, loopback only
45
+ - **Auth**: Bearer token, auto-generated on first start
46
+ - **Stores**: every library is its own Qdrant collection, pinned to its own embedding model at creation
47
+
48
+ ---
49
+
50
+ ## Install
51
+
52
+ Requires Python ≥ 3.10.
53
+
54
+ ```bash
55
+ pipx install "getbased-rag[full]"
56
+ ```
57
+
58
+ Or from source:
59
+
60
+ ```bash
61
+ git clone https://github.com/elkimek/getbased-agents.git
62
+ cd getbased-agents
63
+ uv sync --all-packages --all-extras
64
+ ```
65
+
66
+ ---
67
+
68
+ ## Run
69
+
70
+ ```bash
71
+ lens serve
72
+ ```
73
+
74
+ First start auto-generates an API key at the data dir (see below), prints the bind address, and lazy-loads the embedding model on the first query (~90 MB download for MiniLM).
75
+
76
+ Copy the API key out when you need to configure a client:
77
+
78
+ ```bash
79
+ lens key
80
+ ```
81
+
82
+ Smoke test:
83
+
84
+ ```bash
85
+ curl -s http://127.0.0.1:8322/health
86
+ curl -s -H "Authorization: Bearer $(lens key)" http://127.0.0.1:8322/info | jq
87
+ ```
88
+
89
+ Ingest a file or directory from the CLI:
90
+
91
+ ```bash
92
+ lens ingest ~/Documents/research
93
+ lens stats
94
+ ```
95
+
96
+ Or over HTTP (what the dashboard + PWA use):
97
+
98
+ ```bash
99
+ curl -H "Authorization: Bearer $(lens key)" \
100
+ -F "files=@paper.pdf" -F "files=@notes.md" \
101
+ http://127.0.0.1:8322/ingest
102
+ ```
103
+
104
+ ---
105
+
106
+ ## Per-library embedding models
107
+
108
+ Every library is pinned to one embedding model at creation time — Qdrant collections are dimension-locked, so you can't swap models on an existing library without re-ingesting. Call `GET /models` for the curated list (MiniLM-L6-v2 · BGE-small/base/large-en · BGE-M3) with dims and download sizes, then pass `embedding_model` on create:
109
+
110
+ ```bash
111
+ curl -H "Authorization: Bearer $(lens key)" \
112
+ -H "Content-Type: application/json" \
113
+ -d '{"name":"Research","embedding_model":"BAAI/bge-m3"}' \
114
+ http://127.0.0.1:8322/libraries
115
+ ```
116
+
117
+ Libraries on the same model share one embedder instance in memory. Two libraries both on BGE-M3 use ~2 GB total, not 4.
118
+
119
+ ---
120
+
121
+ ## Streaming ingest progress
122
+
123
+ The HTTP `POST /ingest` endpoint speaks two content types:
124
+
125
+ - Default (no `Accept`): single-shot JSON summary after the run completes
126
+ - `Accept: application/x-ndjson`: newline-delimited JSON progress stream — one `start` event (with total chunks), per-batch `embed` events every ~5 chunks (with current source + index), terminal `result` or `error` event
127
+
128
+ The dashboard uses the streaming path for its bottom-right pill (chunks/sec rate, cancel button, per-filename status). Cancellation works by client disconnect: aborting the fetch causes the server's worker thread to exit at the next batch boundary with `cancelled: true` in the result. Partial-commit — whatever was embedded before the cancel stays.
129
+
130
+ ---
131
+
132
+ ## Wiring into the getbased PWA
133
+
134
+ In the PWA: **Settings → AI → Knowledge Base → External server**
135
+
136
+ | Field | Value |
137
+ |---|---|
138
+ | URL | `http://127.0.0.1:8322` |
139
+ | API key | output of `lens key` |
140
+
141
+ Click **Save**, then **Test connection**. `rag_ready: false` is expected before you ingest anything.
142
+
143
+ ### Agent access (Claude Code, Hermes, OpenClaw, etc.)
144
+
145
+ Pair this server with [getbased-mcp](https://github.com/elkimek/getbased-agents/tree/main/packages/mcp) to expose `knowledge_search`, `knowledge_list_libraries`, `knowledge_activate_library`, and `knowledge_stats` as MCP tools. Typical setup: run both the lens server and getbased-mcp on the same VM, point MCP's `LENS_URL` at `http://localhost:8322`.
146
+
147
+ ### Browser UI
148
+
149
+ Install [getbased-dashboard](https://github.com/elkimek/getbased-agents/tree/main/packages/dashboard) for a web UI on top of this server — library management, drag-drop ingest with live progress pill, search preview, MCP config generator.
150
+
151
+ ---
152
+
153
+ ## Configuration
154
+
155
+ Every setting is an environment variable. Defaults in parentheses.
156
+
157
+ | Variable | Purpose |
158
+ |---|---|
159
+ | `LENS_HOST` (`127.0.0.1`) | Bind interface. Change to `0.0.0.0` only if you really want LAN access |
160
+ | `LENS_PORT` (`8322`) | TCP port |
161
+ | `LENS_DATA_DIR` (platform default) | Where Qdrant DB, API key, and model cache live |
162
+ | `LENS_EMBEDDING_MODEL` (`sentence-transformers/all-MiniLM-L6-v2`) | Default model for new libraries (overridable per library) |
163
+ | `LENS_SIMILARITY_FLOOR` (`0.55`) | Minimum cosine score for a returned chunk |
164
+ | `LENS_ONNX_PROVIDER` (auto) | `cuda` \| `rocm` \| `openvino` \| `coreml` \| `cpu` |
165
+ | `LENS_RERANKER` (`false`) | Enable reranking of top candidates |
166
+ | `LENS_MAX_INGEST_BYTES` (`268435456` — 256 MB) | Cap on a single ingest upload's total size |
167
+ | `LENS_CHUNK_MAX_SIZE` (`800`) | Max chunk size in characters |
168
+ | `LENS_CORS_ORIGINS` (empty) | Comma-separated extra CORS origins to allow, in addition to the PWA + loopback defaults |
169
+
170
+ Default data dir:
171
+
172
+ - Linux: `$XDG_DATA_HOME/getbased/lens` or `~/.local/share/getbased/lens`
173
+ - macOS: `~/Library/Application Support/getbased/lens`
174
+ - Windows: `%APPDATA%\getbased\lens`
175
+
176
+ A legacy `~/.getbased/lens` is honored if it already exists, so pre-v1.21 installs don't lose their data.
177
+
178
+ ### GPU acceleration
179
+
180
+ Install the matching `onnxruntime-*` wheel (e.g. `onnxruntime-gpu` for CUDA), then:
181
+
182
+ ```bash
183
+ LENS_ONNX_PROVIDER=cuda lens serve
184
+ ```
185
+
186
+ ---
187
+
188
+ ## HTTP API
189
+
190
+ All endpoints except `/`, `/health` require `Authorization: Bearer <key>`.
191
+
192
+ | Method | Path | Purpose |
193
+ |---|---|---|
194
+ | `GET` | `/health` | Liveness + `rag_ready` + chunk count. Public |
195
+ | `GET` | `/info` | Embedder engine/model/dim, active library, similarity floor. For UI engine badges |
196
+ | `GET` | `/models` | Curated model picker list (id, label, dim, size_mb) plus the server's default |
197
+ | `POST` | `/query` | `{ query, top_k }` → top-k chunks from the active library, encoded with that library's model |
198
+ | `POST` | `/ingest` | Multipart upload; accepts streaming NDJSON progress when `Accept: application/x-ndjson` |
199
+ | `GET` | `/stats` | Per-source chunk counts for the active library |
200
+ | `DELETE` | `/sources/{source}` | Drop one source from the active library |
201
+ | `DELETE` | `/sources` | Clear the active library's chunks (library stays) |
202
+ | `GET` | `/libraries` | List libraries + active id. Each row includes `chunks`, `lastIngestAt`, `embedding_model` |
203
+ | `POST` | `/libraries` | `{ name, embedding_model? }` → create. 409 on duplicate name (case-insensitive) |
204
+ | `POST` | `/libraries/{id}/activate` | Set active |
205
+ | `PATCH` | `/libraries/{id}` | Rename |
206
+ | `DELETE` | `/libraries/{id}` | Delete (drops Qdrant collection) |
207
+
208
+ ---
209
+
210
+ ## Security notes
211
+
212
+ - Default bind is `127.0.0.1` — queries never leak to the LAN unless you explicitly set `LENS_HOST=0.0.0.0`.
213
+ - The API key file is mode `0600` and never exposed over HTTP. Use `lens key` locally to read it.
214
+ - Bearer comparison uses `secrets.compare_digest` — constant-time, no timing-leak class of bug.
215
+ - Upload paths are basename-sanitised server-side (so `../../etc/passwd` can't escape the ingest temp dir).
216
+ - Zip uploads are zip-slip-guarded — each archive entry must resolve inside its own per-zip subdirectory AND inside the overall ingest root.
217
+ - If you expose the server to a LAN or the internet, front it with a reverse proxy that terminates TLS and rate-limits.
218
+
219
+ ---
220
+
221
+ ## CLI
222
+
223
+ ```
224
+ lens serve Start the HTTP server (default)
225
+ lens ingest <path> Index files into the active library
226
+ lens stats List indexed sources + chunk counts
227
+ lens delete <source> Drop chunks belonging to one source
228
+ lens clear Wipe the active library
229
+ lens info Show config + API key
230
+ lens key Print the API key (creates one if missing)
231
+ ```
232
+
233
+ ---
234
+
235
+ ## License
236
+
237
+ GPL-3.0-only.
238
+
239
+ ---
240
+
241
+ ## Lineage
242
+
243
+ This repo is the Python portion lifted out of [getbased](https://github.com/elkimek/getbased) after the Electron desktop app was retired. The PWA's `external-server` lens backend speaks this same HTTP contract unchanged.
@@ -0,0 +1,15 @@
1
+ getbased_rag-0.6.1.dist-info/licenses/LICENSE,sha256=K-IjLWkez1gJQMrlqA5zgyw8vh19mDzk4hKM9Dslmts,1024
2
+ lens/__init__.py,sha256=E4lyDbXhAwhHbsIONEtqzrBs02OXlA0Z8ds-VihY5pg,75
3
+ lens/api_key.py,sha256=6qyZSGoWzCrVViBQm-X2mA_wz3BK2bUCgmBNxZpSSjA,1693
4
+ lens/cli.py,sha256=-tA5a7VtSiWt--N6lcVSnehX5GY76cIIbqW_CyjTIuc,7072
5
+ lens/config.py,sha256=jhlRf6gzMpWNjDsyonQaHV-rQN-lFQk24Sq6_O5XpKE,6009
6
+ lens/embedder.py,sha256=VVKOE29209FvIa7gJ6J5pwkfACPXFoi8uQRVysYqDrU,19848
7
+ lens/ingest.py,sha256=bRt2RYaM_v03lxnBNuMSqlDn-wGD24eftKjaWwGlKjE,14374
8
+ lens/registry.py,sha256=jgcle03t7tMoODLBUzMuy78-i_hNINLtmlbwJ07i8Qw,10304
9
+ lens/server.py,sha256=hv2wgAwrTJX3Gw8rVOawz61y9I9jV1v--dI_bK51zu0,33648
10
+ lens/store.py,sha256=kiQo6AcQQ3ptHJ2oG-bYkJe2-AE_D6fwv6h97OBTm5M,10308
11
+ getbased_rag-0.6.1.dist-info/METADATA,sha256=S9rCw9FsorD44LA7cWwUyA9zliTqIHMkFraMqHfd2Fk,9655
12
+ getbased_rag-0.6.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
13
+ getbased_rag-0.6.1.dist-info/entry_points.txt,sha256=4ExZzbsCpB1cRlUc4ph9CGGtCsQMABcMs7gvr-yOIkk,38
14
+ getbased_rag-0.6.1.dist-info/top_level.txt,sha256=weQcpg8Ic-kDl-iV7HcZ-TOJbXaihpy6vVm771yjAyw,5
15
+ getbased_rag-0.6.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ lens = lens.cli:app
@@ -0,0 +1,22 @@
1
+ GNU GENERAL PUBLIC LICENSE
2
+ Version 3, 29 June 2007
3
+
4
+ Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
5
+ Everyone is permitted to copy and distribute verbatim copies
6
+ of this license document, but changing it is not allowed.
7
+
8
+ Preamble
9
+
10
+ The GNU General Public License is a free, copyleft license for
11
+ software and other kinds of works.
12
+
13
+ The licenses for most software and other practical works are designed
14
+ to take away your freedom to share and change the works. By contrast,
15
+ the GNU General Public License is intended to guarantee your freedom to
16
+ share and change all versions of a program--to make sure it remains free
17
+ software for all its users. We, the Free Software Foundation, use the
18
+ GNU General Public License for most of our software; it applies also to
19
+ any other work released this way by its authors. You can apply it to
20
+ your programs, too.
21
+
22
+ For the full license text, see <https://www.gnu.org/licenses/gpl-3.0.txt>
@@ -0,0 +1 @@
1
+ lens
lens/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """getbased-lens — local RAG knowledge server."""
2
+
3
+ __version__ = "0.2.0"
lens/api_key.py ADDED
@@ -0,0 +1,55 @@
1
+ """API key generation + persistence for the Lens HTTP server.
2
+
3
+ Auto-generates a key on first start if one doesn't exist. The desktop wrapper
4
+ (Tauri) reads this file via `getbased_lens_config` MCP tool to display the
5
+ key for the user to paste into the getbased web app's Custom Knowledge Source.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ import secrets
12
+ from pathlib import Path
13
+
14
+
15
+ def get_or_create_api_key(key_file: Path) -> str:
16
+ """Read the API key from disk; generate + write one if missing.
17
+
18
+ Creates the file with O_EXCL + mode 0o600 in one syscall, so the key
19
+ is never briefly present with loose permissions (the race the old
20
+ write_text → chmod sequence had).
21
+ """
22
+ if key_file.exists():
23
+ try:
24
+ key = key_file.read_text().strip()
25
+ if key:
26
+ return key
27
+ except OSError:
28
+ pass
29
+
30
+ key_file.parent.mkdir(parents=True, exist_ok=True)
31
+ key = secrets.token_urlsafe(32)
32
+ flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
33
+ try:
34
+ fd = os.open(str(key_file), flags, 0o600)
35
+ except FileExistsError:
36
+ # Another process beat us to it — trust whatever they wrote rather
37
+ # than clobbering it with a fresh key.
38
+ existing = key_file.read_text().strip()
39
+ if existing:
40
+ return existing
41
+ raise
42
+ with os.fdopen(fd, "w") as f:
43
+ f.write(key + "\n")
44
+ return key
45
+
46
+
47
+ def load_api_key(key_file: Path) -> str | None:
48
+ """Read existing API key without generating one."""
49
+ try:
50
+ if key_file.exists():
51
+ key = key_file.read_text().strip()
52
+ return key if key else None
53
+ except OSError:
54
+ pass
55
+ return None
lens/cli.py ADDED
@@ -0,0 +1,225 @@
1
+ """Lens CLI — typer-based commands.
2
+
3
+ lens serve Start the HTTP server (default if no command)
4
+ lens ingest <path> Index files into the local store
5
+ lens info Show config + key + status
6
+ lens key Print the API key (creates one if missing)
7
+
8
+ Configuration comes from environment variables — see config.py for the full list.
9
+ The Tauri desktop wrapper sets LENS_HOST, LENS_PORT, LENS_DATA_DIR,
10
+ LENS_EMBEDDING_MODEL, and LENS_ONNX_PROVIDER for you.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import logging
16
+ import sys
17
+ from pathlib import Path
18
+
19
+ import typer
20
+ from rich.console import Console
21
+ from rich.table import Table
22
+
23
+ from .api_key import get_or_create_api_key
24
+ from .config import LensConfig
25
+ from .server import run_server
26
+
27
+ console = Console()
28
+ app = typer.Typer(
29
+ name="lens",
30
+ help="getbased-lens — local RAG knowledge server.",
31
+ no_args_is_help=False,
32
+ add_completion=False,
33
+ )
34
+
35
+
36
+ def _setup_logging(verbose: bool) -> None:
37
+ level = logging.DEBUG if verbose else logging.INFO
38
+ logging.basicConfig(
39
+ level=level,
40
+ format="%(asctime)s [%(name)s] %(levelname)s: %(message)s",
41
+ datefmt="%H:%M:%S",
42
+ )
43
+
44
+
45
+ @app.callback(invoke_without_command=True)
46
+ def _default(
47
+ ctx: typer.Context,
48
+ verbose: bool = typer.Option(False, "--verbose", "-v", help="Verbose logging"),
49
+ ):
50
+ """When invoked with no subcommand, run `serve`."""
51
+ _setup_logging(verbose)
52
+ if ctx.invoked_subcommand is None:
53
+ ctx.invoke(serve)
54
+
55
+
56
+ @app.command()
57
+ def serve():
58
+ """Start the HTTP server (uvicorn). Blocking."""
59
+ config = LensConfig.from_env()
60
+ console.print(f"[bold cyan]getbased-lens[/] starting on http://{config.host}:{config.port}")
61
+ console.print(f" Data dir: {config.data_dir}")
62
+ console.print(f" Model: {config.embedding_model}")
63
+ console.print(f" Collection: {config.collection}")
64
+ if config.onnx_provider:
65
+ console.print(f" ONNX: {config.onnx_provider}")
66
+ try:
67
+ run_server(config)
68
+ except KeyboardInterrupt:
69
+ console.print("\n[yellow]Stopped.[/]")
70
+
71
+
72
+ @app.command()
73
+ def ingest(
74
+ path: Path = typer.Argument(..., help="File or directory to ingest"),
75
+ json_out: bool = typer.Option(False, "--json", help="Emit machine-parseable JSON"),
76
+ ):
77
+ """Index documents from a path into the local store."""
78
+ from .ingest import ingest_path # lazy import (heavy deps)
79
+ import json as _json
80
+
81
+ config = LensConfig.from_env()
82
+ if not json_out:
83
+ console.print(f"[bold cyan]Ingesting[/] {path}…")
84
+ try:
85
+ # JSONL progress is only useful for a parent process — emit it
86
+ # exactly when --json was requested. Human runs stay clean.
87
+ result = ingest_path(config, path, emit_progress=json_out)
88
+ except FileNotFoundError as e:
89
+ if json_out:
90
+ print(_json.dumps({"error": str(e)}))
91
+ else:
92
+ console.print(f"[red]Error:[/] {e}")
93
+ raise typer.Exit(1)
94
+
95
+ if json_out:
96
+ print(_json.dumps(result))
97
+ return
98
+
99
+ table = Table(title="Ingest result", show_header=False, box=None)
100
+ table.add_row("Files scanned", str(result["files_seen"]))
101
+ table.add_row("Chunks indexed", str(result["chunks_indexed"]))
102
+ if result["skipped"]:
103
+ table.add_row("Skipped", str(len(result["skipped"])))
104
+ console.print(table)
105
+
106
+
107
+ def _active_store(config: LensConfig):
108
+ """CLI helper — resolve a Store bound to the active library.
109
+
110
+ Matches how the server's active_store() works: bootstrap a "Default"
111
+ library on first use so a fresh shell command doesn't 404 on the
112
+ registry being empty."""
113
+ from .registry import Registry
114
+ from .store import QdrantBackend, Store
115
+
116
+ registry = Registry(config)
117
+ registry.ensure_default()
118
+ return Store(
119
+ config,
120
+ collection=registry.active_collection(),
121
+ backend=QdrantBackend(config),
122
+ )
123
+
124
+
125
+ @app.command()
126
+ def stats(json_out: bool = typer.Option(False, "--json", help="Emit JSON")):
127
+ """List knowledge base contents: per-source chunk counts."""
128
+ import json as _json
129
+
130
+ config = LensConfig.from_env()
131
+ store = _active_store(config)
132
+ try:
133
+ docs = store.list_sources()
134
+ except Exception as e:
135
+ if json_out:
136
+ print(_json.dumps({"error": str(e), "total_chunks": 0, "documents": []}))
137
+ else:
138
+ console.print(f"[red]Error:[/] {e}")
139
+ raise typer.Exit(1)
140
+
141
+ total_chunks = sum(d["chunks"] for d in docs)
142
+ if json_out:
143
+ print(_json.dumps({"total_chunks": total_chunks, "documents": docs}))
144
+ return
145
+ if not docs:
146
+ console.print("No documents indexed yet. Use [bold]lens ingest <path>[/] to add some.")
147
+ return
148
+ table = Table(title=f"Indexed: {len(docs)} sources, {total_chunks} chunks")
149
+ table.add_column("Source")
150
+ table.add_column("Chunks", justify="right")
151
+ for d in docs:
152
+ table.add_row(d["source"], str(d["chunks"]))
153
+ console.print(table)
154
+
155
+
156
+ @app.command()
157
+ def delete(
158
+ source: str = typer.Argument(..., help="Source filename to delete (exact match)"),
159
+ json_out: bool = typer.Option(False, "--json", help="Emit JSON"),
160
+ ):
161
+ """Delete all chunks belonging to a source from the knowledge base."""
162
+ import json as _json
163
+
164
+ config = LensConfig.from_env()
165
+ store = _active_store(config)
166
+ deleted = store.delete_by_source(source)
167
+ if json_out:
168
+ print(_json.dumps({"source": source, "deleted_chunks": deleted}))
169
+ return
170
+ console.print(f"Deleted {deleted} chunks matching source '{source}'")
171
+
172
+
173
+ @app.command()
174
+ def clear(
175
+ json_out: bool = typer.Option(False, "--json", help="Emit JSON"),
176
+ yes: bool = typer.Option(False, "--yes", "-y", help="Skip interactive confirmation"),
177
+ ):
178
+ """Delete ALL chunks from the knowledge base (drops the collection)."""
179
+ import json as _json
180
+
181
+ config = LensConfig.from_env()
182
+ store = _active_store(config)
183
+
184
+ if not yes and not json_out:
185
+ console.print(f"[yellow]This will delete all chunks from[/] {config.qdrant_path}")
186
+ confirm = typer.confirm("Proceed?")
187
+ if not confirm:
188
+ console.print("Aborted.")
189
+ raise typer.Exit(0)
190
+
191
+ deleted = store.clear()
192
+ if json_out:
193
+ print(_json.dumps({"deleted_chunks": deleted}))
194
+ return
195
+ console.print(f"Cleared {deleted} chunks.")
196
+
197
+
198
+ @app.command()
199
+ def info():
200
+ """Show current configuration + status."""
201
+ config = LensConfig.from_env()
202
+ console.print(config.display())
203
+ console.print()
204
+ key = get_or_create_api_key(config.api_key_file)
205
+ console.print(f" api_key: {key[:8]}…{key[-4:]} (file: {config.api_key_file})")
206
+
207
+
208
+ @app.command()
209
+ def key():
210
+ """Print the API key (generates one on first invocation)."""
211
+ config = LensConfig.from_env()
212
+ print(get_or_create_api_key(config.api_key_file))
213
+
214
+
215
+ def main():
216
+ """Entry point for `python -m lens`."""
217
+ try:
218
+ app()
219
+ except Exception as e: # noqa: BLE001
220
+ console.print(f"[red]Error:[/] {e}")
221
+ sys.exit(1)
222
+
223
+
224
+ if __name__ == "__main__":
225
+ main()