m365-brain 1.2.0__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.
- m365_brain-1.2.0/.gitignore +26 -0
- m365_brain-1.2.0/LICENSE +21 -0
- m365_brain-1.2.0/PKG-INFO +324 -0
- m365_brain-1.2.0/README.md +265 -0
- m365_brain-1.2.0/m365_brain/__init__.py +1 -0
- m365_brain-1.2.0/m365_brain/atomic_json.py +55 -0
- m365_brain-1.2.0/m365_brain/cli.py +290 -0
- m365_brain-1.2.0/m365_brain/commands/__init__.py +25 -0
- m365_brain-1.2.0/m365_brain/commands/_catalog.py +290 -0
- m365_brain-1.2.0/m365_brain/commands/_context.py +253 -0
- m365_brain-1.2.0/m365_brain/commands/auth.py +98 -0
- m365_brain-1.2.0/m365_brain/commands/config.py +58 -0
- m365_brain-1.2.0/m365_brain/commands/files.py +117 -0
- m365_brain-1.2.0/m365_brain/commands/index.py +249 -0
- m365_brain-1.2.0/m365_brain/commands/ops.py +185 -0
- m365_brain-1.2.0/m365_brain/commands/outbox.py +143 -0
- m365_brain-1.2.0/m365_brain/commands/teams.py +91 -0
- m365_brain-1.2.0/m365_brain/commands/vault.py +63 -0
- m365_brain-1.2.0/m365_brain/config/__init__.py +143 -0
- m365_brain-1.2.0/m365_brain/config/access.py +16 -0
- m365_brain-1.2.0/m365_brain/config/base.py +12 -0
- m365_brain-1.2.0/m365_brain/config/errors.py +5 -0
- m365_brain-1.2.0/m365_brain/config/extractors.py +169 -0
- m365_brain-1.2.0/m365_brain/config/index.py +181 -0
- m365_brain-1.2.0/m365_brain/config/loader.py +164 -0
- m365_brain-1.2.0/m365_brain/config/ops.py +159 -0
- m365_brain-1.2.0/m365_brain/config/outbox.py +76 -0
- m365_brain-1.2.0/m365_brain/config/runtime.py +81 -0
- m365_brain-1.2.0/m365_brain/config/schema.py +210 -0
- m365_brain-1.2.0/m365_brain/config/vault.py +70 -0
- m365_brain-1.2.0/m365_brain/cycle.py +288 -0
- m365_brain-1.2.0/m365_brain/dry_run.py +94 -0
- m365_brain-1.2.0/m365_brain/hooks.py +125 -0
- m365_brain-1.2.0/m365_brain/index/__init__.py +49 -0
- m365_brain-1.2.0/m365_brain/index/backends/__init__.py +29 -0
- m365_brain-1.2.0/m365_brain/index/backends/base.py +181 -0
- m365_brain-1.2.0/m365_brain/index/backends/filters.py +111 -0
- m365_brain-1.2.0/m365_brain/index/backends/memory.py +299 -0
- m365_brain-1.2.0/m365_brain/index/backends/sqlite.py +182 -0
- m365_brain-1.2.0/m365_brain/index/backends/sqlite_catalog.py +176 -0
- m365_brain-1.2.0/m365_brain/index/backends/sqlite_read.py +238 -0
- m365_brain-1.2.0/m365_brain/index/backends/sqlite_schema.py +148 -0
- m365_brain-1.2.0/m365_brain/index/backends/sqlite_write.py +147 -0
- m365_brain-1.2.0/m365_brain/index/catalog.py +93 -0
- m365_brain-1.2.0/m365_brain/index/catalog_extract.py +138 -0
- m365_brain-1.2.0/m365_brain/index/catalog_storage.py +109 -0
- m365_brain-1.2.0/m365_brain/index/fts_query.py +243 -0
- m365_brain-1.2.0/m365_brain/index/fusion.py +59 -0
- m365_brain-1.2.0/m365_brain/index/graph.py +84 -0
- m365_brain-1.2.0/m365_brain/index/query.py +109 -0
- m365_brain-1.2.0/m365_brain/index/search.py +213 -0
- m365_brain-1.2.0/m365_brain/index/sync.py +174 -0
- m365_brain-1.2.0/m365_brain/index/vector/__init__.py +68 -0
- m365_brain-1.2.0/m365_brain/index/vector/base.py +91 -0
- m365_brain-1.2.0/m365_brain/index/vector/chunking.py +112 -0
- m365_brain-1.2.0/m365_brain/index/vector/fastembed_provider.py +64 -0
- m365_brain-1.2.0/m365_brain/index/vector/memory.py +114 -0
- m365_brain-1.2.0/m365_brain/index/vector/sqlite_vec_store.py +210 -0
- m365_brain-1.2.0/m365_brain/index/vector/sync.py +85 -0
- m365_brain-1.2.0/m365_brain/index_step.py +72 -0
- m365_brain-1.2.0/m365_brain/logging_config.py +77 -0
- m365_brain-1.2.0/m365_brain/m365/__init__.py +1 -0
- m365_brain-1.2.0/m365_brain/m365/auth/__init__.py +1 -0
- m365_brain-1.2.0/m365_brain/m365/auth/auth_code.py +67 -0
- m365_brain-1.2.0/m365_brain/m365/auth/device_code.py +108 -0
- m365_brain-1.2.0/m365_brain/m365/auth/profiles.py +154 -0
- m365_brain-1.2.0/m365_brain/m365/auth/token_provider.py +78 -0
- m365_brain-1.2.0/m365_brain/m365/client.py +300 -0
- m365_brain-1.2.0/m365_brain/m365/converters/__init__.py +1 -0
- m365_brain-1.2.0/m365_brain/m365/converters/document.py +35 -0
- m365_brain-1.2.0/m365_brain/m365/converters/html_to_md.py +24 -0
- m365_brain-1.2.0/m365_brain/m365/errors.py +42 -0
- m365_brain-1.2.0/m365_brain/m365/extractors/__init__.py +1 -0
- m365_brain-1.2.0/m365_brain/m365/extractors/_attachment_helpers.py +123 -0
- m365_brain-1.2.0/m365_brain/m365/extractors/_file_helpers.py +274 -0
- m365_brain-1.2.0/m365_brain/m365/extractors/_folder_helpers.py +160 -0
- m365_brain-1.2.0/m365_brain/m365/extractors/_message_helpers.py +70 -0
- m365_brain-1.2.0/m365_brain/m365/extractors/_message_renderer.py +131 -0
- m365_brain-1.2.0/m365_brain/m365/extractors/_message_store.py +107 -0
- m365_brain-1.2.0/m365_brain/m365/extractors/_teams_attachment_helpers.py +268 -0
- m365_brain-1.2.0/m365_brain/m365/extractors/_teams_channel_ingest.py +97 -0
- m365_brain-1.2.0/m365_brain/m365/extractors/_teams_channel_targets.py +51 -0
- m365_brain-1.2.0/m365_brain/m365/extractors/_teams_context.py +33 -0
- m365_brain-1.2.0/m365_brain/m365/extractors/_teams_hosted_content.py +113 -0
- m365_brain-1.2.0/m365_brain/m365/extractors/_teams_ingest.py +101 -0
- m365_brain-1.2.0/m365_brain/m365/extractors/base.py +53 -0
- m365_brain-1.2.0/m365_brain/m365/extractors/calendar.py +217 -0
- m365_brain-1.2.0/m365_brain/m365/extractors/contacts.py +240 -0
- m365_brain-1.2.0/m365_brain/m365/extractors/directory.py +219 -0
- m365_brain-1.2.0/m365_brain/m365/extractors/email.py +300 -0
- m365_brain-1.2.0/m365_brain/m365/extractors/errors.py +9 -0
- m365_brain-1.2.0/m365_brain/m365/extractors/onedrive.py +93 -0
- m365_brain-1.2.0/m365_brain/m365/extractors/sharepoint.py +176 -0
- m365_brain-1.2.0/m365_brain/m365/extractors/teams_channels.py +282 -0
- m365_brain-1.2.0/m365_brain/m365/extractors/teams_chats.py +276 -0
- m365_brain-1.2.0/m365_brain/m365/files.py +253 -0
- m365_brain-1.2.0/m365_brain/m365/frontmatter/__init__.py +71 -0
- m365_brain-1.2.0/m365_brain/m365/frontmatter/_tags.py +27 -0
- m365_brain-1.2.0/m365_brain/m365/frontmatter/calendar.py +101 -0
- m365_brain-1.2.0/m365_brain/m365/frontmatter/email.py +83 -0
- m365_brain-1.2.0/m365_brain/m365/frontmatter/files.py +120 -0
- m365_brain-1.2.0/m365_brain/m365/frontmatter/people.py +174 -0
- m365_brain-1.2.0/m365_brain/m365/frontmatter/teams.py +123 -0
- m365_brain-1.2.0/m365_brain/m365/graph_helpers.py +154 -0
- m365_brain-1.2.0/m365_brain/m365/markdown_writer.py +51 -0
- m365_brain-1.2.0/m365_brain/m365/outboxes/__init__.py +79 -0
- m365_brain-1.2.0/m365_brain/m365/outboxes/attachments.py +96 -0
- m365_brain-1.2.0/m365_brain/m365/outboxes/email.py +197 -0
- m365_brain-1.2.0/m365_brain/m365/outboxes/files.py +65 -0
- m365_brain-1.2.0/m365_brain/m365/outboxes/messages.py +183 -0
- m365_brain-1.2.0/m365_brain/m365/outboxes/rendering.py +83 -0
- m365_brain-1.2.0/m365_brain/m365/outboxes/teams.py +57 -0
- m365_brain-1.2.0/m365_brain/m365/pagination.py +125 -0
- m365_brain-1.2.0/m365_brain/m365/upload.py +69 -0
- m365_brain-1.2.0/m365_brain/manifest.py +285 -0
- m365_brain-1.2.0/m365_brain/model.py +231 -0
- m365_brain-1.2.0/m365_brain/models.py +57 -0
- m365_brain-1.2.0/m365_brain/ops/__init__.py +47 -0
- m365_brain-1.2.0/m365_brain/ops/links.py +146 -0
- m365_brain-1.2.0/m365_brain/ops/names.py +83 -0
- m365_brain-1.2.0/m365_brain/ops/tiers.py +200 -0
- m365_brain-1.2.0/m365_brain/ops/triage.py +213 -0
- m365_brain-1.2.0/m365_brain/outbox/__init__.py +49 -0
- m365_brain-1.2.0/m365_brain/outbox/authority.py +94 -0
- m365_brain-1.2.0/m365_brain/outbox/filesystem_store.py +129 -0
- m365_brain-1.2.0/m365_brain/outbox/reconcile.py +161 -0
- m365_brain-1.2.0/m365_brain/outbox/registry.py +137 -0
- m365_brain-1.2.0/m365_brain/outbox/runner.py +220 -0
- m365_brain-1.2.0/m365_brain/outbox/stores.py +169 -0
- m365_brain-1.2.0/m365_brain/parsers/__init__.py +26 -0
- m365_brain-1.2.0/m365_brain/parsers/document.py +148 -0
- m365_brain-1.2.0/m365_brain/parsers/frontmatter.py +82 -0
- m365_brain-1.2.0/m365_brain/parsers/observations.py +102 -0
- m365_brain-1.2.0/m365_brain/parsers/relations.py +97 -0
- m365_brain-1.2.0/m365_brain/parsers/text.py +43 -0
- m365_brain-1.2.0/m365_brain/schedule.py +145 -0
- m365_brain-1.2.0/m365_brain/state.py +139 -0
- m365_brain-1.2.0/m365_brain/storage/__init__.py +152 -0
- m365_brain-1.2.0/m365_brain/storage/azure_blob.py +88 -0
- m365_brain-1.2.0/m365_brain/storage/base.py +34 -0
- m365_brain-1.2.0/m365_brain/storage/exceptions.py +11 -0
- m365_brain-1.2.0/m365_brain/storage/local.py +65 -0
- m365_brain-1.2.0/m365_brain/sync.py +138 -0
- m365_brain-1.2.0/m365_brain/templates/m365-brain.yaml +431 -0
- m365_brain-1.2.0/m365_brain/validation.py +22 -0
- m365_brain-1.2.0/m365_brain/vault/__init__.py +6 -0
- m365_brain-1.2.0/m365_brain/vault/classify.py +87 -0
- m365_brain-1.2.0/m365_brain/vault/dispatch.py +124 -0
- m365_brain-1.2.0/m365_brain/vault/intent.py +126 -0
- m365_brain-1.2.0/m365_brain/vault/paths.py +213 -0
- m365_brain-1.2.0/m365_brain/vault/payloads.py +157 -0
- m365_brain-1.2.0/m365_brain/vault/removal.py +107 -0
- m365_brain-1.2.0/m365_brain/worker.py +289 -0
- m365_brain-1.2.0/m365_brain/workspace.py +138 -0
- m365_brain-1.2.0/pyproject.toml +97 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
.states
|
|
2
|
+
*.py[cod]
|
|
3
|
+
.web
|
|
4
|
+
.pixi/
|
|
5
|
+
.hypothesis/
|
|
6
|
+
__pycache__/
|
|
7
|
+
*.pyc
|
|
8
|
+
*.egg-info/
|
|
9
|
+
dist/
|
|
10
|
+
build/
|
|
11
|
+
.pytest_cache/
|
|
12
|
+
.ruff_cache/
|
|
13
|
+
.coverage
|
|
14
|
+
site/
|
|
15
|
+
state/
|
|
16
|
+
# the runtime vault at the repo root only -- anchored so it cannot swallow
|
|
17
|
+
# m365_brain/vault/ (the path resolver) or tests/unit/vault/
|
|
18
|
+
/vault/
|
|
19
|
+
*.db
|
|
20
|
+
.env
|
|
21
|
+
.env.*
|
|
22
|
+
!.env.example
|
|
23
|
+
.web/
|
|
24
|
+
.states/
|
|
25
|
+
assets/external/
|
|
26
|
+
.claude/
|
m365_brain-1.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Matthias Christenson
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: m365-brain
|
|
3
|
+
Version: 1.2.0
|
|
4
|
+
Summary: Microsoft 365 data extraction to Obsidian-compatible markdown via Graph API
|
|
5
|
+
Project-URL: Documentation, https://neuralsignal.github.io/m365-brain/
|
|
6
|
+
Project-URL: Repository, https://github.com/neuralsignal/m365-brain
|
|
7
|
+
Author: Matthias Christenson
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: calendar,email,extraction,graph-api,markdown,microsoft-365,obsidian,onedrive,sharepoint,sync,teams
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Communications :: Email
|
|
17
|
+
Classifier: Topic :: Office/Business
|
|
18
|
+
Requires-Python: >=3.12
|
|
19
|
+
Requires-Dist: beautifulsoup4<5,>=4.12
|
|
20
|
+
Requires-Dist: click<9,>=8.3.3
|
|
21
|
+
Requires-Dist: cryptography<51,>=50.0.0
|
|
22
|
+
Requires-Dist: email-validator<3,>=2.2
|
|
23
|
+
Requires-Dist: httpx<1,>=0.28
|
|
24
|
+
Requires-Dist: markdown<4,>=3.7
|
|
25
|
+
Requires-Dist: markdownify<1,>=0.14
|
|
26
|
+
Requires-Dist: msal<2,>=1.34
|
|
27
|
+
Requires-Dist: pydantic<3,>=2.12
|
|
28
|
+
Requires-Dist: pyopenssl<27,>=26.0
|
|
29
|
+
Requires-Dist: python-dotenv<2,>=1.0
|
|
30
|
+
Requires-Dist: python-frontmatter<2,>=1.1
|
|
31
|
+
Requires-Dist: pyyaml<7,>=6.0
|
|
32
|
+
Requires-Dist: structlog<26,>=25.0
|
|
33
|
+
Provides-Extra: admin
|
|
34
|
+
Requires-Dist: psycopg2-binary<3,>=2.9; extra == 'admin'
|
|
35
|
+
Requires-Dist: reflex<0.9,>=0.8.28; extra == 'admin'
|
|
36
|
+
Requires-Dist: sqlmodel<1,>=0.0.22; extra == 'admin'
|
|
37
|
+
Provides-Extra: all
|
|
38
|
+
Requires-Dist: azure-storage-blob<13,>=12.24; extra == 'all'
|
|
39
|
+
Requires-Dist: fastembed<0.9,>=0.8; extra == 'all'
|
|
40
|
+
Requires-Dist: obsidian-import[docling,markitdown]<2,>=1.2.0; extra == 'all'
|
|
41
|
+
Requires-Dist: psycopg2-binary<3,>=2.9; extra == 'all'
|
|
42
|
+
Requires-Dist: reflex<0.9,>=0.8.28; extra == 'all'
|
|
43
|
+
Requires-Dist: sqlite-vec<0.2,>=0.1.6; extra == 'all'
|
|
44
|
+
Requires-Dist: sqlmodel<1,>=0.0.22; extra == 'all'
|
|
45
|
+
Provides-Extra: azure
|
|
46
|
+
Requires-Dist: azure-storage-blob<13,>=12.24; extra == 'azure'
|
|
47
|
+
Provides-Extra: convert
|
|
48
|
+
Requires-Dist: obsidian-import[docling,markitdown]<2,>=1.2.0; extra == 'convert'
|
|
49
|
+
Provides-Extra: dev
|
|
50
|
+
Requires-Dist: hypothesis<7,>=6.100; extra == 'dev'
|
|
51
|
+
Requires-Dist: pytest-cov<6,>=5.0; extra == 'dev'
|
|
52
|
+
Requires-Dist: pytest-httpx<1,>=0.35; extra == 'dev'
|
|
53
|
+
Requires-Dist: pytest<10,>=9.0.3; extra == 'dev'
|
|
54
|
+
Requires-Dist: respx<1,>=0.22; extra == 'dev'
|
|
55
|
+
Provides-Extra: vector
|
|
56
|
+
Requires-Dist: fastembed<0.9,>=0.8; extra == 'vector'
|
|
57
|
+
Requires-Dist: sqlite-vec<0.2,>=0.1.6; extra == 'vector'
|
|
58
|
+
Description-Content-Type: text/markdown
|
|
59
|
+
|
|
60
|
+
# m365-brain
|
|
61
|
+
|
|
62
|
+
[](https://github.com/neuralsignal/m365-brain/actions/workflows/ci.yml)
|
|
63
|
+
[](https://www.python.org/downloads/)
|
|
64
|
+
[](https://opensource.org/licenses/MIT)
|
|
65
|
+
|
|
66
|
+
Sync Microsoft 365 data to Obsidian-compatible markdown via the Graph API.
|
|
67
|
+
|
|
68
|
+
## Features
|
|
69
|
+
|
|
70
|
+
- **8 extractors**: Email, Calendar, Teams Chats, Teams Channels, OneDrive, SharePoint, Contacts, Directory
|
|
71
|
+
- **Delta sync** with pagination, exponential backoff retry, and rate limiting
|
|
72
|
+
- **2 storage backends**: local filesystem and Azure Blob Storage
|
|
73
|
+
- **Document conversion** via [obsidian-import](https://pypi.org/project/obsidian-import/) (PDF, DOCX, PPTX, XLSX to markdown)
|
|
74
|
+
- **MSAL device code authentication** with persistent token caching, and named auth profiles so several Entra apps coexist
|
|
75
|
+
- **Markdown index** — FTS5 full-text, vector and hybrid search, entity/relation traversal, and a catalog of the binary files it found
|
|
76
|
+
- **Write-back outbox** — typed intents gated by a per-outbox authority, dispatched and then reconciled against what Graph actually did
|
|
77
|
+
- **Strict Pydantic config** with no defaults, and environment variable expansion
|
|
78
|
+
- **CLI**: `init`, `auth login`, `run`, `extract`, `index`, `outbox`, `files`, `teams`, `vault`, `ops`, `status`
|
|
79
|
+
- **Bicep IaC** for Azure Storage (dev/prod parameter files)
|
|
80
|
+
- **Docker** + Docker Compose with Azurite profile for local development
|
|
81
|
+
|
|
82
|
+
## Installation
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
pip install m365-brain
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Optional extras:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
pip install m365-brain[azure] # Azure Blob Storage backend
|
|
92
|
+
pip install m365-brain[convert] # Document conversion (obsidian-import)
|
|
93
|
+
pip install m365-brain[admin] # Reflex admin dashboard
|
|
94
|
+
pip install m365-brain[all] # Everything
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Quick Start
|
|
98
|
+
|
|
99
|
+
### Write a config file and create the vault
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
m365-brain init config.yaml --vault ./vault
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`init` writes the complete, commented configuration file and creates the vault directories. It refuses to overwrite an existing file. Every path it writes is absolute.
|
|
106
|
+
|
|
107
|
+
### Authenticate
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
m365-brain --config config.yaml auth login --profile mail
|
|
111
|
+
m365-brain --config config.yaml auth status --json
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`--profile` names one of `auth.profiles` in the config; the shipped template defines `mail`, `chat` and `files`. Login opens a device code flow in your browser, and each profile caches its own token at the `token_cache_path` it names.
|
|
115
|
+
|
|
116
|
+
### Run one cycle
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
m365-brain --config config.yaml run --once
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
A cycle is extract → index → post-cycle hooks. `--once` runs every enabled unit whether or not its `poll_interval_minutes` says it is due; without it, `run` loops and honours the schedule.
|
|
123
|
+
|
|
124
|
+
### Run continuously
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
m365-brain --config config.yaml run
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Each unit runs on its own `poll_interval_minutes`; the loop wakes every `service.continuous_poll_seconds`.
|
|
131
|
+
|
|
132
|
+
### Filter to some units
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
m365-brain --config config.yaml run --once --only email,calendar
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Search what was synced
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
m365-brain --config config.yaml index search "quarterly review" --json
|
|
142
|
+
m365-brain --config config.yaml index recent --timeframe 7d --json
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Results go to **stdout**, logs to **stderr**, so `--json` output parses without being separated from log noise first. Any verb taking a `--limit` reports `total`, `returned` and `limit`, so a truncated answer is visible as one.
|
|
146
|
+
|
|
147
|
+
## Configuration
|
|
148
|
+
|
|
149
|
+
All configuration lives in one YAML file — or several, comma-separated and deep-merged left to right. Environment variables are expanded at load time using `${VAR_NAME}` syntax, and **a missing variable raises** rather than expanding to an empty string.
|
|
150
|
+
|
|
151
|
+
`m365-brain init` writes the reference configuration, whose comments *are* the documentation for every key. It is packaged at `m365_brain/templates/m365-brain.yaml`; the `config/` directory in this repo holds the split fragments the Docker images merge. Rather than restate it here — a copy that rots the first time a key moves — read the file `init` produced:
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
m365-brain --config config.yaml config validate
|
|
155
|
+
m365-brain --config config.yaml config show --json
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
`config validate` also resolves the configured hooks, which makes it a preflight rather than a syntax check. `config show` prints the effective merged config with secrets redacted.
|
|
159
|
+
|
|
160
|
+
Every section is strict: an unknown key is rejected, and no field anywhere has a default. A value the package needs is a value the config states.
|
|
161
|
+
|
|
162
|
+
### Environment variables
|
|
163
|
+
|
|
164
|
+
The config loader expands `${VAR_NAME}` references at load time. Required variables:
|
|
165
|
+
|
|
166
|
+
| Variable | Purpose |
|
|
167
|
+
|----------|---------|
|
|
168
|
+
| `MSAL_CLIENT_ID` | Azure AD app registration client ID |
|
|
169
|
+
| `MSAL_TENANT_ID` | Azure AD tenant ID |
|
|
170
|
+
| `AZURE_STORAGE_CONNECTION_STRING` | Connection string (Azure Blob backend only) |
|
|
171
|
+
| `AZURE_STORAGE_CONTAINER` | Container name (Azure Blob backend only) |
|
|
172
|
+
| `AZURE_STORAGE_PREFIX` | Blob prefix / virtual directory (Azure Blob backend only) |
|
|
173
|
+
|
|
174
|
+
## Azure Blob Storage
|
|
175
|
+
|
|
176
|
+
To use Azure Blob Storage instead of local filesystem, set `storage.backend: "azure_blob"` in your config. See `config/storage/azure_blob.yaml` for a complete example:
|
|
177
|
+
|
|
178
|
+
```yaml
|
|
179
|
+
storage:
|
|
180
|
+
backend: "azure_blob"
|
|
181
|
+
azure_blob:
|
|
182
|
+
connection_string: "${AZURE_STORAGE_CONNECTION_STRING}"
|
|
183
|
+
container_name: "${AZURE_STORAGE_CONTAINER}"
|
|
184
|
+
prefix: "${AZURE_STORAGE_PREFIX}"
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Azurite (local development)
|
|
188
|
+
|
|
189
|
+
Start the Azurite emulator for local blob storage testing:
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
docker compose --profile azurite up -d
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Then set the connection string to Azurite's default:
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
export AZURE_STORAGE_CONNECTION_STRING="DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;"
|
|
199
|
+
export AZURE_STORAGE_CONTAINER="m365-vaults"
|
|
200
|
+
export AZURE_STORAGE_PREFIX="dev"
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Infrastructure
|
|
204
|
+
|
|
205
|
+
Bicep templates in `infra/` provision an Azure Storage account with a private blob container. Parameter files for dev and prod are included.
|
|
206
|
+
|
|
207
|
+
### Deploy
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
# Dev
|
|
211
|
+
az deployment group create \
|
|
212
|
+
--resource-group rg-m365extract-dev \
|
|
213
|
+
--template-file infra/main.bicep \
|
|
214
|
+
--parameters infra/params.dev.bicepparam
|
|
215
|
+
|
|
216
|
+
# Prod
|
|
217
|
+
az deployment group create \
|
|
218
|
+
--resource-group rg-m365extract-prod \
|
|
219
|
+
--template-file infra/main.bicep \
|
|
220
|
+
--parameters infra/params.prod.bicepparam
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
The template creates:
|
|
224
|
+
|
|
225
|
+
- Storage account (`stm365ext{environment}`) in Switzerland North
|
|
226
|
+
- TLS 1.2 minimum, HTTPS only, no public blob access
|
|
227
|
+
- A single blob container (`m365-vaults` by default)
|
|
228
|
+
|
|
229
|
+
## Docker
|
|
230
|
+
|
|
231
|
+
### Full stack (local dev)
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
docker compose up --build # web + postgres (daemon runs inside web)
|
|
235
|
+
docker compose --profile azurite up # include Azurite blob emulator
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## Development
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
git clone https://github.com/neuralsignal/m365-brain.git
|
|
242
|
+
cd m365-brain
|
|
243
|
+
pixi install
|
|
244
|
+
pixi run test # unit tests (excludes integration + azurite markers)
|
|
245
|
+
pixi run test-cov # unit tests with coverage
|
|
246
|
+
pixi run test-azurite # tests requiring Azurite emulator
|
|
247
|
+
pixi run lint # ruff check
|
|
248
|
+
pixi run format # ruff format
|
|
249
|
+
pixi run pre-commit-install # install git hooks
|
|
250
|
+
pixi run docs-serve # local MkDocs dev server
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Project structure
|
|
254
|
+
|
|
255
|
+
```
|
|
256
|
+
m365-brain/
|
|
257
|
+
m365_brain/
|
|
258
|
+
config/ # Strict Pydantic config: loading, merge, env expansion
|
|
259
|
+
model.py # Entity / Observation / Relation and the query types
|
|
260
|
+
parsers/ # Markdown and frontmatter into the model
|
|
261
|
+
storage/ # StorageBackend protocol, local filesystem, Azure Blob
|
|
262
|
+
state.py # StateStore protocol; delta tokens, cursors, cycle history
|
|
263
|
+
vault/ # Every path in the vault, plus the intent envelope
|
|
264
|
+
outbox/ # Vendor-agnostic write-back: authorities, runner, reconcile
|
|
265
|
+
index/ # The knowledge half -- backends, search, vectors, catalog
|
|
266
|
+
m365/ # The Microsoft half -- Graph client, auth, extractors, outboxes
|
|
267
|
+
cycle.py # One cycle: extract, index, hooks
|
|
268
|
+
cli.py # Click CLI -- the whole operating surface
|
|
269
|
+
commands/ # One module per command group
|
|
270
|
+
workspace.py # The library facade: a config path in, a working handle out
|
|
271
|
+
m365_admin/ # Reflex admin dashboard (optional extra)
|
|
272
|
+
skills/ # Bundled agent skills, thin wrappers over the CLI
|
|
273
|
+
config/ # Config fragments the Docker images merge
|
|
274
|
+
tests/ # pytest + hypothesis, mirroring the source layout
|
|
275
|
+
infra/ # Bicep IaC for Azure Storage
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
`index/` never imports `m365/` and the two are peers by construction, so the knowledge layer works end to end on ordinary markdown with no Microsoft 365 present. That rule, the allowed directory list, the 300-line module cap, and the test-presence map are enforced by `scripts/check_structure.py` rather than by review.
|
|
279
|
+
|
|
280
|
+
## Architecture
|
|
281
|
+
|
|
282
|
+
```mermaid
|
|
283
|
+
graph LR
|
|
284
|
+
A[MSAL Auth] --> B[Graph Client]
|
|
285
|
+
B --> C[Extractors]
|
|
286
|
+
C --> D{Convert?}
|
|
287
|
+
D -->|yes| E[obsidian-import]
|
|
288
|
+
D -->|no| F[Markdown Writer]
|
|
289
|
+
E --> G[Storage Backend]
|
|
290
|
+
F --> G
|
|
291
|
+
G --> H[Local FS]
|
|
292
|
+
G --> I[Azure Blob]
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
**Graph Client** (`m365_brain/m365/client.py`) wraps `httpx` with automatic token refresh, exponential backoff on 429/5xx, and paginated response iteration. Each **extractor** under `m365_brain/m365/extractors/` calls Graph endpoints for its data source, renders markdown with YAML frontmatter through the builders in `m365_brain/m365/frontmatter/`, and persists through the **Storage Backend** interface. OneDrive and SharePoint extractors optionally route binary files through **obsidian-import** for document-to-markdown conversion.
|
|
296
|
+
|
|
297
|
+
**Sync state** tracks delta links and timestamps per unit through the `StateStore` protocol, written as JSON under the vault's meta directory. It is bookkeeping, not data: deleting it forces a full re-pull, never a data loss.
|
|
298
|
+
|
|
299
|
+
## Graph API Scopes
|
|
300
|
+
|
|
301
|
+
| Scope | Used by |
|
|
302
|
+
|-------|---------|
|
|
303
|
+
| `User.Read` | Token validation |
|
|
304
|
+
| `Mail.Read` | Email extractor |
|
|
305
|
+
| `Calendars.Read` | Calendar extractor |
|
|
306
|
+
| `Chat.Read` | Teams chats extractor |
|
|
307
|
+
| `ChannelMessage.Read.All` | Teams channels extractor |
|
|
308
|
+
| `Team.ReadBasic.All` + `Channel.ReadBasic.All` | Teams channels extractor — discovery mode only (`channels: null`); not needed with an explicit `channels` list |
|
|
309
|
+
| `Files.Read.All` | OneDrive + SharePoint extractors |
|
|
310
|
+
| `Sites.Read.All` | SharePoint extractor |
|
|
311
|
+
| `Contacts.Read` | Contacts extractor |
|
|
312
|
+
| `User.Read.All` | Directory extractor |
|
|
313
|
+
| `Directory.Read.All` | Directory extractor (manager chain, direct reports) |
|
|
314
|
+
| `offline_access` | Persistent token refresh |
|
|
315
|
+
|
|
316
|
+
All scopes use delegated (user) permissions via the device code flow. No application-level permissions are required.
|
|
317
|
+
|
|
318
|
+
## Releases
|
|
319
|
+
|
|
320
|
+
This project uses [Release Please](https://github.com/googleapis/release-please) for automated versioning and changelog generation. Commits following [Conventional Commits](https://www.conventionalcommits.org/) are parsed to determine version bumps.
|
|
321
|
+
|
|
322
|
+
## License
|
|
323
|
+
|
|
324
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
# m365-brain
|
|
2
|
+
|
|
3
|
+
[](https://github.com/neuralsignal/m365-brain/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.python.org/downloads/)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
|
|
7
|
+
Sync Microsoft 365 data to Obsidian-compatible markdown via the Graph API.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **8 extractors**: Email, Calendar, Teams Chats, Teams Channels, OneDrive, SharePoint, Contacts, Directory
|
|
12
|
+
- **Delta sync** with pagination, exponential backoff retry, and rate limiting
|
|
13
|
+
- **2 storage backends**: local filesystem and Azure Blob Storage
|
|
14
|
+
- **Document conversion** via [obsidian-import](https://pypi.org/project/obsidian-import/) (PDF, DOCX, PPTX, XLSX to markdown)
|
|
15
|
+
- **MSAL device code authentication** with persistent token caching, and named auth profiles so several Entra apps coexist
|
|
16
|
+
- **Markdown index** — FTS5 full-text, vector and hybrid search, entity/relation traversal, and a catalog of the binary files it found
|
|
17
|
+
- **Write-back outbox** — typed intents gated by a per-outbox authority, dispatched and then reconciled against what Graph actually did
|
|
18
|
+
- **Strict Pydantic config** with no defaults, and environment variable expansion
|
|
19
|
+
- **CLI**: `init`, `auth login`, `run`, `extract`, `index`, `outbox`, `files`, `teams`, `vault`, `ops`, `status`
|
|
20
|
+
- **Bicep IaC** for Azure Storage (dev/prod parameter files)
|
|
21
|
+
- **Docker** + Docker Compose with Azurite profile for local development
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install m365-brain
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Optional extras:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install m365-brain[azure] # Azure Blob Storage backend
|
|
33
|
+
pip install m365-brain[convert] # Document conversion (obsidian-import)
|
|
34
|
+
pip install m365-brain[admin] # Reflex admin dashboard
|
|
35
|
+
pip install m365-brain[all] # Everything
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Quick Start
|
|
39
|
+
|
|
40
|
+
### Write a config file and create the vault
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
m365-brain init config.yaml --vault ./vault
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`init` writes the complete, commented configuration file and creates the vault directories. It refuses to overwrite an existing file. Every path it writes is absolute.
|
|
47
|
+
|
|
48
|
+
### Authenticate
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
m365-brain --config config.yaml auth login --profile mail
|
|
52
|
+
m365-brain --config config.yaml auth status --json
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`--profile` names one of `auth.profiles` in the config; the shipped template defines `mail`, `chat` and `files`. Login opens a device code flow in your browser, and each profile caches its own token at the `token_cache_path` it names.
|
|
56
|
+
|
|
57
|
+
### Run one cycle
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
m365-brain --config config.yaml run --once
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
A cycle is extract → index → post-cycle hooks. `--once` runs every enabled unit whether or not its `poll_interval_minutes` says it is due; without it, `run` loops and honours the schedule.
|
|
64
|
+
|
|
65
|
+
### Run continuously
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
m365-brain --config config.yaml run
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Each unit runs on its own `poll_interval_minutes`; the loop wakes every `service.continuous_poll_seconds`.
|
|
72
|
+
|
|
73
|
+
### Filter to some units
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
m365-brain --config config.yaml run --once --only email,calendar
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Search what was synced
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
m365-brain --config config.yaml index search "quarterly review" --json
|
|
83
|
+
m365-brain --config config.yaml index recent --timeframe 7d --json
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Results go to **stdout**, logs to **stderr**, so `--json` output parses without being separated from log noise first. Any verb taking a `--limit` reports `total`, `returned` and `limit`, so a truncated answer is visible as one.
|
|
87
|
+
|
|
88
|
+
## Configuration
|
|
89
|
+
|
|
90
|
+
All configuration lives in one YAML file — or several, comma-separated and deep-merged left to right. Environment variables are expanded at load time using `${VAR_NAME}` syntax, and **a missing variable raises** rather than expanding to an empty string.
|
|
91
|
+
|
|
92
|
+
`m365-brain init` writes the reference configuration, whose comments *are* the documentation for every key. It is packaged at `m365_brain/templates/m365-brain.yaml`; the `config/` directory in this repo holds the split fragments the Docker images merge. Rather than restate it here — a copy that rots the first time a key moves — read the file `init` produced:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
m365-brain --config config.yaml config validate
|
|
96
|
+
m365-brain --config config.yaml config show --json
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`config validate` also resolves the configured hooks, which makes it a preflight rather than a syntax check. `config show` prints the effective merged config with secrets redacted.
|
|
100
|
+
|
|
101
|
+
Every section is strict: an unknown key is rejected, and no field anywhere has a default. A value the package needs is a value the config states.
|
|
102
|
+
|
|
103
|
+
### Environment variables
|
|
104
|
+
|
|
105
|
+
The config loader expands `${VAR_NAME}` references at load time. Required variables:
|
|
106
|
+
|
|
107
|
+
| Variable | Purpose |
|
|
108
|
+
|----------|---------|
|
|
109
|
+
| `MSAL_CLIENT_ID` | Azure AD app registration client ID |
|
|
110
|
+
| `MSAL_TENANT_ID` | Azure AD tenant ID |
|
|
111
|
+
| `AZURE_STORAGE_CONNECTION_STRING` | Connection string (Azure Blob backend only) |
|
|
112
|
+
| `AZURE_STORAGE_CONTAINER` | Container name (Azure Blob backend only) |
|
|
113
|
+
| `AZURE_STORAGE_PREFIX` | Blob prefix / virtual directory (Azure Blob backend only) |
|
|
114
|
+
|
|
115
|
+
## Azure Blob Storage
|
|
116
|
+
|
|
117
|
+
To use Azure Blob Storage instead of local filesystem, set `storage.backend: "azure_blob"` in your config. See `config/storage/azure_blob.yaml` for a complete example:
|
|
118
|
+
|
|
119
|
+
```yaml
|
|
120
|
+
storage:
|
|
121
|
+
backend: "azure_blob"
|
|
122
|
+
azure_blob:
|
|
123
|
+
connection_string: "${AZURE_STORAGE_CONNECTION_STRING}"
|
|
124
|
+
container_name: "${AZURE_STORAGE_CONTAINER}"
|
|
125
|
+
prefix: "${AZURE_STORAGE_PREFIX}"
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Azurite (local development)
|
|
129
|
+
|
|
130
|
+
Start the Azurite emulator for local blob storage testing:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
docker compose --profile azurite up -d
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Then set the connection string to Azurite's default:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
export AZURE_STORAGE_CONNECTION_STRING="DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;"
|
|
140
|
+
export AZURE_STORAGE_CONTAINER="m365-vaults"
|
|
141
|
+
export AZURE_STORAGE_PREFIX="dev"
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Infrastructure
|
|
145
|
+
|
|
146
|
+
Bicep templates in `infra/` provision an Azure Storage account with a private blob container. Parameter files for dev and prod are included.
|
|
147
|
+
|
|
148
|
+
### Deploy
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
# Dev
|
|
152
|
+
az deployment group create \
|
|
153
|
+
--resource-group rg-m365extract-dev \
|
|
154
|
+
--template-file infra/main.bicep \
|
|
155
|
+
--parameters infra/params.dev.bicepparam
|
|
156
|
+
|
|
157
|
+
# Prod
|
|
158
|
+
az deployment group create \
|
|
159
|
+
--resource-group rg-m365extract-prod \
|
|
160
|
+
--template-file infra/main.bicep \
|
|
161
|
+
--parameters infra/params.prod.bicepparam
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
The template creates:
|
|
165
|
+
|
|
166
|
+
- Storage account (`stm365ext{environment}`) in Switzerland North
|
|
167
|
+
- TLS 1.2 minimum, HTTPS only, no public blob access
|
|
168
|
+
- A single blob container (`m365-vaults` by default)
|
|
169
|
+
|
|
170
|
+
## Docker
|
|
171
|
+
|
|
172
|
+
### Full stack (local dev)
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
docker compose up --build # web + postgres (daemon runs inside web)
|
|
176
|
+
docker compose --profile azurite up # include Azurite blob emulator
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Development
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
git clone https://github.com/neuralsignal/m365-brain.git
|
|
183
|
+
cd m365-brain
|
|
184
|
+
pixi install
|
|
185
|
+
pixi run test # unit tests (excludes integration + azurite markers)
|
|
186
|
+
pixi run test-cov # unit tests with coverage
|
|
187
|
+
pixi run test-azurite # tests requiring Azurite emulator
|
|
188
|
+
pixi run lint # ruff check
|
|
189
|
+
pixi run format # ruff format
|
|
190
|
+
pixi run pre-commit-install # install git hooks
|
|
191
|
+
pixi run docs-serve # local MkDocs dev server
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Project structure
|
|
195
|
+
|
|
196
|
+
```
|
|
197
|
+
m365-brain/
|
|
198
|
+
m365_brain/
|
|
199
|
+
config/ # Strict Pydantic config: loading, merge, env expansion
|
|
200
|
+
model.py # Entity / Observation / Relation and the query types
|
|
201
|
+
parsers/ # Markdown and frontmatter into the model
|
|
202
|
+
storage/ # StorageBackend protocol, local filesystem, Azure Blob
|
|
203
|
+
state.py # StateStore protocol; delta tokens, cursors, cycle history
|
|
204
|
+
vault/ # Every path in the vault, plus the intent envelope
|
|
205
|
+
outbox/ # Vendor-agnostic write-back: authorities, runner, reconcile
|
|
206
|
+
index/ # The knowledge half -- backends, search, vectors, catalog
|
|
207
|
+
m365/ # The Microsoft half -- Graph client, auth, extractors, outboxes
|
|
208
|
+
cycle.py # One cycle: extract, index, hooks
|
|
209
|
+
cli.py # Click CLI -- the whole operating surface
|
|
210
|
+
commands/ # One module per command group
|
|
211
|
+
workspace.py # The library facade: a config path in, a working handle out
|
|
212
|
+
m365_admin/ # Reflex admin dashboard (optional extra)
|
|
213
|
+
skills/ # Bundled agent skills, thin wrappers over the CLI
|
|
214
|
+
config/ # Config fragments the Docker images merge
|
|
215
|
+
tests/ # pytest + hypothesis, mirroring the source layout
|
|
216
|
+
infra/ # Bicep IaC for Azure Storage
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
`index/` never imports `m365/` and the two are peers by construction, so the knowledge layer works end to end on ordinary markdown with no Microsoft 365 present. That rule, the allowed directory list, the 300-line module cap, and the test-presence map are enforced by `scripts/check_structure.py` rather than by review.
|
|
220
|
+
|
|
221
|
+
## Architecture
|
|
222
|
+
|
|
223
|
+
```mermaid
|
|
224
|
+
graph LR
|
|
225
|
+
A[MSAL Auth] --> B[Graph Client]
|
|
226
|
+
B --> C[Extractors]
|
|
227
|
+
C --> D{Convert?}
|
|
228
|
+
D -->|yes| E[obsidian-import]
|
|
229
|
+
D -->|no| F[Markdown Writer]
|
|
230
|
+
E --> G[Storage Backend]
|
|
231
|
+
F --> G
|
|
232
|
+
G --> H[Local FS]
|
|
233
|
+
G --> I[Azure Blob]
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
**Graph Client** (`m365_brain/m365/client.py`) wraps `httpx` with automatic token refresh, exponential backoff on 429/5xx, and paginated response iteration. Each **extractor** under `m365_brain/m365/extractors/` calls Graph endpoints for its data source, renders markdown with YAML frontmatter through the builders in `m365_brain/m365/frontmatter/`, and persists through the **Storage Backend** interface. OneDrive and SharePoint extractors optionally route binary files through **obsidian-import** for document-to-markdown conversion.
|
|
237
|
+
|
|
238
|
+
**Sync state** tracks delta links and timestamps per unit through the `StateStore` protocol, written as JSON under the vault's meta directory. It is bookkeeping, not data: deleting it forces a full re-pull, never a data loss.
|
|
239
|
+
|
|
240
|
+
## Graph API Scopes
|
|
241
|
+
|
|
242
|
+
| Scope | Used by |
|
|
243
|
+
|-------|---------|
|
|
244
|
+
| `User.Read` | Token validation |
|
|
245
|
+
| `Mail.Read` | Email extractor |
|
|
246
|
+
| `Calendars.Read` | Calendar extractor |
|
|
247
|
+
| `Chat.Read` | Teams chats extractor |
|
|
248
|
+
| `ChannelMessage.Read.All` | Teams channels extractor |
|
|
249
|
+
| `Team.ReadBasic.All` + `Channel.ReadBasic.All` | Teams channels extractor — discovery mode only (`channels: null`); not needed with an explicit `channels` list |
|
|
250
|
+
| `Files.Read.All` | OneDrive + SharePoint extractors |
|
|
251
|
+
| `Sites.Read.All` | SharePoint extractor |
|
|
252
|
+
| `Contacts.Read` | Contacts extractor |
|
|
253
|
+
| `User.Read.All` | Directory extractor |
|
|
254
|
+
| `Directory.Read.All` | Directory extractor (manager chain, direct reports) |
|
|
255
|
+
| `offline_access` | Persistent token refresh |
|
|
256
|
+
|
|
257
|
+
All scopes use delegated (user) permissions via the device code flow. No application-level permissions are required.
|
|
258
|
+
|
|
259
|
+
## Releases
|
|
260
|
+
|
|
261
|
+
This project uses [Release Please](https://github.com/googleapis/release-please) for automated versioning and changelog generation. Commits following [Conventional Commits](https://www.conventionalcommits.org/) are parsed to determine version bumps.
|
|
262
|
+
|
|
263
|
+
## License
|
|
264
|
+
|
|
265
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""m365-brain: Sync Microsoft 365 data to Obsidian-compatible markdown via Graph API."""
|