@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 +54 -654
- package/docs/architecture.md +59 -0
- package/docs/configuration.md +187 -0
- package/docs/entra-app-registration.md +16 -2
- package/docs/listen-mode.md +123 -0
- package/docs/security.md +116 -0
- package/docs/sign-in.md +110 -0
- package/docs/tools.md +150 -0
- package/docs/troubleshooting.md +55 -0
- package/package.json +1 -1
- package/skills/teams-collaboration/SKILL.md +23 -0
- package/src/config/index.ts +12 -0
- package/src/extension/index.ts +16 -0
- package/src/graph/client.ts +55 -1
- package/src/graph/media.ts +46 -2
- package/src/tools/teams-download-files.ts +47 -3
- package/src/utils/errors.ts +29 -0
- package/src/watch/loop.ts +69 -9
- package/src/watch/pin.ts +109 -0
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|

|
|
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**
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
352
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|