@patimweb/pi-teams 1.0.0

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.
Files changed (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +412 -0
  3. package/index.ts +1 -0
  4. package/package.json +61 -0
  5. package/prompts/teams-catch-up.md +24 -0
  6. package/prompts/teams-channel-digest.md +23 -0
  7. package/prompts/teams-doctor.md +21 -0
  8. package/prompts/teams-meeting-prep.md +22 -0
  9. package/prompts/teams-standup.md +21 -0
  10. package/prompts/teams-triage.md +22 -0
  11. package/skills/teams-collaboration/SKILL.md +143 -0
  12. package/src/auth/client-credentials.ts +72 -0
  13. package/src/auth/device-code.ts +276 -0
  14. package/src/auth/index.ts +105 -0
  15. package/src/auth/jwt.ts +50 -0
  16. package/src/auth/token-store.ts +156 -0
  17. package/src/config/index.ts +685 -0
  18. package/src/config/scope.ts +272 -0
  19. package/src/extension/index.ts +336 -0
  20. package/src/graph/calendar.ts +165 -0
  21. package/src/graph/chats.ts +159 -0
  22. package/src/graph/client.ts +234 -0
  23. package/src/graph/mappers.ts +268 -0
  24. package/src/graph/me.ts +80 -0
  25. package/src/graph/messages.ts +383 -0
  26. package/src/graph/presence.ts +160 -0
  27. package/src/graph/search.ts +111 -0
  28. package/src/graph/teams.ts +271 -0
  29. package/src/safety/audit.ts +57 -0
  30. package/src/safety/index.ts +186 -0
  31. package/src/status.ts +90 -0
  32. package/src/tools/resolve.ts +138 -0
  33. package/src/tools/shared.ts +124 -0
  34. package/src/tools/teams-accounts.ts +169 -0
  35. package/src/tools/teams-create-channel.ts +89 -0
  36. package/src/tools/teams-create-chat.ts +112 -0
  37. package/src/tools/teams-delete-message.ts +114 -0
  38. package/src/tools/teams-doctor.ts +182 -0
  39. package/src/tools/teams-find-user.ts +64 -0
  40. package/src/tools/teams-inbox.ts +142 -0
  41. package/src/tools/teams-list-channels.ts +62 -0
  42. package/src/tools/teams-list-chats.ts +74 -0
  43. package/src/tools/teams-list-files.ts +54 -0
  44. package/src/tools/teams-list-members.ts +72 -0
  45. package/src/tools/teams-list-teams.ts +65 -0
  46. package/src/tools/teams-login.ts +199 -0
  47. package/src/tools/teams-meetings.ts +342 -0
  48. package/src/tools/teams-permissions.ts +169 -0
  49. package/src/tools/teams-presence.ts +216 -0
  50. package/src/tools/teams-react.ts +109 -0
  51. package/src/tools/teams-read-channel.ts +75 -0
  52. package/src/tools/teams-read-chat.ts +73 -0
  53. package/src/tools/teams-read-thread.ts +88 -0
  54. package/src/tools/teams-search-messages.ts +71 -0
  55. package/src/tools/teams-send-channel-message.ts +250 -0
  56. package/src/tools/teams-send-chat-message.ts +152 -0
  57. package/src/tools/teams-setup.ts +245 -0
  58. package/src/tools/teams-status.ts +73 -0
  59. package/src/tools/tool-names.ts +30 -0
  60. package/src/types.ts +192 -0
  61. package/src/utils/errors.ts +133 -0
  62. package/src/utils/formatting.ts +356 -0
  63. package/tsconfig.json +16 -0
@@ -0,0 +1,268 @@
1
+ /**
2
+ * Graph JSON → domain types.
3
+ *
4
+ * Pure functions, no I/O. Graph responses are wide, inconsistently populated
5
+ * and occasionally change shape between endpoints; mapping them once here
6
+ * keeps every tool and formatter working against the stable interfaces in
7
+ * ../types.ts.
8
+ */
9
+
10
+ import type {
11
+ ChannelSummary,
12
+ ChatSummary,
13
+ DriveItemSummary,
14
+ EventSummary,
15
+ MemberSummary,
16
+ MessageAttachment,
17
+ MessageLocation,
18
+ MessageReaction,
19
+ MessageSummary,
20
+ PersonRef,
21
+ PresenceInfo,
22
+ SignedInUser,
23
+ TeamSummary,
24
+ } from "../types.ts";
25
+ import { htmlToText } from "../utils/formatting.ts";
26
+
27
+ /* eslint-disable @typescript-eslint/no-explicit-any */
28
+ type Raw = Record<string, any>;
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // People
32
+ // ---------------------------------------------------------------------------
33
+
34
+ export function mapUser(raw: Raw | undefined): SignedInUser | undefined {
35
+ if (!raw) return undefined;
36
+ return {
37
+ id: raw.id ?? "",
38
+ displayName: raw.displayName ?? raw.userPrincipalName ?? "(unknown)",
39
+ upn: raw.userPrincipalName ?? "",
40
+ mail: raw.mail ?? undefined,
41
+ tenantId: raw.tenantId ?? "",
42
+ jobTitle: raw.jobTitle ?? undefined,
43
+ };
44
+ }
45
+
46
+ export function mapPerson(raw: Raw | undefined): PersonRef | undefined {
47
+ if (!raw) return undefined;
48
+ // Identities appear as {user:{...}}, {application:{...}}, or bare.
49
+ const identity = raw.user ?? raw.application ?? raw.device ?? raw;
50
+ const name = identity.displayName ?? identity.userPrincipalName ?? identity.email;
51
+ if (!name && !identity.id) return undefined;
52
+ return {
53
+ id: identity.id ?? undefined,
54
+ displayName: name ?? "(unknown)",
55
+ upn: identity.userPrincipalName ?? undefined,
56
+ mail: identity.email ?? identity.mail ?? undefined,
57
+ };
58
+ }
59
+
60
+ export function mapMember(raw: Raw): MemberSummary {
61
+ return {
62
+ id: raw.id ?? "",
63
+ displayName: raw.displayName ?? "(unknown)",
64
+ upn: raw.userPrincipalName ?? undefined,
65
+ mail: raw.email ?? raw.mail ?? undefined,
66
+ roles: Array.isArray(raw.roles) ? raw.roles : [],
67
+ };
68
+ }
69
+
70
+ /** Identifiers a person can be matched against by the scope rules. */
71
+ export function personCandidates(person: PersonRef | MemberSummary | undefined): (string | undefined)[] {
72
+ if (!person) return [];
73
+ return [person.displayName, (person as PersonRef).upn, (person as PersonRef).mail, person.id];
74
+ }
75
+
76
+ // ---------------------------------------------------------------------------
77
+ // Teams & channels
78
+ // ---------------------------------------------------------------------------
79
+
80
+ export function mapTeam(raw: Raw): TeamSummary {
81
+ return {
82
+ id: raw.id ?? "",
83
+ displayName: raw.displayName ?? "(unnamed team)",
84
+ description: raw.description ?? undefined,
85
+ visibility: raw.visibility ?? undefined,
86
+ isArchived: raw.isArchived ?? undefined,
87
+ webUrl: raw.webUrl ?? undefined,
88
+ };
89
+ }
90
+
91
+ export function mapChannel(raw: Raw, team?: { id?: string; name?: string }): ChannelSummary {
92
+ return {
93
+ id: raw.id ?? "",
94
+ displayName: raw.displayName ?? "(unnamed channel)",
95
+ description: raw.description ?? undefined,
96
+ membershipType: raw.membershipType ?? undefined,
97
+ webUrl: raw.webUrl ?? undefined,
98
+ teamId: team?.id ?? undefined,
99
+ teamName: team?.name ?? undefined,
100
+ };
101
+ }
102
+
103
+ /** Identifiers a channel can be matched against — including "Team/Channel". */
104
+ export function channelCandidates(channel: ChannelSummary): (string | undefined)[] {
105
+ const path = channel.teamName ? `${channel.teamName}/${channel.displayName}` : undefined;
106
+ return [path, channel.displayName, channel.id, channel.teamName];
107
+ }
108
+
109
+ export function teamCandidates(team: TeamSummary): (string | undefined)[] {
110
+ return [team.displayName, team.id];
111
+ }
112
+
113
+ // ---------------------------------------------------------------------------
114
+ // Chats
115
+ // ---------------------------------------------------------------------------
116
+
117
+ /**
118
+ * Build a chat summary.
119
+ *
120
+ * Teams leaves 1:1 and ad-hoc group chats without a topic, so the label is
121
+ * derived from the other participants — that is also what the scope rules
122
+ * match against, which is why `me` is excluded from it.
123
+ */
124
+ export function mapChat(raw: Raw, meId?: string): ChatSummary {
125
+ const members: PersonRef[] = (raw.members ?? [])
126
+ .map((m: Raw) => mapPerson(m))
127
+ .filter((p: PersonRef | undefined): p is PersonRef => !!p);
128
+
129
+ const others = meId ? members.filter((m) => m.id !== meId) : members;
130
+ const derived = others.map((m) => m.displayName).filter(Boolean).join(", ");
131
+
132
+ const preview = raw.lastMessagePreview?.body?.content
133
+ ? htmlToText(
134
+ raw.lastMessagePreview.body.content,
135
+ raw.lastMessagePreview.body.contentType ?? "text",
136
+ )
137
+ : undefined;
138
+
139
+ return {
140
+ id: raw.id ?? "",
141
+ chatType: raw.chatType ?? "unknown",
142
+ topic: raw.topic ?? undefined,
143
+ label: raw.topic || derived || raw.id || "(chat)",
144
+ members,
145
+ lastUpdated: raw.lastUpdatedDateTime ?? raw.lastMessagePreview?.createdDateTime ?? undefined,
146
+ lastMessagePreview: preview,
147
+ lastMessageFrom: mapPerson(raw.lastMessagePreview?.from)?.displayName,
148
+ webUrl: raw.webUrl ?? undefined,
149
+ };
150
+ }
151
+
152
+ /** Identifiers a chat can be matched against — label, topic, id, participants. */
153
+ export function chatCandidates(chat: ChatSummary): (string | undefined)[] {
154
+ return [
155
+ chat.label,
156
+ chat.topic,
157
+ chat.id,
158
+ ...chat.members.flatMap((m) => [m.displayName, m.upn, m.mail]),
159
+ ];
160
+ }
161
+
162
+ // ---------------------------------------------------------------------------
163
+ // Messages
164
+ // ---------------------------------------------------------------------------
165
+
166
+ function mapReactions(raw: Raw): MessageReaction[] {
167
+ return (raw.reactions ?? []).map((r: Raw) => ({
168
+ type: r.reactionType ?? "?",
169
+ user: mapPerson(r.user)?.displayName,
170
+ createdDateTime: r.createdDateTime ?? undefined,
171
+ }));
172
+ }
173
+
174
+ function mapAttachments(raw: Raw): MessageAttachment[] {
175
+ return (raw.attachments ?? []).map((a: Raw) => ({
176
+ id: a.id ?? undefined,
177
+ name: a.name ?? undefined,
178
+ contentType: a.contentType ?? undefined,
179
+ contentUrl: a.contentUrl ?? undefined,
180
+ }));
181
+ }
182
+
183
+ function mapMentions(raw: Raw): PersonRef[] {
184
+ return (raw.mentions ?? [])
185
+ .map((m: Raw) => mapPerson(m.mentioned) ?? { displayName: m.mentionText ?? "(mention)" })
186
+ .filter((p: PersonRef | undefined): p is PersonRef => !!p);
187
+ }
188
+
189
+ export function mapMessage(raw: Raw, location?: MessageLocation): MessageSummary {
190
+ const contentType = raw.body?.contentType ?? "text";
191
+ return {
192
+ id: raw.id ?? "",
193
+ text: htmlToText(raw.body?.content ?? "", contentType),
194
+ contentType,
195
+ from: mapPerson(raw.from),
196
+ createdDateTime: raw.createdDateTime ?? undefined,
197
+ lastModifiedDateTime: raw.lastModifiedDateTime ?? undefined,
198
+ deletedDateTime: raw.deletedDateTime ?? undefined,
199
+ subject: raw.subject ?? undefined,
200
+ importance: raw.importance ?? undefined,
201
+ replyToId: raw.replyToId ?? undefined,
202
+ mentions: mapMentions(raw),
203
+ reactions: mapReactions(raw),
204
+ attachments: mapAttachments(raw),
205
+ webUrl: raw.webUrl ?? undefined,
206
+ location,
207
+ };
208
+ }
209
+
210
+ // ---------------------------------------------------------------------------
211
+ // Presence
212
+ // ---------------------------------------------------------------------------
213
+
214
+ export function mapPresence(raw: Raw, displayName?: string): PresenceInfo {
215
+ return {
216
+ id: raw.id ?? "",
217
+ displayName,
218
+ availability: raw.availability ?? "Unknown",
219
+ activity: raw.activity ?? "Unknown",
220
+ statusMessage: raw.statusMessage?.message?.content
221
+ ? htmlToText(raw.statusMessage.message.content, raw.statusMessage.message.contentType ?? "text")
222
+ : undefined,
223
+ };
224
+ }
225
+
226
+ // ---------------------------------------------------------------------------
227
+ // Calendar
228
+ // ---------------------------------------------------------------------------
229
+
230
+ export function mapEvent(raw: Raw): EventSummary {
231
+ return {
232
+ id: raw.id ?? "",
233
+ subject: raw.subject ?? "(no subject)",
234
+ start: raw.start?.dateTime ?? undefined,
235
+ end: raw.end?.dateTime ?? undefined,
236
+ timeZone: raw.start?.timeZone ?? undefined,
237
+ isAllDay: raw.isAllDay ?? undefined,
238
+ isOnlineMeeting: raw.isOnlineMeeting ?? undefined,
239
+ joinUrl: raw.onlineMeeting?.joinUrl ?? raw.onlineMeetingUrl ?? undefined,
240
+ organizer: mapPerson(raw.organizer?.emailAddress
241
+ ? { displayName: raw.organizer.emailAddress.name, email: raw.organizer.emailAddress.address }
242
+ : raw.organizer),
243
+ attendees: (raw.attendees ?? [])
244
+ .map((a: Raw) =>
245
+ mapPerson({ displayName: a.emailAddress?.name, email: a.emailAddress?.address }),
246
+ )
247
+ .filter((p: PersonRef | undefined): p is PersonRef => !!p),
248
+ location: raw.location?.displayName ?? undefined,
249
+ bodyPreview: raw.bodyPreview ?? undefined,
250
+ webLink: raw.webLink ?? undefined,
251
+ };
252
+ }
253
+
254
+ // ---------------------------------------------------------------------------
255
+ // Files
256
+ // ---------------------------------------------------------------------------
257
+
258
+ export function mapDriveItem(raw: Raw): DriveItemSummary {
259
+ return {
260
+ id: raw.id ?? "",
261
+ name: raw.name ?? "(unnamed)",
262
+ size: raw.size ?? undefined,
263
+ lastModifiedDateTime: raw.lastModifiedDateTime ?? undefined,
264
+ lastModifiedBy: raw.lastModifiedBy?.user?.displayName ?? undefined,
265
+ webUrl: raw.webUrl ?? undefined,
266
+ isFolder: !!raw.folder,
267
+ };
268
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * The signed-in user and people lookup.
3
+ */
4
+
5
+ import type { TeamsConnection } from "../config/index.ts";
6
+ import type { PersonRef, SignedInUser } from "../types.ts";
7
+ import { graphList, graphRequest } from "./client.ts";
8
+ import { mapPerson, mapUser } from "./mappers.ts";
9
+
10
+ const USER_SELECT = "id,displayName,userPrincipalName,mail,jobTitle,department,officeLocation";
11
+
12
+ /** Who pi is currently acting as. */
13
+ export async function getMe(conn: TeamsConnection, signal?: AbortSignal): Promise<SignedInUser> {
14
+ const raw = await graphRequest<Record<string, unknown>>(conn, "GET", "/me", {
15
+ query: { $select: USER_SELECT },
16
+ signal,
17
+ });
18
+ const user = mapUser(raw as Record<string, never> | undefined);
19
+ if (!user) throw new Error("Could not read the signed-in user from Microsoft Graph.");
20
+ return { ...user, tenantId: conn.tenantId };
21
+ }
22
+
23
+ /**
24
+ * Find people by name, UPN or e-mail.
25
+ *
26
+ * Graph's `$filter startsWith` is the only broadly available option here:
27
+ * `$search` on /users needs ConsistencyLevel=eventual and is not granted in
28
+ * every tenant, so the query is escaped and matched against the three fields
29
+ * people actually type.
30
+ */
31
+ export async function findUsers(
32
+ conn: TeamsConnection,
33
+ query: string,
34
+ max = 15,
35
+ signal?: AbortSignal,
36
+ ): Promise<PersonRef[]> {
37
+ const escaped = query.replace(/'/g, "''");
38
+ const filter =
39
+ `startswith(displayName,'${escaped}') or ` +
40
+ `startswith(userPrincipalName,'${escaped}') or ` +
41
+ `startswith(mail,'${escaped}') or ` +
42
+ `startswith(givenName,'${escaped}') or ` +
43
+ `startswith(surname,'${escaped}')`;
44
+
45
+ const raw = await graphList<Record<string, unknown>>(conn, "/users", {
46
+ query: { $filter: filter, $select: USER_SELECT, $top: Math.min(max, 50) },
47
+ max,
48
+ signal,
49
+ });
50
+
51
+ return raw
52
+ .map((entry) => mapPerson(entry as Record<string, never>))
53
+ .filter((p): p is PersonRef => !!p);
54
+ }
55
+
56
+ /** Resolve a person reference (UPN, e-mail, display name or id) to a user id. */
57
+ export async function resolveUserId(
58
+ conn: TeamsConnection,
59
+ reference: string,
60
+ signal?: AbortSignal,
61
+ ): Promise<PersonRef | undefined> {
62
+ // A UPN or object id can be fetched directly — one call instead of a search.
63
+ if (reference.includes("@") || /^[0-9a-f-]{36}$/i.test(reference)) {
64
+ try {
65
+ const raw = await graphRequest<Record<string, unknown>>(
66
+ conn,
67
+ "GET",
68
+ `/users/${encodeURIComponent(reference)}`,
69
+ { query: { $select: USER_SELECT }, signal },
70
+ );
71
+ const person = mapPerson(raw as Record<string, never> | undefined);
72
+ if (person) return person;
73
+ } catch {
74
+ /* fall through to search */
75
+ }
76
+ }
77
+
78
+ const matches = await findUsers(conn, reference, 5, signal);
79
+ return matches[0];
80
+ }