Skip to content

Commit 11433fd

Browse files
sionsmithclaude
andcommitted
Initial release: teams-cli v0.1.0
Microsoft Teams CLI with agent-first design via Microsoft Graph API. 18 subcommands: auth, user, config, completions, team, channel, message, chat, presence, search, tag, meeting, notify, app, tab, file, subscribe, listen. Features: - Three auth flows: Authorization Code + PKCE, Device Code, Client Credentials - Structured JSON output with standard envelope for agent consumption - Human-readable tables when stdout is a TTY - Webhook listener for real-time change notifications (NDJSON output) - Subscription CRUD for Graph change notifications - Multi-profile support with OS keyring token storage - Automatic retry with exponential backoff and rate-limit handling - Cross-platform: macOS, Linux, Windows 45 unit tests, 34 integration tests, clippy clean. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
0 parents  commit 11433fd

79 files changed

Lines changed: 13798 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/auto-tag.yml

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
name: Auto Tag
2+
3+
on:
4+
push:
5+
branches: [main]
6+
paths:
7+
- Cargo.toml
8+
9+
jobs:
10+
tag:
11+
runs-on: ubuntu-latest
12+
permissions:
13+
contents: write
14+
steps:
15+
- uses: actions/checkout@v4
16+
with:
17+
fetch-depth: 2
18+
persist-credentials: false
19+
20+
- name: Check version bump
21+
id: version
22+
run: |
23+
OLD_VERSION=$(git diff HEAD~1 HEAD -- Cargo.toml | grep '^-version' | head -1 | sed 's/.*"\(.*\)".*/\1/' || echo "")
24+
NEW_VERSION=$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)".*/\1/')
25+
if [ -n "$OLD_VERSION" ] && [ "$OLD_VERSION" != "$NEW_VERSION" ]; then
26+
echo "changed=true" >> "$GITHUB_OUTPUT"
27+
echo "version=$NEW_VERSION" >> "$GITHUB_OUTPUT"
28+
else
29+
echo "changed=false" >> "$GITHUB_OUTPUT"
30+
fi
31+
32+
- name: Check if tag exists
33+
if: steps.version.outputs.changed == 'true'
34+
id: tag-check
35+
run: |
36+
if git rev-parse "v${{ steps.version.outputs.version }}" >/dev/null 2>&1; then
37+
echo "exists=true" >> "$GITHUB_OUTPUT"
38+
else
39+
echo "exists=false" >> "$GITHUB_OUTPUT"
40+
fi
41+
42+
- name: Create and push tag
43+
if: steps.version.outputs.changed == 'true' && steps.tag-check.outputs.exists == 'false'
44+
env:
45+
GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
46+
run: |
47+
git config user.name "github-actions[bot]"
48+
git config user.email "github-actions[bot]@users.noreply.github.com"
49+
git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@github.com/${{ github.repository }}.git"
50+
git tag -a "v${{ steps.version.outputs.version }}" -m "Release v${{ steps.version.outputs.version }}"
51+
git push origin "v${{ steps.version.outputs.version }}"

.github/workflows/ci.yml

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
env:
10+
CARGO_TERM_COLOR: always
11+
RUSTFLAGS: "-D warnings"
12+
13+
jobs:
14+
check:
15+
name: Check
16+
runs-on: ubuntu-latest
17+
steps:
18+
- uses: actions/checkout@v4
19+
- uses: dtolnay/rust-toolchain@stable
20+
- uses: Swatinem/rust-cache@v2
21+
- run: cargo check --all-targets
22+
23+
fmt:
24+
name: Format
25+
runs-on: ubuntu-latest
26+
steps:
27+
- uses: actions/checkout@v4
28+
- uses: dtolnay/rust-toolchain@stable
29+
with:
30+
components: rustfmt
31+
- run: cargo fmt -- --check
32+
33+
clippy:
34+
name: Clippy
35+
runs-on: ubuntu-latest
36+
steps:
37+
- uses: actions/checkout@v4
38+
- uses: dtolnay/rust-toolchain@stable
39+
with:
40+
components: clippy
41+
- uses: Swatinem/rust-cache@v2
42+
- run: cargo clippy --all-targets -- -D warnings
43+
44+
test:
45+
name: Test
46+
runs-on: ${{ matrix.os }}
47+
strategy:
48+
matrix:
49+
os: [ubuntu-latest, macos-latest]
50+
steps:
51+
- uses: actions/checkout@v4
52+
- uses: dtolnay/rust-toolchain@stable
53+
- uses: Swatinem/rust-cache@v2
54+
- run: cargo test --all-targets

.github/workflows/release.yml

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
tags: ["v*"]
6+
7+
env:
8+
CARGO_TERM_COLOR: always
9+
10+
permissions:
11+
contents: write
12+
13+
jobs:
14+
build:
15+
name: Build ${{ matrix.target }}
16+
runs-on: ${{ matrix.runner }}
17+
strategy:
18+
matrix:
19+
include:
20+
- target: x86_64-apple-darwin
21+
runner: macos-14
22+
archive: tar.gz
23+
- target: aarch64-apple-darwin
24+
runner: macos-latest
25+
archive: tar.gz
26+
- target: x86_64-unknown-linux-musl
27+
runner: ubuntu-latest
28+
archive: tar.gz
29+
- target: aarch64-unknown-linux-musl
30+
runner: ubuntu-latest
31+
archive: tar.gz
32+
cross: true
33+
- target: x86_64-pc-windows-msvc
34+
runner: windows-latest
35+
archive: zip
36+
steps:
37+
- uses: actions/checkout@v4
38+
39+
- uses: dtolnay/rust-toolchain@stable
40+
with:
41+
targets: ${{ matrix.target }}
42+
43+
- uses: Swatinem/rust-cache@v2
44+
with:
45+
key: ${{ matrix.target }}
46+
47+
- name: Install musl-tools
48+
if: matrix.target == 'x86_64-unknown-linux-musl'
49+
run: sudo apt-get update && sudo apt-get install -y musl-tools
50+
51+
- name: Install cross
52+
if: matrix.cross
53+
run: cargo install cross --git https://github.com/cross-rs/cross
54+
55+
- name: Build (cross)
56+
if: matrix.cross
57+
run: cross build --release --target ${{ matrix.target }}
58+
59+
- name: Build (native)
60+
if: "!matrix.cross"
61+
run: cargo build --release --target ${{ matrix.target }}
62+
63+
- name: Package (unix)
64+
if: matrix.archive == 'tar.gz'
65+
run: |
66+
cd target/${{ matrix.target }}/release
67+
tar czf ../../../teams-${{ github.ref_name }}-${{ matrix.target }}.tar.gz teams
68+
cd ../../..
69+
70+
- name: Package (windows)
71+
if: matrix.archive == 'zip'
72+
shell: pwsh
73+
run: |
74+
Compress-Archive -Path target/${{ matrix.target }}/release/teams.exe -DestinationPath teams-${{ github.ref_name }}-${{ matrix.target }}.zip
75+
76+
- name: Upload artifact
77+
uses: actions/upload-artifact@v4
78+
with:
79+
name: teams-${{ matrix.target }}
80+
path: teams-${{ github.ref_name }}-${{ matrix.target }}.*
81+
82+
release:
83+
name: Create Release
84+
needs: build
85+
runs-on: ubuntu-latest
86+
steps:
87+
- uses: actions/checkout@v4
88+
89+
- name: Download all artifacts
90+
uses: actions/download-artifact@v4
91+
with:
92+
path: artifacts
93+
merge-multiple: true
94+
95+
- name: Generate checksums
96+
run: |
97+
cd artifacts
98+
sha256sum teams-* > checksums-sha256.txt
99+
cat checksums-sha256.txt
100+
101+
- name: Create GitHub Release
102+
uses: softprops/action-gh-release@v2
103+
with:
104+
generate_release_notes: true
105+
files: |
106+
artifacts/teams-*
107+
artifacts/checksums-sha256.txt
108+
109+
homebrew:
110+
name: Update Homebrew Tap
111+
needs: release
112+
runs-on: ubuntu-latest
113+
steps:
114+
- name: Update homebrew formula
115+
uses: peter-evans/repository-dispatch@v3
116+
with:
117+
token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
118+
repository: osodevops/homebrew-tap
119+
event-type: update-formula
120+
client-payload: |
121+
{
122+
"formula": "teams-cli",
123+
"tag": "${{ github.ref_name }}",
124+
"repo": "${{ github.repository }}"
125+
}

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/target

CLAUDE.md

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# teams-cli
2+
3+
Rust CLI for Microsoft Teams — agent-first design via Microsoft Graph API.
4+
5+
## Build Commands
6+
7+
```bash
8+
cargo build # Debug build
9+
cargo build --release # Release build
10+
cargo check # Type check only
11+
cargo test --all-targets # All tests (unit + integration)
12+
cargo test --lib --bins # Unit tests only
13+
cargo test --features integration # Integration tests (needs auth)
14+
cargo fmt -- --check # Check formatting
15+
cargo clippy --all-targets -- -D warnings # Lint
16+
```
17+
18+
## Architecture
19+
20+
Single-crate Rust binary. Key modules:
21+
22+
- `src/main.rs` — Entry point, tracing init, config load, CLI dispatch
23+
- `src/cli/` — Clap command definitions and handlers
24+
- `auth.rs` — login (PKCE, device code, client credentials), status, list, switch, logout, token
25+
- `team.rs` — list, get, create, update, delete, clone, archive, unarchive, members
26+
- `channel.rs` — list, get, create, update, delete, members
27+
- `message.rs` — send, list, get, reply, delete, react, unreact, pin, unpin
28+
- `chat.rs` — list, get, create, hide, unhide, members
29+
- `presence.rs` — get, set, clear, status, get-batch
30+
- `search.rs` — messages, users, teams
31+
- `tag.rs` — list, get, create, delete, add-member, remove-member
32+
- `meeting.rs` — list, get, create, delete, join-url, attendance
33+
- `notification.rs` — send, send-to-team, send-to-chat
34+
- `app.rs` — list, install, uninstall
35+
- `tab.rs` — list, create, delete
36+
- `file.rs` — list, get, upload, download, delete, share
37+
- `subscribe.rs` — create, list, renew, delete
38+
- `listen.rs` — webhook listener entry point
39+
- `config_cmd.rs` — init, show, get, set, path
40+
- `user.rs` — me, get, list
41+
- `src/api/` — Microsoft Graph HTTP client and endpoint wrappers
42+
- `client.rs` — GraphClient with retry/backoff, rate-limit handling, pagination
43+
- `endpoints.rs` — URL builders for all Graph API endpoints
44+
- `teams.rs`, `channels.rs`, `messages.rs`, `chats.rs`, `presence.rs`, `search.rs`, `tags.rs`, `meetings.rs`, `notifications.rs`, `apps.rs`, `files.rs`, `subscriptions.rs`, `users.rs`
45+
- `src/models/` — Serde data models for Graph API resources
46+
- `team.rs`, `channel.rs`, `message.rs`, `chat.rs`, `presence.rs`, `search.rs`, `tag.rs`, `meeting.rs`, `notification.rs`, `app.rs`, `file.rs`, `subscription.rs`, `member.rs`, `user.rs`, `common.rs`
47+
- `src/listen/` — Webhook listener HTTP server
48+
- `mod.rs` — hyper server with Ctrl+C graceful shutdown
49+
- `handler.rs` — validation token echo, NDJSON notification output, health check
50+
- `src/auth/` — Authentication flows and token management
51+
- `auth_code_pkce.rs` — Authorization Code + PKCE (browser flow)
52+
- `device_code.rs` — Device Code flow
53+
- `client_credentials.rs` — Client Credentials flow
54+
- `token.rs` — TokenInfo struct, expiry checking
55+
- `keyring.rs` — OS keyring storage (macOS Keychain, Windows Credential Manager, Linux Secret Service)
56+
- `src/output/` — Output formatters (JSON envelope, human tables, plain text)
57+
- `src/config.rs` — TOML config file management, profile/credential resolution
58+
- `src/error.rs` — TeamsError enum with exit codes per PRD
59+
60+
## Key Design Patterns
61+
62+
### Output Contract
63+
All commands emit a JSON envelope: `{ "success": bool, "data": ..., "metadata": { "request_id", "timestamp", "duration_ms" } }`.
64+
When stdout is a TTY, defaults to human-readable table format. When piped, defaults to JSON.
65+
66+
### Exit Codes
67+
0=success, 1=general, 2=invalid input, 3=auth, 4=permission denied, 5=not found, 6=rate limited, 7=network, 8=server error, 10=config error
68+
69+
### Credential Resolution
70+
CLI flags > env vars (TEAMS_CLI_CLIENT_ID, TEAMS_CLI_CLIENT_SECRET, TEAMS_CLI_TENANT_ID) > config file profiles
71+
72+
### Token Management
73+
- Login stores tokens in OS keyring via `keyring` crate
74+
- Subsequent commands load tokens from keyring — no re-login needed
75+
- Multiple named profiles supported (--profile flag)
76+
- Profile index tracked in keyring for `auth list`
77+
78+
### Graph API Client
79+
- Automatic retry with exponential backoff on 429/5xx
80+
- Respects `Retry-After` header for rate limiting
81+
- Pagination via `@odata.nextLink` with `--all-pages` flag
82+
- `$top` parameter for page size control
83+
84+
### Webhook Listener
85+
- `teams listen --port 8080` starts a hyper HTTP server
86+
- Handles Graph subscription validation (echoes `?validationToken`)
87+
- Outputs change notifications as NDJSON to stdout
88+
- Multi-connection, graceful Ctrl+C shutdown
89+
- Requires HTTPS via reverse proxy (ngrok) for production use
90+
91+
## Environment Variables
92+
- `TEAMS_CLI_CLIENT_ID` — Azure AD application (client) ID
93+
- `TEAMS_CLI_CLIENT_SECRET` — Azure AD client secret
94+
- `TEAMS_CLI_TENANT_ID` — Azure AD tenant ID
95+
- `TEAMS_CLI_ACCESS_TOKEN` — Pre-obtained access token
96+
- `RUST_LOG` — Tracing filter level
97+
98+
## Config
99+
- Config file: `~/.config/teams-cli/config.toml` (Linux) or `~/Library/Application Support/teams-cli/config.toml` (macOS)
100+
- Profiles define client_id, tenant_id, auth_flow per account
101+
- Network section: timeout, max_retries, retry_backoff_base
102+
- Output section: format, color, page_size

CONTRIBUTING.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Contributing to teams-cli
2+
3+
Thanks for your interest in contributing! This project aims to provide a secure, scriptable Microsoft Teams CLI for developers and AI agents.
4+
5+
- Code: Rust 2021, `clap` v4, async via `tokio`, HTTP via `reqwest`.
6+
- Style: run `cargo fmt` and `cargo clippy -- -D warnings` before pushing.
7+
- Tests: add unit tests near changed code; for HTTP, prefer `wiremock` for integration tests.
8+
- Commits: conventional, clear messages. Small, focused PRs are easier to review.
9+
- Security: never include secrets in tests, examples, or logs.
10+
11+
## Dev setup
12+
13+
```bash
14+
rustup toolchain install stable
15+
cargo fmt --all
16+
cargo clippy --all-targets --all-features -- -D warnings
17+
cargo test --all-targets
18+
```
19+
20+
## Pull Requests
21+
22+
- Write a descriptive title and summary.
23+
- Link related issues.
24+
- Include usage notes and sample JSON if useful.
25+
- Update docs/README where applicable.
26+
27+
## Code of Conduct
28+
29+
This project follows a standard Code of Conduct. Be respectful and inclusive.

0 commit comments

Comments
 (0)