@openclaw/msteams 2026.3.2 → 2026.3.7

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.
@@ -1,5 +1,5 @@
1
1
  import { EventEmitter } from "node:events";
2
- import type { OpenClawConfig, RuntimeEnv } from "openclaw/plugin-sdk";
2
+ import type { OpenClawConfig, RuntimeEnv } from "openclaw/plugin-sdk/msteams";
3
3
  import { afterEach, describe, expect, it, vi } from "vitest";
4
4
  import type { MSTeamsConversationStore } from "./conversation-store.js";
5
5
  import type { MSTeamsPollStore } from "./polls.js";
@@ -15,8 +15,14 @@ const expressControl = vi.hoisted(() => ({
15
15
  mode: { value: "listening" as "listening" | "error" },
16
16
  }));
17
17
 
18
- vi.mock("openclaw/plugin-sdk", () => ({
18
+ vi.mock("openclaw/plugin-sdk/msteams", () => ({
19
19
  DEFAULT_WEBHOOK_MAX_BODY_BYTES: 1024 * 1024,
20
+ normalizeSecretInputString: (value: unknown) =>
21
+ typeof value === "string" && value.trim() ? value.trim() : undefined,
22
+ hasConfiguredSecretInput: (value: unknown) =>
23
+ typeof value === "string" && value.trim().length > 0,
24
+ normalizeResolvedSecretInputString: (params: { value?: unknown }) =>
25
+ typeof params?.value === "string" && params.value.trim() ? params.value.trim() : undefined,
20
26
  keepHttpServerTaskAlive: vi.fn(
21
27
  async (params: { abortSignal?: AbortSignal; onAbort?: () => Promise<void> | void }) => {
22
28
  await new Promise<void>((resolve) => {
@@ -134,7 +140,7 @@ function createConfig(port: number): OpenClawConfig {
134
140
  msteams: {
135
141
  enabled: true,
136
142
  appId: "app-id",
137
- appPassword: "app-password",
143
+ appPassword: "app-password", // pragma: allowlist secret
138
144
  tenantId: "tenant-id",
139
145
  webhook: {
140
146
  port,
package/src/monitor.ts CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  summarizeMapping,
8
8
  type OpenClawConfig,
9
9
  type RuntimeEnv,
10
- } from "openclaw/plugin-sdk";
10
+ } from "openclaw/plugin-sdk/msteams";
11
11
  import { createMSTeamsConversationStoreFs } from "./conversation-store-fs.js";
12
12
  import type { MSTeamsConversationStore } from "./conversation-store.js";
13
13
  import { formatUnknownError } from "./errors.js";
package/src/onboarding.ts CHANGED
@@ -5,14 +5,17 @@ import type {
5
5
  DmPolicy,
6
6
  WizardPrompter,
7
7
  MSTeamsTeamConfig,
8
- } from "openclaw/plugin-sdk";
8
+ } from "openclaw/plugin-sdk/msteams";
9
9
  import {
10
- addWildcardAllowFrom,
11
10
  DEFAULT_ACCOUNT_ID,
12
11
  formatDocsLink,
13
12
  mergeAllowFromEntries,
14
13
  promptChannelAccessConfig,
15
- } from "openclaw/plugin-sdk";
14
+ setTopLevelChannelAllowFrom,
15
+ setTopLevelChannelDmPolicyWithAllowFrom,
16
+ setTopLevelChannelGroupPolicy,
17
+ splitOnboardingEntries,
18
+ } from "openclaw/plugin-sdk/msteams";
16
19
  import {
17
20
  parseMSTeamsTeamEntry,
18
21
  resolveMSTeamsChannelAllowlist,
@@ -24,41 +27,19 @@ import { hasConfiguredMSTeamsCredentials, resolveMSTeamsCredentials } from "./to
24
27
  const channel = "msteams" as const;
25
28
 
26
29
  function setMSTeamsDmPolicy(cfg: OpenClawConfig, dmPolicy: DmPolicy) {
27
- const allowFrom =
28
- dmPolicy === "open"
29
- ? addWildcardAllowFrom(cfg.channels?.msteams?.allowFrom)?.map((entry) => String(entry))
30
- : undefined;
31
- return {
32
- ...cfg,
33
- channels: {
34
- ...cfg.channels,
35
- msteams: {
36
- ...cfg.channels?.msteams,
37
- dmPolicy,
38
- ...(allowFrom ? { allowFrom } : {}),
39
- },
40
- },
41
- };
30
+ return setTopLevelChannelDmPolicyWithAllowFrom({
31
+ cfg,
32
+ channel: "msteams",
33
+ dmPolicy,
34
+ });
42
35
  }
43
36
 
44
37
  function setMSTeamsAllowFrom(cfg: OpenClawConfig, allowFrom: string[]): OpenClawConfig {
45
- return {
46
- ...cfg,
47
- channels: {
48
- ...cfg.channels,
49
- msteams: {
50
- ...cfg.channels?.msteams,
51
- allowFrom,
52
- },
53
- },
54
- };
55
- }
56
-
57
- function parseAllowFromInput(raw: string): string[] {
58
- return raw
59
- .split(/[\n,;]+/g)
60
- .map((entry) => entry.trim())
61
- .filter(Boolean);
38
+ return setTopLevelChannelAllowFrom({
39
+ cfg,
40
+ channel: "msteams",
41
+ allowFrom,
42
+ });
62
43
  }
63
44
 
64
45
  function looksLikeGuid(value: string): boolean {
@@ -115,7 +96,7 @@ async function promptMSTeamsAllowFrom(params: {
115
96
  initialValue: existing[0] ? String(existing[0]) : undefined,
116
97
  validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
117
98
  });
118
- const parts = parseAllowFromInput(String(entry));
99
+ const parts = splitOnboardingEntries(String(entry));
119
100
  if (parts.length === 0) {
120
101
  await params.prompter.note("Enter at least one user.", "MS Teams allowlist");
121
102
  continue;
@@ -171,17 +152,12 @@ function setMSTeamsGroupPolicy(
171
152
  cfg: OpenClawConfig,
172
153
  groupPolicy: "open" | "allowlist" | "disabled",
173
154
  ): OpenClawConfig {
174
- return {
175
- ...cfg,
176
- channels: {
177
- ...cfg.channels,
178
- msteams: {
179
- ...cfg.channels?.msteams,
180
- enabled: true,
181
- groupPolicy,
182
- },
183
- },
184
- };
155
+ return setTopLevelChannelGroupPolicy({
156
+ cfg,
157
+ channel: "msteams",
158
+ groupPolicy,
159
+ enabled: true,
160
+ });
185
161
  }
186
162
 
187
163
  function setMSTeamsTeamsAllowlist(
@@ -0,0 +1,131 @@
1
+ import type { OpenClawConfig } from "openclaw/plugin-sdk/msteams";
2
+ import { beforeEach, describe, expect, it, vi } from "vitest";
3
+
4
+ const mocks = vi.hoisted(() => ({
5
+ sendMessageMSTeams: vi.fn(),
6
+ sendPollMSTeams: vi.fn(),
7
+ createPoll: vi.fn(),
8
+ }));
9
+
10
+ vi.mock("./send.js", () => ({
11
+ sendMessageMSTeams: mocks.sendMessageMSTeams,
12
+ sendPollMSTeams: mocks.sendPollMSTeams,
13
+ }));
14
+
15
+ vi.mock("./polls.js", () => ({
16
+ createMSTeamsPollStoreFs: () => ({
17
+ createPoll: mocks.createPoll,
18
+ }),
19
+ }));
20
+
21
+ vi.mock("./runtime.js", () => ({
22
+ getMSTeamsRuntime: () => ({
23
+ channel: {
24
+ text: {
25
+ chunkMarkdownText: (text: string) => [text],
26
+ },
27
+ },
28
+ }),
29
+ }));
30
+
31
+ import { msteamsOutbound } from "./outbound.js";
32
+
33
+ describe("msteamsOutbound cfg threading", () => {
34
+ beforeEach(() => {
35
+ mocks.sendMessageMSTeams.mockReset();
36
+ mocks.sendPollMSTeams.mockReset();
37
+ mocks.createPoll.mockReset();
38
+ mocks.sendMessageMSTeams.mockResolvedValue({
39
+ messageId: "msg-1",
40
+ conversationId: "conv-1",
41
+ });
42
+ mocks.sendPollMSTeams.mockResolvedValue({
43
+ pollId: "poll-1",
44
+ messageId: "msg-poll-1",
45
+ conversationId: "conv-1",
46
+ });
47
+ mocks.createPoll.mockResolvedValue(undefined);
48
+ });
49
+
50
+ it("passes resolved cfg to sendMessageMSTeams for text sends", async () => {
51
+ const cfg = {
52
+ channels: {
53
+ msteams: {
54
+ appId: "resolved-app-id",
55
+ },
56
+ },
57
+ } as OpenClawConfig;
58
+
59
+ await msteamsOutbound.sendText!({
60
+ cfg,
61
+ to: "conversation:abc",
62
+ text: "hello",
63
+ });
64
+
65
+ expect(mocks.sendMessageMSTeams).toHaveBeenCalledWith({
66
+ cfg,
67
+ to: "conversation:abc",
68
+ text: "hello",
69
+ });
70
+ });
71
+
72
+ it("passes resolved cfg and media roots for media sends", async () => {
73
+ const cfg = {
74
+ channels: {
75
+ msteams: {
76
+ appId: "resolved-app-id",
77
+ },
78
+ },
79
+ } as OpenClawConfig;
80
+
81
+ await msteamsOutbound.sendMedia!({
82
+ cfg,
83
+ to: "conversation:abc",
84
+ text: "photo",
85
+ mediaUrl: "file:///tmp/photo.png",
86
+ mediaLocalRoots: ["/tmp"],
87
+ });
88
+
89
+ expect(mocks.sendMessageMSTeams).toHaveBeenCalledWith({
90
+ cfg,
91
+ to: "conversation:abc",
92
+ text: "photo",
93
+ mediaUrl: "file:///tmp/photo.png",
94
+ mediaLocalRoots: ["/tmp"],
95
+ });
96
+ });
97
+
98
+ it("passes resolved cfg to sendPollMSTeams and stores poll metadata", async () => {
99
+ const cfg = {
100
+ channels: {
101
+ msteams: {
102
+ appId: "resolved-app-id",
103
+ },
104
+ },
105
+ } as OpenClawConfig;
106
+
107
+ await msteamsOutbound.sendPoll!({
108
+ cfg,
109
+ to: "conversation:abc",
110
+ poll: {
111
+ question: "Snack?",
112
+ options: ["Pizza", "Sushi"],
113
+ },
114
+ });
115
+
116
+ expect(mocks.sendPollMSTeams).toHaveBeenCalledWith({
117
+ cfg,
118
+ to: "conversation:abc",
119
+ question: "Snack?",
120
+ options: ["Pizza", "Sushi"],
121
+ maxSelections: 1,
122
+ });
123
+ expect(mocks.createPoll).toHaveBeenCalledWith(
124
+ expect.objectContaining({
125
+ id: "poll-1",
126
+ question: "Snack?",
127
+ options: ["Pizza", "Sushi"],
128
+ }),
129
+ );
130
+ });
131
+ });
package/src/outbound.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ChannelOutboundAdapter } from "openclaw/plugin-sdk";
1
+ import type { ChannelOutboundAdapter } from "openclaw/plugin-sdk/msteams";
2
2
  import { createMSTeamsPollStoreFs } from "./polls.js";
3
3
  import { getMSTeamsRuntime } from "./runtime.js";
4
4
  import { sendMessageMSTeams, sendPollMSTeams } from "./send.js";
@@ -1,4 +1,4 @@
1
- import type { MSTeamsConfig } from "openclaw/plugin-sdk";
1
+ import type { MSTeamsConfig } from "openclaw/plugin-sdk/msteams";
2
2
  import { describe, expect, it } from "vitest";
3
3
  import {
4
4
  isMSTeamsGroupAllowed,
package/src/policy.ts CHANGED
@@ -7,15 +7,16 @@ import type {
7
7
  MSTeamsConfig,
8
8
  MSTeamsReplyStyle,
9
9
  MSTeamsTeamConfig,
10
- } from "openclaw/plugin-sdk";
10
+ } from "openclaw/plugin-sdk/msteams";
11
11
  import {
12
12
  buildChannelKeyCandidates,
13
+ evaluateSenderGroupAccessForPolicy,
13
14
  normalizeChannelSlug,
14
15
  resolveAllowlistMatchSimple,
15
16
  resolveToolsBySender,
16
17
  resolveChannelEntryMatchWithFallback,
17
18
  resolveNestedAllowlistDecision,
18
- } from "openclaw/plugin-sdk";
19
+ } from "openclaw/plugin-sdk/msteams";
19
20
 
20
21
  export type MSTeamsResolvedRouteConfig = {
21
22
  teamConfig?: MSTeamsTeamConfig;
@@ -248,12 +249,10 @@ export function isMSTeamsGroupAllowed(params: {
248
249
  senderName?: string | null;
249
250
  allowNameMatching?: boolean;
250
251
  }): boolean {
251
- const { groupPolicy } = params;
252
- if (groupPolicy === "disabled") {
253
- return false;
254
- }
255
- if (groupPolicy === "open") {
256
- return true;
257
- }
258
- return resolveMSTeamsAllowlistMatch(params).allowed;
252
+ return evaluateSenderGroupAccessForPolicy({
253
+ groupPolicy: params.groupPolicy,
254
+ groupAllowFrom: params.allowFrom.map((entry) => String(entry)),
255
+ senderId: params.senderId,
256
+ isSenderAllowed: () => resolveMSTeamsAllowlistMatch(params).allowed,
257
+ }).allowed;
259
258
  }
package/src/probe.test.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { MSTeamsConfig } from "openclaw/plugin-sdk";
1
+ import type { MSTeamsConfig } from "openclaw/plugin-sdk/msteams";
2
2
  import { describe, expect, it, vi } from "vitest";
3
3
 
4
4
  const hostMockState = vi.hoisted(() => ({
package/src/probe.ts CHANGED
@@ -1,4 +1,8 @@
1
- import type { BaseProbeResult, MSTeamsConfig } from "openclaw/plugin-sdk";
1
+ import {
2
+ normalizeStringEntries,
3
+ type BaseProbeResult,
4
+ type MSTeamsConfig,
5
+ } from "openclaw/plugin-sdk/msteams";
2
6
  import { formatUnknownError } from "./errors.js";
3
7
  import { loadMSTeamsSdkWithAuth } from "./sdk.js";
4
8
  import { readAccessToken } from "./token-response.js";
@@ -35,7 +39,7 @@ function readStringArray(value: unknown): string[] | undefined {
35
39
  if (!Array.isArray(value)) {
36
40
  return undefined;
37
41
  }
38
- const out = value.map((entry) => String(entry).trim()).filter(Boolean);
42
+ const out = normalizeStringEntries(value);
39
43
  return out.length > 0 ? out : undefined;
40
44
  }
41
45
 
@@ -6,7 +6,7 @@ import {
6
6
  type OpenClawConfig,
7
7
  type MSTeamsReplyStyle,
8
8
  type RuntimeEnv,
9
- } from "openclaw/plugin-sdk";
9
+ } from "openclaw/plugin-sdk/msteams";
10
10
  import type { MSTeamsAccessTokenProvider } from "./attachments/types.js";
11
11
  import type { StoredConversationReference } from "./conversation-store.js";
12
12
  import {
@@ -0,0 +1,78 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+
3
+ const {
4
+ listTeamsByName,
5
+ listChannelsForTeam,
6
+ normalizeQuery,
7
+ resolveGraphToken,
8
+ searchGraphUsers,
9
+ } = vi.hoisted(() => ({
10
+ listTeamsByName: vi.fn(),
11
+ listChannelsForTeam: vi.fn(),
12
+ normalizeQuery: vi.fn((value: string) => value.trim().toLowerCase()),
13
+ resolveGraphToken: vi.fn(async () => "graph-token"),
14
+ searchGraphUsers: vi.fn(),
15
+ }));
16
+
17
+ vi.mock("./graph.js", () => ({
18
+ listTeamsByName,
19
+ listChannelsForTeam,
20
+ normalizeQuery,
21
+ resolveGraphToken,
22
+ }));
23
+
24
+ vi.mock("./graph-users.js", () => ({
25
+ searchGraphUsers,
26
+ }));
27
+
28
+ import {
29
+ resolveMSTeamsChannelAllowlist,
30
+ resolveMSTeamsUserAllowlist,
31
+ } from "./resolve-allowlist.js";
32
+
33
+ describe("resolveMSTeamsUserAllowlist", () => {
34
+ it("marks empty input unresolved", async () => {
35
+ const [result] = await resolveMSTeamsUserAllowlist({ cfg: {}, entries: [" "] });
36
+ expect(result).toEqual({ input: " ", resolved: false });
37
+ });
38
+
39
+ it("resolves first Graph user match", async () => {
40
+ searchGraphUsers.mockResolvedValueOnce([
41
+ { id: "user-1", displayName: "Alice One" },
42
+ { id: "user-2", displayName: "Alice Two" },
43
+ ]);
44
+ const [result] = await resolveMSTeamsUserAllowlist({ cfg: {}, entries: ["alice"] });
45
+ expect(result).toEqual({
46
+ input: "alice",
47
+ resolved: true,
48
+ id: "user-1",
49
+ name: "Alice One",
50
+ note: "multiple matches; chose first",
51
+ });
52
+ });
53
+ });
54
+
55
+ describe("resolveMSTeamsChannelAllowlist", () => {
56
+ it("resolves team/channel by team name + channel display name", async () => {
57
+ listTeamsByName.mockResolvedValueOnce([{ id: "team-1", displayName: "Product Team" }]);
58
+ listChannelsForTeam.mockResolvedValueOnce([
59
+ { id: "channel-1", displayName: "General" },
60
+ { id: "channel-2", displayName: "Roadmap" },
61
+ ]);
62
+
63
+ const [result] = await resolveMSTeamsChannelAllowlist({
64
+ cfg: {},
65
+ entries: ["Product Team/Roadmap"],
66
+ });
67
+
68
+ expect(result).toEqual({
69
+ input: "Product Team/Roadmap",
70
+ resolved: true,
71
+ teamId: "team-1",
72
+ teamName: "Product Team",
73
+ channelId: "channel-2",
74
+ channelName: "Roadmap",
75
+ note: "multiple channels; chose first",
76
+ });
77
+ });
78
+ });
@@ -1,3 +1,4 @@
1
+ import { mapAllowlistResolutionInputs } from "openclaw/plugin-sdk/compat";
1
2
  import { searchGraphUsers } from "./graph-users.js";
2
3
  import {
3
4
  listChannelsForTeam,
@@ -105,61 +106,55 @@ export async function resolveMSTeamsChannelAllowlist(params: {
105
106
  entries: string[];
106
107
  }): Promise<MSTeamsChannelResolution[]> {
107
108
  const token = await resolveGraphToken(params.cfg);
108
- const results: MSTeamsChannelResolution[] = [];
109
-
110
- for (const input of params.entries) {
111
- const { team, channel } = parseMSTeamsTeamChannelInput(input);
112
- if (!team) {
113
- results.push({ input, resolved: false });
114
- continue;
115
- }
116
- const teams = /^[0-9a-fA-F-]{16,}$/.test(team)
117
- ? [{ id: team, displayName: team }]
118
- : await listTeamsByName(token, team);
119
- if (teams.length === 0) {
120
- results.push({ input, resolved: false, note: "team not found" });
121
- continue;
122
- }
123
- const teamMatch = teams[0];
124
- const teamId = teamMatch.id?.trim();
125
- const teamName = teamMatch.displayName?.trim() || team;
126
- if (!teamId) {
127
- results.push({ input, resolved: false, note: "team id missing" });
128
- continue;
129
- }
130
- if (!channel) {
131
- results.push({
109
+ return await mapAllowlistResolutionInputs({
110
+ inputs: params.entries,
111
+ mapInput: async (input): Promise<MSTeamsChannelResolution> => {
112
+ const { team, channel } = parseMSTeamsTeamChannelInput(input);
113
+ if (!team) {
114
+ return { input, resolved: false };
115
+ }
116
+ const teams = /^[0-9a-fA-F-]{16,}$/.test(team)
117
+ ? [{ id: team, displayName: team }]
118
+ : await listTeamsByName(token, team);
119
+ if (teams.length === 0) {
120
+ return { input, resolved: false, note: "team not found" };
121
+ }
122
+ const teamMatch = teams[0];
123
+ const teamId = teamMatch.id?.trim();
124
+ const teamName = teamMatch.displayName?.trim() || team;
125
+ if (!teamId) {
126
+ return { input, resolved: false, note: "team id missing" };
127
+ }
128
+ if (!channel) {
129
+ return {
130
+ input,
131
+ resolved: true,
132
+ teamId,
133
+ teamName,
134
+ note: teams.length > 1 ? "multiple teams; chose first" : undefined,
135
+ };
136
+ }
137
+ const channels = await listChannelsForTeam(token, teamId);
138
+ const channelMatch =
139
+ channels.find((item) => item.id === channel) ??
140
+ channels.find((item) => item.displayName?.toLowerCase() === channel.toLowerCase()) ??
141
+ channels.find((item) =>
142
+ item.displayName?.toLowerCase().includes(channel.toLowerCase() ?? ""),
143
+ );
144
+ if (!channelMatch?.id) {
145
+ return { input, resolved: false, note: "channel not found" };
146
+ }
147
+ return {
132
148
  input,
133
149
  resolved: true,
134
150
  teamId,
135
151
  teamName,
136
- note: teams.length > 1 ? "multiple teams; chose first" : undefined,
137
- });
138
- continue;
139
- }
140
- const channels = await listChannelsForTeam(token, teamId);
141
- const channelMatch =
142
- channels.find((item) => item.id === channel) ??
143
- channels.find((item) => item.displayName?.toLowerCase() === channel.toLowerCase()) ??
144
- channels.find((item) =>
145
- item.displayName?.toLowerCase().includes(channel.toLowerCase() ?? ""),
146
- );
147
- if (!channelMatch?.id) {
148
- results.push({ input, resolved: false, note: "channel not found" });
149
- continue;
150
- }
151
- results.push({
152
- input,
153
- resolved: true,
154
- teamId,
155
- teamName,
156
- channelId: channelMatch.id,
157
- channelName: channelMatch.displayName ?? channel,
158
- note: channels.length > 1 ? "multiple channels; chose first" : undefined,
159
- });
160
- }
161
-
162
- return results;
152
+ channelId: channelMatch.id,
153
+ channelName: channelMatch.displayName ?? channel,
154
+ note: channels.length > 1 ? "multiple channels; chose first" : undefined,
155
+ };
156
+ },
157
+ });
163
158
  }
164
159
 
165
160
  export async function resolveMSTeamsUserAllowlist(params: {
@@ -167,32 +162,28 @@ export async function resolveMSTeamsUserAllowlist(params: {
167
162
  entries: string[];
168
163
  }): Promise<MSTeamsUserResolution[]> {
169
164
  const token = await resolveGraphToken(params.cfg);
170
- const results: MSTeamsUserResolution[] = [];
171
-
172
- for (const input of params.entries) {
173
- const query = normalizeQuery(normalizeMSTeamsUserInput(input));
174
- if (!query) {
175
- results.push({ input, resolved: false });
176
- continue;
177
- }
178
- if (/^[0-9a-fA-F-]{16,}$/.test(query)) {
179
- results.push({ input, resolved: true, id: query });
180
- continue;
181
- }
182
- const users = await searchGraphUsers({ token, query, top: 10 });
183
- const match = users[0];
184
- if (!match?.id) {
185
- results.push({ input, resolved: false });
186
- continue;
187
- }
188
- results.push({
189
- input,
190
- resolved: true,
191
- id: match.id,
192
- name: match.displayName ?? undefined,
193
- note: users.length > 1 ? "multiple matches; chose first" : undefined,
194
- });
195
- }
196
-
197
- return results;
165
+ return await mapAllowlistResolutionInputs({
166
+ inputs: params.entries,
167
+ mapInput: async (input): Promise<MSTeamsUserResolution> => {
168
+ const query = normalizeQuery(normalizeMSTeamsUserInput(input));
169
+ if (!query) {
170
+ return { input, resolved: false };
171
+ }
172
+ if (/^[0-9a-fA-F-]{16,}$/.test(query)) {
173
+ return { input, resolved: true, id: query };
174
+ }
175
+ const users = await searchGraphUsers({ token, query, top: 10 });
176
+ const match = users[0];
177
+ if (!match?.id) {
178
+ return { input, resolved: false };
179
+ }
180
+ return {
181
+ input,
182
+ resolved: true,
183
+ id: match.id,
184
+ name: match.displayName ?? undefined,
185
+ note: users.length > 1 ? "multiple matches; chose first" : undefined,
186
+ };
187
+ },
188
+ });
198
189
  }
package/src/runtime.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { PluginRuntime } from "openclaw/plugin-sdk";
1
+ import type { PluginRuntime } from "openclaw/plugin-sdk/msteams";
2
2
 
3
3
  let runtime: PluginRuntime | null = null;
4
4
 
@@ -2,6 +2,6 @@ import {
2
2
  hasConfiguredSecretInput,
3
3
  normalizeResolvedSecretInputString,
4
4
  normalizeSecretInputString,
5
- } from "openclaw/plugin-sdk";
5
+ } from "openclaw/plugin-sdk/msteams";
6
6
 
7
7
  export { hasConfiguredSecretInput, normalizeResolvedSecretInputString, normalizeSecretInputString };
@@ -2,7 +2,7 @@ import {
2
2
  resolveChannelMediaMaxBytes,
3
3
  type OpenClawConfig,
4
4
  type PluginRuntime,
5
- } from "openclaw/plugin-sdk";
5
+ } from "openclaw/plugin-sdk/msteams";
6
6
  import type { MSTeamsAccessTokenProvider } from "./attachments/types.js";
7
7
  import { createMSTeamsConversationStoreFs } from "./conversation-store-fs.js";
8
8
  import type {
package/src/send.test.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { OpenClawConfig } from "openclaw/plugin-sdk";
1
+ import type { OpenClawConfig } from "openclaw/plugin-sdk/msteams";
2
2
  import { beforeEach, describe, expect, it, vi } from "vitest";
3
3
  import { sendMessageMSTeams } from "./send.js";
4
4
 
@@ -11,7 +11,7 @@ const mockState = vi.hoisted(() => ({
11
11
  sendMSTeamsMessages: vi.fn(),
12
12
  }));
13
13
 
14
- vi.mock("openclaw/plugin-sdk", () => ({
14
+ vi.mock("openclaw/plugin-sdk/msteams", () => ({
15
15
  loadOutboundMediaFromUrl: mockState.loadOutboundMediaFromUrl,
16
16
  }));
17
17