agent-activity
Streams what the agent is doing into the room, as rows the desktop client renders in an **events drawer** above the composer (collapsed: avatar, pulsing dots,…
Connect to Microsoft 365 (OneDrive, Outlook mail, Calendar, Teams) via Microsoft Graph using the open-source python-o365 (O365) library. Use when the user wants to list/download OneDrive files, read or send Outlook email, list calendar events, or post a message to a Teams
$ npx -y skills add sonichi/sutando --skill ms365-connect --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
/ms365-connectContext preview
The summary Claude sees to decide when to auto-load this skill.
Connect to Microsoft 365 (OneDrive, Outlook mail, Calendar, Teams) via Microsoft Graph using the open-source python-o365 (O365) library. Use when the user wants to list/download OneDrive files, read or send Outlook email, list calendar events, or post a message to a Teams
name: ms365-connect description: >- Connect to Microsoft 365 (OneDrive, Outlook mail, Calendar, Teams) via Microsoft Graph using the open-source python-o365 (O365) library. Use when the user wants to list/download OneDrive files, read or send Outlook email, list calendar events, or post a message to a Teams channel from a Sutando workflow.
Microsoft 365 connectivity for Sutando, built on the Apache-2.0 [`python-o365`](https://github.com/O365/python-o365) library (`O365` on PyPI), which is a thin wrapper over Microsoft Graph.
All operations go through Microsoft Graph via python-o365:
to a local path (`onedrive-get`).
email (`outlook-send`).
Authentication is a one-time delegated OAuth2 consent flow (`auth`); the refresh token is cached on disk so subsequent commands run non-interactively.
Run the CLI with `python3 scripts/ms365.py <subcommand>`:
# One-time: run the OAuth consent flow and cache the token python3 scripts/ms365.py auth # OneDrive python3 scripts/ms365.py onedrive-list # list root python3 scripts/ms365.py onedrive-list "/Projects/Q3" # list a folder by path python3 scripts/ms365.py onedrive-get "/Reports/x.pdf" ./x.pdf # Outlook mail python3 scripts/ms365.py outlook-list --n 10 python3 scripts/ms365.py outlook-send --to a@b.com --subject "Hi" --body "Hello" # Calendar python3 scripts/ms365.py calendar-list --days 7 # Teams python3 scripts/ms365.py teams-post --team "Engineering" --channel "General" \ --message "Deploy is green"
Every subcommand except `auth` requires that `auth` has already been run and a cached token exists.
Secrets are read from the environment (populate them from the Sutando vault; **never hardcode**):
| Variable | Purpose | |----------|---------| | `MS365_CLIENT_ID` | Azure AD application (client) ID | | `MS365_CLIENT_SECRET` | Azure AD client secret value | | `MS365_TENANT_ID` | Directory (tenant) ID, or `common` / `organizations` | | `MS365_AUTH_FLOW` | `credentials` (default; confidential client, id+secret) or `public` (native client, id only — required when the app's redirect is registered under "Mobile and desktop applications"; symptom of the mismatch: `AADSTS700025` on token exchange). In `public` flow `MS365_CLIENT_SECRET` is not required. |
If any is unset, the CLI exits with a clear message naming the missing variable.
**Token cache:** the OAuth token is stored under the workspace state dir at `state/ms365-token/o365_token.txt` via python-o365's `FileSystemTokenBackend`. Override the base with `MS365_STATE_DIR` if the workspace state dir is elsewhere. Treat this file as a live credential (owner-only permissions).
Delegated Graph access requires an app registration. The owner performs this once in the Azure Portal:
1. **Register the app** — Azure Portal → **Microsoft Entra ID** (Azure AD) → **App registrations** → **New registration**. Name it (e.g. `Sutando M365`). For "Supported account types", pick single-tenant unless multi-tenant is required. Copy the **Application (client) ID** and **Directory (tenant) ID**.
2. **Add a redirect URI** — in the app's **Authentication** blade → **Add a platform** → **Mobile and desktop applications**, add python-o365's default native-client redirect: `https://login.microsoftonline.com/common/oauth2/nativeclient` (A `http://localhost` redirect also works if you prefer a loopback flow.)
3. **Grant delegated Microsoft Graph scopes** — **API permissions** → **Add a permission** → **Microsoft Graph** → **Delegated permissions**, add:
| Scope | Why | |-------|-----| | `offline_access` | Issue a refresh token so the token cache survives | | `User.Read` | Read the signed-in user's basic profile | | `Files.ReadWrite.All` | List and download (and write) OneDrive files | | `Mail.Read` | Read Outlook inbox messages | | `Mail.Send` | Send Outlook email as the user | | `Calendars.ReadWrite` | Read (and create) calendar events | | `Team.ReadBasic.All` | Look up the team by name (`GET /me/joinedTeams`) — required before posting | | `Channel.ReadBasic.All` | Look up the channel by name (`GET /teams/{id}/channels`) — required before posting | | `ChannelMessage.Send` | Post messages to Teams channels | | `Chat.Read` | Read Teams chat/channel context |
> `teams-post` resolves the team and channel **by display name** before it > can send, so the two `*.ReadBasic.All` lookup scopes are mandatory — without > them the lookups return empty and the CLI reports `Team not found` even > though the team exists.
Click **Grant admin consent** for the tenant if your org requires it.
4. **Create a client secret** — **Certificates & secrets** → **New client secret**. Copy the secret **Value** immediately (shown once).
5. **Store the 3 credentials in the vault, not in code** — put the Application (client) ID, the client secret Value, and the Directory (tenant) ID in the Sutando vault, exposed to this skill as `MS365_CLIENT_ID`, `MS365_CLIENT_SECRET`, and `MS365_TENANT_ID`.
Then run `python3 scripts/ms365.py auth` once to complete the consent flow and cache the token.
Install the dependency: `pip install -r requirements.txt` (`O365`).
My AI Stand — Realtime by Day, Rewriting Itself by Night. Summon my AI superpower. Voice, vision, screen, meetings, calls when I'm engaged. Learns my patterns, ships its own code when I'm not. Runs across my Macs, interacts with people & their Stands.
Repo: sonichi/sutando
Streams what the agent is doing into the room, as rows the desktop client renders in an **events drawer** above the composer (collapsed: avatar, pulsing dots,…
Local Agent Registry — a standalone, dependency-free service that tracks running Claude Code (and other) agent instances. Agents self-register on startup and…
**Prefer the `ag2-space` MCP tools when they are connected and the room exposes them** — availability is per-room and per-actor, so check…
Deterministic final-answer normalizer — a last-step pass for any task that ends in a *precise* answer (a number, a short string, a comma-list). Applies the…
Transcribes audio files and voice notes to text via Gemini 2.5-flash. Integrates with Slack, Discord, and Telegram bridges so voice clips surface as readable…
Act back on the owner's Bee wearable — the TOOL half of the Bee integration (channels-vs-tools split). The Bee CHANNEL (ag2-sparrow's `sources/bee.py` watcher)…