@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.
- package/LICENSE +21 -0
- package/README.md +412 -0
- package/index.ts +1 -0
- package/package.json +61 -0
- package/prompts/teams-catch-up.md +24 -0
- package/prompts/teams-channel-digest.md +23 -0
- package/prompts/teams-doctor.md +21 -0
- package/prompts/teams-meeting-prep.md +22 -0
- package/prompts/teams-standup.md +21 -0
- package/prompts/teams-triage.md +22 -0
- package/skills/teams-collaboration/SKILL.md +143 -0
- package/src/auth/client-credentials.ts +72 -0
- package/src/auth/device-code.ts +276 -0
- package/src/auth/index.ts +105 -0
- package/src/auth/jwt.ts +50 -0
- package/src/auth/token-store.ts +156 -0
- package/src/config/index.ts +685 -0
- package/src/config/scope.ts +272 -0
- package/src/extension/index.ts +336 -0
- package/src/graph/calendar.ts +165 -0
- package/src/graph/chats.ts +159 -0
- package/src/graph/client.ts +234 -0
- package/src/graph/mappers.ts +268 -0
- package/src/graph/me.ts +80 -0
- package/src/graph/messages.ts +383 -0
- package/src/graph/presence.ts +160 -0
- package/src/graph/search.ts +111 -0
- package/src/graph/teams.ts +271 -0
- package/src/safety/audit.ts +57 -0
- package/src/safety/index.ts +186 -0
- package/src/status.ts +90 -0
- package/src/tools/resolve.ts +138 -0
- package/src/tools/shared.ts +124 -0
- package/src/tools/teams-accounts.ts +169 -0
- package/src/tools/teams-create-channel.ts +89 -0
- package/src/tools/teams-create-chat.ts +112 -0
- package/src/tools/teams-delete-message.ts +114 -0
- package/src/tools/teams-doctor.ts +182 -0
- package/src/tools/teams-find-user.ts +64 -0
- package/src/tools/teams-inbox.ts +142 -0
- package/src/tools/teams-list-channels.ts +62 -0
- package/src/tools/teams-list-chats.ts +74 -0
- package/src/tools/teams-list-files.ts +54 -0
- package/src/tools/teams-list-members.ts +72 -0
- package/src/tools/teams-list-teams.ts +65 -0
- package/src/tools/teams-login.ts +199 -0
- package/src/tools/teams-meetings.ts +342 -0
- package/src/tools/teams-permissions.ts +169 -0
- package/src/tools/teams-presence.ts +216 -0
- package/src/tools/teams-react.ts +109 -0
- package/src/tools/teams-read-channel.ts +75 -0
- package/src/tools/teams-read-chat.ts +73 -0
- package/src/tools/teams-read-thread.ts +88 -0
- package/src/tools/teams-search-messages.ts +71 -0
- package/src/tools/teams-send-channel-message.ts +250 -0
- package/src/tools/teams-send-chat-message.ts +152 -0
- package/src/tools/teams-setup.ts +245 -0
- package/src/tools/teams-status.ts +73 -0
- package/src/tools/tool-names.ts +30 -0
- package/src/types.ts +192 -0
- package/src/utils/errors.ts +133 -0
- package/src/utils/formatting.ts +356 -0
- 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
|
+
}
|
package/src/graph/me.ts
ADDED
|
@@ -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
|
+
}
|