@openclaw/msteams 2026.2.15 → 2026.2.19

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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 2026.2.19
4
+
5
+ ### Changes
6
+
7
+ - Version alignment with core OpenClaw release numbers.
8
+
9
+ ## 2026.2.16
10
+
11
+ ### Changes
12
+
13
+ - Version alignment with core OpenClaw release numbers.
14
+
3
15
  ## 2026.2.15
4
16
 
5
17
  ### Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/msteams",
3
- "version": "2026.2.15",
3
+ "version": "2026.2.19",
4
4
  "description": "OpenClaw Microsoft Teams channel plugin",
5
5
  "type": "module",
6
6
  "dependencies": {
@@ -1,8 +1,3 @@
1
- import type {
2
- MSTeamsAccessTokenProvider,
3
- MSTeamsAttachmentLike,
4
- MSTeamsInboundMedia,
5
- } from "./types.js";
6
1
  import { getMSTeamsRuntime } from "../runtime.js";
7
2
  import {
8
3
  extractInlineImageCandidates,
@@ -14,6 +9,11 @@ import {
14
9
  resolveAuthAllowedHosts,
15
10
  resolveAllowedHosts,
16
11
  } from "./shared.js";
12
+ import type {
13
+ MSTeamsAccessTokenProvider,
14
+ MSTeamsAttachmentLike,
15
+ MSTeamsInboundMedia,
16
+ } from "./types.js";
17
17
 
18
18
  type DownloadCandidate = {
19
19
  url: string;
@@ -1,9 +1,3 @@
1
- import type {
2
- MSTeamsAccessTokenProvider,
3
- MSTeamsAttachmentLike,
4
- MSTeamsGraphMediaResult,
5
- MSTeamsInboundMedia,
6
- } from "./types.js";
7
1
  import { getMSTeamsRuntime } from "../runtime.js";
8
2
  import { downloadMSTeamsAttachments } from "./download.js";
9
3
  import {
@@ -13,6 +7,12 @@ import {
13
7
  normalizeContentType,
14
8
  resolveAllowedHosts,
15
9
  } from "./shared.js";
10
+ import type {
11
+ MSTeamsAccessTokenProvider,
12
+ MSTeamsAttachmentLike,
13
+ MSTeamsGraphMediaResult,
14
+ MSTeamsInboundMedia,
15
+ } from "./types.js";
16
16
 
17
17
  type GraphHostedContent = {
18
18
  id?: string | null;
@@ -1,4 +1,3 @@
1
- import type { MSTeamsAttachmentLike, MSTeamsHtmlAttachmentSummary } from "./types.js";
2
1
  import {
3
2
  ATTACHMENT_TAG_RE,
4
3
  extractHtmlFromAttachment,
@@ -7,6 +6,7 @@ import {
7
6
  isLikelyImageAttachment,
8
7
  safeHostForUrl,
9
8
  } from "./shared.js";
9
+ import type { MSTeamsAttachmentLike, MSTeamsHtmlAttachmentSummary } from "./types.js";
10
10
 
11
11
  export function summarizeMSTeamsHtmlAttachments(
12
12
  attachments: MSTeamsAttachmentLike[] | undefined,
@@ -10,11 +10,12 @@ const saveMediaBufferMock = vi.fn(async () => ({
10
10
 
11
11
  const runtimeStub = {
12
12
  media: {
13
- detectMime: (...args: unknown[]) => detectMimeMock(...args),
13
+ detectMime: detectMimeMock as unknown as PluginRuntime["media"]["detectMime"],
14
14
  },
15
15
  channel: {
16
16
  media: {
17
- saveMediaBuffer: (...args: unknown[]) => saveMediaBufferMock(...args),
17
+ saveMediaBuffer:
18
+ saveMediaBufferMock as unknown as PluginRuntime["channel"]["media"]["saveMediaBuffer"],
18
19
  },
19
20
  },
20
21
  } as unknown as PluginRuntime;
@@ -1,8 +1,16 @@
1
- import type { OpenClawConfig } from "openclaw/plugin-sdk";
1
+ import type { OpenClawConfig, RuntimeEnv } from "openclaw/plugin-sdk";
2
2
  import { describe, expect, it } from "vitest";
3
3
  import { msteamsPlugin } from "./channel.js";
4
4
 
5
5
  describe("msteams directory", () => {
6
+ const runtimeEnv: RuntimeEnv = {
7
+ log: () => {},
8
+ error: () => {},
9
+ exit: (code: number): never => {
10
+ throw new Error(`exit ${code}`);
11
+ },
12
+ };
13
+
6
14
  it("lists peers and groups from config", async () => {
7
15
  const cfg = {
8
16
  channels: {
@@ -26,7 +34,12 @@ describe("msteams directory", () => {
26
34
  expect(msteamsPlugin.directory?.listGroups).toBeTruthy();
27
35
 
28
36
  await expect(
29
- msteamsPlugin.directory!.listPeers({ cfg, query: undefined, limit: undefined }),
37
+ msteamsPlugin.directory!.listPeers!({
38
+ cfg,
39
+ query: undefined,
40
+ limit: undefined,
41
+ runtime: runtimeEnv,
42
+ }),
30
43
  ).resolves.toEqual(
31
44
  expect.arrayContaining([
32
45
  { kind: "user", id: "user:alice" },
@@ -37,7 +50,12 @@ describe("msteams directory", () => {
37
50
  );
38
51
 
39
52
  await expect(
40
- msteamsPlugin.directory!.listGroups({ cfg, query: undefined, limit: undefined }),
53
+ msteamsPlugin.directory!.listGroups!({
54
+ cfg,
55
+ query: undefined,
56
+ limit: undefined,
57
+ runtime: runtimeEnv,
58
+ }),
41
59
  ).resolves.toEqual(
42
60
  expect.arrayContaining([
43
61
  { kind: "group", id: "conversation:chan1" },
@@ -1,28 +1,15 @@
1
- import type { PluginRuntime } from "openclaw/plugin-sdk";
2
1
  import fs from "node:fs";
3
2
  import os from "node:os";
4
3
  import path from "node:path";
5
4
  import { beforeEach, describe, expect, it } from "vitest";
6
- import type { StoredConversationReference } from "./conversation-store.js";
7
5
  import { createMSTeamsConversationStoreFs } from "./conversation-store-fs.js";
6
+ import type { StoredConversationReference } from "./conversation-store.js";
8
7
  import { setMSTeamsRuntime } from "./runtime.js";
9
-
10
- const runtimeStub = {
11
- state: {
12
- resolveStateDir: (env: NodeJS.ProcessEnv = process.env, homedir?: () => string) => {
13
- const override = env.OPENCLAW_STATE_DIR?.trim() || env.OPENCLAW_STATE_DIR?.trim();
14
- if (override) {
15
- return override;
16
- }
17
- const resolvedHome = homedir ? homedir() : os.homedir();
18
- return path.join(resolvedHome, ".openclaw");
19
- },
20
- },
21
- } as unknown as PluginRuntime;
8
+ import { msteamsRuntimeStub } from "./test-runtime.js";
22
9
 
23
10
  describe("msteams conversation store (fs)", () => {
24
11
  beforeEach(() => {
25
- setMSTeamsRuntime(runtimeStub);
12
+ setMSTeamsRuntime(msteamsRuntimeStub);
26
13
  });
27
14
 
28
15
  it("filters and prunes expired entries (but keeps legacy ones)", async () => {
@@ -125,6 +125,7 @@ describe("msteams messenger", () => {
125
125
 
126
126
  const adapter: MSTeamsAdapter = {
127
127
  continueConversation: async () => {},
128
+ process: async () => {},
128
129
  };
129
130
 
130
131
  const ids = await sendMSTeamsMessages({
@@ -154,6 +155,7 @@ describe("msteams messenger", () => {
154
155
  },
155
156
  });
156
157
  },
158
+ process: async () => {},
157
159
  };
158
160
 
159
161
  const ids = await sendMSTeamsMessages({
@@ -191,6 +193,7 @@ describe("msteams messenger", () => {
191
193
 
192
194
  const adapter: MSTeamsAdapter = {
193
195
  continueConversation: async () => {},
196
+ process: async () => {},
194
197
  };
195
198
 
196
199
  const ids = await sendMSTeamsMessages({
@@ -250,6 +253,7 @@ describe("msteams messenger", () => {
250
253
 
251
254
  const adapter: MSTeamsAdapter = {
252
255
  continueConversation: async () => {},
256
+ process: async () => {},
253
257
  };
254
258
 
255
259
  const ids = await sendMSTeamsMessages({
@@ -277,6 +281,7 @@ describe("msteams messenger", () => {
277
281
 
278
282
  const adapter: MSTeamsAdapter = {
279
283
  continueConversation: async () => {},
284
+ process: async () => {},
280
285
  };
281
286
 
282
287
  await expect(
@@ -310,6 +315,7 @@ describe("msteams messenger", () => {
310
315
  },
311
316
  });
312
317
  },
318
+ process: async () => {},
313
319
  };
314
320
 
315
321
  const ids = await sendMSTeamsMessages({
@@ -1,4 +1,3 @@
1
- import type { MSTeamsTurnContext } from "../sdk-types.js";
2
1
  import {
3
2
  buildMSTeamsGraphMessageUrls,
4
3
  downloadMSTeamsAttachments,
@@ -8,6 +7,7 @@ import {
8
7
  type MSTeamsHtmlAttachmentSummary,
9
8
  type MSTeamsInboundMedia,
10
9
  } from "../attachments.js";
10
+ import type { MSTeamsTurnContext } from "../sdk-types.js";
11
11
 
12
12
  type MSTeamsLogger = {
13
13
  debug?: (message: string, meta?: Record<string, unknown>) => void;
@@ -9,15 +9,13 @@ import {
9
9
  formatAllowlistMatchMeta,
10
10
  type HistoryEntry,
11
11
  } from "openclaw/plugin-sdk";
12
- import type { StoredConversationReference } from "../conversation-store.js";
13
- import type { MSTeamsMessageHandlerDeps } from "../monitor-handler.js";
14
- import type { MSTeamsTurnContext } from "../sdk-types.js";
15
12
  import {
16
13
  buildMSTeamsAttachmentPlaceholder,
17
14
  buildMSTeamsMediaPayload,
18
15
  type MSTeamsAttachmentLike,
19
16
  summarizeMSTeamsHtmlAttachments,
20
17
  } from "../attachments.js";
18
+ import type { StoredConversationReference } from "../conversation-store.js";
21
19
  import { formatUnknownError } from "../errors.js";
22
20
  import {
23
21
  extractMSTeamsConversationMessageId,
@@ -26,6 +24,7 @@ import {
26
24
  stripMSTeamsMentionTags,
27
25
  wasMSTeamsBotMentioned,
28
26
  } from "../inbound.js";
27
+ import type { MSTeamsMessageHandlerDeps } from "../monitor-handler.js";
29
28
  import {
30
29
  isMSTeamsGroupAllowed,
31
30
  resolveMSTeamsAllowlistMatch,
@@ -35,6 +34,7 @@ import {
35
34
  import { extractMSTeamsPollVote } from "../polls.js";
36
35
  import { createMSTeamsReplyDispatcher } from "../reply-dispatcher.js";
37
36
  import { getMSTeamsRuntime } from "../runtime.js";
37
+ import type { MSTeamsTurnContext } from "../sdk-types.js";
38
38
  import { recordMSTeamsSentMessage, wasMSTeamsMessageSent } from "../sent-message-cache.js";
39
39
  import { resolveMSTeamsInboundMedia } from "./inbound-media.js";
40
40
 
@@ -1,12 +1,12 @@
1
1
  import type { OpenClawConfig, RuntimeEnv } from "openclaw/plugin-sdk";
2
2
  import type { MSTeamsConversationStore } from "./conversation-store.js";
3
+ import { buildFileInfoCard, parseFileConsentInvoke, uploadToConsentUrl } from "./file-consent.js";
3
4
  import type { MSTeamsAdapter } from "./messenger.js";
5
+ import { createMSTeamsMessageHandler } from "./monitor-handler/message-handler.js";
4
6
  import type { MSTeamsMonitorLogger } from "./monitor-types.js";
7
+ import { getPendingUpload, removePendingUpload } from "./pending-uploads.js";
5
8
  import type { MSTeamsPollStore } from "./polls.js";
6
9
  import type { MSTeamsTurnContext } from "./sdk-types.js";
7
- import { buildFileInfoCard, parseFileConsentInvoke, uploadToConsentUrl } from "./file-consent.js";
8
- import { createMSTeamsMessageHandler } from "./monitor-handler/message-handler.js";
9
- import { getPendingUpload, removePendingUpload } from "./pending-uploads.js";
10
10
 
11
11
  export type MSTeamsAccessTokenProvider = {
12
12
  getAccessToken: (scope: string) => Promise<string>;
package/src/monitor.ts CHANGED
@@ -6,10 +6,10 @@ import {
6
6
  type OpenClawConfig,
7
7
  type RuntimeEnv,
8
8
  } from "openclaw/plugin-sdk";
9
- import type { MSTeamsConversationStore } from "./conversation-store.js";
10
- import type { MSTeamsAdapter } from "./messenger.js";
11
9
  import { createMSTeamsConversationStoreFs } from "./conversation-store-fs.js";
10
+ import type { MSTeamsConversationStore } from "./conversation-store.js";
12
11
  import { formatUnknownError } from "./errors.js";
12
+ import type { MSTeamsAdapter } from "./messenger.js";
13
13
  import { registerMSTeamsHandlers, type MSTeamsActivityHandler } from "./monitor-handler.js";
14
14
  import { createMSTeamsPollStoreFs, type MSTeamsPollStore } from "./polls.js";
15
15
  import {
package/src/onboarding.ts CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  addWildcardAllowFrom,
11
11
  DEFAULT_ACCOUNT_ID,
12
12
  formatDocsLink,
13
+ mergeAllowFromEntries,
13
14
  promptChannelAccessConfig,
14
15
  } from "openclaw/plugin-sdk";
15
16
  import {
@@ -133,9 +134,7 @@ async function promptMSTeamsAllowFrom(params: {
133
134
  );
134
135
  continue;
135
136
  }
136
- const unique = [
137
- ...new Set([...existing.map((v) => String(v).trim()).filter(Boolean), ...ids]),
138
- ];
137
+ const unique = mergeAllowFromEntries(existing, ids);
139
138
  return setMSTeamsAllowFrom(params.cfg, unique);
140
139
  }
141
140
 
@@ -149,7 +148,7 @@ async function promptMSTeamsAllowFrom(params: {
149
148
  }
150
149
 
151
150
  const ids = resolved.map((item) => item.id as string);
152
- const unique = [...new Set([...existing.map((v) => String(v).trim()).filter(Boolean), ...ids])];
151
+ const unique = mergeAllowFromEntries(existing, ids);
153
152
  return setMSTeamsAllowFrom(params.cfg, unique);
154
153
  }
155
154
  }
package/src/polls.test.ts CHANGED
@@ -1,27 +1,14 @@
1
- import type { PluginRuntime } from "openclaw/plugin-sdk";
2
1
  import fs from "node:fs";
3
2
  import os from "node:os";
4
3
  import path from "node:path";
5
4
  import { beforeEach, describe, expect, it } from "vitest";
6
5
  import { buildMSTeamsPollCard, createMSTeamsPollStoreFs, extractMSTeamsPollVote } from "./polls.js";
7
6
  import { setMSTeamsRuntime } from "./runtime.js";
8
-
9
- const runtimeStub = {
10
- state: {
11
- resolveStateDir: (env: NodeJS.ProcessEnv = process.env, homedir?: () => string) => {
12
- const override = env.OPENCLAW_STATE_DIR?.trim() || env.OPENCLAW_STATE_DIR?.trim();
13
- if (override) {
14
- return override;
15
- }
16
- const resolvedHome = homedir ? homedir() : os.homedir();
17
- return path.join(resolvedHome, ".openclaw");
18
- },
19
- },
20
- } as unknown as PluginRuntime;
7
+ import { msteamsRuntimeStub } from "./test-runtime.js";
21
8
 
22
9
  describe("msteams polls", () => {
23
10
  beforeEach(() => {
24
- setMSTeamsRuntime(runtimeStub);
11
+ setMSTeamsRuntime(msteamsRuntimeStub);
25
12
  });
26
13
 
27
14
  it("builds poll cards with fallback text", () => {
@@ -9,8 +9,6 @@ import {
9
9
  } from "openclaw/plugin-sdk";
10
10
  import type { MSTeamsAccessTokenProvider } from "./attachments/types.js";
11
11
  import type { StoredConversationReference } from "./conversation-store.js";
12
- import type { MSTeamsMonitorLogger } from "./monitor-types.js";
13
- import type { MSTeamsTurnContext } from "./sdk-types.js";
14
12
  import {
15
13
  classifyMSTeamsSendError,
16
14
  formatMSTeamsSendErrorHint,
@@ -21,7 +19,9 @@ import {
21
19
  renderReplyPayloadsToMessages,
22
20
  sendMSTeamsMessages,
23
21
  } from "./messenger.js";
22
+ import type { MSTeamsMonitorLogger } from "./monitor-types.js";
24
23
  import { getMSTeamsRuntime } from "./runtime.js";
24
+ import type { MSTeamsTurnContext } from "./sdk-types.js";
25
25
 
26
26
  export function createMSTeamsReplyDispatcher(params: {
27
27
  cfg: OpenClawConfig;
@@ -4,12 +4,12 @@ import {
4
4
  type PluginRuntime,
5
5
  } from "openclaw/plugin-sdk";
6
6
  import type { MSTeamsAccessTokenProvider } from "./attachments/types.js";
7
+ import { createMSTeamsConversationStoreFs } from "./conversation-store-fs.js";
7
8
  import type {
8
9
  MSTeamsConversationStore,
9
10
  StoredConversationReference,
10
11
  } from "./conversation-store.js";
11
12
  import type { MSTeamsAdapter } from "./messenger.js";
12
- import { createMSTeamsConversationStoreFs } from "./conversation-store-fs.js";
13
13
  import { getMSTeamsRuntime } from "./runtime.js";
14
14
  import { createMSTeamsAdapter, loadMSTeamsSdkWithAuth } from "./sdk.js";
15
15
  import { resolveMSTeamsCredentials } from "./token.js";
package/src/send.ts CHANGED
@@ -374,6 +374,45 @@ async function sendTextWithMedia(
374
374
  };
375
375
  }
376
376
 
377
+ type ProactiveActivityParams = {
378
+ adapter: MSTeamsProactiveContext["adapter"];
379
+ appId: string;
380
+ ref: MSTeamsProactiveContext["ref"];
381
+ activity: Record<string, unknown>;
382
+ errorPrefix: string;
383
+ };
384
+
385
+ async function sendProactiveActivity({
386
+ adapter,
387
+ appId,
388
+ ref,
389
+ activity,
390
+ errorPrefix,
391
+ }: ProactiveActivityParams): Promise<string> {
392
+ const baseRef = buildConversationReference(ref);
393
+ const proactiveRef = {
394
+ ...baseRef,
395
+ activityId: undefined,
396
+ };
397
+
398
+ let messageId = "unknown";
399
+ try {
400
+ await adapter.continueConversation(appId, proactiveRef, async (ctx) => {
401
+ const response = await ctx.sendActivity(activity);
402
+ messageId = extractMessageId(response) ?? "unknown";
403
+ });
404
+ return messageId;
405
+ } catch (err) {
406
+ const classification = classifyMSTeamsSendError(err);
407
+ const hint = formatMSTeamsSendErrorHint(classification);
408
+ const status = classification.statusCode ? ` (HTTP ${classification.statusCode})` : "";
409
+ throw new Error(
410
+ `${errorPrefix} failed${status}: ${formatUnknownError(err)}${hint ? ` (${hint})` : ""}`,
411
+ { cause: err },
412
+ );
413
+ }
414
+ }
415
+
377
416
  /**
378
417
  * Send a poll (Adaptive Card) to a Teams conversation or user.
379
418
  */
@@ -409,27 +448,13 @@ export async function sendPollMSTeams(
409
448
  };
410
449
 
411
450
  // Send poll via proactive conversation (Adaptive Cards require direct activity send)
412
- const baseRef = buildConversationReference(ref);
413
- const proactiveRef = {
414
- ...baseRef,
415
- activityId: undefined,
416
- };
417
-
418
- let messageId = "unknown";
419
- try {
420
- await adapter.continueConversation(appId, proactiveRef, async (ctx) => {
421
- const response = await ctx.sendActivity(activity);
422
- messageId = extractMessageId(response) ?? "unknown";
423
- });
424
- } catch (err) {
425
- const classification = classifyMSTeamsSendError(err);
426
- const hint = formatMSTeamsSendErrorHint(classification);
427
- const status = classification.statusCode ? ` (HTTP ${classification.statusCode})` : "";
428
- throw new Error(
429
- `msteams poll send failed${status}: ${formatUnknownError(err)}${hint ? ` (${hint})` : ""}`,
430
- { cause: err },
431
- );
432
- }
451
+ const messageId = await sendProactiveActivity({
452
+ adapter,
453
+ appId,
454
+ ref,
455
+ activity,
456
+ errorPrefix: "msteams poll send",
457
+ });
433
458
 
434
459
  log.info("sent poll", { conversationId, pollId: pollCard.pollId, messageId });
435
460
 
@@ -469,27 +494,13 @@ export async function sendAdaptiveCardMSTeams(
469
494
  };
470
495
 
471
496
  // Send card via proactive conversation
472
- const baseRef = buildConversationReference(ref);
473
- const proactiveRef = {
474
- ...baseRef,
475
- activityId: undefined,
476
- };
477
-
478
- let messageId = "unknown";
479
- try {
480
- await adapter.continueConversation(appId, proactiveRef, async (ctx) => {
481
- const response = await ctx.sendActivity(activity);
482
- messageId = extractMessageId(response) ?? "unknown";
483
- });
484
- } catch (err) {
485
- const classification = classifyMSTeamsSendError(err);
486
- const hint = formatMSTeamsSendErrorHint(classification);
487
- const status = classification.statusCode ? ` (HTTP ${classification.statusCode})` : "";
488
- throw new Error(
489
- `msteams card send failed${status}: ${formatUnknownError(err)}${hint ? ` (${hint})` : ""}`,
490
- { cause: err },
491
- );
492
- }
497
+ const messageId = await sendProactiveActivity({
498
+ adapter,
499
+ appId,
500
+ ref,
501
+ activity,
502
+ errorPrefix: "msteams card send",
503
+ });
493
504
 
494
505
  log.info("sent adaptive card", { conversationId, messageId });
495
506
 
package/src/store-fs.ts CHANGED
@@ -1,7 +1,5 @@
1
- import crypto from "node:crypto";
2
1
  import fs from "node:fs";
3
- import path from "node:path";
4
- import { safeParseJson } from "openclaw/plugin-sdk";
2
+ import { readJsonFileWithFallback, writeJsonFileAtomically } from "openclaw/plugin-sdk";
5
3
  import { withFileLock as withPathLock } from "./file-lock.js";
6
4
 
7
5
  const STORE_LOCK_OPTIONS = {
@@ -19,31 +17,11 @@ export async function readJsonFile<T>(
19
17
  filePath: string,
20
18
  fallback: T,
21
19
  ): Promise<{ value: T; exists: boolean }> {
22
- try {
23
- const raw = await fs.promises.readFile(filePath, "utf-8");
24
- const parsed = safeParseJson<T>(raw);
25
- if (parsed == null) {
26
- return { value: fallback, exists: true };
27
- }
28
- return { value: parsed, exists: true };
29
- } catch (err) {
30
- const code = (err as { code?: string }).code;
31
- if (code === "ENOENT") {
32
- return { value: fallback, exists: false };
33
- }
34
- return { value: fallback, exists: false };
35
- }
20
+ return await readJsonFileWithFallback(filePath, fallback);
36
21
  }
37
22
 
38
23
  export async function writeJsonFile(filePath: string, value: unknown): Promise<void> {
39
- const dir = path.dirname(filePath);
40
- await fs.promises.mkdir(dir, { recursive: true, mode: 0o700 });
41
- const tmp = path.join(dir, `${path.basename(filePath)}.${crypto.randomUUID()}.tmp`);
42
- await fs.promises.writeFile(tmp, `${JSON.stringify(value, null, 2)}\n`, {
43
- encoding: "utf-8",
44
- });
45
- await fs.promises.chmod(tmp, 0o600);
46
- await fs.promises.rename(tmp, filePath);
24
+ await writeJsonFileAtomically(filePath, value);
47
25
  }
48
26
 
49
27
  async function ensureJsonFile(filePath: string, fallback: unknown) {
@@ -0,0 +1,16 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+ import type { PluginRuntime } from "openclaw/plugin-sdk";
4
+
5
+ export const msteamsRuntimeStub = {
6
+ state: {
7
+ resolveStateDir: (env: NodeJS.ProcessEnv = process.env, homedir?: () => string) => {
8
+ const override = env.OPENCLAW_STATE_DIR?.trim() || env.OPENCLAW_STATE_DIR?.trim();
9
+ if (override) {
10
+ return override;
11
+ }
12
+ const resolvedHome = homedir ? homedir() : os.homedir();
13
+ return path.join(resolvedHome, ".openclaw");
14
+ },
15
+ },
16
+ } as unknown as PluginRuntime;