
Outlook Calendar
- 8 installs
- 33 repo stars
- Updated April 26, 2026
- bighardperson/computer-science-skills-collection
outlook-calendar is a Claude skill that reads a Microsoft 365 Outlook calendar so an agent can answer schedule, meeting, and task questions.
About
outlook-calendar reads a Microsoft 365 Outlook calendar so an agent can answer questions about schedule, meetings, and tasks. It logs in with MFA via a Playwright script, caches cookies and a bearer token, then reads events for a day, week, month, or custom range. It triggers whenever a user asks about their agenda and converts UTC times to local time before answering.
- Reads a Microsoft 365 Outlook calendar for day, week, month, or custom ranges
- Handles MFA login via Playwright and caches cookies plus a bearer token
- Stores credentials outside the skill directory in ~/.outlook/
Outlook Calendar by the numbers
- 8 all-time installs (skills.sh)
- Ranked #2,242 of 3,280 Productivity & Planning skills by installs in the Skillselion catalog
- Data as of Jul 30, 2026 (Skillselion catalog sync)
outlook-calendar capabilities & compatibility
Free skill; requires Microsoft 365 account credentials and Authenticator MFA.
- Capabilities
- planning · project management
- Works with
- outlook
- Use cases
- planning · project management
- Pricing
- Free
What outlook-calendar says it does
python owa_calendar.py --today # 今天
注意:API 返回时间为 UTC,需 +8 转换为上海时间。
npx skills add https://github.com/bighardperson/computer-science-skills-collection --skill outlook-calendarAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 8 |
|---|---|
| repo stars | ★ 33 |
| Last updated | April 26, 2026 |
| Repository | bighardperson/computer-science-skills-collection ↗ |
What it does
Answer a user's schedule and meeting questions by reading their Microsoft 365 Outlook calendar for a chosen date range.
Who is it for?
Reading an enterprise Microsoft 365 Outlook calendar to summarize meetings and schedule.
When should I use this skill?
A user asks about their schedule, meetings, agenda, or tasks in Outlook.
What you get
A summarized answer about the user's meetings, agenda, or total meeting hours for a requested period.
- A summary of calendar events, total meeting hours, or agenda for a date range
By the numbers
- Cookie validity is typically 1-7 days
- Bearer token cache is valid 1 hour
Files
Outlook 日历技能
触发条件
只要用户问任何涉及日程、会议、安排、日历、工作、任务、事情的问题,必须调用此技能。
- "明天有什么安排" / "这周有什么会" / "本月日程"
- "帮我看看日历" / "我今天几点有会"
- "这个月会议一共多少小时"
- "我下周有啥工作" / "明天要做啥" / "这周有什么事"
- "下周安排" / "有啥任务" / "日程表看一下"
- 任何时间 + 安排/会议/事件/工作/任务/事情的组合问法
首次配置
敏感信息(账号密码、Cookie、Token)存放在 ~/.outlook/,不在 skill 目录内。
1. 创建配置文件
创建 ~/.outlook/config.json,内容如下:
{
"email": "your@company.com",
"password": "your_password",
"cookie_file": "/root/.outlook/cookies.json",
"cookie_max_age_days": 7,
"mfa_type": "authenticator_number_match"
}2. 安装依赖
pip install playwright requests
playwright install chromium3. 首次登录(MFA)
cd ~/.agents/skills/outlook-calendar
python login.py脚本输出 [NUMBER:XX] 时,在 Microsoft Authenticator App 输入数字 XX 并批准,Cookie 自动保存到 ~/.outlook/cookies.json。
调用步骤
第一步:读取日历
cd ~/.agents/skills/outlook-calendar
python owa_calendar.py --today # 今天
python owa_calendar.py --tomorrow # 明天
python owa_calendar.py --week # 本周
python owa_calendar.py --month 2026-03 # 指定月份
python owa_calendar.py --range 2026-03-01 2026-03-31 # 自定义范围第二步:处理 AUTH_FAILED
若输出 [AUTH_FAILED],Cookie 或 Token 过期,重新登录:
python login.py第三步:整理输出
按用户问题整理回答:列出事件、统计总时长、按周分组等。 注意:API 返回时间为 UTC,需 +8 转换为上海时间。
文件结构
skill 目录(无敏感信息):
~/.agents/skills/outlook-calendar/
├── SKILL.md
├── login.py # MFA 登录,保存 Cookie 到 ~/.outlook/
└── owa_calendar.py # 日历读取(Token 模式)
敏感数据目录:
~/.outlook/
├── config.json # 账号密码(自行创建)
├── cookies.json # 登录 Cookie(login.py 自动生成)
└── token.json # Bearer Token 缓存(自动生成,1h 有效)MFA 说明
- 类型:Authenticator 数字匹配
- Cookie 有效期:通常 1-7 天,过期重跑 login.py
- 多次失败会触发微软限速,等 15 分钟再试
{
"ownerId": "kn77fsg1stn6vtyadpsnk6adf181t9te",
"slug": "outlook-calendar",
"version": "1.0.2",
"publishedAt": 1772192437110
}{
"version": 1,
"registry": "https://clawhub.ai",
"slug": "outlook-calendar",
"installedVersion": "1.0.2",
"installedAt": 1776070682596
}
#!/usr/bin/env python3
"""
Outlook 日历登录脚本 - MFA 数字匹配版
用法:python login.py
登录成功后自动保存 Cookie 到 data/cookies.json
"""
import json, time, os, re
from pathlib import Path
from playwright.sync_api import sync_playwright
BASE_DIR = Path(__file__).parent
OUTLOOK_DIR = Path.home() / ".outlook"
with open(OUTLOOK_DIR / "config.json") as f:
cfg = json.load(f)
EMAIL = cfg["email"]
PASSWORD = cfg["password"]
COOKIE_FILE = OUTLOOK_DIR / "cookies.json"
COOKIE_FILE.parent.mkdir(exist_ok=True)
STATUS_FILE = OUTLOOK_DIR / "login_status.txt"
CMD_FILE = OUTLOOK_DIR / "login_cmd.txt"
def log(msg):
print(msg, flush=True)
with open(STATUS_FILE, "a") as f:
f.write(f"{msg}\n")
def shot(page, name):
try:
path = OUTLOOK_DIR / f"debug_{name}.png"
page.screenshot(path=str(path))
except:
pass
def safe_content(page):
try:
return page.content()
except:
return ""
def find_number(page):
"""从页面提取数字匹配的数字"""
for sel in ["#idRichContext_DisplaySign", ".displaySign"]:
try:
el = page.query_selector(sel)
if el:
t = el.inner_text().strip()
if t.isdigit():
return t
except:
pass
content = safe_content(page)
m = re.search(r'displaySign["\s:]+(\d+)', content)
if m:
return m.group(1)
return None
def get_url(page):
try:
return page.url
except:
return ""
open(STATUS_FILE, "w").close()
with sync_playwright() as p:
browser = p.chromium.launch(
headless=True,
args=["--no-sandbox", "--disable-dev-shm-usage"]
)
ctx = browser.new_context(
user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
viewport={"width": 1280, "height": 800},
)
page = ctx.new_page()
log("[1] 登录 Microsoft 365...")
page.goto("https://login.microsoftonline.com", timeout=30000)
page.wait_for_load_state("load", timeout=20000)
page.fill('input[type="email"]', EMAIL)
page.click('input[type="submit"]')
page.wait_for_load_state("load", timeout=20000)
page.wait_for_selector('input[type="password"]', timeout=10000)
page.fill('input[type="password"]', PASSWORD)
page.click('input[type="submit"]')
page.wait_for_load_state("load", timeout=20000)
log(f"[1] 完成,URL: {get_url(page)}")
# 等待并提取 MFA 数字
time.sleep(3)
log("[2] 等待 MFA 数字匹配页面...")
for i in range(20):
time.sleep(2)
n = find_number(page)
if n:
log(f"[2] 屏幕数字: 【{n}】")
log(f"[NUMBER:{n}]")
break
shot(page, f"wait_{i:02d}")
# 等用户在 Authenticator 批准(最多 3 分钟)
log("[3] 等待 Authenticator 批准...(最多 180 秒)")
for i in range(90):
time.sleep(2)
url = get_url(page)
# 成功离开登录页
if "login.microsoftonline.com" not in url and url.startswith("https://"):
log(f"[3] 成功!URL: {url}")
break
# KMSI 页面
try:
btn = page.query_selector("#idSIButton9")
if btn:
log("[3] 点击保持登录...")
page.evaluate("document.querySelector('#idSIButton9').click()")
time.sleep(3)
new_url = get_url(page)
if "login.microsoftonline.com" not in new_url:
log(f"[3] 保持登录成功!URL: {new_url}")
break
except:
pass
if i % 10 == 0:
shot(page, f"poll_{i:02d}")
log(f"[3/{i*2}s] 等待中...")
# 导航到 Outlook 获取完整 Cookie
log("[4] 导航到 Outlook 日历...")
page.goto("https://outlook.office.com/calendar/", timeout=30000)
page.wait_for_load_state("load", timeout=30000)
time.sleep(8)
log(f"[4] URL: {get_url(page)}")
# 保存 Cookie
cookies = ctx.cookies()
with open(COOKIE_FILE, "w") as f:
json.dump(cookies, f, indent=2)
outlook_count = len([c for c in cookies if "outlook" in c.get("domain", "")])
log(f"[✓] Cookie 保存完毕:共 {len(cookies)} 条,outlook 域 {outlook_count} 条")
log(f"[DONE]")
browser.close()
#!/usr/bin/env python3
"""
Outlook 日历读取脚本 - Token 模式
1. 用 Playwright + Cookie 打开 Outlook Web,拦截 Bearer Token
2. 用 requests + Bearer Token 调 OWA REST API
3. Token 缓存到 data/token.json,有效期内复用
"""
import json, time, sys, argparse, requests
from pathlib import Path
from datetime import datetime, timedelta
from playwright.sync_api import sync_playwright
BASE_DIR = Path(__file__).parent
OUTLOOK_DIR = Path.home() / ".outlook"
with open(OUTLOOK_DIR / "config.json") as f:
cfg = json.load(f)
COOKIE_FILE = OUTLOOK_DIR / "cookies.json"
TOKEN_FILE = OUTLOOK_DIR / "token.json"
TOKEN_TTL = 3600 # Token 复用 1 小时
def get_date_range(args):
today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
if args.today:
return today.strftime("%Y-%m-%d"), (today + timedelta(days=1)).strftime("%Y-%m-%d")
elif args.tomorrow:
t = today + timedelta(days=1)
return t.strftime("%Y-%m-%d"), (t + timedelta(days=1)).strftime("%Y-%m-%d")
elif args.week:
monday = today - timedelta(days=today.weekday())
sunday = monday + timedelta(days=7)
return monday.strftime("%Y-%m-%d"), sunday.strftime("%Y-%m-%d")
elif args.month:
year, month = map(int, args.month.split("-"))
start = datetime(year, month, 1)
end = datetime(year + 1, 1, 1) if month == 12 else datetime(year, month + 1, 1)
return start.strftime("%Y-%m-%d"), end.strftime("%Y-%m-%d")
elif args.range:
return args.range[0], args.range[1]
else:
return today.strftime("%Y-%m-%d"), (today + timedelta(days=1)).strftime("%Y-%m-%d")
def load_cached_token():
if not TOKEN_FILE.exists():
return None
with open(TOKEN_FILE) as f:
data = json.load(f)
if time.time() - data.get("saved_at", 0) < TOKEN_TTL:
return data.get("bearer")
return None
def fetch_token_via_playwright():
if not COOKIE_FILE.exists():
print("[AUTH_FAILED] Cookie 文件不存在,请先运行 login.py")
sys.exit(1)
with open(COOKIE_FILE) as f:
saved_cookies = json.load(f)
owa_token = None
with sync_playwright() as p:
browser = p.chromium.launch(headless=True, args=["--no-sandbox", "--disable-dev-shm-usage"])
ctx = browser.new_context(
user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36"
)
ctx.add_cookies(saved_cookies)
def handle_request(req):
nonlocal owa_token
if owa_token:
return
auth = req.headers.get("authorization", "")
if auth.startswith("Bearer ") and "outlook.office.com" in req.url:
owa_token = auth[7:]
ctx.on("request", handle_request)
page = ctx.new_page()
page.goto("https://outlook.office.com/calendar/view/month", timeout=30000)
page.wait_for_load_state("load", timeout=20000)
time.sleep(10)
if "login.microsoftonline.com" in page.url:
print("[AUTH_FAILED] Cookie 已过期,请重新运行 login.py")
browser.close()
sys.exit(1)
browser.close()
if not owa_token:
print("[AUTH_FAILED] 未能获取 Bearer Token,请重新运行 login.py", file=sys.stderr)
sys.exit(1)
TOKEN_FILE.parent.mkdir(exist_ok=True)
with open(TOKEN_FILE, "w") as f:
json.dump({"bearer": owa_token, "saved_at": time.time()}, f)
return owa_token
def get_token():
token = load_cached_token()
if token:
return token
return fetch_token_via_playwright()
def fetch_calendar_api(start_date, end_date, token):
headers = {
"Authorization": f"Bearer {token}",
"Accept": "application/json",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
}
params = {
"startDateTime": f"{start_date}T00:00:00",
"endDateTime": f"{end_date}T23:59:59",
"$select": "Subject,Start,End,Organizer,ShowAs,IsAllDay",
"$orderby": "Start/DateTime",
"$top": 100,
}
resp = requests.get(
"https://outlook.office.com/api/v2.0/me/CalendarView",
headers=headers,
params=params,
timeout=15,
)
if resp.status_code == 401:
return None, "token_expired"
if not resp.ok:
return None, f"api_error:{resp.status_code}:{resp.text[:300]}"
return resp.json().get("value", []), None
def parse_events(raw_events):
events = []
for e in raw_events:
start_str = e.get("Start", {}).get("DateTime", "")[:16].replace("T", " ")
end_str = e.get("End", {}).get("DateTime", "")[:16].replace("T", " ")
try:
start_dt = datetime.strptime(start_str, "%Y-%m-%d %H:%M")
end_dt = datetime.strptime(end_str, "%Y-%m-%d %H:%M")
duration = round((end_dt - start_dt).total_seconds() / 3600, 2)
except:
duration = 0
organizer = e.get("Organizer", {}).get("EmailAddress", {}).get("Name", "")
show_as = e.get("ShowAs", "Unknown")
status_map = {"Busy": "Busy", "Tentative": "Tentative", "Free": "Free", "Oof": "Out of Office"}
events.append({
"subject": e.get("Subject", "无标题"),
"start": start_str,
"end": end_str,
"duration_hours": duration,
"status": status_map.get(show_as, show_as),
"organizer": organizer,
"is_all_day": e.get("IsAllDay", False),
})
return events
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--today", action="store_true")
parser.add_argument("--tomorrow", action="store_true")
parser.add_argument("--week", action="store_true")
parser.add_argument("--month", type=str)
parser.add_argument("--range", nargs=2, metavar=("START", "END"))
parser.add_argument("--json", action="store_true")
args = parser.parse_args()
start_date, end_date = get_date_range(args)
token = get_token()
raw_events, err = fetch_calendar_api(start_date, end_date, token)
if err == "token_expired":
if TOKEN_FILE.exists():
TOKEN_FILE.unlink()
token = fetch_token_via_playwright()
raw_events, err = fetch_calendar_api(start_date, end_date, token)
if err:
print(f"[ERROR] {err}", file=sys.stderr)
sys.exit(1)
events = parse_events(raw_events)
if args.json:
print(json.dumps(events, ensure_ascii=False, indent=2))
else:
print(f"\n📅 {start_date} ~ {end_date} 日历事件(共 {len(events)} 个)\n")
total_hours = sum(e["duration_hours"] for e in events if e["status"] in ["Busy", "Tentative"])
for e in events:
icon = {"Busy": "🔵", "Tentative": "🟡", "Free": "⚪", "Out of Office": "🔴"}.get(e["status"], "❓")
tag = "(全天)" if e["is_all_day"] else f"({e['duration_hours']}h)"
print(f"{icon} {e['start']} {tag} | {e['subject']}")
if e["organizer"]:
print(f" 组织者: {e['organizer']}")
print(f"\n⏱ 会议总时长(Busy+Tentative): {total_hours:.1f} 小时")
if __name__ == "__main__":
main()
Related skills
FAQ
How does outlook-calendar handle login?
It uses login.py with Playwright for Authenticator number-match MFA, then caches cookies in ~/.outlook/cookies.json.
Does it convert meeting times?
Yes; the API returns UTC and the skill adds 8 hours to convert to Shanghai local time before answering.