@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,169 @@
1
+ /**
2
+ * teams_permissions — inspect, test and change the guardrails.
3
+ *
4
+ * "Test" is the interesting action: it answers "would pi be allowed to post in
5
+ * Engineering/Announcements?" without posting anything, which is how a user
6
+ * builds confidence in a rule set before turning safetyLevel to "open".
7
+ */
8
+
9
+ import { Type } from "typebox";
10
+ import { setPermissionBlock } from "../config/index.ts";
11
+ import {
12
+ checkScope,
13
+ formatPermissions,
14
+ SCOPE_CATEGORIES,
15
+ type PermissionBlock,
16
+ type ScopeCategory,
17
+ type ScopeList,
18
+ type ScopeMode,
19
+ } from "../config/scope.ts";
20
+ import {
21
+ AccountParam,
22
+ TenantParam,
23
+ connectionFor,
24
+ connectionLabel,
25
+ errorResult,
26
+ run,
27
+ textResult,
28
+ type ToolContext,
29
+ type ToolResult,
30
+ } from "./shared.ts";
31
+
32
+ interface PermissionsParams {
33
+ action?: string;
34
+ account?: string;
35
+ tenant?: string;
36
+ category?: string;
37
+ mode?: string;
38
+ target?: string;
39
+ allow?: string[];
40
+ deny?: string[];
41
+ }
42
+
43
+ const isCategory = (value: string): value is ScopeCategory =>
44
+ (SCOPE_CATEGORIES as string[]).includes(value);
45
+
46
+ export const teamsPermissionsTool = {
47
+ name: "teams_permissions",
48
+ description:
49
+ "Inspect or change what pi is allowed to do in Teams. " +
50
+ "action 'show' (default) prints the effective allow/deny rules; " +
51
+ "action 'test' checks whether a specific team, channel, chat or person is in scope without touching it; " +
52
+ "action 'set' writes an allow/deny list for one category. " +
53
+ "Rules apply per category (teams, channels, chats, people) and per mode (read, write).",
54
+ parameters: Type.Object({
55
+ action: Type.Optional(Type.String({ description: "'show' (default), 'test' or 'set'" })),
56
+ account: AccountParam,
57
+ tenant: TenantParam,
58
+ category: Type.Optional(
59
+ Type.String({ description: "'teams', 'channels', 'chats' or 'people'" }),
60
+ ),
61
+ mode: Type.Optional(Type.String({ description: "'read' or 'write' (default 'write')" })),
62
+ target: Type.Optional(
63
+ Type.String({
64
+ description: "For action 'test': what to check, e.g. 'Engineering/Announcements' or 'anna@contoso.com'",
65
+ }),
66
+ ),
67
+ allow: Type.Optional(Type.Array(Type.String(), { description: "For action 'set': allow patterns" })),
68
+ deny: Type.Optional(Type.Array(Type.String(), { description: "For action 'set': deny patterns" })),
69
+ }),
70
+ promptSnippet: "Show or change what pi may do in Teams",
71
+ promptGuidelines: [
72
+ "Use teams_permissions with action 'test' before telling the user that pi can or cannot reach a channel.",
73
+ "When a tool fails with a scope error, show the user the matching rule instead of retrying.",
74
+ ],
75
+
76
+ async execute(
77
+ _toolCallId: string,
78
+ params: PermissionsParams,
79
+ _signal: AbortSignal | undefined,
80
+ _onUpdate: undefined,
81
+ ctx: ToolContext,
82
+ ): Promise<ToolResult> {
83
+ return run(async () => {
84
+ const action = (params.action ?? "show").toLowerCase();
85
+ const conn = connectionFor(ctx, params.account, params.tenant);
86
+ const label = connectionLabel(conn);
87
+
88
+ if (action === "show") {
89
+ return textResult(
90
+ [
91
+ `## What pi may do in Teams (${label})`,
92
+ "",
93
+ `Safety level: **${conn.safetyLevel}** · auth mode: **${conn.authMode}**`,
94
+ "",
95
+ formatPermissions(conn.permissions),
96
+ "",
97
+ "_Patterns are case-insensitive globs. Channels match on `Team/Channel`, their name, or their ID; " +
98
+ "chats match on topic, participant names and e-mail addresses._",
99
+ ].join("\n"),
100
+ { safetyLevel: conn.safetyLevel, permissions: conn.permissions },
101
+ );
102
+ }
103
+
104
+ if (action === "test") {
105
+ if (!params.target) return errorResult("action 'test' requires a target.");
106
+ const category = (params.category ?? "channels").toLowerCase();
107
+ if (!isCategory(category)) {
108
+ return errorResult(`Unknown category "${category}". Use teams, channels, chats or people.`);
109
+ }
110
+ const mode = ((params.mode ?? "write").toLowerCase() === "read" ? "read" : "write") as ScopeMode;
111
+
112
+ const decision = checkScope(conn.permissions, mode, category, [params.target]);
113
+ return textResult(
114
+ [
115
+ `${decision.allowed ? "✅ Allowed" : "🚫 Blocked"}: ${mode} ${category} "${params.target}" (${label})`,
116
+ "",
117
+ decision.reason,
118
+ ].join("\n"),
119
+ { allowed: decision.allowed, reason: decision.reason, pattern: decision.pattern },
120
+ );
121
+ }
122
+
123
+ if (action === "set") {
124
+ const category = (params.category ?? "").toLowerCase();
125
+ if (!isCategory(category)) {
126
+ return errorResult(
127
+ `action 'set' requires a category: teams, channels, chats or people.`,
128
+ );
129
+ }
130
+ if (!params.allow?.length && !params.deny?.length) {
131
+ return errorResult("action 'set' requires at least one allow or deny pattern.");
132
+ }
133
+
134
+ const list: ScopeList = {};
135
+ if (params.allow?.length) list.allow = params.allow;
136
+ if (params.deny?.length) list.deny = params.deny;
137
+
138
+ const mode = params.mode?.toLowerCase();
139
+ const block: PermissionBlock = {};
140
+ if (mode === "read" || mode === "write") {
141
+ block[mode] = { [category]: list };
142
+ } else {
143
+ block[category] = list;
144
+ }
145
+
146
+ // The block replaces the rules at this level; anything the user wants
147
+ // to keep must be passed again — stated plainly in the result.
148
+ setPermissionBlock(block, params.account, params.tenant);
149
+
150
+ return textResult(
151
+ [
152
+ `✅ Scope rules for **${category}** updated ` +
153
+ `${params.account ? (params.tenant ? `on ${params.account}/${params.tenant}` : `on account ${params.account}`) : "globally"}.`,
154
+ "",
155
+ params.allow?.length ? `allow: ${params.allow.join(", ")}` : "",
156
+ params.deny?.length ? `deny: ${params.deny.join(", ")}` : "",
157
+ "",
158
+ "_This replaces the permission block at that level. Re-run teams_permissions with action 'show' to check the result._",
159
+ ]
160
+ .filter(Boolean)
161
+ .join("\n"),
162
+ { category, allow: params.allow, deny: params.deny },
163
+ );
164
+ }
165
+
166
+ return errorResult(`Unknown action "${action}". Use 'show', 'test' or 'set'.`);
167
+ });
168
+ },
169
+ };
@@ -0,0 +1,216 @@
1
+ /**
2
+ * Presence tools — read who is around, set the user's own status.
3
+ */
4
+
5
+ import { Type } from "typebox";
6
+ import {
7
+ AVAILABILITY_VALUES,
8
+ clearPreferredPresence,
9
+ getChatPresence,
10
+ getMyPresence,
11
+ getPresenceForUser,
12
+ setPreferredPresence,
13
+ setStatusMessage,
14
+ type Availability,
15
+ } from "../graph/presence.ts";
16
+ import { resolveUserId } from "../graph/me.ts";
17
+ import { auditWrite } from "../safety/audit.ts";
18
+ import { assertAccess } from "../safety/index.ts";
19
+ import { formatPresence, formatPresenceList } from "../utils/formatting.ts";
20
+ import { requireChat } from "./resolve.ts";
21
+ import {
22
+ AccountParam,
23
+ TenantParam,
24
+ connectionFor,
25
+ currentUser,
26
+ errorResult,
27
+ run,
28
+ textResult,
29
+ type ToolContext,
30
+ type ToolResult,
31
+ } from "./shared.ts";
32
+
33
+ export const teamsGetPresenceTool = {
34
+ name: "teams_get_presence",
35
+ description:
36
+ "Show Microsoft Teams presence: the signed-in user's own status by default, a colleague's when 'person' " +
37
+ "is given, or everyone in a chat when 'chat' is given. Use this before pinging someone — or to answer " +
38
+ "'is Anna available right now?'.",
39
+ parameters: Type.Object({
40
+ person: Type.Optional(Type.String({ description: "Name, e-mail or UPN of the person to check" })),
41
+ chat: Type.Optional(Type.String({ description: "Chat ID or name — shows presence for every member" })),
42
+ account: AccountParam,
43
+ tenant: TenantParam,
44
+ }),
45
+ promptSnippet: "Check Teams presence",
46
+
47
+ async execute(
48
+ _toolCallId: string,
49
+ params: { person?: string; chat?: string; account?: string; tenant?: string },
50
+ signal: AbortSignal | undefined,
51
+ _onUpdate: undefined,
52
+ ctx: ToolContext,
53
+ ): Promise<ToolResult> {
54
+ return run(async () => {
55
+ const conn = connectionFor(ctx, params.account, params.tenant);
56
+
57
+ if (params.chat) {
58
+ const chat = await requireChat(conn, params.chat, "read", signal);
59
+ const entries = await getChatPresence(conn, chat.id, signal);
60
+ return textResult(
61
+ [`## Presence in ${chat.label}`, "", formatPresenceList(entries)].join("\n"),
62
+ { chatId: chat.id, count: entries.length },
63
+ );
64
+ }
65
+
66
+ if (params.person) {
67
+ const person = await resolveUserId(conn, params.person, signal);
68
+ if (!person?.id) {
69
+ return errorResult(
70
+ `Could not resolve "${params.person}" to a person. Use teams_find_user to look them up.`,
71
+ );
72
+ }
73
+ assertAccess(conn, "read", "people", person.displayName, [
74
+ person.displayName,
75
+ person.upn,
76
+ person.mail,
77
+ person.id,
78
+ ]);
79
+ const presence = await getPresenceForUser(conn, person.id, person.displayName, signal);
80
+ return textResult(formatPresence(presence), { userId: person.id, ...presence });
81
+ }
82
+
83
+ const mine = await getMyPresence(conn, signal);
84
+ return textResult(formatPresence(mine), { ...mine });
85
+ });
86
+ },
87
+ };
88
+
89
+ export const teamsSetPresenceTool = {
90
+ name: "teams_set_presence",
91
+ description:
92
+ "Set the signed-in user's own Microsoft Teams presence — the same thing as picking a status in the " +
93
+ `Teams app. Allowed values: ${AVAILABILITY_VALUES.join(", ")}. ` +
94
+ "Optionally give 'expiresIn' as an ISO 8601 duration (PT2H, PT30M) after which Teams reverts to " +
95
+ "automatic presence. Set reset: true to hand control back to Teams immediately.",
96
+ parameters: Type.Object({
97
+ availability: Type.Optional(
98
+ Type.String({ description: `One of: ${AVAILABILITY_VALUES.join(", ")}` }),
99
+ ),
100
+ expiresIn: Type.Optional(
101
+ Type.String({ description: "ISO 8601 duration, e.g. 'PT2H' for two hours" }),
102
+ ),
103
+ reset: Type.Optional(
104
+ Type.Boolean({ description: "Clear the preferred status and let Teams calculate it again" }),
105
+ ),
106
+ account: AccountParam,
107
+ tenant: TenantParam,
108
+ }),
109
+ promptSnippet: "Set the user's Teams presence",
110
+
111
+ async execute(
112
+ _toolCallId: string,
113
+ params: {
114
+ availability?: string;
115
+ expiresIn?: string;
116
+ reset?: boolean;
117
+ account?: string;
118
+ tenant?: string;
119
+ },
120
+ signal: AbortSignal | undefined,
121
+ _onUpdate: undefined,
122
+ ctx: ToolContext,
123
+ ): Promise<ToolResult> {
124
+ return run(async () => {
125
+ const conn = connectionFor(ctx, params.account, params.tenant);
126
+ const me = await currentUser(conn, signal).catch(() => undefined);
127
+
128
+ if (params.reset) {
129
+ await clearPreferredPresence(conn, signal);
130
+ auditWrite(conn.audit, {
131
+ tool: "teams_set_presence",
132
+ account: conn.account,
133
+ tenant: conn.tenant,
134
+ actor: me?.upn,
135
+ target: "self",
136
+ summary: "cleared preferred presence",
137
+ });
138
+ return textResult("✅ Teams presence is back under Teams' own control.", { reset: true });
139
+ }
140
+
141
+ const value = AVAILABILITY_VALUES.find(
142
+ (candidate) => candidate.toLowerCase() === (params.availability ?? "").toLowerCase(),
143
+ );
144
+ if (!value) {
145
+ return errorResult(
146
+ `availability must be one of: ${AVAILABILITY_VALUES.join(", ")}.`,
147
+ );
148
+ }
149
+
150
+ await setPreferredPresence(conn, value as Availability, {
151
+ expirationDuration: params.expiresIn,
152
+ signal,
153
+ });
154
+
155
+ auditWrite(conn.audit, {
156
+ tool: "teams_set_presence",
157
+ account: conn.account,
158
+ tenant: conn.tenant,
159
+ actor: me?.upn,
160
+ target: "self",
161
+ summary: `presence → ${value}${params.expiresIn ? ` for ${params.expiresIn}` : ""}`,
162
+ });
163
+
164
+ return textResult(
165
+ `✅ Your Teams presence is now **${value}**${params.expiresIn ? ` (until ${params.expiresIn} from now)` : ""}.`,
166
+ { availability: value, expiresIn: params.expiresIn },
167
+ );
168
+ });
169
+ },
170
+ };
171
+
172
+ export const teamsSetStatusMessageTool = {
173
+ name: "teams_set_status_message",
174
+ description:
175
+ "Set the status message shown under the signed-in user's name in Microsoft Teams " +
176
+ "(e.g. 'In a workshop until Thursday'). Pass an empty message to clear it.",
177
+ parameters: Type.Object({
178
+ message: Type.String({ description: "Status text; empty string clears it" }),
179
+ expiresAt: Type.Optional(
180
+ Type.String({ description: "ISO 8601 UTC timestamp when the message should expire" }),
181
+ ),
182
+ account: AccountParam,
183
+ tenant: TenantParam,
184
+ }),
185
+ promptSnippet: "Set the user's Teams status message",
186
+
187
+ async execute(
188
+ _toolCallId: string,
189
+ params: { message: string; expiresAt?: string; account?: string; tenant?: string },
190
+ signal: AbortSignal | undefined,
191
+ _onUpdate: undefined,
192
+ ctx: ToolContext,
193
+ ): Promise<ToolResult> {
194
+ return run(async () => {
195
+ const conn = connectionFor(ctx, params.account, params.tenant);
196
+ await setStatusMessage(conn, params.message, { expiresAt: params.expiresAt, signal });
197
+
198
+ const me = await currentUser(conn, signal).catch(() => undefined);
199
+ auditWrite(conn.audit, {
200
+ tool: "teams_set_status_message",
201
+ account: conn.account,
202
+ tenant: conn.tenant,
203
+ actor: me?.upn,
204
+ target: "self",
205
+ summary: params.message ? `status message → "${params.message}"` : "cleared status message",
206
+ });
207
+
208
+ return textResult(
209
+ params.message
210
+ ? `✅ Status message set to: "${params.message}"${params.expiresAt ? ` (until ${params.expiresAt})` : ""}`
211
+ : "✅ Status message cleared.",
212
+ { message: params.message },
213
+ );
214
+ });
215
+ },
216
+ };
@@ -0,0 +1,109 @@
1
+ /**
2
+ * teams_react — add or remove an emoji reaction on a message.
3
+ */
4
+
5
+ import { Type } from "typebox";
6
+ import { setReaction, unsetReaction } from "../graph/messages.ts";
7
+ import { auditWrite } from "../safety/audit.ts";
8
+ import { requireChannel, requireChat } from "./resolve.ts";
9
+ import {
10
+ AccountParam,
11
+ TenantParam,
12
+ connectionFor,
13
+ currentUser,
14
+ errorResult,
15
+ run,
16
+ textResult,
17
+ type ToolContext,
18
+ type ToolResult,
19
+ } from "./shared.ts";
20
+ import type { MessageLocation } from "../types.ts";
21
+
22
+ interface ReactParams {
23
+ messageId: string;
24
+ reaction: string;
25
+ chat?: string;
26
+ channel?: string;
27
+ team?: string;
28
+ replyId?: string;
29
+ remove?: boolean;
30
+ account?: string;
31
+ tenant?: string;
32
+ }
33
+
34
+ export const teamsReactTool = {
35
+ name: "teams_react",
36
+ description:
37
+ "Add or remove an emoji reaction on a Microsoft Teams message, as the signed-in user. " +
38
+ "Specify either 'chat' or 'channel' (plus 'team') to say where the message lives. " +
39
+ "For a reply inside a channel thread, pass the thread's messageId and the reply's ID as 'replyId'. " +
40
+ "The reaction is a Unicode emoji such as 👍 or ✅.",
41
+ parameters: Type.Object({
42
+ messageId: Type.String({ description: "ID of the message to react to" }),
43
+ reaction: Type.String({ description: "Unicode emoji, e.g. 👍, ❤️, ✅" }),
44
+ chat: Type.Optional(Type.String({ description: "Chat ID or name, when the message is in a chat" })),
45
+ channel: Type.Optional(
46
+ Type.String({ description: "Channel name/ID or 'Team/Channel' path, when it is in a channel" }),
47
+ ),
48
+ team: Type.Optional(Type.String({ description: "Team name or ID" })),
49
+ replyId: Type.Optional(Type.String({ description: "Reply ID, when reacting to a reply in a thread" })),
50
+ remove: Type.Optional(Type.Boolean({ description: "Remove the reaction instead of adding it" })),
51
+ account: AccountParam,
52
+ tenant: TenantParam,
53
+ }),
54
+ promptSnippet: "React to a Teams message",
55
+
56
+ async execute(
57
+ _toolCallId: string,
58
+ params: ReactParams,
59
+ signal: AbortSignal | undefined,
60
+ _onUpdate: undefined,
61
+ ctx: ToolContext,
62
+ ): Promise<ToolResult> {
63
+ return run(async () => {
64
+ const conn = connectionFor(ctx, params.account, params.tenant);
65
+
66
+ let location: MessageLocation;
67
+ let target: string;
68
+
69
+ if (params.chat) {
70
+ const chat = await requireChat(conn, params.chat, "write", signal);
71
+ location = { kind: "chat", chatId: chat.id };
72
+ target = `chat:${chat.label}`;
73
+ } else if (params.channel) {
74
+ const { team, channel } = await requireChannel(conn, params.team, params.channel, "write", signal);
75
+ location = {
76
+ kind: "channel",
77
+ teamId: team.id,
78
+ teamName: team.displayName,
79
+ channelId: channel.id,
80
+ channelName: channel.displayName,
81
+ };
82
+ target = `channel:${team.displayName}/${channel.displayName}`;
83
+ } else {
84
+ return errorResult("Specify either 'chat' or 'channel' so pi knows where the message lives.");
85
+ }
86
+
87
+ const action = params.remove ? unsetReaction : setReaction;
88
+ await action(conn, location, params.messageId, params.reaction, {
89
+ replyId: params.replyId,
90
+ signal,
91
+ });
92
+
93
+ const me = await currentUser(conn, signal).catch(() => undefined);
94
+ auditWrite(conn.audit, {
95
+ tool: "teams_react",
96
+ account: conn.account,
97
+ tenant: conn.tenant,
98
+ actor: me?.upn,
99
+ target,
100
+ summary: `${params.remove ? "removed" : "added"} reaction ${params.reaction} on ${params.messageId}`,
101
+ });
102
+
103
+ return textResult(
104
+ `✅ ${params.remove ? "Removed" : "Added"} ${params.reaction} on message ${params.messageId} (${target}).`,
105
+ { messageId: params.messageId, reaction: params.reaction, removed: !!params.remove },
106
+ );
107
+ });
108
+ },
109
+ };
@@ -0,0 +1,75 @@
1
+ /**
2
+ * teams_read_channel — read the top-level posts of a channel.
3
+ */
4
+
5
+ import { Type } from "typebox";
6
+ import { listChannelMessages } from "../graph/messages.ts";
7
+ import { formatMessageList } 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 teamsReadChannelTool = {
21
+ name: "teams_read_channel",
22
+ description:
23
+ "Read the posts of a Microsoft Teams channel (top-level messages, not the replies inside each thread). " +
24
+ "Pass team and channel by name or ID, or a single 'Team/Channel' path as 'channel'. " +
25
+ "Set withReplyCounts to also show how many replies each thread has — use teams_read_thread for the replies themselves.",
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
+ withReplyCounts: Type.Optional(
33
+ Type.Boolean({ description: "Resolve reply counts for each post (slower, one call per post)" }),
34
+ ),
35
+ }),
36
+ promptSnippet: "Read posts from a Teams channel",
37
+ promptGuidelines: [
38
+ "Channel conversations are threaded: teams_read_channel returns the openers, teams_read_thread the replies.",
39
+ ],
40
+
41
+ async execute(
42
+ _toolCallId: string,
43
+ params: {
44
+ channel: string;
45
+ team?: string;
46
+ account?: string;
47
+ tenant?: string;
48
+ limit?: number;
49
+ withReplyCounts?: boolean;
50
+ },
51
+ signal: AbortSignal | undefined,
52
+ _onUpdate: undefined,
53
+ ctx: ToolContext,
54
+ ): Promise<ToolResult> {
55
+ return run(async () => {
56
+ const conn = connectionFor(ctx, params.account, params.tenant);
57
+ const { team, channel } = await requireChannel(conn, params.team, params.channel, "read", signal);
58
+
59
+ const messages = await listChannelMessages(conn, channel, {
60
+ max: params.limit ?? conn.maxMessages,
61
+ withReplies: params.withReplyCounts,
62
+ signal,
63
+ });
64
+
65
+ return textResult(
66
+ formatMessageList(messages, `${team.displayName}/${channel.displayName}`),
67
+ {
68
+ teamId: team.id,
69
+ channelId: channel.id,
70
+ count: messages.length,
71
+ },
72
+ );
73
+ });
74
+ },
75
+ };
@@ -0,0 +1,73 @@
1
+ /**
2
+ * teams_read_chat — read the messages of one chat.
3
+ */
4
+
5
+ import { Type } from "typebox";
6
+ import { listChatMessages } from "../graph/messages.ts";
7
+ import { formatChatDetail, formatMessageList } from "../utils/formatting.ts";
8
+ import { requireChat } 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 teamsReadChatTool = {
21
+ name: "teams_read_chat",
22
+ description:
23
+ "Read messages from a Microsoft Teams chat. Accepts a chat ID, a group chat topic, or a participant's " +
24
+ "name or e-mail — ambiguous matches are reported rather than guessed. Returns messages oldest first " +
25
+ "with sender, timestamp, reactions and message IDs.",
26
+ parameters: Type.Object({
27
+ chat: Type.String({ description: "Chat ID, group chat topic, or a participant's name/e-mail" }),
28
+ account: AccountParam,
29
+ tenant: TenantParam,
30
+ limit: LimitParam,
31
+ includeDetails: Type.Optional(
32
+ Type.Boolean({ description: "Also show chat metadata (members, type, link)" }),
33
+ ),
34
+ }),
35
+ promptSnippet: "Read messages from a Teams chat",
36
+ promptGuidelines: [
37
+ "Use teams_read_chat to catch up on a conversation before replying.",
38
+ "Quote the messageId when the user wants to react to or delete a specific message.",
39
+ ],
40
+
41
+ async execute(
42
+ _toolCallId: string,
43
+ params: {
44
+ chat: string;
45
+ account?: string;
46
+ tenant?: string;
47
+ limit?: number;
48
+ includeDetails?: boolean;
49
+ },
50
+ signal: AbortSignal | undefined,
51
+ _onUpdate: undefined,
52
+ ctx: ToolContext,
53
+ ): Promise<ToolResult> {
54
+ return run(async () => {
55
+ const conn = connectionFor(ctx, params.account, params.tenant);
56
+ const chat = await requireChat(conn, params.chat, "read", signal);
57
+
58
+ const messages = await listChatMessages(conn, chat.id, {
59
+ max: params.limit ?? conn.maxMessages,
60
+ signal,
61
+ });
62
+
63
+ const body = formatMessageList(messages, chat.label);
64
+ const detail = params.includeDetails ? `${formatChatDetail(chat)}\n\n` : "";
65
+
66
+ return textResult(`${detail}${body}`, {
67
+ chatId: chat.id,
68
+ label: chat.label,
69
+ count: messages.length,
70
+ });
71
+ });
72
+ },
73
+ };