@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,114 @@
1
+ /**
2
+ * teams_delete_message — soft-delete one of the user's own messages.
3
+ *
4
+ * Graph only permits this for the sender, which is also the only case that
5
+ * makes sense here: pi may take back something it said in the user's name, not
6
+ * edit anyone else's history.
7
+ */
8
+
9
+ import { Type } from "typebox";
10
+ import { softDeleteMessage, undoSoftDeleteMessage } from "../graph/messages.ts";
11
+ import { auditWrite } from "../safety/audit.ts";
12
+ import { requireChannel, requireChat } from "./resolve.ts";
13
+ import {
14
+ AccountParam,
15
+ TenantParam,
16
+ connectionFor,
17
+ currentUser,
18
+ errorResult,
19
+ run,
20
+ textResult,
21
+ type ToolContext,
22
+ type ToolResult,
23
+ } from "./shared.ts";
24
+ import type { MessageLocation } from "../types.ts";
25
+
26
+ export const teamsDeleteMessageTool = {
27
+ name: "teams_delete_message",
28
+ description:
29
+ "Delete a message the signed-in user sent in Microsoft Teams (soft delete, exactly like deleting it in " +
30
+ "the app). Specify 'chat' or 'channel' (plus 'team') to say where it lives. " +
31
+ "Set undo: true to restore a message that was soft-deleted. Only the sender's own messages can be deleted.",
32
+ parameters: Type.Object({
33
+ messageId: Type.String({ description: "ID of the message to delete" }),
34
+ chat: Type.Optional(Type.String({ description: "Chat ID or name, when the message is in a chat" })),
35
+ channel: Type.Optional(
36
+ Type.String({ description: "Channel name/ID or 'Team/Channel' path, when it is in a channel" }),
37
+ ),
38
+ team: Type.Optional(Type.String({ description: "Team name or ID" })),
39
+ replyId: Type.Optional(
40
+ Type.String({ description: "Reply ID, when deleting a reply inside a channel thread" }),
41
+ ),
42
+ undo: Type.Optional(Type.Boolean({ description: "Restore a previously deleted message" })),
43
+ account: AccountParam,
44
+ tenant: TenantParam,
45
+ }),
46
+ promptSnippet: "Delete a message the user sent in Teams",
47
+
48
+ async execute(
49
+ _toolCallId: string,
50
+ params: {
51
+ messageId: string;
52
+ chat?: string;
53
+ channel?: string;
54
+ team?: string;
55
+ replyId?: string;
56
+ undo?: boolean;
57
+ account?: string;
58
+ tenant?: string;
59
+ },
60
+ signal: AbortSignal | undefined,
61
+ _onUpdate: undefined,
62
+ ctx: ToolContext,
63
+ ): Promise<ToolResult> {
64
+ return run(async () => {
65
+ const conn = connectionFor(ctx, params.account, params.tenant);
66
+
67
+ let location: MessageLocation;
68
+ let target: string;
69
+
70
+ if (params.chat) {
71
+ const chat = await requireChat(conn, params.chat, "write", signal);
72
+ location = { kind: "chat", chatId: chat.id };
73
+ target = `chat:${chat.label}`;
74
+ } else if (params.channel) {
75
+ const { team, channel } = await requireChannel(conn, params.team, params.channel, "write", signal);
76
+ location = {
77
+ kind: "channel",
78
+ teamId: team.id,
79
+ teamName: team.displayName,
80
+ channelId: channel.id,
81
+ channelName: channel.displayName,
82
+ };
83
+ target = `channel:${team.displayName}/${channel.displayName}`;
84
+ } else {
85
+ return errorResult("Specify either 'chat' or 'channel' so pi knows where the message lives.");
86
+ }
87
+
88
+ // Graph needs an explicit user id for the chat form of this action, and
89
+ // only the sender may delete — so failing here is better than a 403.
90
+ const me = await currentUser(conn, signal);
91
+
92
+ const options = { replyId: params.replyId, signal };
93
+ if (params.undo) {
94
+ await undoSoftDeleteMessage(conn, location, params.messageId, me.id, options);
95
+ } else {
96
+ await softDeleteMessage(conn, location, params.messageId, me.id, options);
97
+ }
98
+
99
+ auditWrite(conn.audit, {
100
+ tool: "teams_delete_message",
101
+ account: conn.account,
102
+ tenant: conn.tenant,
103
+ actor: me.upn,
104
+ target,
105
+ summary: `${params.undo ? "restored" : "deleted"} message ${params.messageId}`,
106
+ });
107
+
108
+ return textResult(
109
+ `✅ Message ${params.messageId} ${params.undo ? "restored" : "deleted"} in ${target}.`,
110
+ { messageId: params.messageId, undone: !!params.undo },
111
+ );
112
+ });
113
+ },
114
+ };
@@ -0,0 +1,182 @@
1
+ /**
2
+ * teams_doctor — diagnose configuration, sign-in, consent and connectivity.
3
+ *
4
+ * Written to be the first thing anyone runs when something does not work, and
5
+ * to answer the question a 403 never answers: *which* permission is missing,
6
+ * and who has to grant it.
7
+ */
8
+
9
+ import { Type } from "typebox";
10
+ import {
11
+ getConfigPath,
12
+ resolveAllConnections,
13
+ DEFAULT_SCOPES,
14
+ type TeamsConnection,
15
+ } from "../config/index.ts";
16
+ import { getToken, getTokenPath, isFresh } from "../auth/token-store.ts";
17
+ import { tokenScopes } from "../auth/jwt.ts";
18
+ import { getAccessToken } from "../auth/index.ts";
19
+ import { getMe } from "../graph/me.ts";
20
+ import { listJoinedTeams } from "../graph/teams.ts";
21
+ import { formatGraphError, describeScope } from "../utils/errors.ts";
22
+ import { formatPermissions } from "../config/scope.ts";
23
+ import { getAuditPath } from "../safety/audit.ts";
24
+ import { run, textResult, type ToolResult } from "./shared.ts";
25
+
26
+ interface DoctorParams {
27
+ account?: string;
28
+ /** Skip the live Graph calls */
29
+ offline?: boolean;
30
+ }
31
+
32
+ async function checkConnection(
33
+ conn: TeamsConnection,
34
+ offline: boolean,
35
+ signal?: AbortSignal,
36
+ ): Promise<string[]> {
37
+ const label = conn.tenant === conn.account ? conn.account : `${conn.account}/${conn.tenant}`;
38
+ const lines = [`### ${label}`, ""];
39
+
40
+ lines.push(`- tenant: \`${conn.tenantId}\``);
41
+ lines.push(`- client: \`${conn.clientId}\``);
42
+ lines.push(`- auth mode: ${conn.authMode}`);
43
+ lines.push(`- safety level: ${conn.safetyLevel}`);
44
+
45
+ if (conn.authMode === "client-credentials") {
46
+ lines.push(
47
+ "- ⚠️ app-only: pi cannot post messages as a person with this mode " +
48
+ "(Microsoft Graph restricts app-only message writes to migration scenarios)",
49
+ );
50
+ }
51
+
52
+ const token = getToken(conn.account, conn.tenantId);
53
+ if (!token) {
54
+ lines.push("- ❌ not signed in — run `teams_login`");
55
+ return lines;
56
+ }
57
+
58
+ lines.push(
59
+ isFresh(token)
60
+ ? `- ✅ token valid until ${new Date(token.expiresAt).toLocaleString()}`
61
+ : token.refreshToken
62
+ ? "- ⚠️ access token expired, refresh token present (will renew on the next call)"
63
+ : "- ❌ token expired and no refresh token — run `teams_login`",
64
+ );
65
+
66
+ // Consent check works offline: the scopes are inside the token.
67
+ const granted = tokenScopes(token.accessToken);
68
+ if (granted.length > 0) {
69
+ const wanted = (conn.scopes.length > 0 ? conn.scopes : DEFAULT_SCOPES).filter(
70
+ (scope) => !["openid", "profile", "offline_access"].includes(scope),
71
+ );
72
+ const missing = wanted.filter(
73
+ (scope) => !granted.some((g) => g.toLowerCase() === scope.toLowerCase()),
74
+ );
75
+ if (missing.length === 0) {
76
+ lines.push(`- ✅ all ${wanted.length} requested scopes granted`);
77
+ } else {
78
+ lines.push("- ⚠️ missing consent:");
79
+ for (const scope of missing) {
80
+ const purpose = describeScope(scope);
81
+ lines.push(` - \`${scope}\`${purpose ? ` — needed for ${purpose}` : ""}`);
82
+ }
83
+ lines.push(
84
+ " Sign in again after an admin grants consent, or remove the unused scopes from the account's `scopes`.",
85
+ );
86
+ }
87
+ }
88
+
89
+ if (offline) return lines;
90
+
91
+ try {
92
+ await getAccessToken(conn, signal);
93
+ } catch (err) {
94
+ lines.push(`- ❌ could not obtain a token: ${formatGraphError(err)}`);
95
+ return lines;
96
+ }
97
+
98
+ try {
99
+ const me = await getMe(conn, signal);
100
+ lines.push(`- ✅ acting as **${me.displayName}** <${me.upn}>`);
101
+ } catch (err) {
102
+ lines.push(`- ❌ /me failed: ${formatGraphError(err)}`);
103
+ return lines;
104
+ }
105
+
106
+ try {
107
+ const teams = await listJoinedTeams(conn, 5, signal);
108
+ lines.push(`- ✅ can list teams (${teams.length === 5 ? "5+" : teams.length} visible)`);
109
+ } catch (err) {
110
+ lines.push(`- ⚠️ listing teams failed: ${formatGraphError(err)}`);
111
+ }
112
+
113
+ return lines;
114
+ }
115
+
116
+ export const teamsDoctorTool = {
117
+ name: "teams_doctor",
118
+ description:
119
+ "Diagnose the Microsoft Teams setup: configuration, sign-in state, which Graph scopes were actually " +
120
+ "granted, and whether live calls succeed — for every configured account and tenant. " +
121
+ "Run this first whenever a Teams tool fails.",
122
+ parameters: Type.Object({
123
+ account: Type.Optional(Type.String({ description: "Check only this account" })),
124
+ offline: Type.Optional(
125
+ Type.Boolean({ description: "Skip live Graph calls and only check config and cached tokens" }),
126
+ ),
127
+ }),
128
+ promptSnippet: "Check the Teams configuration and sign-in health",
129
+ promptGuidelines: [
130
+ "Run teams_doctor when any Teams tool returns an auth, permission or configuration error.",
131
+ "Report the missing scopes verbatim — the user may need an admin to consent to them.",
132
+ ],
133
+
134
+ async execute(
135
+ _toolCallId: string,
136
+ params: DoctorParams,
137
+ signal: AbortSignal | undefined,
138
+ _onUpdate: undefined,
139
+ _ctx: unknown,
140
+ ): Promise<ToolResult> {
141
+ return run(async () => {
142
+ const { connections, errors } = resolveAllConnections();
143
+ const selected = params.account
144
+ ? connections.filter((c) => c.account.toLowerCase() === params.account!.toLowerCase())
145
+ : connections;
146
+
147
+ const lines = ["## Teams doctor", ""];
148
+ lines.push(`- config: \`${getConfigPath()}\``);
149
+ lines.push(`- tokens: \`${getTokenPath()}\``);
150
+ lines.push(`- audit log: \`${getAuditPath()}\``);
151
+ lines.push("");
152
+
153
+ if (errors.length > 0) {
154
+ lines.push("### Configuration problems", "");
155
+ for (const error of errors) lines.push(`- ❌ ${error}`);
156
+ lines.push("");
157
+ }
158
+
159
+ if (selected.length === 0) {
160
+ lines.push(
161
+ params.account
162
+ ? `No connection found for account "${params.account}".`
163
+ : "No usable connections configured. Run `teams_setup` to add one.",
164
+ );
165
+ return textResult(lines.join("\n"), { ok: false });
166
+ }
167
+
168
+ for (const conn of selected) {
169
+ lines.push(...(await checkConnection(conn, params.offline === true, signal)));
170
+ lines.push("");
171
+ }
172
+
173
+ const first = selected[0]!;
174
+ lines.push("### Scope rules of the default connection", "", formatPermissions(first.permissions));
175
+
176
+ return textResult(lines.join("\n"), {
177
+ accounts: selected.map((c) => `${c.account}/${c.tenant}`),
178
+ configErrors: errors,
179
+ });
180
+ });
181
+ },
182
+ };
@@ -0,0 +1,64 @@
1
+ /**
2
+ * teams_find_user — look someone up in the directory.
3
+ */
4
+
5
+ import { Type } from "typebox";
6
+ import { findUsers } from "../graph/me.ts";
7
+ import { hasAccess } from "../safety/index.ts";
8
+ import {
9
+ AccountParam,
10
+ LimitParam,
11
+ TenantParam,
12
+ connectionFor,
13
+ run,
14
+ textResult,
15
+ type ToolContext,
16
+ type ToolResult,
17
+ } from "./shared.ts";
18
+
19
+ export const teamsFindUserTool = {
20
+ name: "teams_find_user",
21
+ description:
22
+ "Find people in the organization's directory by name, e-mail or user principal name. " +
23
+ "Use this to get the exact identity before starting a chat, @-mentioning someone, or " +
24
+ "inviting them to a meeting.",
25
+ parameters: Type.Object({
26
+ query: Type.String({ description: "Name, e-mail or UPN to search for" }),
27
+ account: AccountParam,
28
+ tenant: TenantParam,
29
+ limit: LimitParam,
30
+ }),
31
+ promptSnippet: "Find a person in the Teams directory",
32
+ promptGuidelines: [
33
+ "Use teams_find_user when the user refers to a colleague by first name only and an exact identity is needed.",
34
+ ],
35
+
36
+ async execute(
37
+ _toolCallId: string,
38
+ params: { query: string; account?: string; tenant?: string; limit?: number },
39
+ signal: AbortSignal | undefined,
40
+ _onUpdate: undefined,
41
+ ctx: ToolContext,
42
+ ): Promise<ToolResult> {
43
+ return run(async () => {
44
+ const conn = connectionFor(ctx, params.account, params.tenant);
45
+ const people = await findUsers(conn, params.query, params.limit ?? 15, signal);
46
+
47
+ const visible = people.filter((person) =>
48
+ hasAccess(conn, "read", "people", [person.displayName, person.upn, person.mail, person.id]),
49
+ );
50
+
51
+ if (visible.length === 0) {
52
+ return textResult(`No directory match for "${params.query}".`, { count: 0 });
53
+ }
54
+
55
+ const lines = [`Found ${visible.length} person(s) for "${params.query}":`, ""];
56
+ for (const person of visible) {
57
+ lines.push(`- **${person.displayName}**${person.upn ? ` <${person.upn}>` : ""}`);
58
+ if (person.id) lines.push(` userId: ${person.id}`);
59
+ }
60
+
61
+ return textResult(lines.join("\n"), { count: visible.length, people: visible });
62
+ });
63
+ },
64
+ };
@@ -0,0 +1,142 @@
1
+ /**
2
+ * teams_inbox — what needs the user's attention right now.
3
+ *
4
+ * Teams has no single "unread" endpoint a delegated token can rely on, so this
5
+ * composes the two signals that are reliable: recent chat activity (with the
6
+ * last message and who sent it) and a search for mentions of the user. It
7
+ * deliberately answers "what changed since you last looked", not "list
8
+ * everything".
9
+ */
10
+
11
+ import { Type } from "typebox";
12
+ import { listChats } from "../graph/chats.ts";
13
+ import { searchMessages } from "../graph/search.ts";
14
+ import { chatCandidates } from "../graph/mappers.ts";
15
+ import { hasAccess } from "../safety/index.ts";
16
+ import { formatRelative, truncate } from "../utils/formatting.ts";
17
+ import {
18
+ AccountParam,
19
+ LimitParam,
20
+ TenantParam,
21
+ connectionFor,
22
+ currentUser,
23
+ run,
24
+ textResult,
25
+ type ToolContext,
26
+ type ToolResult,
27
+ } from "./shared.ts";
28
+
29
+ export const teamsInboxTool = {
30
+ name: "teams_inbox",
31
+ description:
32
+ "Show what needs the user's attention in Microsoft Teams: chats with recent activity where the last " +
33
+ "message came from someone else, plus recent @-mentions of the user across chats and channels. " +
34
+ "Use this for 'what did I miss', 'anything new in Teams', or a morning catch-up.",
35
+ parameters: Type.Object({
36
+ account: AccountParam,
37
+ tenant: TenantParam,
38
+ limit: LimitParam,
39
+ hours: Type.Optional(
40
+ Type.Number({ description: "Only consider activity from the last N hours (default 24)" }),
41
+ ),
42
+ includeMentions: Type.Optional(
43
+ Type.Boolean({ description: "Also search for @-mentions of the user (default true)" }),
44
+ ),
45
+ }),
46
+ promptSnippet: "Show new Teams activity and mentions",
47
+ promptGuidelines: [
48
+ "Use teams_inbox for open-ended catch-up questions about Teams.",
49
+ "Summarize what is waiting and who is waiting on it; do not reply to anything unless the user asks.",
50
+ ],
51
+
52
+ async execute(
53
+ _toolCallId: string,
54
+ params: {
55
+ account?: string;
56
+ tenant?: string;
57
+ limit?: number;
58
+ hours?: number;
59
+ includeMentions?: boolean;
60
+ },
61
+ signal: AbortSignal | undefined,
62
+ _onUpdate: undefined,
63
+ ctx: ToolContext,
64
+ ): Promise<ToolResult> {
65
+ return run(async () => {
66
+ const conn = connectionFor(ctx, params.account, params.tenant);
67
+ const me = await currentUser(conn, signal).catch(() => undefined);
68
+ const cutoff = Date.now() - (params.hours ?? 24) * 3600 * 1000;
69
+ const limit = params.limit ?? 15;
70
+
71
+ const chats = await listChats(conn, { max: 50, meId: me?.id, signal });
72
+
73
+ const active = chats
74
+ .filter((chat) => hasAccess(conn, "read", "chats", chatCandidates(chat)))
75
+ .filter((chat) => {
76
+ if (!chat.lastUpdated) return false;
77
+ if (new Date(chat.lastUpdated).getTime() < cutoff) return false;
78
+ // Something the user themselves said last is not waiting on them.
79
+ if (me && chat.lastMessageFrom && chat.lastMessageFrom === me.displayName) return false;
80
+ return true;
81
+ })
82
+ .slice(0, limit);
83
+
84
+ const lines = [`## Teams inbox — last ${params.hours ?? 24} h`, ""];
85
+
86
+ if (active.length === 0) {
87
+ lines.push("No chat activity waiting for you.", "");
88
+ } else {
89
+ lines.push(`### ${active.length} chat(s) with new messages`, "");
90
+ for (const chat of active) {
91
+ lines.push(`- **${chat.label}** · ${formatRelative(chat.lastUpdated)}`);
92
+ if (chat.lastMessagePreview) {
93
+ const who = chat.lastMessageFrom ? `${chat.lastMessageFrom}: ` : "";
94
+ lines.push(` ${who}${truncate(chat.lastMessagePreview, 140)}`);
95
+ }
96
+ lines.push(` chatId: ${chat.id}`);
97
+ }
98
+ lines.push("");
99
+ }
100
+
101
+ if (params.includeMentions !== false && me) {
102
+ try {
103
+ const hits = await searchMessages(conn, `"${me.displayName}"`, { max: 15, signal });
104
+ const recent = hits.filter((hit) => {
105
+ const created = hit.message.createdDateTime;
106
+ if (!created || new Date(created).getTime() < cutoff) return false;
107
+ // Only real mentions, not every message containing the name.
108
+ return hit.message.mentions.some(
109
+ (mention) => mention.id === me.id || mention.displayName === me.displayName,
110
+ );
111
+ });
112
+
113
+ if (recent.length > 0) {
114
+ lines.push(`### ${recent.length} mention(s) of you`, "");
115
+ for (const hit of recent) {
116
+ const where = hit.channelIdentity?.channelId
117
+ ? `channel ${hit.channelIdentity.channelId}`
118
+ : hit.chatId
119
+ ? `chat ${hit.chatId}`
120
+ : "unknown";
121
+ lines.push(
122
+ `- **${hit.message.from?.displayName ?? "(unknown)"}** · ${formatRelative(hit.message.createdDateTime)} · ${where}`,
123
+ );
124
+ lines.push(` ${truncate(hit.message.text, 160)}`);
125
+ lines.push(` messageId: ${hit.message.id}`);
126
+ }
127
+ lines.push("");
128
+ }
129
+ } catch {
130
+ // Search needs its own consent and is not granted everywhere; the
131
+ // chat half of the answer is still worth returning.
132
+ lines.push("_Mention search unavailable (Microsoft Search may not be consented)._", "");
133
+ }
134
+ }
135
+
136
+ return textResult(lines.join("\n").trimEnd(), {
137
+ chats: active.length,
138
+ account: conn.account,
139
+ });
140
+ });
141
+ },
142
+ };
@@ -0,0 +1,62 @@
1
+ /**
2
+ * teams_list_channels — channels of a team, filtered by the read rules.
3
+ */
4
+
5
+ import { Type } from "typebox";
6
+ import { listChannels } from "../graph/teams.ts";
7
+ import { channelCandidates } from "../graph/mappers.ts";
8
+ import { hasAccess } from "../safety/index.ts";
9
+ import { formatChannelList } from "../utils/formatting.ts";
10
+ import { requireTeam } from "./resolve.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 teamsListChannelsTool = {
23
+ name: "teams_list_channels",
24
+ description:
25
+ "List the channels of a Microsoft Teams team. Accepts the team's display name or ID. " +
26
+ "Channels excluded by the configured read rules are not shown.",
27
+ parameters: Type.Object({
28
+ team: Type.String({ description: "Team display name or ID" }),
29
+ account: AccountParam,
30
+ tenant: TenantParam,
31
+ limit: LimitParam,
32
+ }),
33
+ promptSnippet: "List the channels of a Teams team",
34
+
35
+ async execute(
36
+ _toolCallId: string,
37
+ params: { team: string; account?: string; tenant?: string; limit?: number },
38
+ signal: AbortSignal | undefined,
39
+ _onUpdate: undefined,
40
+ ctx: ToolContext,
41
+ ): Promise<ToolResult> {
42
+ return run(async () => {
43
+ const conn = connectionFor(ctx, params.account, params.tenant);
44
+ const team = await requireTeam(conn, params.team, "read", signal);
45
+
46
+ const all = await listChannels(conn, team, params.limit ?? 100, signal);
47
+ const visible = all.filter((channel) =>
48
+ hasAccess(conn, "read", "channels", channelCandidates(channel)),
49
+ );
50
+
51
+ const hidden = all.length - visible.length;
52
+ const note = hidden > 0 ? `\n\n_${hidden} channel(s) hidden by your scope rules._` : "";
53
+
54
+ return textResult(`${formatChannelList(visible)}${note}`, {
55
+ team: team.displayName,
56
+ teamId: team.id,
57
+ count: visible.length,
58
+ hidden,
59
+ });
60
+ });
61
+ },
62
+ };
@@ -0,0 +1,74 @@
1
+ /**
2
+ * teams_list_chats — recent chats with a preview of the last message.
3
+ */
4
+
5
+ import { Type } from "typebox";
6
+ import { listChats } from "../graph/chats.ts";
7
+ import { chatCandidates } from "../graph/mappers.ts";
8
+ import { hasAccess } from "../safety/index.ts";
9
+ import { formatChatList } from "../utils/formatting.ts";
10
+ import {
11
+ AccountParam,
12
+ LimitParam,
13
+ TenantParam,
14
+ connectionFor,
15
+ currentUser,
16
+ run,
17
+ textResult,
18
+ type ToolContext,
19
+ type ToolResult,
20
+ } from "./shared.ts";
21
+
22
+ export const teamsListChatsTool = {
23
+ name: "teams_list_chats",
24
+ description:
25
+ "List the signed-in user's Microsoft Teams chats, most recent activity first, each with a preview of " +
26
+ "the last message. Chats excluded by the configured read rules are not shown. " +
27
+ "Use this to find a chat ID before reading or replying.",
28
+ parameters: Type.Object({
29
+ account: AccountParam,
30
+ tenant: TenantParam,
31
+ limit: LimitParam,
32
+ filter: Type.Optional(
33
+ Type.String({ description: "Only show chats whose label or participants contain this text" }),
34
+ ),
35
+ }),
36
+ promptSnippet: "List the user's recent Teams chats",
37
+
38
+ async execute(
39
+ _toolCallId: string,
40
+ params: { account?: string; tenant?: string; limit?: number; filter?: string },
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
+ const me = await currentUser(conn, signal).catch(() => undefined);
48
+
49
+ const all = await listChats(conn, {
50
+ max: params.limit ?? conn.maxMessages,
51
+ meId: me?.id,
52
+ signal,
53
+ });
54
+
55
+ const visible = all.filter((chat) => hasAccess(conn, "read", "chats", chatCandidates(chat)));
56
+ const filtered = params.filter
57
+ ? visible.filter((chat) =>
58
+ `${chat.label} ${chat.members.map((m) => m.displayName).join(" ")}`
59
+ .toLowerCase()
60
+ .includes(params.filter!.toLowerCase()),
61
+ )
62
+ : visible;
63
+
64
+ const hidden = all.length - visible.length;
65
+ const note = hidden > 0 ? `\n\n_${hidden} chat(s) hidden by your scope rules._` : "";
66
+
67
+ return textResult(`${formatChatList(filtered)}${note}`, {
68
+ count: filtered.length,
69
+ hidden,
70
+ account: conn.account,
71
+ });
72
+ });
73
+ },
74
+ };
@@ -0,0 +1,54 @@
1
+ /**
2
+ * teams_list_files — files in a channel's SharePoint folder.
3
+ */
4
+
5
+ import { Type } from "typebox";
6
+ import { listChannelFiles } from "../graph/teams.ts";
7
+ import { formatFileList } from "../utils/formatting.ts";
8
+ import { requireChannel } from "./resolve.ts";
9
+ import {
10
+ AccountParam,
11
+ LimitParam,
12
+ TenantParam,
13
+ connectionFor,
14
+ run,
15
+ textResult,
16
+ type ToolContext,
17
+ type ToolResult,
18
+ } from "./shared.ts";
19
+
20
+ export const teamsListFilesTool = {
21
+ name: "teams_list_files",
22
+ description:
23
+ "List the files stored in a Microsoft Teams channel (its SharePoint folder). Returns names, sizes, " +
24
+ "last-modified times and web links. Use the link to open or download a file — this tool does not " +
25
+ "download file contents.",
26
+ parameters: Type.Object({
27
+ channel: Type.String({ description: "Channel name or ID, or a 'Team/Channel' path" }),
28
+ team: Type.Optional(Type.String({ description: "Team name or ID (omit when using a path)" })),
29
+ account: AccountParam,
30
+ tenant: TenantParam,
31
+ limit: LimitParam,
32
+ }),
33
+ promptSnippet: "List files in a Teams channel",
34
+
35
+ async execute(
36
+ _toolCallId: string,
37
+ params: { channel: string; team?: string; account?: string; tenant?: string; limit?: number },
38
+ signal: AbortSignal | undefined,
39
+ _onUpdate: undefined,
40
+ ctx: ToolContext,
41
+ ): Promise<ToolResult> {
42
+ return run(async () => {
43
+ const conn = connectionFor(ctx, params.account, params.tenant);
44
+ const { team, channel } = await requireChannel(conn, params.team, params.channel, "read", signal);
45
+
46
+ const files = await listChannelFiles(conn, team.id, channel.id, params.limit ?? 50, signal);
47
+
48
+ return textResult(
49
+ formatFileList(files, `Files in ${team.displayName}/${channel.displayName}`),
50
+ { teamId: team.id, channelId: channel.id, count: files.length },
51
+ );
52
+ });
53
+ },
54
+ };