brainiac-cli 0.16.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.
Files changed (77) hide show
  1. brain/__init__.py +39 -0
  2. brain/__main__.py +14 -0
  3. brain/_assets/AGENTS.md +564 -0
  4. brain/_assets/overlay/template/brand/brand-guide.md +24 -0
  5. brain/_assets/overlay/template/keywords/glossary.md +15 -0
  6. brain/_assets/overlay/template/people/roster.md +14 -0
  7. brain/_assets/overlay/template/voice/voice-profile.md +34 -0
  8. brain/_assets/routines/manifest.json +257 -0
  9. brain/_assets/scripts/brain-brief-mac.plist +59 -0
  10. brain/_assets/scripts/brain-brief.sh +74 -0
  11. brain/_assets/scripts/brain-synthesis-mac.plist +48 -0
  12. brain/_assets/scripts/brain-synthesis.sh +214 -0
  13. brain/_assets/scripts/install-brief-mac.sh +152 -0
  14. brain/_assets/scripts/install-brief-windows.ps1 +97 -0
  15. brain/_assets/scripts/register_tasks.py +386 -0
  16. brain/_assets/templates/company.md +21 -0
  17. brain/_assets/templates/concept.md +27 -0
  18. brain/_assets/templates/daily.md +20 -0
  19. brain/_assets/templates/decision.md +42 -0
  20. brain/_assets/templates/meeting.md +33 -0
  21. brain/_assets/templates/person.md +20 -0
  22. brain/_assets/templates/project.md +23 -0
  23. brain/_assets/templates/state-moc.md +40 -0
  24. brain/_version.py +12 -0
  25. brain/anchor.py +121 -0
  26. brain/audit.py +422 -0
  27. brain/backup.py +210 -0
  28. brain/brief.py +417 -0
  29. brain/capture.py +117 -0
  30. brain/chunk.py +249 -0
  31. brain/classification.py +134 -0
  32. brain/cli.py +1906 -0
  33. brain/config.py +368 -0
  34. brain/connect.py +362 -0
  35. brain/context.py +108 -0
  36. brain/core.py +3018 -0
  37. brain/doctor.py +1161 -0
  38. brain/egress.py +148 -0
  39. brain/embed.py +857 -0
  40. brain/encryption.py +217 -0
  41. brain/frontmatter.py +102 -0
  42. brain/golden_probe.py +678 -0
  43. brain/graph.py +369 -0
  44. brain/graphify.py +352 -0
  45. brain/index.py +1576 -0
  46. brain/ingest/__init__.py +19 -0
  47. brain/ingest/handlers/__init__.py +43 -0
  48. brain/ingest/handlers/base.py +95 -0
  49. brain/ingest/handlers/docx.py +78 -0
  50. brain/ingest/handlers/email.py +228 -0
  51. brain/ingest/handlers/html.py +142 -0
  52. brain/ingest/handlers/image.py +91 -0
  53. brain/ingest/handlers/pdf.py +99 -0
  54. brain/ingest/handlers/pptx.py +69 -0
  55. brain/ingest/handlers/tables.py +41 -0
  56. brain/ingest/handlers/text.py +43 -0
  57. brain/ingest/handlers/xlsx.py +100 -0
  58. brain/ingest/handlers/zip.py +163 -0
  59. brain/ingest/pipeline.py +839 -0
  60. brain/ingest/transcript.py +158 -0
  61. brain/init.py +870 -0
  62. brain/maintenance.py +2266 -0
  63. brain/mcp_adapter.py +217 -0
  64. brain/multihop.py +232 -0
  65. brain/notes.py +195 -0
  66. brain/overlay.py +183 -0
  67. brain/projection.py +79 -0
  68. brain/rerank.py +425 -0
  69. brain/snapshot.py +231 -0
  70. brain/update.py +743 -0
  71. brain/vectors.py +225 -0
  72. brainiac_cli-0.16.0.dist-info/METADATA +306 -0
  73. brainiac_cli-0.16.0.dist-info/RECORD +77 -0
  74. brainiac_cli-0.16.0.dist-info/WHEEL +5 -0
  75. brainiac_cli-0.16.0.dist-info/entry_points.txt +4 -0
  76. brainiac_cli-0.16.0.dist-info/licenses/LICENSE +202 -0
  77. brainiac_cli-0.16.0.dist-info/top_level.txt +1 -0
brain/snapshot.py ADDED
@@ -0,0 +1,231 @@
1
+ """Read-only snapshot publisher (r2-codex; pairs with S06 host/VM split).
2
+
3
+ The **authoritative writable index lives on the HOST** under %LOCALAPPDATA% /
4
+ Application Support (WAL, single-writer). The Cowork VM must NEVER write it.
5
+ Instead the host publishes a **read-only snapshot** that the VM mounts read-only
6
+ in ``./.brain/``.
7
+
8
+ Two correctness properties this module guarantees:
9
+
10
+ 1. **Atomic publish.** The snapshot DB is written to a temp file then
11
+ ``os.replace``d into place (atomic on POSIX & Windows for same-filesystem
12
+ moves). A reader never sees a half-written index. The manifest is written the
13
+ same way and last, so a present manifest always describes a complete DB.
14
+
15
+ 2. **Generation id.** Each publish increments a monotonic ``generation`` and
16
+ records it (plus age inputs, counts, embed model/dim, sha256) in
17
+ ``snapshot.manifest.json``. ``brain status`` reports the generation + age so a
18
+ VM session can tell whether its read-only view is fresh or stale.
19
+
20
+ WAL caveat (documented, S06): SQLCipher + WAL over VirtioFS with two concurrent
21
+ Cowork sessions on one mounted index risks lock contention / corruption. The
22
+ snapshot is therefore a *copy*, published by the single host writer — the VM
23
+ never opens the authoritative WAL DB. The index stays rebuildable-from-markdown,
24
+ so a corrupt snapshot is always recoverable by re-publishing.
25
+ """
26
+ from __future__ import annotations
27
+
28
+ import json
29
+ import os
30
+ import shutil
31
+ import sqlite3
32
+ import time
33
+ from dataclasses import dataclass, asdict
34
+ from pathlib import Path
35
+
36
+ from . import config
37
+
38
+ SNAPSHOT_DB = "index.snapshot.sqlite"
39
+ MANIFEST = "snapshot.manifest.json"
40
+
41
+
42
+ @dataclass
43
+ class SnapshotManifest:
44
+ generation: int
45
+ created_epoch: float
46
+ created_iso: str
47
+ source_db: str
48
+ snapshot_db: str
49
+ sha256: str
50
+ bytes: int
51
+ notes: int
52
+ chunks: int
53
+ embed_model: str | None
54
+ embed_dim: str | None
55
+ schema_version: str | None
56
+
57
+ def to_dict(self) -> dict:
58
+ return asdict(self)
59
+
60
+
61
+ def _sha256_file(path: Path) -> str:
62
+ import hashlib
63
+
64
+ h = hashlib.sha256()
65
+ with path.open("rb") as fh:
66
+ for blk in iter(lambda: fh.read(1 << 20), b""):
67
+ h.update(blk)
68
+ return h.hexdigest()
69
+
70
+
71
+ def _finalize_readonly(db_path: Path) -> None:
72
+ """Convert the copied snapshot to a self-contained rollback-journal DB.
73
+
74
+ The source index runs in WAL mode; a raw copy carries WAL in its header, so a
75
+ pure read-only consumer (the VM, opening ``mode=ro`` on a possibly read-only
76
+ mount) would need ``-wal``/``-shm`` sidecars it cannot create. Checkpointing
77
+ then switching to ``journal_mode=DELETE`` and closing leaves a single,
78
+ self-contained file the VM can open read-only with NO sidecars — the property
79
+ the host/VM split depends on.
80
+ """
81
+ con = sqlite3.connect(str(db_path))
82
+ try:
83
+ con.execute("PRAGMA wal_checkpoint(TRUNCATE)")
84
+ con.execute("PRAGMA journal_mode=DELETE")
85
+ con.commit()
86
+ except sqlite3.OperationalError:
87
+ pass
88
+ finally:
89
+ con.close()
90
+ # Remove any residual sidecars so the published snapshot is one file.
91
+ for suffix in ("-wal", "-shm"):
92
+ side = Path(str(db_path) + suffix)
93
+ if side.exists():
94
+ try:
95
+ side.unlink()
96
+ except OSError:
97
+ pass
98
+
99
+
100
+ def _read_counts_and_meta(db_path: Path) -> dict:
101
+ out = {"notes": 0, "chunks": 0, "embed_model": None, "embed_dim": None,
102
+ "schema_version": None}
103
+ con = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
104
+ try:
105
+ out["notes"] = int(con.execute("SELECT COUNT(*) FROM notes").fetchone()[0])
106
+ try:
107
+ out["chunks"] = int(con.execute("SELECT COUNT(*) FROM chunks").fetchone()[0])
108
+ except sqlite3.OperationalError:
109
+ out["chunks"] = 0
110
+ for k in ("embed_model", "embed_dim", "schema_version"):
111
+ r = con.execute("SELECT v FROM meta WHERE k=?", (k,)).fetchone()
112
+ out[k] = r[0] if r else None
113
+ finally:
114
+ con.close()
115
+ return out
116
+
117
+
118
+ def read_manifest(dest_dir: Path) -> SnapshotManifest | None:
119
+ p = Path(dest_dir) / MANIFEST
120
+ if not p.is_file():
121
+ return None
122
+ try:
123
+ d = json.loads(p.read_text(encoding="utf-8"))
124
+ return SnapshotManifest(**d)
125
+ except Exception:
126
+ return None
127
+
128
+
129
+ def publish_snapshot(source_db: Path, dest_dir: Path) -> SnapshotManifest:
130
+ """Atomically publish ``source_db`` as a read-only snapshot in ``dest_dir``.
131
+
132
+ Returns the new manifest. ``generation`` = previous generation + 1 (1 on the
133
+ first publish).
134
+ """
135
+ source_db = Path(source_db)
136
+ dest_dir = Path(dest_dir)
137
+ if not source_db.is_file():
138
+ raise FileNotFoundError(f"source index not found: {source_db}")
139
+ dest_dir.mkdir(parents=True, exist_ok=True)
140
+
141
+ prev = read_manifest(dest_dir)
142
+ generation = (prev.generation + 1) if prev else 1
143
+
144
+ # Checkpoint the WAL into the main DB before snapshotting so the copy is a
145
+ # self-contained, consistent point-in-time (no dependence on -wal/-shm).
146
+ con = sqlite3.connect(str(source_db))
147
+ try:
148
+ con.execute("PRAGMA wal_checkpoint(TRUNCATE)")
149
+ except sqlite3.OperationalError:
150
+ pass
151
+ finally:
152
+ con.close()
153
+
154
+ final_db = dest_dir / SNAPSHOT_DB
155
+ tmp_db = dest_dir / (SNAPSHOT_DB + f".tmp.{os.getpid()}.{generation}")
156
+ shutil.copy2(source_db, tmp_db)
157
+ try:
158
+ os.replace(tmp_db, final_db) # atomic swap
159
+ finally:
160
+ if tmp_db.exists():
161
+ tmp_db.unlink()
162
+ # Make the snapshot a self-contained rollback-journal DB BEFORE tightening
163
+ # its permissions, so a VM read-only open needs no -wal/-shm sidecars.
164
+ _finalize_readonly(final_db)
165
+ # Owner-only (0600), NOT the previous world-readable 0444 -- the snapshot
166
+ # can carry note bodies up to and including MNPI-tier content, and a
167
+ # shared/multi-user machine is exactly the case the classification gate
168
+ # cannot protect against (it is an egress *decision*, not containment; see
169
+ # docs/operations/egress-provider-posture.md §2). Real read-only
170
+ # enforcement is the ``mode=ro`` + ``PRAGMA query_only=ON`` SQLite connection
171
+ # (BrainIndex.conn, read_only=True), NOT the filesystem bit, so this change
172
+ # does not weaken the write-protection guarantee -- it only removes the
173
+ # world-readable exposure.
174
+ config.secure_file_permissions(final_db)
175
+
176
+ cm = _read_counts_and_meta(final_db)
177
+ now = time.time()
178
+ manifest = SnapshotManifest(
179
+ generation=generation,
180
+ created_epoch=now,
181
+ created_iso=time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime(now)) + "Z",
182
+ source_db=str(source_db),
183
+ snapshot_db=str(final_db),
184
+ sha256=_sha256_file(final_db),
185
+ bytes=final_db.stat().st_size,
186
+ notes=cm["notes"],
187
+ chunks=cm["chunks"],
188
+ embed_model=cm["embed_model"],
189
+ embed_dim=cm["embed_dim"],
190
+ schema_version=cm["schema_version"],
191
+ )
192
+ # Write manifest LAST, atomically — a present manifest always implies a
193
+ # complete DB at the recorded generation.
194
+ tmp_man = dest_dir / (MANIFEST + f".tmp.{os.getpid()}")
195
+ tmp_man.write_text(json.dumps(manifest.to_dict(), indent=2) + "\n", encoding="utf-8")
196
+ os.replace(tmp_man, dest_dir / MANIFEST)
197
+ return manifest
198
+
199
+
200
+ def snapshot_status(dest_dir: Path) -> dict:
201
+ """Report snapshot generation + age for ``brain status`` (VM-side)."""
202
+ m = read_manifest(dest_dir)
203
+ if m is None:
204
+ return {"snapshot": "absent", "dest": str(dest_dir)}
205
+ age_s = max(0.0, time.time() - m.created_epoch)
206
+ return {
207
+ "snapshot": "present",
208
+ "generation": m.generation,
209
+ "created_iso": m.created_iso,
210
+ "age_seconds": round(age_s, 1),
211
+ "age_human": _human_age(age_s),
212
+ "notes": m.notes,
213
+ "chunks": m.chunks,
214
+ "embed_model": m.embed_model,
215
+ "embed_dim": m.embed_dim,
216
+ "sha256": m.sha256,
217
+ "dest": str(dest_dir),
218
+ }
219
+
220
+
221
+ def _human_age(seconds: float) -> str:
222
+ s = int(seconds)
223
+ if s < 90:
224
+ return f"{s}s"
225
+ m = s // 60
226
+ if m < 90:
227
+ return f"{m}m"
228
+ h = m // 60
229
+ if h < 48:
230
+ return f"{h}h"
231
+ return f"{h // 24}d"