@patimweb/pi-teams 1.8.0 → 1.9.1

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.
package/README.md CHANGED
@@ -2,10 +2,10 @@
2
2
 
3
3
  Microsoft Teams integration for the [pi coding agent](https://github.com/earendil-works/pi).
4
4
 
5
- pi acts **as you**: it reads what you can read, and anything it sends comes
6
- from your account, in your name, in the right thread. Multiple Teams accounts
7
- and multiple company tenants live side by side, and a configuration file
8
- decides exactly which teams, channels, chats and people pi may touch.
5
+ pi acts **as you**: it reads what you can read, and anything it sends comes from
6
+ your account, in your name, in the right thread. Multiple Teams accounts and
7
+ multiple company tenants live side by side, and a configuration file decides
8
+ exactly which teams, channels, chats and people pi may touch.
9
9
 
10
10
  ![pi-teams in the pi coding agent](screenshot.png)
11
11
 
@@ -21,10 +21,13 @@ pi install /path/to/pi-teams
21
21
 
22
22
  ## Quick start
23
23
 
24
- 1. **Register an app in Entra ID** (once per organization) — the step-by-step
25
- walkthrough, including the redirect URI and public-client settings that sign-in
26
- needs, is in [docs/entra-app-registration.md](docs/entra-app-registration.md).
24
+ 1. **Register an app in Entra ID** — once per organization. Two settings decide
25
+ whether sign-in works at all (a redirect URI and public client flows), so
26
+ follow [docs/entra-app-registration.md](docs/entra-app-registration.md)
27
+ rather than guessing from the portal.
28
+
27
29
  2. **Configure the account:**
30
+
28
31
  ```
29
32
  teams_setup:
30
33
  name: work
@@ -32,656 +35,53 @@ pi install /path/to/pi-teams
32
35
  clientId: <application (client) ID>
33
36
  setDefault: true
34
37
  ```
35
- 3. **Sign in:** run `/teams-login`. Your browser opens at the Microsoft
36
- sign-in page and closes itself when you are done — nothing to type.
37
- 4. **Check it works:** `/teams-status`, then `/teams-inbox`.
38
-
39
- Full walkthrough with the exact portal paths: **[docs/entra-app-registration.md](docs/entra-app-registration.md)**.
40
-
41
- ---
42
-
43
- ## Listen mode
44
-
45
- Off by default. When it is on, pi polls your chats and turns an incoming message
46
- into a prompt it answers — as you, in the chat it arrived in.
47
-
48
- ```
49
- /teams-listen on # switch it on for the session's account
50
- /teams-listen status # what is configured, and what is running right now
51
- /teams-listen off
52
- ```
53
-
54
- Listen mode never starts on its own. It belongs to the session it was switched
55
- on in and ends with it: quitting pi closes the watcher, and `/new` or `/resume`
56
- stops it too. A stored `enabled: true` is a setting, not an instruction — set
57
- `watch.autoStart` if a session is meant to start listening by itself, which is
58
- only sensible for an unattended one.
59
-
60
- ```json
61
- "watch": {
62
- "enabled": false,
63
- "autoStart": false,
64
- "intervalSeconds": 60,
65
- "chats": ["Anna*", "Vertrieb*"],
66
- "from": ["anna.schmidt@contoso.com"],
67
- "mentionOnly": false,
68
- "cooldownSeconds": 300,
69
- "maxTriggersPerHour": 10
70
- }
71
- ```
72
-
73
- | Key | Default | Meaning |
74
- |-----|---------|---------|
75
- | `enabled` | `false` | whether listen mode is switched on |
76
- | `autoStart` | `false` | whether a pi session may start the watcher without being asked |
77
- | `intervalSeconds` | `60` | seconds between polls (minimum 15) |
78
- | `chats` | `[]` | **where** pi listens: glob patterns matched against topic, label, chat ID and participant names. Empty means every recent chat. |
79
- | `from` | `[]` | **who** pi listens to: glob patterns matched against display name, UPN and e-mail. Empty means any sender. |
80
- | `mentionOnly` | `false` | only wake where you are mentioned |
81
- | `cooldownSeconds` | `300` | stay quiet in a chat after waking pi for it |
82
- | `maxTriggersPerHour` | `10` | hard cap on wakes per hour |
83
-
84
- Set it globally, or per account (the account level wins field by field).
85
- `teams_watch` writes the same keys; `/teams-listen` writes to the session's
86
- account.
87
-
88
- What it does, and what it deliberately does not:
89
-
90
- - **Costs a model turn per wake.** That is what the filters and the hourly cap
91
- are for — narrow `chats` and `from`, and enable it for a handful of
92
- conversations rather than the whole company.
93
- - **Answers what is still unread.** A chat wakes pi when two things are true: it
94
- moved since pi last looked (a cursor per account, kept on disk), and its newest
95
- message is still unread for you in Teams. So switching listen mode back on
96
- answers what you missed while pi was not running — and a chat you already read
97
- in Teams stays quiet. A backlog is drained a few chats per tick and capped by
98
- `maxTriggersPerHour`, so a long absence does not produce a burst. A chat held
99
- back by its `cooldownSeconds`, or by that hourly cap, is *delayed, never
100
- dropped*: it keeps its place in line and is answered as soon as the wait is
101
- over. `teams_watch action: status` shows how many are waiting right now.
102
- - **Never wakes for its own messages.** pi posts as you, so its own reply comes
103
- back as "my own message" and stops the loop.
104
- - **Respects the read rules.** A chat excluded by `permissions.read.chats` is
105
- not even polled.
106
- - **Does not bypass the safety model.** A reply it decides to send goes through
107
- the same safety level and scope rules as one you asked for. At `confirm` you
108
- approve each one; for hands-free answers, narrow `permissions.write.chats` and
109
- set `safetyLevel: open` — deliberately, and on an account where you mean it.
110
- - **Chats only, not channels.** Channel polling needs `ChannelMessage.Read.All`
111
- (admin consent) and multiplies the calls; answering in channels stays a
112
- requested action.
113
-
114
- Why polling and not a webhook: Graph change notifications for chats need a
115
- publicly reachable HTTPS endpoint plus subscription renewal, which a laptop on a
116
- home network cannot offer. Polling costs one `/me/chats` call per interval and
117
- works anywhere.
118
-
119
- ---
120
-
121
- ## Entra ID app registration
122
-
123
- Full walkthrough: **[docs/entra-app-registration.md](docs/entra-app-registration.md)**.
124
- The short version, with the two settings that sign-in actually depends on:
125
-
126
- pi signs in as you with a delegated OAuth 2.0 flow — the browser opens, you sign
127
- in with your own credentials and MFA, and pi never sees your password.
128
-
129
- ### 1. Create the registration
130
-
131
- Entra admin center → **App registrations** → **New registration**
132
-
133
- | Field | Value |
134
- |-------|-------|
135
- | Name | `pi-teams` (anything you like) |
136
- | Supported account types | **Accounts in any organizational directory (multitenant)** — required if pi should also reach customer or guest tenants. Single tenant is fine for one company. |
137
- | Redirect URI | leave empty |
138
-
139
- Note the **Application (client) ID** and **Directory (tenant) ID**.
140
-
141
- ### 2. Enable public client flows
142
-
143
- **Authentication** → *Advanced settings* → **Allow public client flows** →
144
- **Yes**.
145
-
146
- Without this, sign-in fails with `AADSTS7000218`.
147
-
148
- ### 3. Add delegated permissions
149
-
150
- **API permissions** → **Add a permission** → **Microsoft Graph** →
151
- **Delegated permissions**:
152
-
153
- | Permission | Needed for |
154
- |-----------|-----------|
155
- | `User.Read`, `User.ReadBasic.All` | identity, looking colleagues up |
156
- | `Team.ReadBasic.All`, `Channel.ReadBasic.All` | listing teams and channels |
157
- | `ChannelMessage.Read.All` | reading channel messages |
158
- | `ChannelMessage.Send` | posting and replying in channels |
159
- | `ChannelMember.Read.All` | channel member lists |
160
- | `Chat.ReadWrite` | reading chats, creating chats |
161
- | `ChatMessage.Send` | sending chat messages |
162
- | `Presence.ReadWrite`, `Presence.Read.All` | your status, colleagues' availability |
163
- | `Calendars.ReadWrite` | reading and creating calendar events |
164
- | `OnlineMeetings.ReadWrite` | Teams meeting links |
165
- | `Files.Read.All`, `Sites.Read.All` | channel files |
166
-
167
- `openid`, `profile` and `offline_access` are not listed: MSAL always requests
168
- them and rejects them in an explicit scope list, so the lasting sign-in they
169
- buy is automatic.
170
-
171
- ### 4. Add the redirect URI for the browser sign-in
172
-
173
- **Authentication** → **Add a platform** → **Mobile and desktop applications** →
174
- tick `http://localhost`.
175
-
176
- MSAL picks a free loopback port at sign-in time, and Entra accepts any port on
177
- localhost for a public client, so this one entry covers every run. If your
178
- tenant insists on an exact URI, register e.g. `http://localhost:3000` and set
179
- `loopbackPort: 3000` on the account.
180
-
181
- ---
182
-
183
- ## Who has to approve what
184
-
185
- Two separate hurdles, and only one of them normally needs an administrator.
186
-
187
- ### Creating the registration
188
-
189
- By default any member of the tenant may register an application (Entra ID →
190
- **User settings** → *Users can register applications*). Where that is switched
191
- off, the least-privileged role that can do it is **Application Developer**;
192
- Cloud Application Administrator and Application Administrator also work.
193
- Changing *Allow public client flows* and adding the redirect URI need no extra
194
- role once the registration is yours.
195
-
196
- ### Consenting to the permissions
197
-
198
- Only two of the default scopes require **admin consent**:
199
-
200
- | Scope | Consent | What you lose without it |
201
- |-------|---------|--------------------------|
202
- | `ChannelMessage.Read.All` | **admin** | reading channel messages and threads |
203
- | `ChannelMember.Read.All` | **admin** | channel member lists |
204
- | `ChannelMessage.Send` | user | — |
205
- | `Chat.ReadWrite`, `ChatMessage.Send` | user | — |
206
- | `Team.ReadBasic.All`, `Channel.ReadBasic.All` | user | — |
207
- | `User.Read`, `User.ReadBasic.All` | user | — |
208
- | `Presence.ReadWrite`, `Presence.Read.All` | user | — |
209
- | `Calendars.ReadWrite`, `OnlineMeetings.ReadWrite` | user | — |
210
- | `Files.Read.All`, `Sites.Read.All` | user | — |
211
-
212
- Posting in a channel needs no admin; *reading* the channel does. Microsoft
213
- treats bulk reading of channel content as the more sensitive right.
214
-
215
- **Without an administrator**, drop the two admin-consent scopes and everything
216
- else still works — chats in full, posting and replying in channels, presence,
217
- calendar, meetings and directory lookup:
218
-
219
- ```json
220
- "scopes": [
221
- "User.Read", "User.ReadBasic.All",
222
- "Team.ReadBasic.All", "Channel.ReadBasic.All", "ChannelMessage.Send",
223
- "Chat.ReadWrite", "ChatMessage.Send",
224
- "Presence.ReadWrite", "Presence.Read.All",
225
- "Calendars.ReadWrite", "OnlineMeetings.ReadWrite",
226
- "Files.Read.All", "Sites.Read.All"
227
- ]
228
- ```
229
-
230
- One caveat: if the tenant has *Users can consent to applications* switched off,
231
- even the user-consentable scopes need an administrator — usually through the
232
- admin consent request workflow.
233
-
234
- `teams_doctor` reports exactly which scopes were actually granted, so a missing
235
- consent shows up as a named permission rather than a 403.
236
-
237
- ### Guest and customer tenants
238
-
239
- For a tenant that is not your own:
240
-
241
- - the registration must be **multitenant**,
242
- - your account must be a guest in that tenant,
243
- - an administrator **of that tenant** consents to the app once.
244
-
245
- In practice this is the bigger hurdle, not your own tenant. Once it is done,
246
- add the tenant beneath your account (`parentAccount` in `teams_setup`) and sign
247
- in against it.
248
-
249
- ---
250
-
251
- ## How the sign-in works
252
-
253
- `teams_login` uses MSAL (`@azure/msal-node`) and picks the flow that fits the
254
- machine:
255
-
256
- | | `interactive` (default) | `device-code` | `client-credentials` |
257
- |---|---|---|---|
258
- | What you do | browser opens, you sign in | type a short code elsewhere | nothing |
259
- | Identity | you | you | the application |
260
- | Needs a browser | yes | no | no |
261
- | Reading chats/channels | yes | yes | only via protected APIs |
262
- | **Sending messages** | **yes** | **yes** | **no** — refused with an explanation |
263
- | Presence, calendar | yes | yes | partially |
264
-
265
- **Interactive** is chosen automatically wherever a browser can actually be
266
- reached. MSAL opens the system browser, listens on a loopback port for the
267
- redirect, and handles PKCE — nothing to copy or type.
268
-
269
- **Device code** takes over automatically over SSH, in containers, and on any
270
- machine without a display server, because a browser opened there would appear
271
- where nobody is looking. Force it anywhere with `PI_TEAMS_NO_BROWSER=1`, per
272
- account with `"authMode": "device-code"`, or per call with
273
- `teams_login mode: device-code`.
274
-
275
- Either way the session is cached on disk and renewed silently, so signing in is
276
- a once-per-account affair.
277
-
278
- In **WSL** the URL is handed to Windows PowerShell, so the browser on the
279
- Windows side opens: no X server, no `DISPLAY`, and the authorize URL survives
280
- whole — including its `&`, which a `cmd.exe` handoff would cut at the first one.
281
- If the built-in launchers do not fit the machine, set `PI_TEAMS_BROWSER` to a
282
- command line that opens a URL: `{}` is replaced by the URL, otherwise the URL is
283
- appended. Every failure carries the URL in its message, so it can be pasted
284
- into a browser by hand — the one route that needs nothing installed.
285
-
286
- Microsoft Graph only permits app-only writes to chats and channels for
287
- migration scenarios (`Teamwork.Migrate.All`), so pi-teams refuses those calls
288
- up front instead of letting them fail deep inside a Graph request. Use
289
- `client-credentials` for unattended read-only jobs, and a delegated flow for
290
- everything that speaks in your name.
291
-
292
- ---
293
-
294
- ## Configuration
295
-
296
- Everything lives in `~/.pi/agent/pi-teams.json` (mode `0600`, created from a
297
- template on first run). `teams_setup` writes the same file — use whichever you
298
- prefer.
299
-
300
- ```json
301
- {
302
- "accounts": [
303
- {
304
- "name": "work",
305
- "displayName": "Contoso",
306
- "tenantId": "contoso.onmicrosoft.com",
307
- "clientId": "00000000-0000-0000-0000-000000000000",
308
- "authMode": "interactive",
309
- "safetyLevel": "confirm",
310
- "permissions": {
311
- "read": {
312
- "teams": { "allow": ["*"] },
313
- "chats": { "allow": ["*"] }
314
- },
315
- "write": {
316
- "channels": {
317
- "allow": ["Engineering/*", "Project Alpha/General"],
318
- "deny": ["*/Announcements"]
319
- },
320
- "chats": { "allow": ["*"] },
321
- "people": { "deny": ["ceo@contoso.com"] }
322
- }
323
- },
324
- "tenants": [
325
- {
326
- "name": "customer-alpha",
327
- "tenantId": "customeralpha.onmicrosoft.com",
328
- "safetyLevel": "readonly",
329
- "permissions": {
330
- "read": { "channels": { "allow": ["Project Alpha/*"] } }
331
- }
332
- }
333
- ]
334
- },
335
- {
336
- "name": "private",
337
- "tenantId": "othercompany.onmicrosoft.com",
338
- "clientId": "11111111-1111-1111-1111-111111111111",
339
- "safetyLevel": "open"
340
- }
341
- ],
342
- "defaultAccount": "work",
343
- "safetyLevel": "confirm",
344
- "maxMessages": 25,
345
- "audit": true
346
- }
347
- ```
348
-
349
- ### Accounts and tenants
350
38
 
351
- - An **account** is a Teams identity you sign in as. Each has its own saved
352
- session.
353
- - A **tenant** beneath an account is another directory that identity reaches:
354
- a customer, a subsidiary, a guest tenant. It gets its own sign-in, its own
355
- safety level and its own rules.
39
+ 3. **Sign in:** run `/teams-login`. Your browser opens at the Microsoft sign-in
40
+ page and closes itself when you are done — nothing to type.
356
41
 
357
- Every tool takes `account` and `tenant`:
358
-
359
- ```yaml
360
- teams_send_channel_message:
361
- account: work
362
- tenant: customer-alpha
363
- channel: "Project Alpha/General"
364
- body: "Sprint review moved to Thursday."
365
- ```
366
-
367
- ### Safety levels
368
-
369
- | Level | Effect |
370
- |-------|--------|
371
- | `open` | writes go through without asking |
372
- | `confirm` | every write shows a confirmation with the destination and the exact text (**default**) |
373
- | `readonly` | writes are blocked entirely |
374
-
375
- Set globally, per account, or per tenant — the most specific setting wins. So
376
- your own company can be `confirm` while a customer tenant stays `readonly`.
377
-
378
- ### AI disclosure footer
379
-
380
- On by default. Every message, reply and edit pi sends in your name ends with a
381
- short note saying it was written by an AI:
382
-
383
- ```json
384
- "aiFooter": { "enabled": true, "text": "🤖 Erstellt mit pi (KI-Assistent)" }
385
- ```
386
-
387
- | Key | Default | Meaning |
388
- |-----|---------|---------|
389
- | `enabled` | `true` | append the note to everything pi sends |
390
- | `text` | `🤖 Generated with pi (an AI agent)` | the wording; empty means the default |
391
-
392
- Turn it off with `"enabled": false` if the disclosure does not fit the people
393
- you write to. Set it globally or per account (the account level wins field by
394
- field, so one account can use its own wording while another keeps the global
395
- one).
396
-
397
- Details worth knowing:
398
-
399
- - It is added by the tool that sends, not asked for in a prompt, so it cannot be
400
- forgotten by the model. The confirmation dialog at `safetyLevel: confirm`
401
- shows the text **with** the footer — you confirm what actually goes out.
402
- - It is idempotent: editing an already-footered message does not stack a second
403
- one, and a model that copied the footer from the chat it is answering does not
404
- produce two.
405
- - It applies to chat messages, channel posts and replies, the first message of a
406
- newly created chat, and edits. It does **not** apply to things pi sends on
407
- your behalf that are not messages — meeting invitations, presence changes.
408
- - It says nothing about the *content*: the note is a disclosure, not a
409
- disclaimer, and it is the only part of an outgoing message pi adds on its own.
410
-
411
- ### Scope rules
412
-
413
- Rules exist per **category** (`teams`, `channels`, `chats`, `people`) and per
414
- **mode** (`read`, `write`). Category keys written directly in the block apply
415
- to both modes; a `read` or `write` block refines one of them.
416
-
417
- ```json
418
- "permissions": {
419
- "channels": { "deny": ["*/Announcements"] },
420
- "write": { "channels": { "allow": ["Engineering/*"] } }
421
- }
422
- ```
423
-
424
- - **`deny` accumulates** across global → account → tenant and always wins.
425
- - **`allow` narrows**: the most specific level that defines one wins. Nothing
426
- configured means everything the signed-in user can reach.
427
- - Patterns are case-insensitive globs: `*` matches any run of characters
428
- (including `/`), `?` matches one.
429
- - A target matches if **any** of its identifiers does: channels match on
430
- `Team/Channel`, their name or their ID; chats match on topic, participant
431
- names and e-mail addresses; people match on name, UPN and e-mail.
432
-
433
- Check a rule without touching anything:
434
-
435
- ```yaml
436
- teams_permissions:
437
- action: test
438
- category: channels
439
- mode: write
440
- target: "Engineering/Announcements"
441
- ```
442
-
443
- ### Other settings
444
-
445
- | Key | Default | Meaning |
446
- |-----|---------|---------|
447
- | `defaultAccount` | first account | account used when a tool omits `account` |
448
- | `maxMessages` | `25` | default page size for message listings |
449
- | `audit` | `true` | append every write to `~/.pi/agent/pi-teams-audit.jsonl` |
450
- | `scopes` | see above | override the requested Graph scopes per account/tenant |
451
- | `aiFooter` | on | append an AI disclosure to every message pi sends — see above |
452
- | `graphBaseUrl` / `authorityHost` | Microsoft public cloud | sovereign cloud endpoints |
453
-
454
- ---
455
-
456
- ## Tools
457
-
458
- ### Session and configuration
459
-
460
- | Tool | Description |
461
- |------|-------------|
462
- | `teams_setup` | Write an account or tenant to the config file |
463
- | `teams_login` | Sign in as yourself (browser, or device code where there is none) |
464
- | `teams_logout` | Remove a saved session |
465
- | `teams_accounts` | List accounts, switch the default, change a safety level, delete |
466
- | `teams_status` | Which account is active, who pi acts as, what it may do |
467
- | `teams_permissions` | Show, test or change the allow/deny rules |
468
- | `teams_doctor` | Diagnose config, sign-in, consent and connectivity |
469
- | `teams_watch` | Listen mode: status, enable, disable, and the filters for who and where |
470
-
471
- ### Reading
472
-
473
- | Tool | Description |
474
- |------|-------------|
475
- | `teams_inbox` | Chats with new messages plus recent mentions of you |
476
- | `teams_list_chats` | Recent chats with a preview of the last message |
477
- | `teams_read_chat` | Messages of one chat |
478
- | `teams_list_teams` | Teams you belong to |
479
- | `teams_list_channels` | Channels of a team |
480
- | `teams_read_channel` | Posts in a channel |
481
- | `teams_read_thread` | One thread: opener plus every reply |
482
- | `teams_search_messages` | Full-text search across chats and channels |
483
- | `teams_list_members` | Members of a team or channel |
484
- | `teams_find_user` | Directory lookup by name, e-mail or UPN |
485
- | `teams_list_files` | Files in a channel's SharePoint folder |
486
- | `teams_download_files` | Download the images and files of messages to a local directory |
487
- | `teams_get_presence` | Your status, a colleague's, or a whole chat's |
488
- | `teams_availability` | When several people are all free, from calendar free/busy |
489
- | `teams_list_meetings` | Calendar events and meetings in a range |
490
- | `teams_get_meeting` | One meeting in full |
491
-
492
- ### Writing
493
-
494
- | Tool | Description |
495
- |------|-------------|
496
- | `teams_send_chat_message` | Send a chat message as you |
497
- | `teams_create_chat` | Start a 1:1 or group chat, optionally with the first message |
498
- | `teams_send_channel_message` | Post in a channel as you |
499
- | `teams_reply_channel_message` | Reply inside an existing thread |
500
- | `teams_react` | Add or remove an emoji reaction |
501
- | `teams_mark_read` | Mark a chat as read (or unread again) — the natural end of a catch-up |
502
- | `teams_update_message` | Edit a message you already sent |
503
- | `teams_chat_members` | Add someone to a group chat, or remove them |
504
- | `teams_delete_message` | Soft-delete (or restore) a message you sent |
505
- | `teams_set_presence` | Set your Teams status |
506
- | `teams_set_status_message` | Set the note under your name |
507
- | `teams_create_meeting` | Schedule a Teams meeting with invitations |
508
- | `teams_update_meeting` | Change a meeting you organize |
509
- | `teams_respond_invite` | Accept, decline or tentatively accept an invitation |
510
- | `teams_cancel_meeting` | Cancel a meeting |
511
-
512
- Editing and deleting apply to **chat** messages. A channel post cannot be
513
- created, edited or deleted through Graph with the permissions pi asks for
514
- (`Channel.Create`, `ChannelMessage.ReadWrite`) — that stays in the Teams client.
515
- Posting and replying in channels, and reacting to channel messages, work as
516
- usual.
517
-
518
- ## Commands
519
-
520
- | Command | Description |
521
- |---------|-------------|
522
- | `/teams-status` | Show the connection and permissions |
523
- | `/teams-login` | Sign in to Teams as yourself |
524
- | `/teams-inbox` | What needs your attention right now |
525
- | `/teams-listen` | Listen mode: `on`, `off`, or `status` |
526
- | `/teams-permissions` | What pi may and may not do |
527
- | `/teams-doctor` | Diagnose the setup |
528
-
529
- ## Prompts
530
-
531
- | Prompt | Description |
532
- |--------|-------------|
533
- | `/teams-catch-up` | Summarize what happened while you were away |
534
- | `/teams-triage` | Work through unanswered messages one by one |
535
- | `/teams-channel-digest` | Decisions, open questions and action items in a channel |
536
- | `/teams-standup` | Draft and post a standup update |
537
- | `/teams-meeting-prep` | Context and talking points for your next meeting |
538
- | `/teams-schedule` | Find a time everyone is free and book it |
539
- | `/teams-doctor` | Guided troubleshooting |
540
-
541
- ---
542
-
543
- ## How messages are formatted
544
-
545
- A message body is treated as **lightweight markdown** and converted to the HTML
546
- subset Teams renders in a chat bubble:
547
-
548
- | You write | Teams shows |
549
- |-----------|-------------|
550
- | `**bold**`, `*italic*`, `~~struck~~` | bold, italic, struck through |
551
- | `` `code` ``, fenced blocks | inline code, code block |
552
- | `- item`, `1. item` | bullet list, numbered list |
553
- | `[label](https://…)` | link |
554
- | `# Heading` | a bold line — Teams has no headings in a chat |
555
-
556
- Tables and images are deliberately **not** translated: Teams renders neither in
557
- a chat, so passing them through would look worse than plain text. Put a table in
558
- a file and link it instead.
559
-
560
- Pass `html: true` on a send tool to bypass the conversion and supply raw HTML
561
- instead. Everything else is escaped, so a message body cannot inject markup.
562
-
563
- This is also why pi writes the way it does: see *How to format a Teams message*
564
- in the bundled skill. Short, answer-first, bullets for anything enumerable — the
565
- medium is a chat bubble, not a document.
566
-
567
- ---
568
-
569
- ## Addressing things
570
-
571
- **Channels** — a `Team/Channel` path, or `team` and `channel` separately.
572
- Names resolve case-insensitively; an ambiguous name is reported, never guessed.
573
-
574
- ```yaml
575
- teams_read_channel:
576
- channel: "Engineering/General"
577
- limit: 20
578
- ```
579
-
580
- **Chats** — an ID, a group chat topic, or a participant's name or e-mail. If
581
- more than one chat matches, the tool lists the candidates instead of picking
582
- one.
583
-
584
- ```yaml
585
- teams_send_chat_message:
586
- chat: "anna@contoso.com"
587
- body: "Running five minutes late."
588
- ```
589
-
590
- **Mentions** — pass `mentions` and write `@Display Name` in the body where the
591
- mention belongs:
592
-
593
- ```yaml
594
- teams_send_channel_message:
595
- channel: "Engineering/General"
596
- body: "@Anna Schmidt the pipeline is green again."
597
- mentions: ["anna@contoso.com"]
598
- ```
599
-
600
- ---
601
-
602
- ## Files pi writes
603
-
604
- | File | Contents | Mode |
605
- |------|----------|------|
606
- | `~/.pi/agent/pi-teams.json` | accounts, tenants, safety levels, scope rules | `0600` |
607
- | `~/.pi/agent/pi-teams-tokens/` | one MSAL token cache per account+tenant | `0600` |
608
- | `~/.pi/agent/pi-teams-audit.jsonl` | one line per write: when, who, where, what | `0600` |
609
- | `~/.pi/agent/pi-teams-watch/` | listen mode's cursor: the newest message it has looked at per chat | `0600` |
610
- | `~/.pi/agent/pi-teams-files/` | images and files downloaded from messages, one folder per chat or channel | — |
611
-
612
- Both the config and the token cache are written through a private temp file and
613
- an atomic rename, so an interrupted write cannot truncate them. The watch
614
- cursor is what keeps listen mode from re-answering what it has already seen, and
615
- deleting it only costs one re-examination of the chats in the list — it is not
616
- silently re-answering
617
- anything, because the decision also asks Teams whether the message is still
618
- unread. To revoke pi's access entirely, run `teams_logout` with `all: true` and
619
- remove the app's consent in Entra ID.
620
-
621
- ## Architecture
622
-
623
- Data-oriented: plain immutable data, I/O at the edges, pure functions in the
624
- middle.
625
-
626
- - **`src/types.ts`** — domain data as plain interfaces. No behavior.
627
- - **`src/config/index.ts`** — accounts, tenants, the safety cascade, listen-mode settings, persistence.
628
- - **`src/config/scope.ts`** — allow/deny matching. Pure, and the most heavily
629
- tested module in the package.
630
- - **`src/auth/`** — MSAL applications, the file-backed token cache, JWT claims.
631
- - **`src/graph/client.ts`** — fetch wrapper: bearer token, paging, throttling.
632
- - **`src/graph/*.ts`** — one module per resource area, returning domain types.
633
- - **`src/graph/mappers.ts`** — Graph JSON → domain types. Pure.
634
- - **`src/watch/`** — listen mode: the decision rules (`index.ts`, pure), the
635
- polling loop (`loop.ts`), and the prompt an incoming message turns into
636
- (`prompt.ts`).
637
- - **`src/utils/formatting.ts`** — domain types → display strings. Pure.
638
- - **`src/utils/richtext.ts`** — markdown → the HTML subset Teams renders. Pure.
639
- - **`src/utils/slots.ts`** — availability view → the free slots everybody
640
- shares. Pure, and the part of scheduling that can be wrong without looking
641
- wrong.
642
- - **`src/safety/`** — the three gates and the audit log.
643
- - **`src/tools/`** — one module per tool.
644
- - **`src/extension/index.ts`** — registration, commands, the `tool_call`
645
- interceptor, and the listen-mode lifecycle.
646
-
647
- ## Development
648
-
649
- ```bash
650
- npm install
651
- npm run typecheck
652
- npm test
653
- ```
654
-
655
- Tests cover the pure logic — scope matching, the config cascade, message body
656
- construction, markdown rendering, HTML flattening, free-slot arithmetic, the
657
- listen-mode decision rules, the safety gates — and run without a tenant or a
658
- network connection.
659
-
660
- ## Troubleshooting
661
-
662
- | Symptom | Cause and fix |
663
- |---------|---------------|
664
- | `AADSTS500113` (no reply address) | The registration has no redirect URI. Add `http://localhost` under *Mobile and desktop applications* — see [docs/entra-app-registration.md](docs/entra-app-registration.md) |
665
- | `AADSTS7000218` | "Allow public client flows" is off in the app registration |
666
- | `AADSTS65001` | Consent missing — sign in again, or have an admin consent |
667
- | `AADSTS50011` (redirect URI mismatch) | Add `http://localhost` under *Mobile and desktop applications*, or pin `loopbackPort` |
668
- | `AADSTS50020` | The account is not a member/guest of that tenant, or the app is single-tenant |
669
- | Signed out after about an hour | The tenant grants no `offline_access`, so there is no refresh token. `teams_doctor` names the missing scope; otherwise sign in again when it expires |
670
- | Browser never opens | The launcher failed: the error names it, and carries the URL to open by hand. `teams_doctor` says whether a browser counts as reachable here, and `PI_TEAMS_BROWSER` points pi at your own command |
671
- | Browser opens, page never returns | Read what the browser showed before suspecting a firewall: a pending admin consent (`AADSTS65004`), a Conditional Access policy (`AADSTS53003`), or a redirect URI that does not match the port (`AADSTS50011`). The timeout message names all three. A blocked loopback port is the fourth possibility — pin `loopbackPort` and allow it |
672
- | A screenshot in a message is missing from the output | The text of a message cannot carry a picture. `teams_read_chat` says how many images a message has; `teams_download_files` fetches them so they can be opened |
673
- | "Not signed in" | Run `teams_login`; the saved session may have been revoked |
674
- | Status line says "not signed in" although sign-in worked | The line is written at session start and after every auth tool; if it lags, `/teams-status` repaints it |
675
- | "Blocked by configuration" | Your own scope rules. `teams_permissions action: test` shows which rule |
676
- | "cannot run on an app-only token" | Set `authMode: "interactive"` and run `teams_login` |
677
- | 403 reading channel messages | `ChannelMessage.Read.All` needs **admin** consent — see "Who has to approve what" |
678
- | Listen mode does nothing | `teams_watch action: status` says whether it is enabled and running, how many chats it tracks, and where the cursor lives. A chat you have already read in Teams stays quiet by design, and one held back by its cooldown or the hourly cap shows up as *waiting* |
679
- | Listen mode never answers one specific chat | `waiting` in `teams_watch action: status` says whether it is sitting out a `cooldownSeconds` window; chats it has already decided about are in the cursor under `~/.pi/agent/pi-teams-watch/` |
680
- | Listen mode answers too much | Narrow `watch.chats` and `watch.from`, raise `cooldownSeconds`, lower `maxTriggersPerHour` |
681
- | Listen mode stopped by itself | `teams_watch action: status` shows the last polling error; a revoked session or a sleeping laptop is the usual cause, and it resumes on its own |
682
- | Throttled | Graph rate limit; the client retries with back-off, then reports it |
42
+ 4. **Check it works:** `/teams-status`, then `/teams-inbox`.
683
43
 
684
- Run `teams_doctor` first — it checks all of the above at once.
44
+ If anything goes wrong, run `teams_doctor` before anything else: it checks the
45
+ configuration, the sign-in, which scopes were actually granted, and whether live
46
+ calls succeed, for every configured account.
47
+
48
+ ## What it can do
49
+
50
+ Read and write chats and channel threads, search your Teams history, react to
51
+ and edit your own messages, download the files and pictures a message carries,
52
+ see who is available, and schedule, change or cancel meetings — across several
53
+ accounts and tenants at once. The full list is in
54
+ [docs/tools.md](docs/tools.md).
55
+
56
+ **Listen mode** is the one feature that acts without being asked: pi polls your
57
+ chats and answers incoming messages as you. It is off by default, never starts
58
+ on its own, and is worth reading about before switching on —
59
+ [docs/listen-mode.md](docs/listen-mode.md).
60
+
61
+ ## Documentation
62
+
63
+ | Document | What it answers |
64
+ |----------|-----------------|
65
+ | [entra-app-registration.md](docs/entra-app-registration.md) | How do I register the app, and who has to approve which permission? |
66
+ | [sign-in.md](docs/sign-in.md) | How does pi sign in, where is the session kept, what happens on a headless machine? |
67
+ | [configuration.md](docs/configuration.md) | Every key in `pi-teams.json`: accounts, tenants, safety levels, scope rules, the AI footer. |
68
+ | [tools.md](docs/tools.md) | Every tool, command and prompt — and how to address a chat, a channel or a mention. |
69
+ | [listen-mode.md](docs/listen-mode.md) | How pi answers on its own, and how to keep that narrow. |
70
+ | [security.md](docs/security.md) | What pi may do in your name, what stops it, and what it writes to disk. |
71
+ | [troubleshooting.md](docs/troubleshooting.md) | A symptom-to-cause table, including the `AADSTS` codes. |
72
+ | [architecture.md](docs/architecture.md) | Module map and development setup, for contributing. |
73
+
74
+ The bundled skill (`skills/teams-collaboration/SKILL.md`) is documentation too,
75
+ but for the model rather than for you: it is what tells pi how to write a Teams
76
+ message, when to read before replying, and which tool fits a request.
77
+
78
+ ## A note on what this is
79
+
80
+ pi sends messages that are indistinguishable from ones you typed. There is no
81
+ "sent by a bot" marker in Teams, which is why an AI disclosure footer is
82
+ appended by default and why the default safety level asks before every write.
83
+ Both are configurable; both are deliberate. See
84
+ [docs/security.md](docs/security.md) before you turn either off.
685
85
 
686
86
  ## License
687
87