@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,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* teams_list_members — members of a team or a channel.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Type } from "typebox";
|
|
6
|
+
import { listChannelMembers, listTeamMembers } from "../graph/teams.ts";
|
|
7
|
+
import { hasAccess } from "../safety/index.ts";
|
|
8
|
+
import { formatMemberList } from "../utils/formatting.ts";
|
|
9
|
+
import { requireChannel, requireTeam } from "./resolve.ts";
|
|
10
|
+
import {
|
|
11
|
+
AccountParam,
|
|
12
|
+
LimitParam,
|
|
13
|
+
TenantParam,
|
|
14
|
+
connectionFor,
|
|
15
|
+
run,
|
|
16
|
+
textResult,
|
|
17
|
+
type ToolContext,
|
|
18
|
+
type ToolResult,
|
|
19
|
+
} from "./shared.ts";
|
|
20
|
+
|
|
21
|
+
export const teamsListMembersTool = {
|
|
22
|
+
name: "teams_list_members",
|
|
23
|
+
description:
|
|
24
|
+
"List the members of a Microsoft Teams team, or of one channel when 'channel' is given. " +
|
|
25
|
+
"Returns display names, e-mail addresses and roles — useful before @-mentioning someone or " +
|
|
26
|
+
"starting a chat with the right person.",
|
|
27
|
+
parameters: Type.Object({
|
|
28
|
+
team: Type.String({ description: "Team display name or ID" }),
|
|
29
|
+
channel: Type.Optional(
|
|
30
|
+
Type.String({ description: "Channel name or ID; omit to list the whole team" }),
|
|
31
|
+
),
|
|
32
|
+
account: AccountParam,
|
|
33
|
+
tenant: TenantParam,
|
|
34
|
+
limit: LimitParam,
|
|
35
|
+
}),
|
|
36
|
+
promptSnippet: "List members of a Teams team or channel",
|
|
37
|
+
|
|
38
|
+
async execute(
|
|
39
|
+
_toolCallId: string,
|
|
40
|
+
params: { team: string; channel?: string; account?: string; tenant?: string; limit?: number },
|
|
41
|
+
signal: AbortSignal | undefined,
|
|
42
|
+
_onUpdate: undefined,
|
|
43
|
+
ctx: ToolContext,
|
|
44
|
+
): Promise<ToolResult> {
|
|
45
|
+
return run(async () => {
|
|
46
|
+
const conn = connectionFor(ctx, params.account, params.tenant);
|
|
47
|
+
|
|
48
|
+
if (params.channel) {
|
|
49
|
+
const { team, channel } = await requireChannel(conn, params.team, params.channel, "read", signal);
|
|
50
|
+
const members = await listChannelMembers(conn, team.id, channel.id, params.limit ?? 200, signal);
|
|
51
|
+
const visible = members.filter((member) =>
|
|
52
|
+
hasAccess(conn, "read", "people", [member.displayName, member.upn, member.mail, member.id]),
|
|
53
|
+
);
|
|
54
|
+
return textResult(
|
|
55
|
+
formatMemberList(visible, `${team.displayName}/${channel.displayName}`),
|
|
56
|
+
{ team: team.displayName, channel: channel.displayName, count: visible.length },
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const team = await requireTeam(conn, params.team, "read", signal);
|
|
61
|
+
const members = await listTeamMembers(conn, team.id, params.limit ?? 200, signal);
|
|
62
|
+
const visible = members.filter((member) =>
|
|
63
|
+
hasAccess(conn, "read", "people", [member.displayName, member.upn, member.mail, member.id]),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
return textResult(formatMemberList(visible, team.displayName), {
|
|
67
|
+
team: team.displayName,
|
|
68
|
+
count: visible.length,
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
},
|
|
72
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* teams_list_teams — the teams the signed-in user belongs to, filtered by the
|
|
3
|
+
* read rules.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Type } from "typebox";
|
|
7
|
+
import { listJoinedTeams } from "../graph/teams.ts";
|
|
8
|
+
import { teamCandidates } from "../graph/mappers.ts";
|
|
9
|
+
import { hasAccess } from "../safety/index.ts";
|
|
10
|
+
import { formatTeamList } from "../utils/formatting.ts";
|
|
11
|
+
import {
|
|
12
|
+
AccountParam,
|
|
13
|
+
LimitParam,
|
|
14
|
+
TenantParam,
|
|
15
|
+
connectionFor,
|
|
16
|
+
run,
|
|
17
|
+
textResult,
|
|
18
|
+
type ToolContext,
|
|
19
|
+
type ToolResult,
|
|
20
|
+
} from "./shared.ts";
|
|
21
|
+
|
|
22
|
+
export const teamsListTeamsTool = {
|
|
23
|
+
name: "teams_list_teams",
|
|
24
|
+
description:
|
|
25
|
+
"List the Microsoft Teams teams the signed-in user is a member of. Teams excluded by the configured " +
|
|
26
|
+
"read rules are not shown. Use this to discover team names and IDs before reading or posting in a channel.",
|
|
27
|
+
parameters: Type.Object({
|
|
28
|
+
account: AccountParam,
|
|
29
|
+
tenant: TenantParam,
|
|
30
|
+
limit: LimitParam,
|
|
31
|
+
filter: Type.Optional(
|
|
32
|
+
Type.String({ description: "Only show teams whose name contains this text" }),
|
|
33
|
+
),
|
|
34
|
+
}),
|
|
35
|
+
promptSnippet: "List the user's Microsoft Teams teams",
|
|
36
|
+
|
|
37
|
+
async execute(
|
|
38
|
+
_toolCallId: string,
|
|
39
|
+
params: { account?: string; tenant?: string; limit?: number; filter?: string },
|
|
40
|
+
signal: AbortSignal | undefined,
|
|
41
|
+
_onUpdate: undefined,
|
|
42
|
+
ctx: ToolContext,
|
|
43
|
+
): Promise<ToolResult> {
|
|
44
|
+
return run(async () => {
|
|
45
|
+
const conn = connectionFor(ctx, params.account, params.tenant);
|
|
46
|
+
const all = await listJoinedTeams(conn, params.limit ?? 100, signal);
|
|
47
|
+
|
|
48
|
+
const visible = all.filter((team) => hasAccess(conn, "read", "teams", teamCandidates(team)));
|
|
49
|
+
const filtered = params.filter
|
|
50
|
+
? visible.filter((team) =>
|
|
51
|
+
team.displayName.toLowerCase().includes(params.filter!.toLowerCase()),
|
|
52
|
+
)
|
|
53
|
+
: visible;
|
|
54
|
+
|
|
55
|
+
const hidden = all.length - visible.length;
|
|
56
|
+
const note = hidden > 0 ? `\n\n_${hidden} team(s) hidden by your scope rules._` : "";
|
|
57
|
+
|
|
58
|
+
return textResult(`${formatTeamList(filtered)}${note}`, {
|
|
59
|
+
count: filtered.length,
|
|
60
|
+
hidden,
|
|
61
|
+
account: conn.account,
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
},
|
|
65
|
+
};
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* teams_login — sign in as the user via the device code flow.
|
|
3
|
+
*
|
|
4
|
+
* The tool streams the code and URL through `onUpdate` as soon as Microsoft
|
|
5
|
+
* issues them, then keeps polling. That ordering matters: the user has a
|
|
6
|
+
* couple of minutes to act, and they cannot act on a code that only appears
|
|
7
|
+
* after the tool returns.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Type } from "typebox";
|
|
11
|
+
import { pollForToken, requestDeviceCode } from "../auth/device-code.ts";
|
|
12
|
+
import { acquireAppToken } from "../auth/client-credentials.ts";
|
|
13
|
+
import { clearMemoryCache, forgetToken, storeToken } from "../auth/index.ts";
|
|
14
|
+
import { clearAllTokens, deleteToken } from "../auth/token-store.ts";
|
|
15
|
+
import { tokenScopes } from "../auth/jwt.ts";
|
|
16
|
+
import { getMe } from "../graph/me.ts";
|
|
17
|
+
import { formatGraphError } from "../utils/errors.ts";
|
|
18
|
+
import {
|
|
19
|
+
AccountParam,
|
|
20
|
+
TenantParam,
|
|
21
|
+
clearUserCache,
|
|
22
|
+
connectionFor,
|
|
23
|
+
connectionLabel,
|
|
24
|
+
errorResult,
|
|
25
|
+
run,
|
|
26
|
+
textResult,
|
|
27
|
+
type ToolContext,
|
|
28
|
+
type ToolResult,
|
|
29
|
+
} from "./shared.ts";
|
|
30
|
+
|
|
31
|
+
interface LoginParams {
|
|
32
|
+
account?: string;
|
|
33
|
+
tenant?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const teamsLoginTool = {
|
|
37
|
+
name: "teams_login",
|
|
38
|
+
description:
|
|
39
|
+
"Sign in to Microsoft Teams as yourself using the device code flow. Shows a short code and a URL; " +
|
|
40
|
+
"open the URL, enter the code, and sign in with your normal credentials. The session is saved " +
|
|
41
|
+
"(including a refresh token) so this is only needed once per account/tenant. " +
|
|
42
|
+
"Use the 'account' and 'tenant' parameters to sign in to a second company or a guest tenant.",
|
|
43
|
+
parameters: Type.Object({
|
|
44
|
+
account: AccountParam,
|
|
45
|
+
tenant: TenantParam,
|
|
46
|
+
}),
|
|
47
|
+
promptSnippet: "Sign in to Microsoft Teams as the user",
|
|
48
|
+
promptGuidelines: [
|
|
49
|
+
"Use teams_login when a Teams tool reports that pi is not signed in, or when the user wants to add a second account.",
|
|
50
|
+
"Show the user code and the verification URL to the user verbatim — they cannot complete sign-in without them.",
|
|
51
|
+
],
|
|
52
|
+
|
|
53
|
+
async execute(
|
|
54
|
+
_toolCallId: string,
|
|
55
|
+
params: LoginParams,
|
|
56
|
+
signal: AbortSignal | undefined,
|
|
57
|
+
onUpdate: ((update: { content: Array<{ type: "text"; text: string }> }) => void) | undefined,
|
|
58
|
+
ctx: ToolContext,
|
|
59
|
+
): Promise<ToolResult> {
|
|
60
|
+
return run(async () => {
|
|
61
|
+
const conn = connectionFor(ctx, params.account, params.tenant);
|
|
62
|
+
const label = connectionLabel(conn);
|
|
63
|
+
|
|
64
|
+
// App-only needs no user interaction at all.
|
|
65
|
+
if (conn.authMode === "client-credentials") {
|
|
66
|
+
const token = await acquireAppToken(conn, signal);
|
|
67
|
+
storeToken(conn, token);
|
|
68
|
+
return textResult(
|
|
69
|
+
[
|
|
70
|
+
`✅ Acquired an app-only token for ${label}.`,
|
|
71
|
+
"",
|
|
72
|
+
`Granted app roles: ${tokenScopes(token.accessToken).join(", ") || "(none visible in the token)"}`,
|
|
73
|
+
"",
|
|
74
|
+
"Note: app-only tokens cannot post messages as a person. Switch this account to " +
|
|
75
|
+
'authMode "device-code" if pi should act as you.',
|
|
76
|
+
].join("\n"),
|
|
77
|
+
{ account: conn.account, tenant: conn.tenant, authMode: "client-credentials" },
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const challenge = await requestDeviceCode(conn, signal);
|
|
82
|
+
|
|
83
|
+
const instructions = [
|
|
84
|
+
`## Sign in to Teams (${label})`,
|
|
85
|
+
"",
|
|
86
|
+
`1. Open **${challenge.verificationUri}**`,
|
|
87
|
+
`2. Enter the code: **${challenge.userCode}**`,
|
|
88
|
+
"3. Sign in with the account pi should act as.",
|
|
89
|
+
"",
|
|
90
|
+
`The code expires at ${new Date(challenge.expiresAt).toLocaleTimeString()}. Waiting…`,
|
|
91
|
+
].join("\n");
|
|
92
|
+
|
|
93
|
+
onUpdate?.({ content: [{ type: "text", text: instructions }] });
|
|
94
|
+
|
|
95
|
+
let lastNotice = 0;
|
|
96
|
+
const token = await pollForToken(conn, challenge, {
|
|
97
|
+
signal,
|
|
98
|
+
onPending: (secondsLeft) => {
|
|
99
|
+
// One nudge every 30s — enough to show progress, not enough to spam.
|
|
100
|
+
if (secondsLeft > 0 && Date.now() - lastNotice > 30_000) {
|
|
101
|
+
lastNotice = Date.now();
|
|
102
|
+
onUpdate?.({
|
|
103
|
+
content: [
|
|
104
|
+
{
|
|
105
|
+
type: "text",
|
|
106
|
+
text: `${instructions}\n\nStill waiting for sign-in… (${secondsLeft}s left)`,
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
storeToken(conn, token);
|
|
115
|
+
clearUserCache();
|
|
116
|
+
|
|
117
|
+
// Confirm the token actually works and report who pi now is.
|
|
118
|
+
let identity = token.user?.upn ?? token.user?.displayName ?? "(unknown)";
|
|
119
|
+
let jobTitle: string | undefined;
|
|
120
|
+
try {
|
|
121
|
+
const me = await getMe(conn, signal);
|
|
122
|
+
identity = `${me.displayName} <${me.upn}>`;
|
|
123
|
+
jobTitle = me.jobTitle;
|
|
124
|
+
} catch (err) {
|
|
125
|
+
return textResult(
|
|
126
|
+
[
|
|
127
|
+
`✅ Signed in to ${label}, but the first Graph call failed: ${formatGraphError(err)}`,
|
|
128
|
+
"",
|
|
129
|
+
"Run teams_doctor to see which permissions are missing.",
|
|
130
|
+
].join("\n"),
|
|
131
|
+
{ account: conn.account, tenant: conn.tenant, warning: true },
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const granted = tokenScopes(token.accessToken);
|
|
136
|
+
const requested = conn.scopes.filter((s) => !["openid", "profile", "offline_access"].includes(s));
|
|
137
|
+
const missing = requested.filter(
|
|
138
|
+
(scope) => !granted.some((g) => g.toLowerCase() === scope.toLowerCase()),
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
return textResult(
|
|
142
|
+
[
|
|
143
|
+
`✅ Signed in to ${label} as **${identity}**${jobTitle ? ` (${jobTitle})` : ""}.`,
|
|
144
|
+
"",
|
|
145
|
+
`pi now acts as this user in Teams. Safety level: **${conn.safetyLevel}**.`,
|
|
146
|
+
missing.length > 0
|
|
147
|
+
? `\n⚠️ Not granted: ${missing.join(", ")}\nSome tools will fail until an admin consents to these scopes.`
|
|
148
|
+
: "\nAll requested scopes were granted.",
|
|
149
|
+
].join("\n"),
|
|
150
|
+
{
|
|
151
|
+
account: conn.account,
|
|
152
|
+
tenant: conn.tenant,
|
|
153
|
+
user: identity,
|
|
154
|
+
grantedScopes: granted,
|
|
155
|
+
missingScopes: missing,
|
|
156
|
+
},
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
export const teamsLogoutTool = {
|
|
163
|
+
name: "teams_logout",
|
|
164
|
+
description:
|
|
165
|
+
"Sign out of a Teams account: removes the saved access and refresh tokens from " +
|
|
166
|
+
"~/.pi/agent/pi-teams-tokens.json. The account stays configured, so teams_login can sign in again.",
|
|
167
|
+
parameters: Type.Object({
|
|
168
|
+
account: AccountParam,
|
|
169
|
+
tenant: TenantParam,
|
|
170
|
+
all: Type.Optional(Type.Boolean({ description: "Remove the cached tokens of every account" })),
|
|
171
|
+
}),
|
|
172
|
+
promptSnippet: "Sign out of a Teams account",
|
|
173
|
+
|
|
174
|
+
async execute(
|
|
175
|
+
_toolCallId: string,
|
|
176
|
+
params: { account?: string; tenant?: string; all?: boolean },
|
|
177
|
+
_signal: AbortSignal | undefined,
|
|
178
|
+
_onUpdate: undefined,
|
|
179
|
+
ctx: ToolContext,
|
|
180
|
+
): Promise<ToolResult> {
|
|
181
|
+
return run(async () => {
|
|
182
|
+
clearUserCache();
|
|
183
|
+
|
|
184
|
+
if (params.all) {
|
|
185
|
+
const count = clearAllTokens();
|
|
186
|
+
clearMemoryCache();
|
|
187
|
+
return textResult(`✅ Removed ${count} cached Teams session(s).`, { removed: count });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const conn = connectionFor(ctx, params.account, params.tenant);
|
|
191
|
+
const removed = deleteToken(conn.account, conn.tenantId);
|
|
192
|
+
forgetToken(conn);
|
|
193
|
+
|
|
194
|
+
return removed
|
|
195
|
+
? textResult(`✅ Signed out of ${connectionLabel(conn)}.`, { account: conn.account })
|
|
196
|
+
: errorResult(`No saved session for ${connectionLabel(conn)}.`);
|
|
197
|
+
});
|
|
198
|
+
},
|
|
199
|
+
};
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calendar and meeting tools.
|
|
3
|
+
*
|
|
4
|
+
* "Create a Teams meeting" means a calendar event with a join link and real
|
|
5
|
+
* invitations, so that is what `teams_create_meeting` does by default; passing
|
|
6
|
+
* no attendees and `calendarEvent: false` gives the bare join link instead.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Type } from "typebox";
|
|
10
|
+
import {
|
|
11
|
+
cancelEvent,
|
|
12
|
+
createEvent,
|
|
13
|
+
createOnlineMeeting,
|
|
14
|
+
getEvent,
|
|
15
|
+
listEvents,
|
|
16
|
+
updateEvent,
|
|
17
|
+
} from "../graph/calendar.ts";
|
|
18
|
+
import { auditWrite } from "../safety/audit.ts";
|
|
19
|
+
import { assertAccess } from "../safety/index.ts";
|
|
20
|
+
import { formatEventDetail, formatEventList } from "../utils/formatting.ts";
|
|
21
|
+
import {
|
|
22
|
+
AccountParam,
|
|
23
|
+
LimitParam,
|
|
24
|
+
TenantParam,
|
|
25
|
+
connectionFor,
|
|
26
|
+
currentUser,
|
|
27
|
+
errorResult,
|
|
28
|
+
run,
|
|
29
|
+
textResult,
|
|
30
|
+
type ToolContext,
|
|
31
|
+
type ToolResult,
|
|
32
|
+
} from "./shared.ts";
|
|
33
|
+
|
|
34
|
+
export const teamsListMeetingsTool = {
|
|
35
|
+
name: "teams_list_meetings",
|
|
36
|
+
description:
|
|
37
|
+
"List the signed-in user's calendar events and Teams meetings in a time range (default: the next 7 days). " +
|
|
38
|
+
"Recurring series are expanded into actual occurrences. Use this for 'what's on my calendar', " +
|
|
39
|
+
"'when is my next meeting', or to find an event ID.",
|
|
40
|
+
parameters: Type.Object({
|
|
41
|
+
start: Type.Optional(
|
|
42
|
+
Type.String({ description: "ISO 8601 start of the range; defaults to now" }),
|
|
43
|
+
),
|
|
44
|
+
end: Type.Optional(
|
|
45
|
+
Type.String({ description: "ISO 8601 end of the range; defaults to 7 days from now" }),
|
|
46
|
+
),
|
|
47
|
+
account: AccountParam,
|
|
48
|
+
tenant: TenantParam,
|
|
49
|
+
limit: LimitParam,
|
|
50
|
+
}),
|
|
51
|
+
promptSnippet: "List the user's Teams meetings and calendar events",
|
|
52
|
+
|
|
53
|
+
async execute(
|
|
54
|
+
_toolCallId: string,
|
|
55
|
+
params: { start?: string; end?: string; account?: string; tenant?: string; limit?: number },
|
|
56
|
+
signal: AbortSignal | undefined,
|
|
57
|
+
_onUpdate: undefined,
|
|
58
|
+
ctx: ToolContext,
|
|
59
|
+
): Promise<ToolResult> {
|
|
60
|
+
return run(async () => {
|
|
61
|
+
const conn = connectionFor(ctx, params.account, params.tenant);
|
|
62
|
+
const events = await listEvents(conn, {
|
|
63
|
+
start: params.start,
|
|
64
|
+
end: params.end,
|
|
65
|
+
max: params.limit ?? 25,
|
|
66
|
+
signal,
|
|
67
|
+
});
|
|
68
|
+
return textResult(formatEventList(events), { count: events.length });
|
|
69
|
+
});
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const teamsGetMeetingTool = {
|
|
74
|
+
name: "teams_get_meeting",
|
|
75
|
+
description:
|
|
76
|
+
"Show one calendar event or Teams meeting in full: time, organizer, attendees, join link and notes. " +
|
|
77
|
+
"Get the eventId from teams_list_meetings.",
|
|
78
|
+
parameters: Type.Object({
|
|
79
|
+
eventId: Type.String({ description: "Calendar event ID" }),
|
|
80
|
+
account: AccountParam,
|
|
81
|
+
tenant: TenantParam,
|
|
82
|
+
}),
|
|
83
|
+
promptSnippet: "Show details of a Teams meeting",
|
|
84
|
+
|
|
85
|
+
async execute(
|
|
86
|
+
_toolCallId: string,
|
|
87
|
+
params: { eventId: string; account?: string; tenant?: string },
|
|
88
|
+
signal: AbortSignal | undefined,
|
|
89
|
+
_onUpdate: undefined,
|
|
90
|
+
ctx: ToolContext,
|
|
91
|
+
): Promise<ToolResult> {
|
|
92
|
+
return run(async () => {
|
|
93
|
+
const conn = connectionFor(ctx, params.account, params.tenant);
|
|
94
|
+
const event = await getEvent(conn, params.eventId, signal);
|
|
95
|
+
if (!event) return errorResult(`Event ${params.eventId} not found.`);
|
|
96
|
+
return textResult(formatEventDetail(event), { eventId: event.id });
|
|
97
|
+
});
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export const teamsCreateMeetingTool = {
|
|
102
|
+
name: "teams_create_meeting",
|
|
103
|
+
description:
|
|
104
|
+
"Schedule a Microsoft Teams meeting as the signed-in user: creates a calendar event with a join link " +
|
|
105
|
+
"and sends invitations to the attendees. Times are ISO 8601 local date-times " +
|
|
106
|
+
"(e.g. '2026-09-15T10:00:00') interpreted in 'timeZone'. " +
|
|
107
|
+
"Set calendarEvent: false for a bare join link with no calendar entry or invitations.",
|
|
108
|
+
parameters: Type.Object({
|
|
109
|
+
subject: Type.String({ description: "Meeting title" }),
|
|
110
|
+
start: Type.String({ description: "Start, ISO 8601 local date-time, e.g. '2026-09-15T10:00:00'" }),
|
|
111
|
+
end: Type.String({ description: "End, ISO 8601 local date-time" }),
|
|
112
|
+
attendees: Type.Optional(
|
|
113
|
+
Type.Array(Type.String(), { description: "Required attendees, by e-mail address" }),
|
|
114
|
+
),
|
|
115
|
+
optionalAttendees: Type.Optional(
|
|
116
|
+
Type.Array(Type.String(), { description: "Optional attendees, by e-mail address" }),
|
|
117
|
+
),
|
|
118
|
+
timeZone: Type.Optional(
|
|
119
|
+
Type.String({ description: "IANA or Windows time zone, e.g. 'Europe/Berlin'. Default UTC." }),
|
|
120
|
+
),
|
|
121
|
+
body: Type.Optional(Type.String({ description: "Agenda or invitation text" })),
|
|
122
|
+
location: Type.Optional(Type.String({ description: "Physical location, if any" })),
|
|
123
|
+
calendarEvent: Type.Optional(
|
|
124
|
+
Type.Boolean({ description: "Create a calendar event (default true). False = join link only." }),
|
|
125
|
+
),
|
|
126
|
+
account: AccountParam,
|
|
127
|
+
tenant: TenantParam,
|
|
128
|
+
}),
|
|
129
|
+
promptSnippet: "Schedule a Teams meeting",
|
|
130
|
+
promptGuidelines: [
|
|
131
|
+
"Confirm date, time, time zone and attendee list with the user before creating a meeting — invitations cannot be un-sent.",
|
|
132
|
+
"Pass attendees as e-mail addresses; use teams_find_user to resolve names first.",
|
|
133
|
+
],
|
|
134
|
+
|
|
135
|
+
async execute(
|
|
136
|
+
_toolCallId: string,
|
|
137
|
+
params: {
|
|
138
|
+
subject: string;
|
|
139
|
+
start: string;
|
|
140
|
+
end: string;
|
|
141
|
+
attendees?: string[];
|
|
142
|
+
optionalAttendees?: string[];
|
|
143
|
+
timeZone?: string;
|
|
144
|
+
body?: string;
|
|
145
|
+
location?: string;
|
|
146
|
+
calendarEvent?: boolean;
|
|
147
|
+
account?: string;
|
|
148
|
+
tenant?: string;
|
|
149
|
+
},
|
|
150
|
+
signal: AbortSignal | undefined,
|
|
151
|
+
_onUpdate: undefined,
|
|
152
|
+
ctx: ToolContext,
|
|
153
|
+
): Promise<ToolResult> {
|
|
154
|
+
return run(async () => {
|
|
155
|
+
const conn = connectionFor(ctx, params.account, params.tenant);
|
|
156
|
+
const me = await currentUser(conn, signal).catch(() => undefined);
|
|
157
|
+
|
|
158
|
+
// Inviting someone is contacting them, so the people rules apply.
|
|
159
|
+
for (const address of [...(params.attendees ?? []), ...(params.optionalAttendees ?? [])]) {
|
|
160
|
+
assertAccess(conn, "write", "people", address, [address]);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (params.calendarEvent === false) {
|
|
164
|
+
const meeting = await createOnlineMeeting(
|
|
165
|
+
conn,
|
|
166
|
+
{ subject: params.subject, start: params.start, end: params.end },
|
|
167
|
+
signal,
|
|
168
|
+
);
|
|
169
|
+
auditWrite(conn.audit, {
|
|
170
|
+
tool: "teams_create_meeting",
|
|
171
|
+
account: conn.account,
|
|
172
|
+
tenant: conn.tenant,
|
|
173
|
+
actor: me?.upn,
|
|
174
|
+
target: "calendar",
|
|
175
|
+
summary: `created ad-hoc meeting "${params.subject}"`,
|
|
176
|
+
});
|
|
177
|
+
return textResult(
|
|
178
|
+
[`✅ Meeting link created for "${params.subject}".`, "", `Join: ${meeting.joinUrl}`].join("\n"),
|
|
179
|
+
{ meetingId: meeting.id, joinUrl: meeting.joinUrl },
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const event = await createEvent(
|
|
184
|
+
conn,
|
|
185
|
+
{
|
|
186
|
+
subject: params.subject,
|
|
187
|
+
start: params.start,
|
|
188
|
+
end: params.end,
|
|
189
|
+
timeZone: params.timeZone,
|
|
190
|
+
attendees: params.attendees,
|
|
191
|
+
optionalAttendees: params.optionalAttendees,
|
|
192
|
+
body: params.body,
|
|
193
|
+
location: params.location,
|
|
194
|
+
onlineMeeting: true,
|
|
195
|
+
},
|
|
196
|
+
signal,
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
auditWrite(conn.audit, {
|
|
200
|
+
tool: "teams_create_meeting",
|
|
201
|
+
account: conn.account,
|
|
202
|
+
tenant: conn.tenant,
|
|
203
|
+
actor: me?.upn,
|
|
204
|
+
target: "calendar",
|
|
205
|
+
summary:
|
|
206
|
+
`created "${params.subject}" ${params.start}–${params.end} ` +
|
|
207
|
+
`with ${(params.attendees ?? []).join(", ") || "no attendees"}`,
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
return textResult(
|
|
211
|
+
[
|
|
212
|
+
`✅ Meeting **${event.subject}** scheduled as ${me?.displayName ?? "you"}.`,
|
|
213
|
+
"",
|
|
214
|
+
formatEventDetail(event),
|
|
215
|
+
].join("\n"),
|
|
216
|
+
{ eventId: event.id, joinUrl: event.joinUrl },
|
|
217
|
+
);
|
|
218
|
+
});
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
export const teamsUpdateMeetingTool = {
|
|
223
|
+
name: "teams_update_meeting",
|
|
224
|
+
description:
|
|
225
|
+
"Change an existing calendar event the signed-in user organizes: time, subject, attendees, location or " +
|
|
226
|
+
"agenda. Attendees receive an update. Only the fields you pass are changed.",
|
|
227
|
+
parameters: Type.Object({
|
|
228
|
+
eventId: Type.String({ description: "Calendar event ID" }),
|
|
229
|
+
subject: Type.Optional(Type.String({ description: "New title" })),
|
|
230
|
+
start: Type.Optional(Type.String({ description: "New start, ISO 8601 local date-time" })),
|
|
231
|
+
end: Type.Optional(Type.String({ description: "New end, ISO 8601 local date-time" })),
|
|
232
|
+
timeZone: Type.Optional(Type.String({ description: "Time zone for the new times" })),
|
|
233
|
+
attendees: Type.Optional(
|
|
234
|
+
Type.Array(Type.String(), { description: "Replacement list of required attendees (e-mail)" }),
|
|
235
|
+
),
|
|
236
|
+
optionalAttendees: Type.Optional(
|
|
237
|
+
Type.Array(Type.String(), { description: "Replacement list of optional attendees (e-mail)" }),
|
|
238
|
+
),
|
|
239
|
+
body: Type.Optional(Type.String({ description: "New agenda text" })),
|
|
240
|
+
location: Type.Optional(Type.String({ description: "New location" })),
|
|
241
|
+
account: AccountParam,
|
|
242
|
+
tenant: TenantParam,
|
|
243
|
+
}),
|
|
244
|
+
promptSnippet: "Update a Teams meeting",
|
|
245
|
+
|
|
246
|
+
async execute(
|
|
247
|
+
_toolCallId: string,
|
|
248
|
+
params: {
|
|
249
|
+
eventId: string;
|
|
250
|
+
subject?: string;
|
|
251
|
+
start?: string;
|
|
252
|
+
end?: string;
|
|
253
|
+
timeZone?: string;
|
|
254
|
+
attendees?: string[];
|
|
255
|
+
optionalAttendees?: string[];
|
|
256
|
+
body?: string;
|
|
257
|
+
location?: string;
|
|
258
|
+
account?: string;
|
|
259
|
+
tenant?: string;
|
|
260
|
+
},
|
|
261
|
+
signal: AbortSignal | undefined,
|
|
262
|
+
_onUpdate: undefined,
|
|
263
|
+
ctx: ToolContext,
|
|
264
|
+
): Promise<ToolResult> {
|
|
265
|
+
return run(async () => {
|
|
266
|
+
const conn = connectionFor(ctx, params.account, params.tenant);
|
|
267
|
+
|
|
268
|
+
for (const address of [...(params.attendees ?? []), ...(params.optionalAttendees ?? [])]) {
|
|
269
|
+
assertAccess(conn, "write", "people", address, [address]);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const event = await updateEvent(
|
|
273
|
+
conn,
|
|
274
|
+
params.eventId,
|
|
275
|
+
{
|
|
276
|
+
subject: params.subject,
|
|
277
|
+
start: params.start,
|
|
278
|
+
end: params.end,
|
|
279
|
+
timeZone: params.timeZone,
|
|
280
|
+
attendees: params.attendees,
|
|
281
|
+
optionalAttendees: params.optionalAttendees,
|
|
282
|
+
body: params.body,
|
|
283
|
+
location: params.location,
|
|
284
|
+
},
|
|
285
|
+
signal,
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
const me = await currentUser(conn, signal).catch(() => undefined);
|
|
289
|
+
auditWrite(conn.audit, {
|
|
290
|
+
tool: "teams_update_meeting",
|
|
291
|
+
account: conn.account,
|
|
292
|
+
tenant: conn.tenant,
|
|
293
|
+
actor: me?.upn,
|
|
294
|
+
target: "calendar",
|
|
295
|
+
summary: `updated event ${params.eventId}`,
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
return textResult([`✅ Meeting updated.`, "", formatEventDetail(event)].join("\n"), {
|
|
299
|
+
eventId: event.id,
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
},
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
export const teamsCancelMeetingTool = {
|
|
306
|
+
name: "teams_cancel_meeting",
|
|
307
|
+
description:
|
|
308
|
+
"Cancel a meeting the signed-in user organizes — attendees receive a cancellation notice. " +
|
|
309
|
+
"For a meeting organized by somebody else, the event is removed from the user's own calendar instead.",
|
|
310
|
+
parameters: Type.Object({
|
|
311
|
+
eventId: Type.String({ description: "Calendar event ID" }),
|
|
312
|
+
comment: Type.Optional(Type.String({ description: "Note sent with the cancellation" })),
|
|
313
|
+
account: AccountParam,
|
|
314
|
+
tenant: TenantParam,
|
|
315
|
+
}),
|
|
316
|
+
promptSnippet: "Cancel a Teams meeting",
|
|
317
|
+
|
|
318
|
+
async execute(
|
|
319
|
+
_toolCallId: string,
|
|
320
|
+
params: { eventId: string; comment?: string; account?: string; tenant?: string },
|
|
321
|
+
signal: AbortSignal | undefined,
|
|
322
|
+
_onUpdate: undefined,
|
|
323
|
+
ctx: ToolContext,
|
|
324
|
+
): Promise<ToolResult> {
|
|
325
|
+
return run(async () => {
|
|
326
|
+
const conn = connectionFor(ctx, params.account, params.tenant);
|
|
327
|
+
await cancelEvent(conn, params.eventId, params.comment, signal);
|
|
328
|
+
|
|
329
|
+
const me = await currentUser(conn, signal).catch(() => undefined);
|
|
330
|
+
auditWrite(conn.audit, {
|
|
331
|
+
tool: "teams_cancel_meeting",
|
|
332
|
+
account: conn.account,
|
|
333
|
+
tenant: conn.tenant,
|
|
334
|
+
actor: me?.upn,
|
|
335
|
+
target: "calendar",
|
|
336
|
+
summary: `cancelled event ${params.eventId}`,
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
return textResult(`✅ Meeting ${params.eventId} cancelled.`, { eventId: params.eventId });
|
|
340
|
+
});
|
|
341
|
+
},
|
|
342
|
+
};
|