Metadata-Version: 2.4
Name: ms365-toolkit
Version: 0.1.17
Summary: Local-first Microsoft 365 email, calendar, and Teams toolkit with MCP support and guarded write flows
Project-URL: Homepage, https://github.com/sadhiappan/ms365-toolkit
Project-URL: Repository, https://github.com/sadhiappan/ms365-toolkit
Project-URL: Issues, https://github.com/sadhiappan/ms365-toolkit/issues
Author-email: Shiv Adhiappan <shivaram1190@gmail.com>
Maintainer-email: Shiv Adhiappan <shivaram1190@gmail.com>
License-Expression: MIT
License-File: LICENSE
Keywords: calendar,email,mcp,microsoft-365,microsoft-graph,teams
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Communications :: Email
Classifier: Topic :: Office/Business
Requires-Python: >=3.11
Requires-Dist: beautifulsoup4<5,>=4.12
Requires-Dist: keyring<26,>=25.0
Requires-Dist: msal<2,>=1.31
Requires-Dist: openpyxl<4,>=3.1
Requires-Dist: packaging<26,>=24
Requires-Dist: pyjwt<3,>=2.8
Requires-Dist: pypdf<6,>=5.0
Requires-Dist: python-docx<2,>=1.1
Requires-Dist: python-pptx<2,>=1.0
Requires-Dist: requests<3,>=2.33
Requires-Dist: structlog<26,>=24.4
Provides-Extra: dev
Requires-Dist: mypy<2,>=1.13; extra == 'dev'
Requires-Dist: pytest-asyncio<1,>=0.24; extra == 'dev'
Requires-Dist: pytest-cov<7,>=6.0; extra == 'dev'
Requires-Dist: pytest<9,>=8.3; extra == 'dev'
Requires-Dist: ruff<1,>=0.8; extra == 'dev'
Requires-Dist: types-openpyxl<4,>=3.1; extra == 'dev'
Provides-Extra: mcp
Requires-Dist: fastmcp<4,>=3.0; extra == 'mcp'
Requires-Dist: pygments<3,>=2.20; extra == 'mcp'
Description-Content-Type: text/markdown

# ms365-toolkit

`ms365-toolkit` is a generic Microsoft 365 email, calendar, and Teams toolkit with:
- device-code auth
- Graph read clients and guarded email write flows
- local mailbox indexing
- triage and briefing workflows
- a local labeling workflow for improving per-user email intelligence

Public beta note: this is local-first tooling for delegated Microsoft 365 access.
Users configure their own Microsoft Entra public-client app and grant the delegated
Graph permissions required by the features they use.

See [CHANGELOG.md](CHANGELOG.md) for release notes.

## Security and Privacy

- Tokens, local profile config, mailbox indexes, labels, audit logs, and usage analytics stay on the user's machine.
- Device-code auth uses a public-client Entra app registration; no client secret is required.
- Usage analytics record coarse command/tool metadata, durations, sizes, and sanitized errors, not email bodies, recipients, subjects, transcript text, raw Graph IDs, or tokens.
- Guarded email writes require a write token, `user.endpoint = "me"`, configured allowlists, rate limits, confirmation/audit checks, and a content hash for send.
- Microsoft Graph access is delegated and tenant-controlled. Some Teams channel-message, file, transcript, and Copilot notes features require additional scopes, admin consent, licenses, or available tenant data.

## Self-Service Setup

This is the public setup path for using the Microsoft 365 MCP from Codex or
Claude. You do not need to clone this repository.

### What You Are Setting Up

- Your AI client launches a local stdio MCP server named `ms365`.
- The MCP server runs on your machine through `uvx ms365-toolkit-mcp@latest`.
- The MCP server uses your Microsoft 365 delegated login to call Microsoft Graph.
- Tokens are stored in your OS keychain and profile config stays under `~/.ms365-toolkit`.
- No client secret is used or stored.

### Before You Start

You need:

- Python 3.11 or newer.
- `uv`, verified with `uv --version`.
- Codex CLI or Claude CLI, verified with `codex --version` or `claude --version`.
- A Microsoft 365 work or school account with access to the mailbox, calendar, and Teams data you want to use.
- Permission to create or update a Microsoft Entra app registration. If you cannot access app registrations or admin consent, ask your Microsoft 365 tenant admin.

Install `uv` if it is missing:

```bash
curl -LsSf https://astral.sh/uv/install.sh | sh
```

### 1. Create the Microsoft Entra App

Open the Microsoft Entra admin center:

```text
https://entra.microsoft.com
```

Create the app registration:

- Go to `Identity` -> `Applications` -> `App registrations` -> `New registration`.
- Name it something recognizable, such as `ms365-toolkit-local`.
- Choose `Accounts in this organizational directory only` unless you intentionally need a multi-tenant app.
- Leave redirect URI blank.
- Select `Register`.

Copy these two values from the app `Overview` page:

- `Application (client) ID`: this becomes `client_id`.
- `Directory (tenant) ID`: this becomes `tenant_id`.

These are IDs, not passwords or API keys.

Enable device-code login:

- Go to `Authentication`.
- Under `Advanced settings`, set `Allow public client flows` to `Yes`.
- Select `Save`.

Do not create a client secret for this toolkit. Device-code auth uses a public
client app registration. No client secret is required.

Microsoft references:

- App registration: https://learn.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app
- Public client flows: https://learn.microsoft.com/en-us/entra/identity-platform/msal-client-applications

### 2. Add Microsoft Graph Permissions

In the app registration:

- Go to `API permissions`.
- Select `Add a permission`.
- Choose `Microsoft Graph`.
- Choose `Delegated permissions`.
- Add the permissions for the features you want.
- If your tenant requires admin consent, have an admin select `Grant admin consent`.

Start with the base read-only permissions:

| Feature | Delegated permissions | Login command |
| --- | --- | --- |
| Email, calendar, basic Teams reads | `Mail.Read`, `Calendars.Read`, `MailboxSettings.Read`, `Chat.Read`, `Team.ReadBasic.All`, `Channel.ReadBasic.All` | `auth login` |
| Teams channel messages | `ChannelMessage.Read.All` | `auth login --teams-channel-messages` |
| Teams file downloads | `Files.Read` | `auth login --teams-files` |
| Email inline SharePoint/OneDrive link downloads | `Files.Read.All`, `Sites.Read.All` | `auth login --share-links` |
| Meeting transcripts | `OnlineMeetings.Read`, `OnlineMeetingTranscript.Read.All` | `auth login --meeting-transcripts` |
| Meeting notes and Copilot insights | `OnlineMeetings.Read`, `OnlineMeetingAiInsight.Read.All` | `auth login --meeting-notes` |
| Guarded email and calendar writes | `Mail.ReadWrite`, `Mail.Send`, `Calendars.ReadWrite`, `MailboxSettings.ReadWrite` | `auth login --write` |

Only add optional permissions for features you plan to use. Channel messages,
meeting transcripts, meeting notes, inline SharePoint/OneDrive link downloads,
and some tenant-wide Teams data commonly require admin consent, tenant policy
support, the right license, and actual available data.

Microsoft references:

- Microsoft Graph permissions: https://learn.microsoft.com/en-us/graph/permissions-reference
- Admin consent behavior: https://learn.microsoft.com/en-us/entra/identity-platform/consent-types-developer

### 3. Create Local Config

Create the default profile config:

```bash
mkdir -p ~/.ms365-toolkit/profiles/default
cat > ~/.ms365-toolkit/profiles/default/config.toml <<'EOF'
[auth]
tenant_id = "<paste-directory-tenant-id-here>"
client_id = "<paste-application-client-id-here>"

[user]
endpoint = "me"
user_principal_name = ""
timezone = "America/Chicago"

[safety]
domain_allowlist = []
folder_allowlist = ["Archive", "Follow Up"]
send_email_per_hour = 5
send_email_per_day = 20
draft_email_per_hour = 10
draft_email_per_day = 30
reply_email_per_hour = 5
reply_email_per_day = 20
forward_email_per_hour = 5
forward_email_per_day = 20
move_email_per_hour = 20
flag_email_per_hour = 50
create_event_per_hour = 5
create_event_per_day = 20
update_event_per_hour = 10
respond_event_per_hour = 10

[vip]
emails = []
domains = []

[intelligence]
critical_keywords = ["exec", "leadership", "decision", "board"]
noise_keywords = ["ooo", "focus", "block"]
action_keywords = ["urgent", "P1", "approve", "deadline"]
EOF
```

Edit the file and replace:

- `<paste-directory-tenant-id-here>` with the `Directory (tenant) ID`.
- `<paste-application-client-id-here>` with the `Application (client) ID`.
- `timezone` with your timezone if `America/Chicago` is not correct.

Keep `endpoint = "me"` for normal personal mailbox, calendar, and Teams access.
`endpoint = "users"` is only for delegated/shared mailbox scenarios and requires
`user_principal_name`.

`domain_allowlist = []` is safe for read-only use. Guarded email writes will stay
blocked until you explicitly add allowed recipient domains.

### 4. Authenticate

Run the base read-only login:

```bash
uvx --from ms365-toolkit==0.1.17 ms365-toolkit auth login
```

The CLI prints a verification URL and user code. Open the URL in a browser,
enter the code, and sign in with your Microsoft 365 account.

Run one login command with the optional flags you configured. For example, you
can combine flags:

```bash
uvx --from ms365-toolkit==0.1.17 ms365-toolkit auth login --teams-channel-messages --teams-files --share-links --meeting-transcripts --meeting-notes
uvx --from ms365-toolkit==0.1.17 ms365-toolkit auth login --write
```

Use `--write` separately because write tokens are stored separately from read
tokens.

Check status:

```bash
uvx --from ms365-toolkit==0.1.17 ms365-toolkit auth status
uvx --from ms365-toolkit==0.1.17 ms365-toolkit doctor
```

### 5. Add the MCP to Codex or Claude

Recommended Codex registration:

```bash
codex mcp add ms365 --env MS365_TOOLKIT_PROFILE=default --env MS365_TOOLKIT_WARN_STALE=1 --env UV_CACHE_DIR=/tmp/ms365-toolkit-uv-cache --env UV_TOOL_DIR=/tmp/ms365-toolkit-uv-tools -- sh -lc 'cd /tmp && exec uvx ms365-toolkit-mcp@latest'
```

Recommended Claude registration:

```bash
claude mcp add -s user ms365 -e MS365_TOOLKIT_PROFILE=default -e MS365_TOOLKIT_WARN_STALE=1 -e UV_CACHE_DIR=/tmp/ms365-toolkit-uv-cache -e UV_TOOL_DIR=/tmp/ms365-toolkit-uv-tools -- sh -lc 'cd /tmp && exec uvx ms365-toolkit-mcp@latest'
```

Restart any open Codex or Claude sessions after registration. Existing sessions
usually keep using the MCP processes they started with.

Verify the saved registration:

```bash
codex mcp get ms365
claude mcp get ms365
```

Both should show a command ending with:

```bash
uvx ms365-toolkit-mcp@latest
```

The `@latest` suffix asks `uvx` to refresh cached package metadata and resolve
the latest published MCP wrapper when a new MCP process starts. The
`MS365_TOOLKIT_WARN_STALE=1` environment variable enables startup and response
warnings if a process is still running an older version.

### 6. Verify Inside Your AI Session

After restarting Codex or Claude, ask the AI to run these MCP checks:

```text
Use the ms365 MCP tool get_toolkit_status.
Use the ms365 MCP tool list_inbox with top 3 and max_chars 12000.
```

A healthy setup should return toolkit status and a small inbox summary. If the
tool is missing, the AI session probably started before MCP registration or is
not using the profile where `ms365` was added.

You can also verify from a terminal:

```bash
uvx --from ms365-toolkit==0.1.17 ms365-toolkit inbox --top 5
uvx --from ms365-toolkit==0.1.17 ms365-toolkit version --check
```

### Example Use Cases

After setup, ask Codex or Claude to use the `ms365` MCP with prompts like these.

#### Check Toolkit Health

```text
Use the ms365 MCP tool get_toolkit_status and tell me whether the toolkit is connected, current, and ready to use.
```

Needs: base setup.

#### Triage Recent Inbox

```text
Use ms365 to list my latest inbox messages, then summarize the top actionable items, deadlines, and who I need to reply to.
```

Needs: base read-only login.

#### Understand an Email Thread

```text
Search my email for "Q3 Vendor Planning", read the conversation thread, and tell me what is going on, open decisions, blockers, and next actions.
```

Needs: base read-only login.

#### Prepare for a Meeting

```text
List my calendar for tomorrow, identify the highest-priority meeting, then search related email and Teams context to create a short prep brief.
```

Needs: base read-only login. Teams context requires the relevant Teams permissions.

#### Pull a Meeting Transcript

```text
Find yesterday's "Pricing Review Knowledge Transfer" meeting and pull the transcript if it was recorded and transcribed. Summarize decisions, risks, and action items.
```

Needs: `auth login --meeting-transcripts`.

#### Pull Meeting Notes

```text
Find the meeting notes or Copilot insights for my latest project meeting and summarize key decisions and follow-ups.
```

Needs: `auth login --meeting-notes`.

#### Search Teams Context

```text
Search Teams messages for "forecast model" and summarize the relevant threads, who said what, and what needs follow-up.
```

Needs: base Teams read permissions. Channel messages require
`auth login --teams-channel-messages`.

#### Read Teams-Shared Files

```text
Find files attached to this Teams message or thread, read the supported documents, and summarize what they contain.
```

Needs: `auth login --teams-files`.

#### Download Files Linked Inside Email

```text
Read this email, list any SharePoint or OneDrive links embedded in the body, download the linked documents, and summarize the extracted text.
```

Needs: `auth login --share-links`.

#### Draft a Guarded Reply

```text
Draft a reply to this email thread that confirms the next steps. Do not send it. Create a draft only and show me the content hash.
```

Needs: `auth login --write` and `domain_allowlist` configured.

#### Find Availability

```text
Find 30-minute windows this week when these attendees are free: person1@example.com and person2@example.com.
```

Needs: base calendar read permissions.

### Common Problems

- `config.toml not found`: rerun the config creation command and confirm the file exists at `~/.ms365-toolkit/profiles/default/config.toml`.
- `invalid_client` or login says the client cannot be found: check that `client_id` is the `Application (client) ID` and `tenant_id` is the `Directory (tenant) ID`.
- Device-code login is blocked: confirm `Authentication` -> `Advanced settings` -> `Allow public client flows` is set to `Yes`.
- Permission or consent error: add the missing Microsoft Graph delegated permission and ask a tenant admin to grant admin consent if required.
- Teams channel messages fail: confirm `ChannelMessage.Read.All` was added and login was run with `--teams-channel-messages`.
- Teams file downloads fail: confirm `Files.Read` was added and login was run with `--teams-files`.
- Email-linked SharePoint/OneDrive downloads fail: confirm `Files.Read.All` and `Sites.Read.All` were added and login was run with `--share-links`.
- Meeting transcripts are empty or skipped: the meeting must have been recorded/transcribed, the signed-in user must have access, and login must include `--meeting-transcripts`.
- Meeting notes are empty or skipped: Copilot notes must exist, the signed-in user must have access, and login must include `--meeting-notes`.
- MCP is not visible in Codex or Claude: run `codex mcp get ms365` or `claude mcp get ms365`, then restart the AI session.
- MCP version is stale: use the `@latest` registration command again, restart the AI session, and run `get_toolkit_status`.
- Email writes are blocked: run `auth login --write`, keep `endpoint = "me"`, and add allowed recipient domains to `domain_allowlist`.

## MCP Tool Behavior

Email, Teams, and calendar list/search MCP tools return compact summaries by
default and accept `max_chars` to cap response size. Use the corresponding
read/get tool when you need the full message, event, thread, attachment,
transcript, or meeting-notes payload.

When a compact MCP response returns `truncated: true` with `next_offset`, repeat
the same tool call with `offset=<next_offset>` to continue through the current
ordered result set. If `next_offset` is `null`, increase `max_chars` because the
next item did not fit.

Graph-backed MCP collection tools can also return `next_cursor`. Repeat the
same tool with `cursor=<next_cursor>` to continue with Microsoft Graph's next
page. If both `next_offset` and `next_cursor` are possible, consume
`next_offset` first; `next_cursor` is only returned after the current response
page fits inside `max_chars`.

Cursor support covers `list_inbox`, `search_emails`, `list_events`,
`search_events`, `list_calendars`, `list_chats`, `list_chat_messages`,
`search_teams_messages`, `list_teams`, `list_channels`,
`list_channel_messages`, and `list_channel_replies`.

Local derived searches such as `search_chats`, `search_chat_messages`, and
`search_channel_messages` remain bounded first-page convenience filters. Use
`search_teams_messages` for paged global Teams message search.

Supported v1 tools include guarded email writes:
- `get_toolkit_status`
- `list_inbox`
- `search_emails`
- `read_email`
- `read_conversation_thread`
- `list_email_attachments`
- `read_email_attachment`
- `list_email_share_links`
- `download_share_link`
- `create_email_draft`
- `create_reply_draft`
- `send_email_draft`
- `list_folders`
- `list_events`
- `get_event`
- `search_events`
- `list_calendars`
- `find_free_slots`
- `list_chats`
- `search_chats`
- `list_chat_messages`
- `search_chat_messages`
- `search_teams_messages`
- `read_chat_message`
- `list_teams`
- `list_channels`
- `list_channel_messages`
- `search_channel_messages`
- `list_channel_replies`
- `read_channel_message`
- `read_channel_thread`
- `list_teams_message_files`
- `read_teams_message_file`
- `list_meeting_transcripts`
- `read_meeting_transcript`
- `read_meeting_notes`

All MCP datetime inputs must be timezone-aware ISO 8601 strings. Email write
operations require a write token, `user.endpoint = "me"`, and a configured
domain allowlist.

## CLI Verification and Useful Commands

Try a basic Teams read:

```bash
uvx --from ms365-toolkit==0.1.17 ms365-toolkit list-chats --top 5
uvx --from ms365-toolkit==0.1.17 ms365-toolkit search-chats "Alex Rivera" --top 3
uvx --from ms365-toolkit==0.1.17 ms365-toolkit list-teams
uvx --from ms365-toolkit==0.1.17 ms365-toolkit search-teams-messages "budget owner" --top 5
uvx --from ms365-toolkit==0.1.17 ms365-toolkit search-channel-messages <team_id> <channel_id> "approval rate" --top 5
uvx --from ms365-toolkit==0.1.17 ms365-toolkit read-channel-thread <team_id> <channel_id> <message_id> --top 10
```

Global Teams search returns ranked search hits and snippets, not hydrated full
message bodies.

Try attachment download:

```bash
uvx --from ms365-toolkit==0.1.17 ms365-toolkit download-email-attachments <message_id> --output-dir ./downloads
uvx --from ms365-toolkit==0.1.17 ms365-toolkit list-email-attachments <message_id>
uvx --from ms365-toolkit==0.1.17 ms365-toolkit read-email-attachment <message_id> <attachment_id> --max-chars 50000
uvx --from ms365-toolkit==0.1.17 ms365-toolkit list-email-share-links <message_id>
uvx --from ms365-toolkit==0.1.17 ms365-toolkit download-share-link "https://contoso.sharepoint.com/sites/example/Shared%20Documents/brief.docx" --output-dir ./downloads
```

Try the guarded email write flow:

```bash
uvx --from ms365-toolkit==0.1.17 ms365-toolkit create-email-draft --to user@example.com --subject "Hello" --body "<p>Hello</p>"
uvx --from ms365-toolkit==0.1.17 ms365-toolkit create-reply-draft <message_id> --body "<p>Reply</p>"
uvx --from ms365-toolkit==0.1.17 ms365-toolkit send-email-draft <draft_id> <content_hash>
```

Inspect local usage analytics:

```bash
uvx --from ms365-toolkit==0.1.17 ms365-toolkit usage summary --days 7
uvx --from ms365-toolkit==0.1.17 ms365-toolkit usage tools --days 7
uvx --from ms365-toolkit==0.1.17 ms365-toolkit usage errors --days 7
uvx --from ms365-toolkit==0.1.17 ms365-toolkit usage slow --days 7 --top 10
```

Usage analytics are local-only JSONL files under the active profile. They record
coarse tool/command metadata, durations, result sizes, and sanitized error
categories. They do not record email bodies, subjects, recipients, message IDs,
transcript text, or tokens. Use `usage slow` to find slow tools and oversized
MCP responses.

Pinned/reproducible MCP registration:

```bash
codex mcp add ms365 --env MS365_TOOLKIT_PROFILE=default --env MS365_TOOLKIT_WARN_STALE=1 --env UV_CACHE_DIR=/tmp/ms365-toolkit-uv-cache --env UV_TOOL_DIR=/tmp/ms365-toolkit-uv-tools -- sh -lc 'cd /tmp && exec uvx --from ms365-toolkit-mcp==0.1.17 ms365-toolkit-mcp'
claude mcp add -s user ms365 -e MS365_TOOLKIT_PROFILE=default -e MS365_TOOLKIT_WARN_STALE=1 -e UV_CACHE_DIR=/tmp/ms365-toolkit-uv-cache -e UV_TOOL_DIR=/tmp/ms365-toolkit-uv-tools -- sh -lc 'cd /tmp && exec uvx --from ms365-toolkit-mcp==0.1.17 ms365-toolkit-mcp'
```

Pinned registrations are deterministic but can become stale. Use them only when
you need an exactly reproducible MCP version. To inspect MCP version state in a
session, call `get_toolkit_status` or run `ms365-toolkit doctor`.
`MS365_TOOLKIT_WARN_STALE=1` performs a short live version check when the MCP
process starts, prints a startup warning if stale, caches the result, and adds a
`version_warning` field to MCP tool responses while the cached status is stale.

## Developer Workflow

### Repo-Local MCP Fallback

Install the MCP extra:

```bash
uv sync --extra mcp
```

Run the local stdio MCP server:

```bash
uv run --extra mcp ms365-toolkit-mcp
```

Or run it directly from the module entrypoint:

```bash
uv run --extra mcp python -m ms365_toolkit.mcp
```

### Real MCP Smoke Test

Run the opt-in real-world MCP smoke test from the local checkout:

```bash
uv run --extra mcp python scripts/smoke_mcp.py --profile default
```

The smoke test launches the local stdio MCP server and calls read-only tools
against real Microsoft Graph data. Email, calendar, toolkit-status, compact
response, and continuation-shape checks are required. Teams, channel messages,
files, transcripts, and meeting notes are tiered optional checks and report
`SKIP` when tenant data or optional delegated scopes are unavailable.

The output is redacted by design. It prints only tool names, pass/fail/skip
status, counts, booleans, and hashed ID prefixes. It does not print subjects,
senders, recipients, message previews, event bodies, Teams message text,
transcript text, meeting notes, tokens, or raw Graph IDs.

Useful variants:

```bash
uv run --extra mcp python scripts/smoke_mcp.py --profile work
uv run --extra mcp python scripts/smoke_mcp.py --timeout 60
```

### Repo-Local One-Command Setup

Use the installer script to wire everything up for both tools:

```bash
./scripts/setup_mcp.sh
```

This is the developer fallback, not the primary public install path. It runs the
MCP server from the local checkout and does not use the published
`ms365-toolkit-mcp@latest` package.

What it does:
- installs the MCP extra with `uv`
- checks that your selected MS365 profile is already authenticated
- registers the `ms365` server with Codex
- registers the `ms365` server with Claude using `user` scope so it is available across sessions

Useful variants:

```bash
./scripts/setup_mcp.sh --target codex
./scripts/setup_mcp.sh --target claude
./scripts/setup_mcp.sh --profile default
./scripts/setup_mcp.sh --dry-run
```

Equivalent Make targets:

```bash
make mcp-setup
make mcp-setup-codex
make mcp-setup-claude
```

Run unit tests:

```bash
uv run --with '.[dev]' pytest tests/unit/ -q
```

### Claude Review Workflow

This repo includes project-local Claude review commands and a shell workflow.

In an interactive `claude` session, use:

```text
/review-critical
/review-critical teams changes
/review-commit f9c3079
/consult Should we expose participant names in 1:1 chats?
```

For terminal or Codex-driven runs, use:

```bash
./scripts/claude_workflow.sh review
./scripts/claude_workflow.sh review-commit f9c3079
./scripts/claude_workflow.sh consult "Should Teams support user.endpoint=users?"
```

Equivalent Make targets:

```bash
make claude-review
make claude-review-commit COMMIT=f9c3079
make claude-consult PROMPT="Should Teams support user.endpoint=users?"
```

## Where Secrets and Local State Live

Do not put secrets or mailbox data into the repo.

Local config:

- `~/.ms365-toolkit/profiles/<profile>/config.toml`

Token caches:

- stored in the OS keychain via `keyring`
- read cache key: `ms365-read-token-<profile>`
- write cache key: `ms365-write-token-<profile>`

Mailbox-derived local state:

- `~/.ms365-toolkit/profiles/<profile>/mail_index.db`
- `~/.ms365-toolkit/profiles/<profile>/thread_labels.jsonl`

## Shareable Code vs Private Local State

The repository is intended to be generic and shareable.

Generic, shareable parts:
- source code under `src/`
- tests under `tests/`
- build and dependency config
- generic CLI commands and labeling UI

Private, per-user local state:
- `~/.ms365-toolkit/profiles/<profile>/config.toml`
- token caches
- local mailbox index databases
- exported label datasets such as `thread_labels.jsonl`
- any mailbox-derived training or evaluation data

Do not commit or share profile directories, token caches, mailbox indexes, or label datasets unless you explicitly want to share mailbox-derived data.

## Local Workflow

1. Configure a local profile under `~/.ms365-toolkit/profiles/<profile>/config.toml`
2. Authenticate with:

```bash
uvx --from ms365-toolkit==0.1.17 ms365-toolkit auth login
```

3. Build a local mailbox index:

```bash
uv run --with '.[dev]' ms365-toolkit sync-mail-index
```

4. Export thread candidates for labeling:

```bash
uv run --with '.[dev]' ms365-toolkit export-label-candidates --top 30 --sync-index
```

5. Review and label locally:

```bash
uv run --with '.[dev]' ms365-toolkit label-ui
```

The learned behavior from triage is driven by each user’s local labeled dataset, not by checked-in project state.
