From adc900419fc075db78db4efae79424abbb1290d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Balatoni?= Date: Wed, 21 Oct 2026 14:53:12 +0200 Subject: [PATCH 1/2] feat(skills): add agent skills for setup, messaging, admin and automation Four installable SKILL.md files under skills/, following the osodevops skills convention (one focused skill per workflow surface, each with agents/openai.yaml): - teams-cli-setup: install, delegated sign-in, profiles, CI auth, the JSON envelope, exit codes and pagination - teams-cli-messaging: find ids, read, send, reply, react, delete and restore, attachments and inline images, channel files, chats - teams-cli-admin: teams, channels, members, tags, apps and tabs, with a confirmation procedure for destructive commands - teams-cli-automation: presence, meetings, activity notifications, change-notification subscriptions, the local listener, and polling Every command line in the skills was checked against the binary's --help output. README gets an "Agent Skills" section with the npx skills add install command; the docs index and CHANGELOG link to the skills, and the attachments spec names the real skill file. --- CHANGELOG.md | 1 + README.md | 16 +++ docs/README.md | 1 + docs/attachments-spec.md | 4 +- skills/teams-cli-admin/SKILL.md | 99 +++++++++++++ skills/teams-cli-admin/agents/openai.yaml | 4 + skills/teams-cli-automation/SKILL.md | 100 +++++++++++++ .../teams-cli-automation/agents/openai.yaml | 4 + skills/teams-cli-messaging/SKILL.md | 135 ++++++++++++++++++ skills/teams-cli-messaging/agents/openai.yaml | 4 + skills/teams-cli-setup/SKILL.md | 114 +++++++++++++++ skills/teams-cli-setup/agents/openai.yaml | 4 + 12 files changed, 484 insertions(+), 2 deletions(-) create mode 100644 skills/teams-cli-admin/SKILL.md create mode 100644 skills/teams-cli-admin/agents/openai.yaml create mode 100644 skills/teams-cli-automation/SKILL.md create mode 100644 skills/teams-cli-automation/agents/openai.yaml create mode 100644 skills/teams-cli-messaging/SKILL.md create mode 100644 skills/teams-cli-messaging/agents/openai.yaml create mode 100644 skills/teams-cli-setup/SKILL.md create mode 100644 skills/teams-cli-setup/agents/openai.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index 9961f09..c80116e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - `teams listen --client-state SECRET` (or `TEAMS_CLI_CLIENT_STATE`) drops notifications whose `clientState` is missing or different, with a warning on stderr that does not include either value. Without it, the listener warns at startup that notifications are not authenticated. - `teams listen --bind ADDR` chooses the listening address, and `--port 0` picks a free port; the startup line names the address actually bound. - Each `teams listen` line now carries every field Graph sent (`subscriptionExpirationDateTime`, `lifecycleEvent`, `encryptedContent` and so on) and `kind`: `lifecycle` when `lifecycleEvent` is present, else `change`. The README's "Running behind a relay" says what a relay should POST and which statuses to retry. +- Agent skills for AI agents under `skills/`: `teams-cli-setup`, `teams-cli-messaging`, `teams-cli-admin` and `teams-cli-automation`. Install them with `npx skills add osodevops/ms-teams-cli`. ### Changed diff --git a/README.md b/README.md index f8061e2..2e40fb9 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,22 @@ scoop install teams cargo install --git https://github.com/osodevops/ms-teams-cli ``` +## Agent Skills + +Install the agent skills so Claude, Codex and other AI agents know how to drive `teams`: sign-in and output contract, messaging, team administration, and automation. + +```bash +npx skills add osodevops/ms-teams-cli +npx skills add osodevops/ms-teams-cli --skill teams-cli-messaging +``` + +| Skill | Use it for | +|-------|------------| +| `teams-cli-setup` | Install, sign in, profiles, CI auth, JSON envelope and exit codes | +| `teams-cli-messaging` | Read, send and reply in chats and channels, attachments, files | +| `teams-cli-admin` | Teams, channels, members, tags, apps and tabs | +| `teams-cli-automation` | Presence, meetings, notifications, webhooks and polling | + ## Documentation - [Project website](http://msteamscli.com/) diff --git a/docs/README.md b/docs/README.md index 8a15161..f31cc01 100644 --- a/docs/README.md +++ b/docs/README.md @@ -6,6 +6,7 @@ Project website: [msteamscli.com](http://msteamscli.com/) ## Start here +- [Agent skills](../skills/): installable SKILL.md files for AI agents that drive the CLI. - [Quickstarts](quickstarts/README.md): short paths for local setup, agents, agencies, enterprises, and Windows. - [Authentication guide](auth.md): commercial auth direction, OSO app defaults, BYO app setup, consent, and app-only limits. - [Command reference](command-reference.md): practical syntax for every command group. diff --git a/docs/attachments-spec.md b/docs/attachments-spec.md index db682c5..44eb7b0 100644 --- a/docs/attachments-spec.md +++ b/docs/attachments-spec.md @@ -256,8 +256,8 @@ Ordered, each step compiles and passes tests independently. 403 from `/shares`, the error message must say: add `Files.Read.All` to the profile scopes and run `teams auth refresh`. 8. **Docs + skill**: update `docs/command-reference.md`, `docs/examples.md`, and the - `teams-cli` agent skill so agents learn the pattern: `message get` → see - `attachment_items` → `attachments download --dir`. + `skills/teams-cli-messaging/SKILL.md` agent skill so agents learn the pattern: + `message get` → see `attachment_items` → `attachments download --dir`. 9. **Tests**: unit tests per step; integration tests (feature `integration`) that list and download hosted contents from a known message; a wiremock-style test for the 403 scope-error message if the client tests already use a mock server (they do — diff --git a/skills/teams-cli-admin/SKILL.md b/skills/teams-cli-admin/SKILL.md new file mode 100644 index 0000000..004a0a6 --- /dev/null +++ b/skills/teams-cli-admin/SKILL.md @@ -0,0 +1,99 @@ +--- +name: teams-cli-admin +description: "Manage Microsoft Teams structure with the teams CLI: create, update, clone, archive, unarchive or delete a team; add or remove team and channel members; create, rename or delete channels; manage tags; list, install or uninstall team apps; list, add or remove channel tabs. Use when asked to create a team or channel, add someone to a team, archive a team, clone a team, tag a group, install a Teams app or add a tab." +--- + +# Teams CLI Admin + +Create and maintain teams, channels, tags, apps and tabs. These commands change shared resources: read first, write second. + +## Before you start + +```bash +teams auth status --output json +``` + +Exit 1 or 3 means you are not signed in: use `teams-cli-setup`. Always pass `--output json` when parsing. Exit 4 usually means you are not an owner of the team or lack admin consent. Report it. Do not retry. + +## Teams + +```bash +teams team list --output json +teams team get --output json +teams team create --name "Project Atlas" --description "Atlas delivery" +teams team update --name "Project Atlas 2" --description "Updated" +teams team clone --name "Atlas Template" --parts channels,tabs,settings --visibility private +teams team archive +teams team unarchive +teams team delete +``` + +- `team create` and `team clone` may finish asynchronously. `team list` afterwards confirms it exists. +- `clone --parts` takes a comma-separated subset of `apps,tabs,settings,channels,members`. The default is all five, so say what you drop. +- `archive` makes the team read-only. `unarchive` reverses it. `delete` removes the team and its Microsoft 365 group; only an admin can restore it, and only for a limited time. + +## Team members + +```bash +teams team members list --output json +teams team members add --user-id --role member +teams team members remove +``` + +`remove` takes the membership id from `members list`, not the user id. Find a user id with `teams user resolve ""`. Check the roster before and after. + +## Channels + +```bash +teams channel list --output json +teams channel create --name "Releases" --description "Release notes" --membership-type standard +teams channel update --name "Releases 2026" +teams channel delete +teams channel members list --output json +teams channel members add --user-id --role member +teams channel members remove +``` + +`--membership-type` is `standard`, `private` or `shared`. Channel member commands apply to private and shared channels. + +## Tags + +```bash +teams tag list --output json +teams tag create --name "oncall" --members , +teams tag update --name "oncall-eu" +teams tag add-member --user +teams tag remove-member --user +teams tag delete +``` + +## Apps and tabs + +```bash +teams app list --output json +teams app install --app-id +teams app uninstall --app-id +teams tab list --output json +teams tab create --app-id --name "Board" --content-url +teams tab delete --tab-id +``` + +- `app install` takes the catalog app id. `app uninstall --app-id` takes the installation id from `app list`, a different value. +- There is no tab update command. Delete and re-create the tab. + +## Destructive commands + +These cannot be undone, or are disruptive: `team delete`, `team archive`, `team unarchive`, `channel delete`, `app uninstall`, `tag delete`, `tab delete`, and any `members remove`. + +1. Read the target first (`team get`, `channel get `, `tag get`, `app list`) and show the user its name and id. +2. Ask for explicit confirmation naming that exact target, for example "Delete channel Releases () in team Project Atlas ()?". +3. Run the single command only after a clear yes. Never loop destructive commands over a list without confirming the full list. +4. Read back (`team list`, `channel list`) to confirm the result. + +None of the admin commands above has a `--yes` flag (only `message delete` and `message attachments delete` do; see `teams-cli-messaging`), so the user's confirmation is the only guard. + +## Related skills + +- `teams-cli-setup`: install, sign in, exit codes, output contract. +- `teams-cli-messaging`: read, send and reply in chats and channels. +- `teams-cli-automation`: presence, meetings, notifications, webhooks. diff --git a/skills/teams-cli-admin/agents/openai.yaml b/skills/teams-cli-admin/agents/openai.yaml new file mode 100644 index 0000000..6cdcbf6 --- /dev/null +++ b/skills/teams-cli-admin/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Teams CLI Admin" + short_description: "Manage Teams teams, channels, tags, apps and tabs" + default_prompt: "Use $teams-cli-admin to create or manage Microsoft Teams teams, channels, tags, apps and tabs safely." diff --git a/skills/teams-cli-automation/SKILL.md b/skills/teams-cli-automation/SKILL.md new file mode 100644 index 0000000..7decbc5 --- /dev/null +++ b/skills/teams-cli-automation/SKILL.md @@ -0,0 +1,100 @@ +--- +name: teams-cli-automation +description: "Automate Microsoft Teams with the teams CLI: read or set presence and status messages, create and manage online meetings and attendance reports, send activity notifications, subscribe to Graph change notifications, run the local webhook listener, or poll for new messages. Use when asked to set my Teams status, check if someone is available, schedule a Teams meeting, get a meeting join link, notify a user, watch a channel for new messages, or create a webhook subscription." +--- + +# Teams CLI Automation + +Presence, meetings, notifications and event watching. + +## Before you start + +```bash +teams auth status --output json +``` + +Exit 1 or 3 means you are not signed in: use `teams-cli-setup`. Always pass `--output json` when parsing. Exit 4 means a missing scope or consent. Exit 6 means back off. + +## Presence + +```bash +teams presence get --output json +teams presence get --user --output json +teams presence get-batch --user-ids , --output json +teams presence set --availability Busy --activity InACall --expiration PT1H +teams presence set-preferred --availability DoNotDisturb --expiration PT2H +teams presence status --message "Back at 14:00" --expiry 2026-01-01T14:00:00Z +teams presence clear +teams presence clear-preferred +``` + +- `set` needs a valid pair: `--availability` and `--activity` (for example `Available` + `Available`, `Busy` + `InACall`). Expiration is `PT5M` to `PT4H`. +- `set-preferred` overrides every session until it expires. Always set an expiration and tell the user to `clear-preferred` afterwards. +- Read presence for other people only when the user asked for it. + +## Meetings + +```bash +teams meeting list --output json +teams meeting get --output json +teams meeting create --subject "Sprint review" --start 2026-01-01T10:00:00Z --end 2026-01-01T10:30:00Z +teams meeting update --subject "Sprint review (moved)" --start 2026-01-01T11:00:00Z --end 2026-01-01T11:30:00Z +teams meeting join-url --output json +teams meeting attendance --output json +teams meeting attendance --report-id --output json +teams meeting delete +``` + +- Times are ISO 8601 with an explicit offset or `Z`. Ask for the time zone if the user gave none. +- `meeting list` only shows meetings you created. +- `attendance` lists reports. Pass `--report-id` for the individual records. +- `meeting delete` removes the meeting and its join link for every participant: confirm the subject and id with the user first. + +## Activity notifications + +```bash +teams notify send --user --topic "Deploy" --activity-type --preview "Deploy finished" +teams notify send-to-team --topic "Deploy" --activity-type --preview "Deploy finished" +teams notify send-to-chat --topic "Deploy" --activity-type --preview "Deploy finished" --recipient-user +``` + +`--activity-type` must match an activity declared in the manifest of a Teams app installed for the recipient. Without one the call fails. A notification is not a chat message: use `teams-cli-messaging` to post text. + +## Change notifications + +Graph pushes events to a public HTTPS URL. `teams listen` is plain HTTP on localhost, so put an HTTPS reverse proxy or tunnel in front of it. + +```bash +teams listen --port 8080 +teams subscribe create --resource --change-type created --webhook-url --expiration 2026-01-01T12:00:00Z --client-state +teams subscribe list --output json +teams subscribe renew --expiration 2026-01-02T12:00:00Z +teams subscribe delete +``` + +- `listen` prints each notification as one JSON object per line (NDJSON). Read it line by line, and keep other output off that stream. +- Graph validates the webhook URL when you create the subscription, so start `listen` and the tunnel first. +- Use a random `--client-state` and compare it on every notification. +- Subscriptions expire quickly: Teams message, chat and presence resources last at most one hour, which is also the default when `--expiration` is omitted. Renew before `expirationDateTime`, or re-create. +- Message resources need extra permissions, and some need admin consent. See `teams subscribe --help` and the Graph docs. +- Delete subscriptions you created when done. `subscribe delete` stops delivery: confirm the id and its `resource` from `subscribe list` with the user first. There is no `subscribe get`. + +## Polling instead + +When no HTTPS endpoint is possible, poll and diff by message id. + +```bash +teams message list --chat --output json +teams message list --team --channel --page-size 20 --output json +``` + +1. Store the ids already seen (a file or state store you own). +2. Poll every 30 to 60 seconds or more. On exit 6, double the wait. +3. Process ids not seen. Follow `metadata.has_more` or add `--all-pages` for the first full pass only. +4. Do not poll many chats in parallel. + +## Related skills + +- `teams-cli-setup`: install, sign in, exit codes, output contract. +- `teams-cli-messaging`: read, send and reply in chats and channels. +- `teams-cli-admin`: teams, channels, tags, apps and tabs. diff --git a/skills/teams-cli-automation/agents/openai.yaml b/skills/teams-cli-automation/agents/openai.yaml new file mode 100644 index 0000000..ffef112 --- /dev/null +++ b/skills/teams-cli-automation/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Teams CLI Automation" + short_description: "Presence, meetings, notifications and Teams webhooks" + default_prompt: "Use $teams-cli-automation to set presence, manage meetings, send notifications or watch Teams for new messages." diff --git a/skills/teams-cli-messaging/SKILL.md b/skills/teams-cli-messaging/SKILL.md new file mode 100644 index 0000000..116d6ac --- /dev/null +++ b/skills/teams-cli-messaging/SKILL.md @@ -0,0 +1,135 @@ +--- +name: teams-cli-messaging +description: "Read, send and reply in Microsoft Teams chats and channels with the teams CLI. Use when asked to post a Teams message, read a chat, list channel messages, reply in a thread, edit, delete or restore a message, react or pin, @mention someone, find a channel id, chat id or user id, search Teams messages, send or download attachments and inline images, upload or share channel files, or create and manage a chat." +--- + +# Teams CLI Messaging + +Read and write Teams messages as the signed-in user. + +## Before you start + +```bash +teams auth status --output json +``` + +Exit 1 or 3 means you are not signed in: use `teams-cli-setup`. Always pass `--output json` when parsing. Read before you write, and echo the target (chat or channel name) back to the user before sending. + +## Find ids + +```bash +teams team list --output json +teams channel list --output json +teams chat list --order-by activity --output json +teams user me --output json +teams user resolve "" --output json +teams search teams "" --output json +teams search messages "" --top 10 --output json +``` + +- `chat list --order-by activity` puts the newest chat first and adds `lastMessagePreview`. +- `user resolve` turns a name or email into candidate user ids. Ask the user to pick when there are several. +- Chat ids look like `19:...@thread.v2`. Quote them in the shell. + +## Read + +```bash +teams message list --chat --output json +teams message list --team --channel --output json +teams message list --team --channel --message-id --output json +teams message get --team --channel --output json +``` + +- `message list` returns one page. Check `metadata.has_more`, or add `--all-pages` to get everything. +- `--message-id` lists the replies in a channel thread instead of the thread roots. +- There are no date or text filters on `message list`. Filter the JSON yourself, or use `search messages`. +- `message get` is for channel messages. Read a single chat message from `message list --chat`. +- Some meeting chats list but reject reads (403) once you left the roster. Skip them. + +## Write + +```bash +teams message send --chat --body "Build is green" +teams message send --team --channel --subject "Release" --body "v1.2 is out" +teams message reply --team --channel --message-id --body "Thanks" +teams message send --chat --quote --body "Yes, that works" +teams message send --chat --mention --body "Can you review?" +teams message send --chat --content-type html --body "Done" +teams message send --chat --stdin +teams message update --chat --body "Corrected text" +teams message react --chat --message-id like +teams message unreact --chat --message-id like +teams message pin --team --channel +teams message unpin --team --channel +``` + +- Content is `text` by default. `--content-type html` sends HTML; there is no markdown option. Use `--stdin` for long or multi-line bodies. +- `--mention` takes an Entra object id or UPN and builds a real @mention. Do not type raw `` markup. +- `--subject` works on channel root messages only. `--quote` works in chats only. +- Replies exist for channel threads. In a chat, send a normal message or use `--quote`. +- A send that fails with exit 7 or 8 may still have posted. List recent messages before re-sending. +- `pin` and `unpin` are channel only. `unpin` takes the pinned-message id, not the message id. + +## Delete and restore + +Delete is a soft delete and only works on your own messages. Confirm with the user and echo the target first. + +```bash +teams message delete --chat --yes +teams message undelete --chat +teams message delete --team --channel --reply --yes +``` + +Without `--yes` the command exits 2 and sends nothing. Channel deletes and edits need `ChannelMessage.ReadWrite`, which needs admin consent and is not in the default scopes: expect exit 4 until an admin grants it (`teams auth consent-url` prints the URL for them). + +## Attachments + +```bash +teams message get --team --channel --with-attachments --output json +teams message attachments list --chat --output json +teams message attachments download --chat --dir ./downloads +teams message attachments download --chat --index 0 --path ./file.bin +teams message send --chat --body "Report" --attach ./report.pdf +teams message send --chat --body "Screenshot" --image ./shot.png +``` + +Pattern: `message get --with-attachments` (channel) or `attachments list` (chat or channel), read `attachment_items`, then `attachments download --dir`. `--index` picks one item. File attachments need `Files.Read.All`. `--attach` needs `Files.ReadWrite` (chats) or `Files.ReadWrite.All` (channels); `--image` needs no extra scope and is capped at 3 MB. Never print downloaded bytes to a terminal. + +`message attachments delete ... --yes` removes only files you attached from your own OneDrive. Confirm first. + +## Channel files + +```bash +teams file list --team --channel --output json +teams file get --team --channel --file-id --output json +teams file upload --team --channel --file ./notes.md +teams file download --team --channel --file-id --path ./notes.md +teams file share --team --channel --file-id --link-type view --scope organization +teams file delete --team --channel --file-id +``` + +`file download` writes to stdout without `--path`. Always set it. Use `--scope organization` unless the user asks for `anonymous`. `file delete` is permanent: confirm first. + +## Chats + +```bash +teams chat get --output json +teams chat create --chat-type group --topic "Release war room" --members , +teams chat update --topic "New topic" +teams chat hide --user-id +teams chat unhide --user-id +teams chat members list --output json +``` + +Include your own id (`teams user me`) in `--members`; the CLI does not add it. Mark guest users as `:guest`. Members default to owner. + +## Limits + +- Client credentials (app-only tokens) cannot post as a user. Use a delegated login. +- Exit 4 means a missing scope or consent. Report it instead of retrying. + +## Related skills + +- `teams-cli-setup`: install, sign in, exit codes, output contract. +- `teams-cli-admin`: teams, channels, tags, apps and tabs. +- `teams-cli-automation`: presence, meetings, notifications, webhooks. diff --git a/skills/teams-cli-messaging/agents/openai.yaml b/skills/teams-cli-messaging/agents/openai.yaml new file mode 100644 index 0000000..9174da5 --- /dev/null +++ b/skills/teams-cli-messaging/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Teams CLI Messaging" + short_description: "Read, send and reply in Teams chats and channels" + default_prompt: "Use $teams-cli-messaging to read, send or reply to Microsoft Teams messages and handle attachments." diff --git a/skills/teams-cli-setup/SKILL.md b/skills/teams-cli-setup/SKILL.md new file mode 100644 index 0000000..f47d272 --- /dev/null +++ b/skills/teams-cli-setup/SKILL.md @@ -0,0 +1,114 @@ +--- +name: teams-cli-setup +description: "Install and sign in to the teams CLI (Microsoft Teams via Microsoft Graph) and learn its agent output contract. Use when asked to install teams-cli, log in to Teams, fix 'not authenticated' or AUTH_TOKEN_EXPIRED, run auth doctor, switch profiles, configure a bring-your-own Entra app, set up CI or unattended access, or interpret JSON envelopes, exit codes, pagination and stderr logs from teams commands." +--- + +# Teams CLI Setup + +Install `teams`, sign in once, and understand how every command reports results. Other teams-cli skills assume this is done. + +## Install + +```bash +# Homebrew (macOS/Linux) +brew install osodevops/tap/teams-cli + +# Scoop (Windows) +scoop bucket add osodevops https://github.com/osodevops/scoop-bucket +scoop install teams + +# From source +cargo install --git https://github.com/osodevops/ms-teams-cli +``` + +Pre-built binaries are on https://github.com/osodevops/ms-teams-cli/releases. Check with `teams --version`. + +## First sign-in + +Messages are sent as the signed-in user, so use a delegated login. No client ID is needed: the default is OSO's public client app. + +```bash +teams auth login +teams auth login --device-code +teams auth status --output json +teams auth doctor --output json +``` + +- `auth login` opens a browser (authorization code + PKCE). Use it on a desktop. +- `--device-code` prints a code for a human to approve on another device. Use it over SSH or headless. +- The human must complete the approval. Do not try to automate it. +- If `auth doctor` reports missing admin consent, `teams auth consent-url` prints the URL for a tenant admin. + +Bring your own Entra app when a tenant blocks the default app: + +```bash +teams auth login --device-code --client-id --tenant-id +``` + +A successful delegated login saves the client and tenant IDs in the profile, so later logins reuse them. Environment variables are not saved. + +## Sessions and profiles + +```bash +teams auth refresh +teams auth list +teams auth switch +teams auth logout +teams --profile user me --output json +teams config path +teams config show --output json +teams config set +``` + +`--profile` (or `TEAMS_CLI_PROFILE`) picks a profile per command. `--config ` selects another config file. `teams config init` writes a default file. Run `teams config --help` for the keys. `auth logout` removes the stored sign-in; run it only when asked. + +## CI and unattended use + +- `TEAMS_CLI_ACCESS_TOKEN` supplies a token for normal commands. Take it from a secret store. Never print it, echo it or commit it. +- `TEAMS_CLI_TOKEN_STORE=file` keeps tokens in `0600` files under the config directory instead of the OS keyring. Use it for daemons and SSH. +- `TEAMS_CLI_DISABLE_KEYRING=1` is for tests only. It stops tokens being stored. +- `teams auth login --client-credentials` gives an app-only token (`TEAMS_CLI_CLIENT_ID`, `TEAMS_CLI_TENANT_ID`, `TEAMS_CLI_CLIENT_SECRET`). It can NOT send chat or channel messages as a user, and many Teams reads fail with it. Delegated login is the path for messaging. +- Pass the secret by environment variable, not `--client-secret`, which leaks into process listings. + +## Output contract + +- Stdout is JSON when it is not a TTY. `--output json|human|plain` overrides. Always pass `--output json` when parsing. +- Success: `{"success": true, "data": ..., "metadata": {...}}`. +- Failure: `{"success": false, "error": {"code": "...", "message": "..."}, "metadata": {...}}`. +- Logs and progress go to stderr, so stdout stays parseable. `RUST_LOG=teams=debug` adds detail there. +- A closed pipe (`| head -1`) is a normal exit 0. + +| Exit | Meaning | Do this | +|------|---------|---------| +| 0 | Success | Parse `.data` | +| 1 | General error | Read `.error.message` | +| 2 | Invalid input | Fix the command | +| 3 | Auth or token error | Sign in again | +| 4 | Permission denied (403) | Check scopes and consent, or stop | +| 5 | Not found (404) | Re-check the id | +| 6 | Rate limited (429) | Back off, then retry | +| 7 | Network error | Retry reads with backoff; a write may have gone through, so check first | +| 8 | Server error (5xx) | Retry reads with backoff; for writes, check first | +| 10 | Config or keyring error | Check config, env vars, keyring | + +`teams auth status` exits 1 when unauthenticated and still prints `{"authenticated": false}` as a success payload. A locked keychain is exit 10, not 3. + +## Pagination + +List commands return one page. Read `metadata.has_more`; when true, `metadata.next_link` is Graph's next-page link. Add `--all-pages` to fetch everything (`has_more` is then false). `--page-size ` sets the page size. + +## Troubleshooting + +- 403 or exit 4: the error lists the scopes the token holds. Run `teams auth doctor`, then `teams auth login` to consent to the missing scope, or `teams auth consent-url` when only an admin can grant it. After admin consent, `teams auth refresh` picks it up without a browser. +- macOS keychain prompts on every run: each unsigned build is a new app. Use a release build or `TEAMS_CLI_TOKEN_STORE=file`. +- Windows or Linux keyring errors (exit 10): unlock or install a keyring service, or use `TEAMS_CLI_TOKEN_STORE=file`. +- Exit 6: honor the wait, avoid parallel fan-out into large tenants. +- Browser callback fails: use `--device-code`. + +Full details: https://github.com/osodevops/ms-teams-cli/tree/main/docs (`troubleshooting.md`), and `man teams-agent-contract`. + +## Related skills + +- `teams-cli-messaging`: read, send and reply in chats and channels. +- `teams-cli-admin`: teams, channels, tags, apps and tabs. +- `teams-cli-automation`: presence, meetings, notifications, webhooks. diff --git a/skills/teams-cli-setup/agents/openai.yaml b/skills/teams-cli-setup/agents/openai.yaml new file mode 100644 index 0000000..6b0a633 --- /dev/null +++ b/skills/teams-cli-setup/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Teams CLI Setup" + short_description: "Install, sign in and configure the teams CLI" + default_prompt: "Use $teams-cli-setup to install the teams CLI, sign in to Microsoft Teams and verify auth." From b3c197ed35981e7468d8f0bb4d4f66532ad323d3 Mon Sep 17 00:00:00 2001 From: Sion Smith Date: Mon, 26 Oct 2026 23:02:40 +0000 Subject: [PATCH 2/2] feat(skills): cover listen hardening and --redirect-uri; test skill commands Bring the skills up to date with #119 and #125: - automation: listen binds 127.0.0.1, takes the subscription's clientState through TEAMS_CLI_CLIENT_STATE, and answers /lifecycle; subscribe create gets --lifecycle-url. The lifetimes now match Graph's table (Teams messages and chats up to three days, presence one hour, 45 minutes minimum, lifecycle URL over an hour), replacing "at most one hour", and say what to do on each lifecycle event. - setup: --redirect-uri for a bring-your-own app's browser login, what is saved to the profile, and what AADSTS50011, a "Sign-in failed" error and the 10-minute wait mean. agent_skill_commands_parse runs every teams line in the skills' code blocks through Cli::try_parse_from, so a renamed flag or a missing argument fails the build instead of teaching agents a broken command. AGENTS.md lists skills/ and adds it to the keep-in-sync rule. Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 3 +- skills/teams-cli-automation/SKILL.md | 14 ++-- skills/teams-cli-setup/SKILL.md | 6 +- src/cli/mod.rs | 102 +++++++++++++++++++++++++++ 4 files changed, 116 insertions(+), 9 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index dcaf875..7b61a58 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -34,6 +34,7 @@ cargo run -- team list --output json - `docs/teams-cli-prd.md` and `README.md` describe intended product behavior and command surface. - `docs/README.md` is the documentation index for quickstarts, auth, commands, examples, FAQ, troubleshooting, use cases, and release readiness. - `docs/auth-implementation-plan.md` is the commercial auth direction: delegated Graph for CLI user actions, BYO app support for locked-down tenants, and Teams bot proactive messaging for unattended posting. +- `skills//SKILL.md` are installable agent skills (`npx skills add osodevops/ms-teams-cli`), each with `agents/openai.yaml` metadata for Codex. - `docs/man/` contains shipped man pages: - `teams.1` for command reference. - `teams-config.5` for config file format. @@ -180,7 +181,7 @@ CI runs on GitHub Actions: - Most tests do not hit Microsoft Graph. Add mocked tests for API behavior instead of requiring live credentials. - macOS binds keychain access grants to the exact binary signature, so every local build is a "new" application and triggers a fresh keychain prompt when it touches stored tokens. Set `TEAMS_CLI_DISABLE_KEYRING=1` in tests (the CLI test harness already does), and note that debug builds use the separate `teams-cli-dev` storage namespace, so they never touch an installed release's items, though each rebuild is still prompted for its own (re-signing with a self-signed certificate does not help; see the macOS keychain section in `docs/troubleshooting.md`). For an unattended process, `TEAMS_CLI_TOKEN_STORE=file` keeps tokens in `0600` files under the config directory instead, with no keychain involved. - Keep global option names reserved. In particular, command-specific file paths must not reuse global `--output`, which is the JSON/human/plain output selector. -- Keep `README.md`, `docs/man/teams.1`, and CLI help in sync. If a documented flag form exists in README, add a CLI regression test for it. +- Keep `README.md`, `docs/man/teams.1`, the agent skills in `skills/`, and CLI help in sync. If a documented flag form exists in README, add a CLI regression test for it. Every `teams` line in a skill's code blocks must parse (`agent_skill_commands_parse` in `src/cli/mod.rs` checks this). - When verifying whether a PR is good to merge, check whether it is intended to release. Release automation is version-bump driven: a feature PR without a `Cargo.toml` package version change only runs CI on `main`; a release needs `Cargo.toml` and the root `teams-cli` entry in `Cargo.lock` bumped together, plus a matching `CHANGELOG.md` entry. - `cargo audit` may report advisory warnings that are not failing vulnerabilities. Treat actual vulnerabilities as release blockers and document any warning accepted for release. diff --git a/skills/teams-cli-automation/SKILL.md b/skills/teams-cli-automation/SKILL.md index 7decbc5..bee3213 100644 --- a/skills/teams-cli-automation/SKILL.md +++ b/skills/teams-cli-automation/SKILL.md @@ -62,20 +62,22 @@ teams notify send-to-chat --topic "Deploy" --activity-type --change-type created --webhook-url --expiration 2026-01-01T12:00:00Z --client-state +teams subscribe create --resource --change-type created --webhook-url /webhook --lifecycle-url /lifecycle --expiration 2026-01-01T12:00:00Z --client-state "$TEAMS_CLI_CLIENT_STATE" teams subscribe list --output json teams subscribe renew --expiration 2026-01-02T12:00:00Z teams subscribe delete ``` -- `listen` prints each notification as one JSON object per line (NDJSON). Read it line by line, and keep other output off that stream. -- Graph validates the webhook URL when you create the subscription, so start `listen` and the tunnel first. -- Use a random `--client-state` and compare it on every notification. -- Subscriptions expire quickly: Teams message, chat and presence resources last at most one hour, which is also the default when `--expiration` is omitted. Renew before `expirationDateTime`, or re-create. +- `listen` prints each notification as one JSON object per line (NDJSON), without `clientState`. Read it line by line, and keep other output off that stream. `kind` is `change`, or `lifecycle` for a `lifecycleEvent`. +- Give `listen` (through `TEAMS_CLI_CLIENT_STATE`, which stays out of process listings) the same secret as `subscribe create --client-state`. It drops notifications without it; without a secret, anyone who reaches the port can inject lines. +- Graph validates the webhook URL and the lifecycle URL when you create the subscription, so start `listen` and the tunnel first. `listen` answers on `/`, `/webhook` and `/lifecycle`. +- Teams chat and channel message subscriptions last up to three days, presence up to one hour; Graph raises anything under 45 minutes to 45. Over one hour, a Teams subscription needs `--lifecycle-url`. Without `--expiration` it lasts one hour. Renew before `expirationDateTime`. +- Lifecycle events: on `reauthorizationRequired`, run `subscribe renew`; on `subscriptionRemoved`, create the subscription again; on `missed`, catch up with `message list`. - Message resources need extra permissions, and some need admin consent. See `teams subscribe --help` and the Graph docs. - Delete subscriptions you created when done. `subscribe delete` stops delivery: confirm the id and its `resource` from `subscribe list` with the user first. There is no `subscribe get`. diff --git a/skills/teams-cli-setup/SKILL.md b/skills/teams-cli-setup/SKILL.md index f47d272..8edb848 100644 --- a/skills/teams-cli-setup/SKILL.md +++ b/skills/teams-cli-setup/SKILL.md @@ -43,9 +43,10 @@ Bring your own Entra app when a tenant blocks the default app: ```bash teams auth login --device-code --client-id --tenant-id +teams auth login --client-id --tenant-id --redirect-uri http://localhost ``` -A successful delegated login saves the client and tenant IDs in the profile, so later logins reuse them. Environment variables are not saved. +The browser login needs a loopback redirect URI registered on the app (the default is `http://localhost/callback`). Pass the registered one with `--redirect-uri`; without a port, a free one is picked at each login. A successful delegated login saves the client ID, tenant ID and redirect URI in the profile, so later logins reuse them. Environment variables are not saved. ## Sessions and profiles @@ -103,7 +104,8 @@ List commands return one page. Read `metadata.has_more`; when true, `metadata.ne - macOS keychain prompts on every run: each unsigned build is a new app. Use a release build or `TEAMS_CLI_TOKEN_STORE=file`. - Windows or Linux keyring errors (exit 10): unlock or install a keyring service, or use `TEAMS_CLI_TOKEN_STORE=file`. - Exit 6: honor the wait, avoid parallel fan-out into large tenants. -- Browser callback fails: use `--device-code`. +- The browser page shows `AADSTS50011`: the redirect URI is not registered on the app, and the CLI keeps waiting. Stop it, then pass the registered URI with `--redirect-uri`, or use `--device-code`. +- Browser login fails with `Sign-in failed: `: the user declined, or the tenant blocked the app. Read the description; `auth consent-url` helps when admin consent is missing. With no callback at all, the login gives up after 10 minutes. Full details: https://github.com/osodevops/ms-teams-cli/tree/main/docs (`troubleshooting.md`), and `man teams-agent-contract`. diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 7ff39c1..99f0df0 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -270,3 +270,105 @@ fn generate_completions(shell: clap_complete::Shell) -> Result<()> { crate::output::write_stdout(&output); Ok(()) } + +#[cfg(test)] +mod tests { + use super::Cli; + use clap::Parser; + + /// Split a shell command line on whitespace, honouring double quotes. + fn shell_words(line: &str) -> Vec { + let mut words = Vec::new(); + let mut word = String::new(); + let mut quoted = false; + let mut in_word = false; + for c in line.chars() { + match c { + '"' => { + quoted = !quoted; + in_word = true; + } + c if c.is_whitespace() && !quoted => { + if in_word { + words.push(std::mem::take(&mut word)); + in_word = false; + } + } + c => { + word.push(c); + in_word = true; + } + } + } + if in_word { + words.push(word); + } + words + } + + /// Replace each `` with a value of the right shape. + fn fill_placeholders(line: &str) -> String { + let mut out = String::new(); + let mut rest = line; + while let Some(start) = rest.find('<') { + let Some(len) = rest[start..].find('>') else { + break; + }; + let name = &rest[start + 1..start + len]; + out.push_str(&rest[..start]); + out.push_str(if name.contains("url") { + "https://example.com/x" + } else { + "x1" + }); + rest = &rest[start + len + 1..]; + } + out.push_str(rest); + out + } + + /// Every `teams` line in a fenced block of an agent skill + /// (`skills/*/SKILL.md`) has to parse, so renaming a flag or subcommand + /// cannot leave the skills teaching agents a command that fails. + #[test] + fn agent_skill_commands_parse() { + let skills = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("skills"); + let mut checked = 0; + for entry in std::fs::read_dir(&skills).unwrap() { + let path = entry.unwrap().path().join("SKILL.md"); + let text = std::fs::read_to_string(&path).unwrap(); + let mut in_block = false; + for (n, line) in text.lines().enumerate() { + if line.starts_with("```") { + in_block = !in_block; + continue; + } + if !in_block || !line.starts_with("teams ") { + continue; + } + if let Err(e) = Cli::try_parse_from(shell_words(&fill_placeholders(line))) { + panic!("{}:{}: {line}\n{e}", path.display(), n + 1); + } + checked += 1; + } + } + assert!(checked >= 100, "found only {checked} skill commands"); + } + + #[test] + fn shell_words_honours_double_quotes() { + assert_eq!( + shell_words(r#"teams message send --body "Build is green" --chat c"#), + [ + "teams", + "message", + "send", + "--body", + "Build is green", + "--chat", + "c" + ] + ); + assert_eq!(shell_words(r#"a "" b"#), ["a", "", "b"]); + } +}