@codemieai/code 0.0.46 → 0.0.47

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.
@@ -5,5 +5,5 @@
5
5
  "name": "AI/Run CodeMie",
6
6
  "email": "support@codemieai.com"
7
7
  },
8
- "version": "1.0.12"
8
+ "version": "1.0.13"
9
9
  }
@@ -0,0 +1,183 @@
1
+ # Microsoft Graph API Skill for Claude Code
2
+
3
+ Work with your Microsoft 365 account from Claude Code — emails, calendar, SharePoint, Teams, OneDrive, contacts, and org chart — all via the Microsoft Graph API.
4
+
5
+ ---
6
+
7
+ ## Quick Start
8
+
9
+ ### 1. Install dependencies
10
+
11
+ ```bash
12
+ pip install msal requests
13
+ ```
14
+
15
+ ### 2. Log in (first time only)
16
+
17
+ ```bash
18
+ python .codemie/claude-plugin/skills/msgraph/msgraph.py login
19
+ ```
20
+
21
+ You'll see a message like:
22
+ ```
23
+ ==============================
24
+ To sign in, use a web browser to open the page https://microsoft.com/devicelogin
25
+ and enter the code ABCD-1234 to authenticate.
26
+ ==============================
27
+ ```
28
+
29
+ Open the URL in your browser, enter the code, and sign in with your Microsoft account.
30
+ Your token is cached at `~/.ms_graph_token_cache.json` — you won't need to log in again until it expires (tokens refresh automatically).
31
+
32
+ ### 3. Ask Claude anything
33
+
34
+ After logging in, just ask Claude naturally:
35
+
36
+ - *"Show me my unread emails"*
37
+ - *"What meetings do I have this week?"*
38
+ - *"Find the Q4 report in SharePoint"*
39
+ - *"What did Alice send me in Teams?"*
40
+ - *"Who reports to me?"*
41
+
42
+ Claude will use this skill automatically.
43
+
44
+ ---
45
+
46
+ ## CLI Reference
47
+
48
+ ```
49
+ python .codemie/claude-plugin/skills/msgraph/msgraph.py <command> [options]
50
+ ```
51
+
52
+ ### Auth commands
53
+
54
+ | Command | Description |
55
+ |---------|-------------|
56
+ | `login` | Authenticate (device code flow) |
57
+ | `logout` | Remove cached credentials |
58
+ | `status` | Check if you're logged in |
59
+
60
+ ### Data commands
61
+
62
+ | Command | Key options | Description |
63
+ |---------|-------------|-------------|
64
+ | `me` | | Your profile |
65
+ | `emails` | `--limit N`, `--unread`, `--search QUERY`, `--read ID`, `--send TO --subject S --body B`, `--folder NAME` | Work with Outlook emails |
66
+ | `calendar` | `--limit N`, `--create TITLE --start DT --end DT`, `--availability` | Calendar events |
67
+ | `sharepoint` | `--sites`, `--site ID [--path P]`, `--download ID` | SharePoint files |
68
+ | `teams` | `--chats`, `--messages CHAT_ID`, `--send MSG --chat-id ID`, `--teams-list` | Teams messages |
69
+ | `onedrive` | `[--path P]`, `--upload FILE`, `--download ID`, `--info ID` | OneDrive files |
70
+ | `people` | `--search NAME`, `--contacts` | People & contacts |
71
+ | `org` | `--manager`, `--reports` | Org chart |
72
+
73
+ Add `--json` to any command for machine-readable JSON output.
74
+
75
+ ---
76
+
77
+ ## Examples
78
+
79
+ ```bash
80
+ # List 20 most recent emails
81
+ python .codemie/claude-plugin/skills/msgraph/msgraph.py emails --limit 20
82
+
83
+ # Read a specific email (paste the ID from list output)
84
+ python .codemie/claude-plugin/skills/msgraph/msgraph.py emails --read AAMkAGI2...
85
+
86
+ # Send an email
87
+ python .codemie/claude-plugin/skills/msgraph/msgraph.py emails \
88
+ --send colleague@company.com \
89
+ --subject "Quick question" \
90
+ --body "Are you free tomorrow at 2pm?"
91
+
92
+ # Upcoming calendar events
93
+ python .codemie/claude-plugin/skills/msgraph/msgraph.py calendar --limit 5
94
+
95
+ # Create a meeting
96
+ python .codemie/claude-plugin/skills/msgraph/msgraph.py calendar \
97
+ --create "Design Review" \
98
+ --start "2024-03-20T14:00" \
99
+ --end "2024-03-20T15:00" \
100
+ --timezone "Europe/Berlin"
101
+
102
+ # List SharePoint sites you follow
103
+ python .codemie/claude-plugin/skills/msgraph/msgraph.py sharepoint --sites
104
+
105
+ # Browse a site's documents
106
+ python .codemie/claude-plugin/skills/msgraph/msgraph.py sharepoint \
107
+ --site "contoso.sharepoint.com,abc123,def456" \
108
+ --path "Documents/2024"
109
+
110
+ # Download a OneDrive file
111
+ python .codemie/claude-plugin/skills/msgraph/msgraph.py onedrive --download ITEM_ID --output report.xlsx
112
+
113
+ # Upload to OneDrive
114
+ python .codemie/claude-plugin/skills/msgraph/msgraph.py onedrive \
115
+ --upload ./presentation.pptx \
116
+ --dest "Documents/presentations/deck.pptx"
117
+
118
+ # Recent Teams chats
119
+ python .codemie/claude-plugin/skills/msgraph/msgraph.py teams --chats
120
+
121
+ # Read a chat conversation
122
+ python .codemie/claude-plugin/skills/msgraph/msgraph.py teams --messages 19:abc123@thread.v2
123
+
124
+ # Search your contacts
125
+ python .codemie/claude-plugin/skills/msgraph/msgraph.py people --search "John"
126
+
127
+ # Your manager
128
+ python .codemie/claude-plugin/skills/msgraph/msgraph.py org --manager
129
+ ```
130
+
131
+ ---
132
+
133
+ ## Token Cache
134
+
135
+ Credentials are stored in `~/.ms_graph_token_cache.json`.
136
+ - Access tokens refresh automatically (they last ~1 hour, but MSAL handles renewal)
137
+ - Refresh tokens last longer (~90 days by default in Azure)
138
+ - Run `logout` to remove the cache: `python msgraph.py logout`
139
+
140
+ ---
141
+
142
+ ## Permissions
143
+
144
+ The script requests these Microsoft Graph scopes on first login:
145
+
146
+ | Scope | Used for |
147
+ |-------|---------|
148
+ | `User.Read` | Profile (`me`, `org`) |
149
+ | `Mail.Read` | Reading emails |
150
+ | `Mail.Send` | Sending emails |
151
+ | `Calendars.Read` / `Calendars.ReadWrite` | Calendar events |
152
+ | `Files.Read` / `Files.ReadWrite` | OneDrive files |
153
+ | `Sites.Read.All` | SharePoint sites |
154
+ | `Chat.Read` / `Chat.ReadWrite` | Teams chats |
155
+ | `People.Read` | People rankings |
156
+ | `Contacts.Read` | Outlook contacts |
157
+
158
+ If your organization restricts some permissions, certain commands may return `Permission denied`.
159
+
160
+ ---
161
+
162
+ ## Troubleshooting
163
+
164
+ **`ModuleNotFoundError: No module named 'msal'`**
165
+ ```bash
166
+ pip install msal requests
167
+ ```
168
+
169
+ **`NOT_LOGGED_IN`**
170
+ ```bash
171
+ python .codemie/claude-plugin/skills/msgraph/msgraph.py login
172
+ ```
173
+
174
+ **`Permission denied (403)`**
175
+ Your organization may have restricted that Graph API permission. Contact your IT admin.
176
+
177
+ **`Authentication expired (401)`**
178
+ ```bash
179
+ python .codemie/claude-plugin/skills/msgraph/msgraph.py login
180
+ ```
181
+
182
+ **Token cache is at wrong path**
183
+ Edit `CACHE_FILE` at the top of `msgraph.py` to change the location.
@@ -0,0 +1,233 @@
1
+ ---
2
+ name: msgraph
3
+ description: >
4
+ Work with Microsoft 365 services via the Graph API — emails, calendar events, SharePoint sites,
5
+ Teams chats, OneDrive files, contacts, and org chart. Use this skill whenever the user asks
6
+ about their emails, inbox, unread messages, meetings, calendar, Teams messages or chats,
7
+ SharePoint documents, OneDrive files, colleagues, manager, direct reports, or any personal/
8
+ organizational Microsoft data. Invoke proactively any time the user mentions Outlook, Teams,
9
+ SharePoint, OneDrive, or wants to interact with their Microsoft 365 account. The skill uses
10
+ a local Python CLI (msgraph.py) that handles authentication, token caching, and all API calls.
11
+ ---
12
+
13
+ # Microsoft Graph API Skill
14
+
15
+ This skill lets you interact with Microsoft 365 services on behalf of the user using the
16
+ Microsoft Graph API. The Python CLI at `scripts/msgraph.py` handles everything.
17
+
18
+ ## Setup & Authentication
19
+
20
+ **Check login status first** — always run this before any other command:
21
+
22
+ ```bash
23
+ python scripts/msgraph.py status
24
+ ```
25
+
26
+ **Output interpretation:**
27
+ - `Logged in as: user@company.com` → proceed with any command below
28
+ - `NOT_LOGGED_IN` → follow the Login Flow below
29
+ - `TOKEN_EXPIRED` → session expired, also follow the Login Flow below
30
+
31
+ ### Login Flow (first time or after expiry)
32
+
33
+ ```bash
34
+ python scripts/msgraph.py login
35
+ ```
36
+
37
+ This starts the **Device Code Flow** — it will print a URL and a short code like:
38
+ ```
39
+ To sign in, use a web browser to open the page https://microsoft.com/devicelogin
40
+ and enter the code ABCD-1234 to authenticate.
41
+ ```
42
+
43
+ Tell the user exactly that message, then wait. Once they complete login in the browser,
44
+ the token is cached at `~/.ms_graph_token_cache.json` and all subsequent commands run silently.
45
+
46
+ ### When NOT logged in or token expired
47
+
48
+ If status returns `NOT_LOGGED_IN` or `TOKEN_EXPIRED`, tell the user:
49
+
50
+ > "You need to log in to Microsoft first. Run this command in your terminal:
51
+ > ```
52
+ > python scripts/msgraph.py login
53
+ > ```
54
+ > A code and URL will appear — open the URL in your browser and enter the code."
55
+
56
+ ---
57
+
58
+ ## Available Commands
59
+
60
+ ### Profile & Org
61
+
62
+ ```bash
63
+ # Your profile
64
+ python scripts/msgraph.py me
65
+
66
+ # Your manager
67
+ python scripts/msgraph.py org --manager
68
+
69
+ # Your direct reports
70
+ python scripts/msgraph.py org --reports
71
+ ```
72
+
73
+ ### Emails
74
+
75
+ ```bash
76
+ # List recent emails (default 10)
77
+ python scripts/msgraph.py emails
78
+
79
+ # More emails
80
+ python scripts/msgraph.py emails --limit 25
81
+
82
+ # Unread only
83
+ python scripts/msgraph.py emails --unread
84
+
85
+ # Search emails
86
+ python scripts/msgraph.py emails --search "invoice Q4"
87
+
88
+ # Read a specific email by ID (copy ID from list output)
89
+ python scripts/msgraph.py emails --read MESSAGE_ID
90
+
91
+ # Send an email
92
+ python scripts/msgraph.py emails --send recipient@example.com --subject "Hello" --body "Message text"
93
+
94
+ # Browse specific folder (inbox, sentitems, drafts, deleteditems, junkemail)
95
+ python scripts/msgraph.py emails --folder sentitems --limit 5
96
+
97
+ # Machine-readable JSON output
98
+ python scripts/msgraph.py emails --json
99
+ ```
100
+
101
+ ### Calendar
102
+
103
+ ```bash
104
+ # Upcoming events (default 10)
105
+ python scripts/msgraph.py calendar
106
+
107
+ # More events
108
+ python scripts/msgraph.py calendar --limit 20
109
+
110
+ # Create an event
111
+ python scripts/msgraph.py calendar --create "Team Standup" \
112
+ --start "2024-03-20T09:00" --end "2024-03-20T09:30" \
113
+ --location "Teams" --timezone "Europe/Berlin"
114
+
115
+ # Check availability for a time window
116
+ python scripts/msgraph.py calendar --availability \
117
+ --start "2024-03-20T09:00:00" --end "2024-03-20T18:00:00"
118
+ ```
119
+
120
+ ### SharePoint
121
+
122
+ ```bash
123
+ # List followed/joined SharePoint sites
124
+ python scripts/msgraph.py sharepoint --sites
125
+
126
+ # Browse files in a specific site (use ID from --sites output)
127
+ python scripts/msgraph.py sharepoint --site SITE_ID
128
+
129
+ # Browse a subfolder within a site
130
+ python scripts/msgraph.py sharepoint --site SITE_ID --path "Documents/Reports"
131
+
132
+ # Download a file
133
+ python scripts/msgraph.py sharepoint --download ITEM_ID --output report.xlsx
134
+ ```
135
+
136
+ ### Teams
137
+
138
+ ```bash
139
+ # List all Teams chats
140
+ python scripts/msgraph.py teams --chats
141
+
142
+ # Read messages from a chat (use chat ID from --chats output)
143
+ python scripts/msgraph.py teams --messages CHAT_ID
144
+
145
+ # Send a message to a chat
146
+ python scripts/msgraph.py teams --send "Hello team!" --chat-id CHAT_ID
147
+
148
+ # List teams you're a member of
149
+ python scripts/msgraph.py teams --teams-list
150
+ ```
151
+
152
+ ### OneDrive
153
+
154
+ ```bash
155
+ # List root files
156
+ python scripts/msgraph.py onedrive
157
+
158
+ # Browse a folder
159
+ python scripts/msgraph.py onedrive --path "Documents"
160
+
161
+ # Upload a file
162
+ python scripts/msgraph.py onedrive --upload ./report.pdf --dest "Documents/report.pdf"
163
+
164
+ # Download a file by ID
165
+ python scripts/msgraph.py onedrive --download ITEM_ID --output local_copy.pdf
166
+
167
+ # File metadata
168
+ python scripts/msgraph.py onedrive --info ITEM_ID
169
+ ```
170
+
171
+ ### People & Contacts
172
+
173
+ ```bash
174
+ # Frequent collaborators (AI-ranked by Microsoft)
175
+ python scripts/msgraph.py people
176
+
177
+ # Search people by name
178
+ python scripts/msgraph.py people --search "Alice"
179
+
180
+ # Outlook address book contacts
181
+ python scripts/msgraph.py people --contacts
182
+ ```
183
+
184
+ ---
185
+
186
+ ## Workflow Patterns
187
+
188
+ ### "Show me my emails"
189
+ 1. Run `status` → check login
190
+ 2. Run `emails --limit 15` → show results
191
+ 3. If user wants to read one, run `emails --read ID`
192
+
193
+ ### "What's on my calendar today/this week?"
194
+ 1. Run `calendar --limit 10`
195
+ 2. Parse dates in output and filter for user's timeframe
196
+
197
+ ### "Find a file in SharePoint"
198
+ 1. Run `sharepoint --sites` → list sites
199
+ 2. Run `sharepoint --site SITE_ID` → browse files
200
+ 3. Use `--path` to drill into folders
201
+ 4. Offer `--download ITEM_ID` if user wants the file
202
+
203
+ ### "Check my Teams messages"
204
+ 1. Run `teams --chats` → list chats
205
+ 2. User picks a chat → run `teams --messages CHAT_ID`
206
+
207
+ ### "Who's my manager?" / "Who reports to me?"
208
+ - Run `org --manager` or `org --reports`
209
+
210
+ ---
211
+
212
+ ## Error Handling
213
+
214
+ | Exit code | Meaning |
215
+ |-----------|---------|
216
+ | 0 | Success |
217
+ | 1 | API error (shown in output) |
218
+ | 2 | NOT_LOGGED_IN — user must run `login` |
219
+
220
+ When you see `Permission denied` errors, it means the OAuth scope isn't granted for that operation.
221
+ This can happen if the user's organization has restricted certain Graph API permissions.
222
+
223
+ ---
224
+
225
+ ## Dependencies
226
+
227
+ The script requires Python 3.10+ and two packages:
228
+ ```bash
229
+ pip install msal requests
230
+ ```
231
+
232
+ If `msal` or `requests` are not installed, the script prints the install command and exits.
233
+ IMPORTANT: you must work with current date (get it from sh/bash)
@@ -0,0 +1,785 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ msgraph.py — Microsoft Graph API CLI for Claude Code skill
4
+
5
+ Authentication: MSAL device code flow with persistent token cache.
6
+ First time: python msgraph.py login
7
+ Subsequent: token refreshed silently from cache.
8
+ """
9
+
10
+ import argparse
11
+ import json
12
+ import sys
13
+ from datetime import datetime, timezone
14
+ from pathlib import Path
15
+
16
+ try:
17
+ from msal import PublicClientApplication, SerializableTokenCache
18
+ import requests
19
+ except ImportError:
20
+ print("Missing dependencies. Install with:")
21
+ print(" pip install msal requests")
22
+ sys.exit(1)
23
+
24
+ # ── Config ────────────────────────────────────────────────────────────────────
25
+ CLIENT_ID = "14d82eec-204b-4c2f-b7e8-296a70dab67e" # MS GraphAPI public client
26
+ AUTHORITY = "https://login.microsoftonline.com/common"
27
+ SCOPES = [
28
+ "User.Read",
29
+ "Mail.Read",
30
+ "Mail.Send",
31
+ "Calendars.Read",
32
+ "Calendars.ReadWrite",
33
+ "Files.Read",
34
+ "Files.ReadWrite",
35
+ "Sites.Read.All",
36
+ "Chat.Read",
37
+ "Chat.ReadWrite",
38
+ "People.Read",
39
+ "Contacts.Read",
40
+ ]
41
+ CACHE_FILE = Path.home() / ".ms_graph_token_cache.json"
42
+ GRAPH_BASE = "https://graph.microsoft.com/v1.0"
43
+
44
+ # ── Token Cache ───────────────────────────────────────────────────────────────
45
+ def load_cache() -> SerializableTokenCache:
46
+ cache = SerializableTokenCache()
47
+ if CACHE_FILE.exists():
48
+ cache.deserialize(CACHE_FILE.read_text())
49
+ return cache
50
+
51
+ def save_cache(cache: SerializableTokenCache):
52
+ if cache.has_state_changed:
53
+ CACHE_FILE.write_text(cache.serialize())
54
+
55
+ # ── Authentication ────────────────────────────────────────────────────────────
56
+ def get_access_token(force_login: bool = False) -> str:
57
+ cache = load_cache()
58
+ app = PublicClientApplication(
59
+ client_id=CLIENT_ID,
60
+ authority=AUTHORITY,
61
+ token_cache=cache
62
+ )
63
+
64
+ if not force_login:
65
+ accounts = app.get_accounts()
66
+ if accounts:
67
+ result = app.acquire_token_silent(SCOPES, account=accounts[0])
68
+ if result and "access_token" in result:
69
+ save_cache(cache)
70
+ return result["access_token"]
71
+
72
+ # Device Code Flow
73
+ flow = app.initiate_device_flow(scopes=SCOPES)
74
+ if "user_code" not in flow:
75
+ raise RuntimeError(f"Failed to initiate device flow: {flow.get('error_description')}")
76
+
77
+ print("\n" + "=" * 60)
78
+ print(flow["message"])
79
+ print("=" * 60 + "\n")
80
+
81
+ result = app.acquire_token_by_device_flow(flow)
82
+ if "access_token" not in result:
83
+ raise RuntimeError(f"Authentication failed: {result.get('error_description')}")
84
+
85
+ save_cache(cache)
86
+ return result["access_token"]
87
+
88
+ def get_token_or_exit() -> str:
89
+ """Get token from cache only — exit with helpful message if not logged in or token expired."""
90
+ cache = load_cache()
91
+ app = PublicClientApplication(
92
+ client_id=CLIENT_ID,
93
+ authority=AUTHORITY,
94
+ token_cache=cache
95
+ )
96
+ accounts = app.get_accounts()
97
+ if not accounts:
98
+ print("NOT_LOGGED_IN")
99
+ sys.exit(2)
100
+ result = app.acquire_token_silent(SCOPES, account=accounts[0])
101
+ if not result or "access_token" not in result:
102
+ print("TOKEN_EXPIRED")
103
+ sys.exit(2)
104
+ save_cache(cache)
105
+ return result["access_token"]
106
+
107
+ # ── Graph API Helpers ─────────────────────────────────────────────────────────
108
+ def graph_get(endpoint: str, token: str, params: dict | None = None) -> dict:
109
+ r = requests.get(
110
+ f"{GRAPH_BASE}{endpoint}",
111
+ headers={"Authorization": f"Bearer {token}"},
112
+ params=params or {}
113
+ )
114
+ r.raise_for_status()
115
+ return r.json()
116
+
117
+ def graph_post(endpoint: str, token: str, body: dict) -> dict:
118
+ r = requests.post(
119
+ f"{GRAPH_BASE}{endpoint}",
120
+ headers={
121
+ "Authorization": f"Bearer {token}",
122
+ "Content-Type": "application/json"
123
+ },
124
+ json=body
125
+ )
126
+ r.raise_for_status()
127
+ return r.json() if r.content else {}
128
+
129
+ def graph_download(endpoint: str, token: str) -> bytes:
130
+ r = requests.get(
131
+ f"{GRAPH_BASE}{endpoint}",
132
+ headers={"Authorization": f"Bearer {token}"},
133
+ allow_redirects=True
134
+ )
135
+ r.raise_for_status()
136
+ return r.content
137
+
138
+ def graph_upload(endpoint: str, token: str, content: bytes, content_type: str = "application/octet-stream") -> dict:
139
+ r = requests.put(
140
+ f"{GRAPH_BASE}{endpoint}",
141
+ headers={
142
+ "Authorization": f"Bearer {token}",
143
+ "Content-Type": content_type
144
+ },
145
+ data=content
146
+ )
147
+ r.raise_for_status()
148
+ return r.json()
149
+
150
+ # ── Formatters ────────────────────────────────────────────────────────────────
151
+ def fmt_dt(iso: str) -> str:
152
+ """Format ISO datetime to readable string."""
153
+ try:
154
+ dt = datetime.fromisoformat(iso.replace("Z", "+00:00"))
155
+ return dt.strftime("%Y-%m-%d %H:%M")
156
+ except Exception:
157
+ return iso[:16].replace("T", " ")
158
+
159
+ def fmt_size(size: int) -> str:
160
+ for unit in ["B", "KB", "MB", "GB"]:
161
+ if size < 1024:
162
+ return f"{size:.1f} {unit}"
163
+ size /= 1024
164
+ return f"{size:.1f} TB"
165
+
166
+ def print_json(data: dict | list):
167
+ print(json.dumps(data, indent=2, ensure_ascii=False))
168
+
169
+ # ── Commands ──────────────────────────────────────────────────────────────────
170
+
171
+ def cmd_login(args):
172
+ """Authenticate and cache credentials."""
173
+ print("Starting Microsoft authentication...")
174
+ token = get_access_token(force_login=True)
175
+ me = graph_get("/me", token)
176
+ print(f"\nLogged in as: {me['displayName']} <{me['userPrincipalName']}>")
177
+ print(f"User ID: {me['id']}")
178
+ print(f"Token cached at: {CACHE_FILE}")
179
+
180
+ def cmd_logout(args):
181
+ """Remove cached credentials."""
182
+ if CACHE_FILE.exists():
183
+ CACHE_FILE.unlink()
184
+ print(f"Logged out. Cache removed: {CACHE_FILE}")
185
+ else:
186
+ print("No cached credentials found.")
187
+
188
+ def cmd_status(args):
189
+ """Check login status."""
190
+ cache = load_cache()
191
+ app = PublicClientApplication(client_id=CLIENT_ID, authority=AUTHORITY, token_cache=cache)
192
+ accounts = app.get_accounts()
193
+ if not accounts:
194
+ print("NOT_LOGGED_IN")
195
+ print(f"\nTo login, run:\n python {Path(__file__).name} login")
196
+ return
197
+ # Verify token can actually be acquired (not just that account exists in cache)
198
+ result = app.acquire_token_silent(SCOPES, account=accounts[0])
199
+ if result and "access_token" in result:
200
+ save_cache(cache)
201
+ print(f"Logged in as: {accounts[0]['username']}")
202
+ print(f"Cache file: {CACHE_FILE}")
203
+ else:
204
+ print("TOKEN_EXPIRED")
205
+ print(f"Account: {accounts[0]['username']}")
206
+ print(f"\nSession expired. Re-authenticate with:\n python {Path(__file__).name} login")
207
+
208
+ def cmd_me(args):
209
+ """Show user profile information."""
210
+ token = get_token_or_exit()
211
+ me = graph_get("/me", token)
212
+ fields = ["displayName", "userPrincipalName", "id", "mail", "jobTitle",
213
+ "department", "officeLocation", "businessPhones", "mobilePhone"]
214
+ if args.json:
215
+ print_json({k: me.get(k) for k in fields if me.get(k)})
216
+ return
217
+ print(f"Name : {me.get('displayName', 'N/A')}")
218
+ print(f"Email : {me.get('userPrincipalName', 'N/A')}")
219
+ print(f"Job Title : {me.get('jobTitle', 'N/A')}")
220
+ print(f"Department : {me.get('department', 'N/A')}")
221
+ print(f"Office : {me.get('officeLocation', 'N/A')}")
222
+ print(f"Phone : {me.get('businessPhones', ['N/A'])[0] if me.get('businessPhones') else 'N/A'}")
223
+ print(f"User ID : {me.get('id', 'N/A')}")
224
+
225
+ def cmd_emails(args):
226
+ """List, read, send or search emails."""
227
+ token = get_token_or_exit()
228
+
229
+ if args.send:
230
+ # Send email: --send "To <email>" --subject "Subject" --body "Body"
231
+ to_email = args.send
232
+ body_content = args.body or ""
233
+ subject = args.subject or "(no subject)"
234
+ payload = {
235
+ "message": {
236
+ "subject": subject,
237
+ "body": {"contentType": "Text", "content": body_content},
238
+ "toRecipients": [{"emailAddress": {"address": to_email}}]
239
+ }
240
+ }
241
+ graph_post("/me/sendMail", token, payload)
242
+ print(f"Email sent to {to_email}")
243
+ return
244
+
245
+ if args.read:
246
+ # Read a specific email by ID
247
+ msg = graph_get(f"/me/messages/{args.read}", token)
248
+ print(f"Subject : {msg.get('subject')}")
249
+ print(f"From : {msg['from']['emailAddress']['name']} <{msg['from']['emailAddress']['address']}>")
250
+ print(f"Date : {fmt_dt(msg['receivedDateTime'])}")
251
+ print(f"Read : {'Yes' if msg['isRead'] else 'No'}")
252
+ print(f"\n{'─'*60}")
253
+ body = msg.get("body", {})
254
+ if body.get("contentType") == "text":
255
+ print(body.get("content", ""))
256
+ else:
257
+ # Strip basic HTML tags for readability
258
+ import re
259
+ text = re.sub(r'<[^>]+>', '', body.get("content", ""))
260
+ print(text[:2000])
261
+ return
262
+
263
+ # List/search emails
264
+ params: dict = {
265
+ "$top": args.limit,
266
+ "$select": "id,subject,from,receivedDateTime,isRead,hasAttachments,importance",
267
+ "$orderby": "receivedDateTime desc"
268
+ }
269
+ if args.search:
270
+ params["$search"] = f'"{args.search}"'
271
+ params.pop("$orderby", None) # $search incompatible with $orderby
272
+ if args.folder:
273
+ endpoint = f"/me/mailFolders/{args.folder}/messages"
274
+ elif args.unread:
275
+ params["$filter"] = "isRead eq false"
276
+ endpoint = "/me/messages"
277
+ else:
278
+ endpoint = "/me/messages"
279
+
280
+ data = graph_get(endpoint, token, params)
281
+ emails = data.get("value", [])
282
+
283
+ if args.json:
284
+ print_json(emails)
285
+ return
286
+
287
+ if not emails:
288
+ print("No emails found.")
289
+ return
290
+
291
+ print(f"\n{'ID':<36} {'Date':<16} {'Rd'} {'Subject'}")
292
+ print("─" * 80)
293
+ for e in emails:
294
+ read_mark = "✓" if e["isRead"] else "●"
295
+ attach = "📎" if e.get("hasAttachments") else " "
296
+ subject = e.get("subject", "(no subject)")[:45]
297
+ sender = e["from"]["emailAddress"].get("name", "")[:20]
298
+ date = fmt_dt(e["receivedDateTime"])
299
+ print(f"{e['id'][:36]} {date} {read_mark} {attach} {subject} ({sender})")
300
+
301
+ def cmd_calendar(args):
302
+ """List or create calendar events."""
303
+ token = get_token_or_exit()
304
+
305
+ if args.create:
306
+ # Create event: --create "Title" --start "2024-03-15T10:00" --end "2024-03-15T11:00"
307
+ if not args.start or not args.end:
308
+ print("Error: --create requires --start and --end (format: YYYY-MM-DDTHH:MM)")
309
+ sys.exit(1)
310
+ tz = args.timezone or "UTC"
311
+ payload = {
312
+ "subject": args.create,
313
+ "start": {"dateTime": args.start, "timeZone": tz},
314
+ "end": {"dateTime": args.end, "timeZone": tz},
315
+ }
316
+ if args.location:
317
+ payload["location"] = {"displayName": args.location}
318
+ if args.body:
319
+ payload["body"] = {"contentType": "Text", "content": args.body}
320
+ event = graph_post("/me/events", token, payload)
321
+ print(f"Event created: {event.get('subject')}")
322
+ print(f"ID: {event.get('id')}")
323
+ return
324
+
325
+ if args.availability:
326
+ # Check free/busy for a time range
327
+ start = args.start or datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S")
328
+ end = args.end or datetime.now(timezone.utc).replace(hour=23, minute=59).strftime("%Y-%m-%dT%H:%M:%S")
329
+ view = graph_get(
330
+ "/me/calendarView",
331
+ token,
332
+ {"startDateTime": start, "endDateTime": end,
333
+ "$select": "subject,start,end,showAs", "$orderby": "start/dateTime"}
334
+ )
335
+ events = view.get("value", [])
336
+ if not events:
337
+ print(f"You're free between {start[:16]} and {end[:16]}.")
338
+ else:
339
+ print(f"\nBusy slots ({len(events)} events):")
340
+ for e in events:
341
+ print(f" {fmt_dt(e['start']['dateTime'])} — {fmt_dt(e['end']['dateTime'])}: {e.get('subject', '(no title)')}")
342
+ return
343
+
344
+ # List upcoming events
345
+ now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
346
+ data = graph_get(
347
+ "/me/calendarView",
348
+ token,
349
+ {
350
+ "startDateTime": now,
351
+ "endDateTime": f"{datetime.now(timezone.utc).year + 1}-01-01T00:00:00Z",
352
+ "$top": args.limit,
353
+ "$select": "id,subject,start,end,location,organizer,isOnlineMeeting,onlineMeetingUrl",
354
+ "$orderby": "start/dateTime"
355
+ }
356
+ )
357
+ events = data.get("value", [])
358
+
359
+ if args.json:
360
+ print_json(events)
361
+ return
362
+
363
+ if not events:
364
+ print("No upcoming events.")
365
+ return
366
+
367
+ print(f"\n{'Date & Time':<20} {'Duration':<10} {'Title'}")
368
+ print("─" * 80)
369
+ for e in events:
370
+ start = fmt_dt(e["start"]["dateTime"])
371
+ title = e.get("subject", "(no title)")[:45]
372
+ organizer = e["organizer"]["emailAddress"].get("name", "")[:20]
373
+ online = "🎥" if e.get("isOnlineMeeting") else " "
374
+ loc = e.get("location", {}).get("displayName", "")[:20]
375
+ print(f"{start:<20} {online} {title} ({organizer})" + (f" @ {loc}" if loc else ""))
376
+
377
+ def cmd_sharepoint(args):
378
+ """Browse SharePoint sites and files."""
379
+ token = get_token_or_exit()
380
+
381
+ if args.sites:
382
+ # List all joined sites
383
+ data = graph_get("/me/followedSites", token, {"$select": "id,displayName,webUrl"})
384
+ sites = data.get("value", [])
385
+ if not sites:
386
+ # Fallback: search for sites
387
+ data = graph_get("/sites", token, {"search": "*", "$top": args.limit})
388
+ sites = data.get("value", [])
389
+ if args.json:
390
+ print_json(sites)
391
+ return
392
+ print(f"\n{'ID':<50} {'Name'}")
393
+ print("─" * 80)
394
+ for s in sites[:args.limit]:
395
+ print(f"{s['id']:<50} {s.get('displayName', 'N/A')}")
396
+ print(f" URL: {s.get('webUrl', 'N/A')}")
397
+ return
398
+
399
+ if args.site:
400
+ # Browse files in a site's drive
401
+ site_id = args.site
402
+ path = args.path or "root"
403
+ endpoint = f"/sites/{site_id}/drive/root/children" if path == "root" else f"/sites/{site_id}/drive/root:/{path}:/children"
404
+ data = graph_get(endpoint, token, {"$top": args.limit, "$select": "id,name,size,lastModifiedDateTime,file,folder"})
405
+ items = data.get("value", [])
406
+ if args.json:
407
+ print_json(items)
408
+ return
409
+ print(f"\nFiles in site {site_id} / {path}:")
410
+ print("─" * 60)
411
+ for item in items:
412
+ kind = "📁" if "folder" in item else "📄"
413
+ size = fmt_size(item.get("size", 0)) if "file" in item else ""
414
+ modified = fmt_dt(item.get("lastModifiedDateTime", ""))
415
+ print(f" {kind} {item['name']:<40} {size:<10} {modified}")
416
+ return
417
+
418
+ if args.download:
419
+ # Download a file by item ID from personal drive (use --site for SharePoint)
420
+ content = graph_download(f"/me/drive/items/{args.download}/content", token)
421
+ out_path = Path(args.output or f"downloaded_{args.download[:8]}")
422
+ out_path.write_bytes(content)
423
+ print(f"Downloaded {len(content)} bytes to {out_path}")
424
+ return
425
+
426
+ print("SharePoint commands: --sites | --site SITE_ID [--path PATH] | --download ITEM_ID")
427
+
428
+ def cmd_teams(args):
429
+ """List Teams chats and messages."""
430
+ token = get_token_or_exit()
431
+
432
+ if args.chats:
433
+ # List all chats
434
+ data = graph_get("/me/chats", token, {
435
+ "$top": args.limit,
436
+ "$select": "id,topic,chatType,lastUpdatedDateTime"
437
+ })
438
+ chats = data.get("value", [])
439
+ if args.json:
440
+ print_json(chats)
441
+ return
442
+ print(f"\n{'Chat ID':<50} {'Type':<10} {'Topic'}")
443
+ print("─" * 80)
444
+ for c in chats:
445
+ topic = c.get("topic") or "(direct message)"
446
+ print(f"{c['id']:<50} {c.get('chatType',''):<10} {topic}")
447
+ return
448
+
449
+ if args.messages:
450
+ # Get messages in a chat
451
+ data = graph_get(f"/me/chats/{args.messages}/messages", token, {
452
+ "$top": args.limit,
453
+ "$select": "id,from,body,createdDateTime"
454
+ })
455
+ msgs = data.get("value", [])
456
+ if args.json:
457
+ print_json(msgs)
458
+ return
459
+ print(f"\nMessages in chat {args.messages[:20]}...:")
460
+ print("─" * 60)
461
+ for m in reversed(msgs):
462
+ sender = m.get("from", {}).get("user", {}).get("displayName", "Unknown") if m.get("from") else "System"
463
+ time = fmt_dt(m.get("createdDateTime", ""))
464
+ import re
465
+ body = re.sub(r'<[^>]+>', '', m.get("body", {}).get("content", ""))[:200]
466
+ print(f"[{time}] {sender}: {body}")
467
+ return
468
+
469
+ if args.send and args.chat_id:
470
+ # Send message to a chat
471
+ payload = {"body": {"content": args.send}}
472
+ result = graph_post(f"/me/chats/{args.chat_id}/messages", token, payload)
473
+ print(f"Message sent. ID: {result.get('id')}")
474
+ return
475
+
476
+ if args.teams_list:
477
+ # List joined teams
478
+ data = graph_get("/me/joinedTeams", token, {"$select": "id,displayName,description"})
479
+ teams = data.get("value", [])
480
+ if args.json:
481
+ print_json(teams)
482
+ return
483
+ for t in teams:
484
+ print(f"{t['id'][:36]} {t['displayName']}")
485
+ return
486
+
487
+ print("Teams commands: --chats | --messages CHAT_ID | --send MSG --chat-id CHAT_ID | --teams-list")
488
+
489
+ def cmd_onedrive(args):
490
+ """Browse, upload and download OneDrive files."""
491
+ token = get_token_or_exit()
492
+
493
+ if args.upload:
494
+ src = Path(args.upload)
495
+ if not src.exists():
496
+ print(f"File not found: {src}")
497
+ sys.exit(1)
498
+ dest_path = args.dest or src.name
499
+ content = src.read_bytes()
500
+ result = graph_upload(f"/me/drive/root:/{dest_path}:/content", token, content)
501
+ print(f"Uploaded: {result.get('name')} ({fmt_size(result.get('size', 0))})")
502
+ print(f"ID: {result.get('id')}")
503
+ return
504
+
505
+ if args.download:
506
+ content = graph_download(f"/me/drive/items/{args.download}/content", token)
507
+ out_path = Path(args.output or f"download_{args.download[:8]}")
508
+ out_path.write_bytes(content)
509
+ print(f"Downloaded {fmt_size(len(content))} to {out_path}")
510
+ return
511
+
512
+ if args.info:
513
+ item = graph_get(f"/me/drive/items/{args.info}", token)
514
+ if args.json:
515
+ print_json(item)
516
+ return
517
+ print(f"Name : {item.get('name')}")
518
+ print(f"Size : {fmt_size(item.get('size', 0))}")
519
+ print(f"Modified: {fmt_dt(item.get('lastModifiedDateTime', ''))}")
520
+ print(f"URL : {item.get('webUrl', 'N/A')}")
521
+ return
522
+
523
+ # List files in a path
524
+ path = args.path or ""
525
+ endpoint = "/me/drive/root/children" if not path else f"/me/drive/root:/{path}:/children"
526
+ data = graph_get(endpoint, token, {
527
+ "$top": args.limit,
528
+ "$select": "id,name,size,lastModifiedDateTime,file,folder",
529
+ "$orderby": "name"
530
+ })
531
+ items = data.get("value", [])
532
+
533
+ if args.json:
534
+ print_json(items)
535
+ return
536
+
537
+ if not items:
538
+ print(f"No files found in /{path or ''}")
539
+ return
540
+
541
+ print(f"\nOneDrive: /{path or ''}")
542
+ print("─" * 60)
543
+ for item in items:
544
+ kind = "📁" if "folder" in item else "📄"
545
+ size = fmt_size(item.get("size", 0)) if "file" in item else ""
546
+ modified = fmt_dt(item.get("lastModifiedDateTime", ""))
547
+ count = f" ({item['folder']['childCount']} items)" if "folder" in item else ""
548
+ print(f" {kind} {item['id'][:16]} {item['name']:<40} {size:<10} {modified}{count}")
549
+
550
+ def cmd_people(args):
551
+ """List relevant people and contacts."""
552
+ token = get_token_or_exit()
553
+
554
+ if args.contacts:
555
+ # Outlook contacts
556
+ params = {"$top": args.limit, "$select": "displayName,emailAddresses,mobilePhone,jobTitle,companyName"}
557
+ if args.search:
558
+ params["$search"] = f'"{args.search}"'
559
+ data = graph_get("/me/contacts", token, params)
560
+ contacts = data.get("value", [])
561
+ if args.json:
562
+ print_json(contacts)
563
+ return
564
+ print(f"\n{'Name':<30} {'Email':<35} {'Title'}")
565
+ print("─" * 80)
566
+ for c in contacts:
567
+ emails = [e["address"] for e in c.get("emailAddresses", [])]
568
+ email = emails[0] if emails else "N/A"
569
+ print(f"{c.get('displayName',''):<30} {email:<35} {c.get('jobTitle','')}")
570
+ return
571
+
572
+ # Relevant people (AI-ranked by interaction)
573
+ params = {"$top": args.limit}
574
+ if args.search:
575
+ params["$search"] = f'"{args.search}"'
576
+ data = graph_get("/me/people", token, params)
577
+ people = data.get("value", [])
578
+
579
+ if args.json:
580
+ print_json(people)
581
+ return
582
+
583
+ if not people:
584
+ print("No people found.")
585
+ return
586
+
587
+ print(f"\n{'Name':<30} {'Email':<35} {'Title'}")
588
+ print("─" * 80)
589
+ for p in people:
590
+ emails = [s["address"] for s in p.get("scoredEmailAddresses", [])]
591
+ email = emails[0] if emails else "N/A"
592
+ print(f"{p.get('displayName',''):<30} {email:<35} {p.get('jobTitle','')}")
593
+
594
+ def cmd_org(args):
595
+ """Show organizational info: manager, reports, colleagues."""
596
+ token = get_token_or_exit()
597
+
598
+ if args.manager:
599
+ try:
600
+ mgr = graph_get("/me/manager", token)
601
+ print(f"Manager: {mgr.get('displayName')} <{mgr.get('userPrincipalName')}>")
602
+ print(f"Title : {mgr.get('jobTitle', 'N/A')}")
603
+ except requests.HTTPError as e:
604
+ if e.response.status_code == 404:
605
+ print("No manager found (you may be at the top of the org).")
606
+ else:
607
+ raise
608
+ return
609
+
610
+ if args.reports:
611
+ data = graph_get("/me/directReports", token,
612
+ {"$select": "displayName,userPrincipalName,jobTitle"})
613
+ reports = data.get("value", [])
614
+ print(f"\nDirect Reports ({len(reports)}):")
615
+ for r in reports:
616
+ print(f" {r.get('displayName'):<30} {r.get('userPrincipalName')}")
617
+ return
618
+
619
+ # Default: show org context
620
+ try:
621
+ mgr = graph_get("/me/manager", token)
622
+ print(f"Manager: {mgr.get('displayName')}")
623
+ except Exception:
624
+ pass
625
+ reports = graph_get("/me/directReports", token, {"$select": "displayName"}).get("value", [])
626
+ print(f"Direct Reports: {len(reports)}")
627
+ colleagues = graph_get("/me/people", token, {"$top": 5}).get("value", [])
628
+ print(f"\nFrequent colleagues:")
629
+ for p in colleagues:
630
+ print(f" {p.get('displayName')}")
631
+
632
+ # ── CLI Setup ─────────────────────────────────────────────────────────────────
633
+ def main():
634
+ parser = argparse.ArgumentParser(
635
+ description="Microsoft Graph API CLI",
636
+ formatter_class=argparse.RawDescriptionHelpFormatter,
637
+ epilog="""
638
+ Examples:
639
+ python msgraph.py login # Authenticate
640
+ python msgraph.py status # Check login status
641
+ python msgraph.py me # Show profile
642
+ python msgraph.py emails --limit 10 # List 10 emails
643
+ python msgraph.py emails --unread # Unread emails
644
+ python msgraph.py emails --search "invoice" # Search emails
645
+ python msgraph.py emails --read MSG_ID # Read specific email
646
+ python msgraph.py emails --send "a@b.com" --subject "Hi" --body "Hello"
647
+ python msgraph.py calendar --limit 10 # Upcoming events
648
+ python msgraph.py calendar --create "Meeting" --start 2024-03-15T10:00 --end 2024-03-15T11:00
649
+ python msgraph.py sharepoint --sites # List SharePoint sites
650
+ python msgraph.py teams --chats # List Teams chats
651
+ python msgraph.py teams --messages CHAT_ID # Read chat messages
652
+ python msgraph.py onedrive # List OneDrive root
653
+ python msgraph.py onedrive --path "Documents" # Browse folder
654
+ python msgraph.py onedrive --upload file.txt # Upload file
655
+ python msgraph.py people # Frequent contacts
656
+ python msgraph.py org --manager # Show your manager
657
+ """
658
+ )
659
+
660
+ # Global flags
661
+ parser.add_argument("--json", action="store_true", help="Output as JSON")
662
+
663
+ subparsers = parser.add_subparsers(dest="command")
664
+
665
+ # login
666
+ subparsers.add_parser("login", help="Authenticate with Microsoft")
667
+
668
+ # logout
669
+ subparsers.add_parser("logout", help="Remove cached credentials")
670
+
671
+ # status
672
+ subparsers.add_parser("status", help="Check login status")
673
+
674
+ # me
675
+ p_me = subparsers.add_parser("me", help="Show user profile")
676
+ p_me.add_argument("--json", action="store_true")
677
+
678
+ # emails
679
+ p_email = subparsers.add_parser("emails", help="Work with emails")
680
+ p_email.add_argument("--limit", type=int, default=10, help="Number of emails to show")
681
+ p_email.add_argument("--search", help="Search query")
682
+ p_email.add_argument("--unread", action="store_true", help="Show only unread emails")
683
+ p_email.add_argument("--folder", help="Mail folder (inbox, sentitems, drafts, etc.)")
684
+ p_email.add_argument("--read", metavar="ID", help="Read email by ID")
685
+ p_email.add_argument("--send", metavar="TO", help="Send email to address")
686
+ p_email.add_argument("--subject", help="Email subject (for --send)")
687
+ p_email.add_argument("--body", help="Email body text")
688
+ p_email.add_argument("--json", action="store_true")
689
+
690
+ # calendar
691
+ p_cal = subparsers.add_parser("calendar", help="Work with calendar")
692
+ p_cal.add_argument("--limit", type=int, default=10)
693
+ p_cal.add_argument("--create", metavar="TITLE", help="Create event with this title")
694
+ p_cal.add_argument("--start", help="Start datetime (YYYY-MM-DDTHH:MM)")
695
+ p_cal.add_argument("--end", help="End datetime (YYYY-MM-DDTHH:MM)")
696
+ p_cal.add_argument("--location", help="Event location")
697
+ p_cal.add_argument("--body", help="Event description")
698
+ p_cal.add_argument("--timezone", default="UTC", help="Timezone (e.g. Europe/Berlin)")
699
+ p_cal.add_argument("--availability", action="store_true", help="Check free/busy")
700
+ p_cal.add_argument("--json", action="store_true")
701
+
702
+ # sharepoint
703
+ p_sp = subparsers.add_parser("sharepoint", help="Browse SharePoint")
704
+ p_sp.add_argument("--sites", action="store_true", help="List followed sites")
705
+ p_sp.add_argument("--site", metavar="SITE_ID", help="Browse files in site")
706
+ p_sp.add_argument("--path", help="Path within site drive")
707
+ p_sp.add_argument("--download", metavar="ITEM_ID", help="Download file by ID")
708
+ p_sp.add_argument("--output", help="Output file path")
709
+ p_sp.add_argument("--limit", type=int, default=20)
710
+ p_sp.add_argument("--json", action="store_true")
711
+
712
+ # teams
713
+ p_teams = subparsers.add_parser("teams", help="Work with Teams")
714
+ p_teams.add_argument("--chats", action="store_true", help="List all chats")
715
+ p_teams.add_argument("--messages", metavar="CHAT_ID", help="Get messages from chat")
716
+ p_teams.add_argument("--send", metavar="TEXT", help="Send a message")
717
+ p_teams.add_argument("--chat-id", dest="chat_id", help="Target chat ID for --send")
718
+ p_teams.add_argument("--teams-list", action="store_true", help="List joined teams")
719
+ p_teams.add_argument("--limit", type=int, default=20)
720
+ p_teams.add_argument("--json", action="store_true")
721
+
722
+ # onedrive
723
+ p_od = subparsers.add_parser("onedrive", help="Work with OneDrive")
724
+ p_od.add_argument("--path", help="Folder path to browse")
725
+ p_od.add_argument("--upload", metavar="FILE", help="Upload a local file")
726
+ p_od.add_argument("--dest", help="Destination path in OneDrive (for --upload)")
727
+ p_od.add_argument("--download", metavar="ITEM_ID", help="Download by item ID")
728
+ p_od.add_argument("--output", help="Output file path")
729
+ p_od.add_argument("--info", metavar="ITEM_ID", help="Get file metadata")
730
+ p_od.add_argument("--limit", type=int, default=20)
731
+ p_od.add_argument("--json", action="store_true")
732
+
733
+ # people
734
+ p_ppl = subparsers.add_parser("people", help="Browse people and contacts")
735
+ p_ppl.add_argument("--contacts", action="store_true", help="Use Outlook contacts (not people)")
736
+ p_ppl.add_argument("--search", help="Search by name")
737
+ p_ppl.add_argument("--limit", type=int, default=20)
738
+ p_ppl.add_argument("--json", action="store_true")
739
+
740
+ # org
741
+ p_org = subparsers.add_parser("org", help="Organizational info")
742
+ p_org.add_argument("--manager", action="store_true", help="Show your manager")
743
+ p_org.add_argument("--reports", action="store_true", help="Show direct reports")
744
+ p_org.add_argument("--json", action="store_true")
745
+
746
+ args = parser.parse_args()
747
+
748
+ if not args.command:
749
+ parser.print_help()
750
+ sys.exit(0)
751
+
752
+ command_map = {
753
+ "login": cmd_login,
754
+ "logout": cmd_logout,
755
+ "status": cmd_status,
756
+ "me": cmd_me,
757
+ "emails": cmd_emails,
758
+ "calendar": cmd_calendar,
759
+ "sharepoint": cmd_sharepoint,
760
+ "teams": cmd_teams,
761
+ "onedrive": cmd_onedrive,
762
+ "people": cmd_people,
763
+ "org": cmd_org,
764
+ }
765
+
766
+ try:
767
+ command_map[args.command](args)
768
+ except requests.HTTPError as e:
769
+ status = e.response.status_code
770
+ if status == 401:
771
+ print("Error: Authentication expired. Run: python msgraph.py login")
772
+ elif status == 403:
773
+ print(f"Error: Permission denied for this operation ({e.response.url})")
774
+ print("You may need additional OAuth scopes.")
775
+ elif status == 404:
776
+ print(f"Error: Resource not found ({e.response.url})")
777
+ else:
778
+ print(f"HTTP Error {status}: {e.response.text[:200]}")
779
+ sys.exit(1)
780
+ except KeyboardInterrupt:
781
+ print("\nCancelled.")
782
+ sys.exit(0)
783
+
784
+ if __name__ == "__main__":
785
+ main()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codemieai/code",
3
- "version": "0.0.46",
3
+ "version": "0.0.47",
4
4
  "description": "Unified AI coding assistant CLI - Manage Claude Code, Gemini & custom agents. Multi-provider support (OpenAI, Azure, LiteLLM, SSO). Built-in LangGraph agent with file operations & git integration.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",