secondbrain/connectors/base.py
Atlas eb5b67dee2 Add connectors + setup wizard (files / IMAP email / Nextcloud-WebDAV) + encrypted vault
Product layer on top of the pure engine: a wizard to connect the apps that feed
the brain, an encrypted credential vault (Fernet/PBKDF2; app logins never enter
brain.db), and 3 working connectors. Connector data flows through the same
credential-scrub path as the engine. Atlas Corporation proprietary.
2026-07-01 21:50:20 +02:00

60 lines
2.4 KiB
Python

# SecondBrain — (c) 2026 Atlas Corporation. All Rights Reserved. Proprietary; see LICENSE.
"""Connector framework — the "senses" made concrete.
Each connector authenticates to an app, pulls data, and feeds it through the SAME
scrub + dedup path as the core engine (`brain._stage`). Consequences:
* connector data is credential-scrubbed too (secrets never land in brain.db);
* app logins live only in the encrypted vault (see vault.py), never in brain.db;
* the engine (brain.py) stays completely connector-agnostic.
"""
import importlib, os, sys, pkgutil
REGISTRY = {}
def register(cls):
REGISTRY[cls.id] = cls
return cls
class Field:
def __init__(self, key, label, secret=False, default="", required=True):
self.key, self.label, self.secret = key, label, secret
self.default, self.required = default, required
class Connector:
id = "" # short stable id (vault key)
name = "" # display name
blurb = "" # one-line description
fields = [] # list[Field] the wizard collects
def test(self, cfg): # verify creds; raise on failure; return status str
return "ok"
def sync(self, cfg): # pull -> feed(); return new-chunk count
raise NotImplementedError
def _brain():
"""Import the sibling engine regardless of CWD."""
here = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if here not in sys.path:
sys.path.insert(0, here)
import brain
return brain
def feed(items, source):
"""Stage connector items through the engine's scrub+dedup.
items: iterable of dict(text[, project, path, ref, ts, role]).
Returns new-chunk count. Does NOT embed (run brain.embed() after a sync batch)."""
brain = _brain()
conn = brain.db(); n = 0
for it in items:
if not it.get("text"):
continue
n += brain._stage(conn, source, it.get("project", ""), it.get("path", ""),
it.get("ref", ""), it.get("ts", ""), it.get("role", ""), it["text"])
conn.commit(); conn.close()
return n
def load_all():
"""Import every connector module so it self-registers."""
pkgdir = os.path.dirname(os.path.abspath(__file__))
for m in pkgutil.iter_modules([pkgdir]):
if m.name != "base" and not m.name.startswith("_") and m.name != "vault":
importlib.import_module(f"connectors.{m.name}")
return REGISTRY