From 4d183af0cf734127f2ebc3a3b310c12aff545478 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Tue, 3 Mar 2026 22:15:28 -0800 Subject: [PATCH 01/74] fix: code/cli acpx reliability 20260304 (#34020) * agents: switch claude-cli defaults to bypassPermissions * agents: add claude-cli default args coverage * agents: emit watchdog stall system event for cli runs * agents: test cli watchdog stall system event * acpx: fallback to sessions new when ensure returns no ids * acpx tests: mock sessions new fallback path * acpx tests: cover ensure-empty fallback flow * skills: clarify claude print mode without pty * docs: update cli-backends claude default args * docs: refresh cli live test default args * gateway tests: align live claude args defaults * changelog: credit claude/acpx reliability fixes * Agents: normalize legacy Claude permission flag overrides * Tests: cover legacy Claude permission override normalization * Changelog: note legacy Claude permission flag auto-normalization * ACPX: fail fast when ensure/new return no session IDs * ACPX tests: support empty sessions new fixture output * ACPX tests: assert ensureSession failure when IDs missing * CLI runner: scope watchdog heartbeat wake to session * CLI runner tests: assert session-scoped watchdog wake * Update CHANGELOG.md --- CHANGELOG.md | 2 + docs/gateway/cli-backends.md | 4 +- docs/help/testing.md | 2 +- .../src/runtime-internals/test-fixtures.ts | 37 ++++-- extensions/acpx/src/runtime.test.ts | 47 ++++++++ extensions/acpx/src/runtime.ts | 28 ++++- skills/coding-agent/SKILL.md | 35 ++++-- src/agents/cli-backends.test.ts | 107 ++++++++++++++++++ src/agents/cli-backends.ts | 56 ++++++++- src/agents/cli-runner.test.ts | 52 +++++++++ src/agents/cli-runner.ts | 14 +++ src/gateway/gateway-cli-backend.live.test.ts | 8 +- 12 files changed, 362 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1302dc00187..42f4d644203 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,6 +70,8 @@ Docs: https://docs.openclaw.ai - Agents/Compaction continuity: expand staged-summary merge instructions to preserve active task status, batch progress, latest user request, and follow-up commitments so compaction handoffs retain in-flight work context. (#8903) thanks @joetomasone. - Gateway/status self version reporting: make Gateway self version in `openclaw status` prefer runtime `VERSION` (while preserving explicit `OPENCLAW_VERSION` override), preventing stale post-upgrade app version output. (#32655) thanks @liuxiaopai-ai. - Memory/QMD index isolation: set `QMD_CONFIG_DIR` alongside `XDG_CONFIG_HOME` so QMD config state stays per-agent despite upstream XDG handling bugs, preventing cross-agent collection indexing and excess disk/CPU usage. (#27028) thanks @HenryLoenwind. +- CLI/Coding-agent reliability: switch default `claude-cli` non-interactive args to `--permission-mode bypassPermissions`, auto-normalize legacy `--dangerously-skip-permissions` backend overrides to the modern permission-mode form, align coding-agent + live-test docs with the non-PTY Claude path, and emit session system-event heartbeat notices when CLI watchdog no-output timeouts terminate runs. Related to #28261. Landed from contributor PRs #28610 and #31149. Thanks @niceysam, @cryptomaltese and @vincentkoc. +- ACP/ACPX session bootstrap: retry with `sessions new` when `sessions ensure` returns no session identifiers so ACP spawns avoid `NO_SESSION`/`ACP_TURN_FAILED` failures on affected agents. Related to #28786. Landed from contributor PR #31338. Thanks @Sid-Qin and @vincentkoc. - LINE/auth boundary hardening synthesis: enforce strict LINE webhook authn/z boundary semantics across pairing-store account scoping, DM/group allowlist separation, fail-closed webhook auth/runtime behavior, and replay/duplication controls (including in-flight replay reservation and post-success dedupe marking). (from #26701, #26683, #25978, #17593, #16619, #31990, #26047, #30584, #18777) Thanks @bmendonca3, @davidahmann, @harshang03, @haosenwang1018, @liuxiaopai-ai, @coygeek, and @Takhoffman. - LINE/media download synthesis: fix file-media download handling and M4A audio classification across overlapping LINE regressions. (from #26386, #27761, #27787, #29509, #29755, #29776, #29785, #32240) Thanks @kevinWangSheng, @loiie45e, @carrotRakko, @Sid-Qin, @codeafridi, and @bmendonca3. - LINE/context and routing synthesis: fix group/room peer routing and command-authorization context propagation, and keep processing later events in mixed-success webhook batches. (from #21955, #24475, #27035, #28286) Thanks @lailoo, @mcaxtr, @jervyclaw, @Glucksberg, and @Takhoffman. diff --git a/docs/gateway/cli-backends.md b/docs/gateway/cli-backends.md index 186a5355d33..1c96302462a 100644 --- a/docs/gateway/cli-backends.md +++ b/docs/gateway/cli-backends.md @@ -185,8 +185,8 @@ Input modes: OpenClaw ships a default for `claude-cli`: - `command: "claude"` -- `args: ["-p", "--output-format", "json", "--dangerously-skip-permissions"]` -- `resumeArgs: ["-p", "--output-format", "json", "--dangerously-skip-permissions", "--resume", "{sessionId}"]` +- `args: ["-p", "--output-format", "json", "--permission-mode", "bypassPermissions"]` +- `resumeArgs: ["-p", "--output-format", "json", "--permission-mode", "bypassPermissions", "--resume", "{sessionId}"]` - `modelArg: "--model"` - `systemPromptArg: "--append-system-prompt"` - `sessionArg: "--session-id"` diff --git a/docs/help/testing.md b/docs/help/testing.md index 7c647f11eb2..efb889f1950 100644 --- a/docs/help/testing.md +++ b/docs/help/testing.md @@ -219,7 +219,7 @@ OPENCLAW_LIVE_SETUP_TOKEN=1 OPENCLAW_LIVE_SETUP_TOKEN_PROFILE=anthropic:setup-to - Defaults: - Model: `claude-cli/claude-sonnet-4-6` - Command: `claude` - - Args: `["-p","--output-format","json","--dangerously-skip-permissions"]` + - Args: `["-p","--output-format","json","--permission-mode","bypassPermissions"]` - Overrides (optional): - `OPENCLAW_LIVE_CLI_BACKEND_MODEL="claude-cli/claude-opus-4-6"` - `OPENCLAW_LIVE_CLI_BACKEND_MODEL="codex-cli/gpt-5.3-codex"` diff --git a/extensions/acpx/src/runtime-internals/test-fixtures.ts b/extensions/acpx/src/runtime-internals/test-fixtures.ts index 928867418b8..f5d79122546 100644 --- a/extensions/acpx/src/runtime-internals/test-fixtures.ts +++ b/extensions/acpx/src/runtime-internals/test-fixtures.ts @@ -75,14 +75,35 @@ const setValue = command === "set" ? String(args[commandIndex + 2] || "") : ""; if (command === "sessions" && args[commandIndex + 1] === "ensure") { writeLog({ kind: "ensure", agent, args, sessionName: ensureName }); - emitJson({ - action: "session_ensured", - acpxRecordId: "rec-" + ensureName, - acpxSessionId: "sid-" + ensureName, - agentSessionId: "inner-" + ensureName, - name: ensureName, - created: true, - }); + if (process.env.MOCK_ACPX_ENSURE_EMPTY === "1") { + emitJson({ action: "session_ensured", name: ensureName }); + } else { + emitJson({ + action: "session_ensured", + acpxRecordId: "rec-" + ensureName, + acpxSessionId: "sid-" + ensureName, + agentSessionId: "inner-" + ensureName, + name: ensureName, + created: true, + }); + } + process.exit(0); +} + +if (command === "sessions" && args[commandIndex + 1] === "new") { + writeLog({ kind: "new", agent, args, sessionName: ensureName }); + if (process.env.MOCK_ACPX_NEW_EMPTY === "1") { + emitJson({ action: "session_created", name: ensureName }); + } else { + emitJson({ + action: "session_created", + acpxRecordId: "rec-" + ensureName, + acpxSessionId: "sid-" + ensureName, + agentSessionId: "inner-" + ensureName, + name: ensureName, + created: true, + }); + } process.exit(0); } diff --git a/extensions/acpx/src/runtime.test.ts b/extensions/acpx/src/runtime.test.ts index 44f02cabd5a..5e4baf7f3cb 100644 --- a/extensions/acpx/src/runtime.test.ts +++ b/extensions/acpx/src/runtime.test.ts @@ -377,4 +377,51 @@ describe("AcpxRuntime", () => { expect(report.code).toBe("ACP_BACKEND_UNAVAILABLE"); expect(report.installCommand).toContain("acpx"); }); + + it("falls back to 'sessions new' when 'sessions ensure' returns no session IDs", async () => { + process.env.MOCK_ACPX_ENSURE_EMPTY = "1"; + try { + const { runtime, logPath } = await createMockRuntimeFixture(); + const handle = await runtime.ensureSession({ + sessionKey: "agent:claude:acp:fallback-test", + agent: "claude", + mode: "persistent", + }); + expect(handle.backend).toBe("acpx"); + expect(handle.acpxRecordId).toBe("rec-agent:claude:acp:fallback-test"); + expect(handle.agentSessionId).toBe("inner-agent:claude:acp:fallback-test"); + + const logs = await readMockRuntimeLogEntries(logPath); + expect(logs.some((entry) => entry.kind === "ensure")).toBe(true); + expect(logs.some((entry) => entry.kind === "new")).toBe(true); + } finally { + delete process.env.MOCK_ACPX_ENSURE_EMPTY; + } + }); + + it("fails with ACP_SESSION_INIT_FAILED when both ensure and new omit session IDs", async () => { + process.env.MOCK_ACPX_ENSURE_EMPTY = "1"; + process.env.MOCK_ACPX_NEW_EMPTY = "1"; + try { + const { runtime, logPath } = await createMockRuntimeFixture(); + + await expect( + runtime.ensureSession({ + sessionKey: "agent:claude:acp:fallback-fail", + agent: "claude", + mode: "persistent", + }), + ).rejects.toMatchObject({ + code: "ACP_SESSION_INIT_FAILED", + message: expect.stringContaining("neither 'sessions ensure' nor 'sessions new'"), + }); + + const logs = await readMockRuntimeLogEntries(logPath); + expect(logs.some((entry) => entry.kind === "ensure")).toBe(true); + expect(logs.some((entry) => entry.kind === "new")).toBe(true); + } finally { + delete process.env.MOCK_ACPX_ENSURE_EMPTY; + delete process.env.MOCK_ACPX_NEW_EMPTY; + } + }); }); diff --git a/extensions/acpx/src/runtime.ts b/extensions/acpx/src/runtime.ts index 0d9973afe70..c4a00f008a8 100644 --- a/extensions/acpx/src/runtime.ts +++ b/extensions/acpx/src/runtime.ts @@ -179,7 +179,7 @@ export class AcpxRuntime implements AcpRuntime { const cwd = asTrimmedString(input.cwd) || this.config.cwd; const mode = input.mode; - const events = await this.runControlCommand({ + let events = await this.runControlCommand({ args: this.buildControlArgs({ cwd, command: [agent, "sessions", "ensure", "--name", sessionName], @@ -187,12 +187,36 @@ export class AcpxRuntime implements AcpRuntime { cwd, fallbackCode: "ACP_SESSION_INIT_FAILED", }); - const ensuredEvent = events.find( + let ensuredEvent = events.find( (event) => asOptionalString(event.agentSessionId) || asOptionalString(event.acpxSessionId) || asOptionalString(event.acpxRecordId), ); + + if (!ensuredEvent) { + events = await this.runControlCommand({ + args: this.buildControlArgs({ + cwd, + command: [agent, "sessions", "new", "--name", sessionName], + }), + cwd, + fallbackCode: "ACP_SESSION_INIT_FAILED", + }); + ensuredEvent = events.find( + (event) => + asOptionalString(event.agentSessionId) || + asOptionalString(event.acpxSessionId) || + asOptionalString(event.acpxRecordId), + ); + if (!ensuredEvent) { + throw new AcpRuntimeError( + "ACP_SESSION_INIT_FAILED", + `ACP session init failed: neither 'sessions ensure' nor 'sessions new' returned valid session identifiers for ${sessionName}.`, + ); + } + } + const acpxRecordId = ensuredEvent ? asOptionalString(ensuredEvent.acpxRecordId) : undefined; const agentSessionId = ensuredEvent ? asOptionalString(ensuredEvent.agentSessionId) : undefined; const backendSessionId = ensuredEvent diff --git a/skills/coding-agent/SKILL.md b/skills/coding-agent/SKILL.md index cca6ef83ad5..50db2c14570 100644 --- a/skills/coding-agent/SKILL.md +++ b/skills/coding-agent/SKILL.md @@ -1,6 +1,6 @@ --- name: coding-agent -description: 'Delegate coding tasks to Codex, Claude Code, or Pi agents via background process. Use when: (1) building/creating new features or apps, (2) reviewing PRs (spawn in temp dir), (3) refactoring large codebases, (4) iterative coding that needs file exploration. NOT for: simple one-liner fixes (just edit), reading code (use read tool), thread-bound ACP harness requests in chat (for example spawn/run Codex or Claude Code in a Discord thread; use sessions_spawn with runtime:"acp"), or any work in ~/clawd workspace (never spawn agents here). Requires a bash tool that supports pty:true.' +description: 'Delegate coding tasks to Codex, Claude Code, or Pi agents via background process. Use when: (1) building/creating new features or apps, (2) reviewing PRs (spawn in temp dir), (3) refactoring large codebases, (4) iterative coding that needs file exploration. NOT for: simple one-liner fixes (just edit), reading code (use read tool), thread-bound ACP harness requests in chat (for example spawn/run Codex or Claude Code in a Discord thread; use sessions_spawn with runtime:"acp"), or any work in ~/clawd workspace (never spawn agents here). Claude Code: use --print --permission-mode bypassPermissions (no PTY). Codex/Pi/OpenCode: pty:true required.' metadata: { "openclaw": { "emoji": "🧩", "requires": { "anyBins": ["claude", "codex", "opencode", "pi"] } }, @@ -11,18 +11,27 @@ metadata: Use **bash** (with optional background mode) for all coding agent work. Simple and effective. -## ⚠️ PTY Mode Required! +## ⚠️ PTY Mode: Codex/Pi/OpenCode yes, Claude Code no -Coding agents (Codex, Claude Code, Pi) are **interactive terminal applications** that need a pseudo-terminal (PTY) to work correctly. Without PTY, you'll get broken output, missing colors, or the agent may hang. - -**Always use `pty:true`** when running coding agents: +For **Codex, Pi, and OpenCode**, PTY is still required (interactive terminal apps): ```bash -# ✅ Correct - with PTY +# ✅ Correct for Codex/Pi/OpenCode bash pty:true command:"codex exec 'Your prompt'" +``` -# ❌ Wrong - no PTY, agent may break -bash command:"codex exec 'Your prompt'" +For **Claude Code** (`claude` CLI), use `--print --permission-mode bypassPermissions` instead. +`--dangerously-skip-permissions` with PTY can exit after the confirmation dialog. +`--print` mode keeps full tool access and avoids interactive confirmation: + +```bash +# ✅ Correct for Claude Code (no PTY needed) +cd /path/to/project && claude --permission-mode bypassPermissions --print 'Your task' + +# For background execution: use background:true on the exec tool + +# ❌ Wrong for Claude Code +bash pty:true command:"claude --dangerously-skip-permissions 'task'" ``` ### Bash Tool Parameters @@ -158,11 +167,11 @@ gh pr comment --body "" ## Claude Code ```bash -# With PTY for proper terminal output -bash pty:true workdir:~/project command:"claude 'Your task'" +# Foreground +bash workdir:~/project command:"claude --permission-mode bypassPermissions --print 'Your task'" # Background -bash pty:true workdir:~/project background:true command:"claude 'Your task'" +bash workdir:~/project background:true command:"claude --permission-mode bypassPermissions --print 'Your task'" ``` --- @@ -222,7 +231,9 @@ git worktree remove /tmp/issue-99 ## ⚠️ Rules -1. **Always use pty:true** - coding agents need a terminal! +1. **Use the right execution mode per agent**: + - Codex/Pi/OpenCode: `pty:true` + - Claude Code: `--print --permission-mode bypassPermissions` (no PTY required) 2. **Respect tool choice** - if user asks for Codex, use Codex. - Orchestrator mode: do NOT hand-code patches yourself. - If an agent fails/hangs, respawn it or ask the user for direction, but don't silently take over. diff --git a/src/agents/cli-backends.test.ts b/src/agents/cli-backends.test.ts index c78dfdb87fc..3075462b12e 100644 --- a/src/agents/cli-backends.test.ts +++ b/src/agents/cli-backends.test.ts @@ -34,3 +34,110 @@ describe("resolveCliBackendConfig reliability merge", () => { expect(resolved?.config.reliability?.watchdog?.fresh?.noOutputTimeoutRatio).toBe(0.8); }); }); + +describe("resolveCliBackendConfig claude-cli defaults", () => { + it("uses non-interactive permission-mode defaults for fresh and resume args", () => { + const resolved = resolveCliBackendConfig("claude-cli"); + + expect(resolved).not.toBeNull(); + expect(resolved?.config.args).toContain("--permission-mode"); + expect(resolved?.config.args).toContain("bypassPermissions"); + expect(resolved?.config.args).not.toContain("--dangerously-skip-permissions"); + expect(resolved?.config.resumeArgs).toContain("--permission-mode"); + expect(resolved?.config.resumeArgs).toContain("bypassPermissions"); + expect(resolved?.config.resumeArgs).not.toContain("--dangerously-skip-permissions"); + }); + + it("retains default claude safety args when only command is overridden", () => { + const cfg = { + agents: { + defaults: { + cliBackends: { + "claude-cli": { + command: "/usr/local/bin/claude", + }, + }, + }, + }, + } satisfies OpenClawConfig; + + const resolved = resolveCliBackendConfig("claude-cli", cfg); + + expect(resolved).not.toBeNull(); + expect(resolved?.config.command).toBe("/usr/local/bin/claude"); + expect(resolved?.config.args).toContain("--permission-mode"); + expect(resolved?.config.args).toContain("bypassPermissions"); + expect(resolved?.config.resumeArgs).toContain("--permission-mode"); + expect(resolved?.config.resumeArgs).toContain("bypassPermissions"); + }); + + it("normalizes legacy skip-permissions overrides to permission-mode bypassPermissions", () => { + const cfg = { + agents: { + defaults: { + cliBackends: { + "claude-cli": { + command: "claude", + args: ["-p", "--dangerously-skip-permissions", "--output-format", "json"], + resumeArgs: [ + "-p", + "--dangerously-skip-permissions", + "--output-format", + "json", + "--resume", + "{sessionId}", + ], + }, + }, + }, + }, + } satisfies OpenClawConfig; + + const resolved = resolveCliBackendConfig("claude-cli", cfg); + + expect(resolved).not.toBeNull(); + expect(resolved?.config.args).not.toContain("--dangerously-skip-permissions"); + expect(resolved?.config.args).toContain("--permission-mode"); + expect(resolved?.config.args).toContain("bypassPermissions"); + expect(resolved?.config.resumeArgs).not.toContain("--dangerously-skip-permissions"); + expect(resolved?.config.resumeArgs).toContain("--permission-mode"); + expect(resolved?.config.resumeArgs).toContain("bypassPermissions"); + }); + + it("keeps explicit permission-mode overrides while removing legacy skip flag", () => { + const cfg = { + agents: { + defaults: { + cliBackends: { + "claude-cli": { + command: "claude", + args: ["-p", "--dangerously-skip-permissions", "--permission-mode", "acceptEdits"], + resumeArgs: [ + "-p", + "--dangerously-skip-permissions", + "--permission-mode=acceptEdits", + "--resume", + "{sessionId}", + ], + }, + }, + }, + }, + } satisfies OpenClawConfig; + + const resolved = resolveCliBackendConfig("claude-cli", cfg); + + expect(resolved).not.toBeNull(); + expect(resolved?.config.args).not.toContain("--dangerously-skip-permissions"); + expect(resolved?.config.args).toEqual(["-p", "--permission-mode", "acceptEdits"]); + expect(resolved?.config.resumeArgs).not.toContain("--dangerously-skip-permissions"); + expect(resolved?.config.resumeArgs).toEqual([ + "-p", + "--permission-mode=acceptEdits", + "--resume", + "{sessionId}", + ]); + expect(resolved?.config.args).not.toContain("bypassPermissions"); + expect(resolved?.config.resumeArgs).not.toContain("bypassPermissions"); + }); +}); diff --git a/src/agents/cli-backends.ts b/src/agents/cli-backends.ts index cf3cdb4bb18..92992effa0a 100644 --- a/src/agents/cli-backends.ts +++ b/src/agents/cli-backends.ts @@ -33,14 +33,19 @@ const CLAUDE_MODEL_ALIASES: Record = { "claude-haiku-3-5": "haiku", }; +const CLAUDE_LEGACY_SKIP_PERMISSIONS_ARG = "--dangerously-skip-permissions"; +const CLAUDE_PERMISSION_MODE_ARG = "--permission-mode"; +const CLAUDE_BYPASS_PERMISSIONS_MODE = "bypassPermissions"; + const DEFAULT_CLAUDE_BACKEND: CliBackendConfig = { command: "claude", - args: ["-p", "--output-format", "json", "--dangerously-skip-permissions"], + args: ["-p", "--output-format", "json", "--permission-mode", "bypassPermissions"], resumeArgs: [ "-p", "--output-format", "json", - "--dangerously-skip-permissions", + "--permission-mode", + "bypassPermissions", "--resume", "{sessionId}", ], @@ -147,6 +152,48 @@ function mergeBackendConfig(base: CliBackendConfig, override?: CliBackendConfig) }; } +function normalizeClaudePermissionArgs(args?: string[]): string[] | undefined { + if (!args) { + return args; + } + const normalized: string[] = []; + let sawLegacySkip = false; + let hasPermissionMode = false; + for (let i = 0; i < args.length; i += 1) { + const arg = args[i]; + if (arg === CLAUDE_LEGACY_SKIP_PERMISSIONS_ARG) { + sawLegacySkip = true; + continue; + } + if (arg === CLAUDE_PERMISSION_MODE_ARG) { + hasPermissionMode = true; + normalized.push(arg); + const maybeValue = args[i + 1]; + if (typeof maybeValue === "string") { + normalized.push(maybeValue); + i += 1; + } + continue; + } + if (arg.startsWith(`${CLAUDE_PERMISSION_MODE_ARG}=`)) { + hasPermissionMode = true; + } + normalized.push(arg); + } + if (sawLegacySkip && !hasPermissionMode) { + normalized.push(CLAUDE_PERMISSION_MODE_ARG, CLAUDE_BYPASS_PERMISSIONS_MODE); + } + return normalized; +} + +function normalizeClaudeBackendConfig(config: CliBackendConfig): CliBackendConfig { + return { + ...config, + args: normalizeClaudePermissionArgs(config.args), + resumeArgs: normalizeClaudePermissionArgs(config.resumeArgs), + }; +} + export function resolveCliBackendIds(cfg?: OpenClawConfig): Set { const ids = new Set([ normalizeBackendKey("claude-cli"), @@ -169,11 +216,12 @@ export function resolveCliBackendConfig( if (normalized === "claude-cli") { const merged = mergeBackendConfig(DEFAULT_CLAUDE_BACKEND, override); - const command = merged.command?.trim(); + const config = normalizeClaudeBackendConfig(merged); + const command = config.command?.trim(); if (!command) { return null; } - return { id: normalized, config: { ...merged, command } }; + return { id: normalized, config: { ...config, command } }; } if (normalized === "codex-cli") { const merged = mergeBackendConfig(DEFAULT_CODEX_BACKEND, override); diff --git a/src/agents/cli-runner.test.ts b/src/agents/cli-runner.test.ts index ec2ea4768c5..ec1b0b09ac8 100644 --- a/src/agents/cli-runner.test.ts +++ b/src/agents/cli-runner.test.ts @@ -7,6 +7,8 @@ import { runCliAgent } from "./cli-runner.js"; import { resolveCliNoOutputTimeoutMs } from "./cli-runner/helpers.js"; const supervisorSpawnMock = vi.fn(); +const enqueueSystemEventMock = vi.fn(); +const requestHeartbeatNowMock = vi.fn(); vi.mock("../process/supervisor/index.js", () => ({ getProcessSupervisor: () => ({ @@ -18,6 +20,14 @@ vi.mock("../process/supervisor/index.js", () => ({ }), })); +vi.mock("../infra/system-events.js", () => ({ + enqueueSystemEvent: (...args: unknown[]) => enqueueSystemEventMock(...args), +})); + +vi.mock("../infra/heartbeat-wake.js", () => ({ + requestHeartbeatNow: (...args: unknown[]) => requestHeartbeatNowMock(...args), +})); + type MockRunExit = { reason: | "manual-cancel" @@ -49,6 +59,8 @@ function createManagedRun(exit: MockRunExit, pid = 1234) { describe("runCliAgent with process supervisor", () => { beforeEach(() => { supervisorSpawnMock.mockClear(); + enqueueSystemEventMock.mockClear(); + requestHeartbeatNowMock.mockClear(); }); it("runs CLI through supervisor and returns payload", async () => { @@ -124,6 +136,46 @@ describe("runCliAgent with process supervisor", () => { ).rejects.toThrow("produced no output"); }); + it("enqueues a system event and heartbeat wake on no-output watchdog timeout for session runs", async () => { + supervisorSpawnMock.mockResolvedValueOnce( + createManagedRun({ + reason: "no-output-timeout", + exitCode: null, + exitSignal: "SIGKILL", + durationMs: 200, + stdout: "", + stderr: "", + timedOut: true, + noOutputTimedOut: true, + }), + ); + + await expect( + runCliAgent({ + sessionId: "s1", + sessionKey: "agent:main:main", + sessionFile: "/tmp/session.jsonl", + workspaceDir: "/tmp", + prompt: "hi", + provider: "codex-cli", + model: "gpt-5.2-codex", + timeoutMs: 1_000, + runId: "run-2b", + cliSessionId: "thread-123", + }), + ).rejects.toThrow("produced no output"); + + expect(enqueueSystemEventMock).toHaveBeenCalledTimes(1); + const [notice, opts] = enqueueSystemEventMock.mock.calls[0] ?? []; + expect(String(notice)).toContain("produced no output"); + expect(String(notice)).toContain("interactive input or an approval prompt"); + expect(opts).toMatchObject({ sessionKey: "agent:main:main" }); + expect(requestHeartbeatNowMock).toHaveBeenCalledWith({ + reason: "cli:watchdog:stall", + sessionKey: "agent:main:main", + }); + }); + it("fails with timeout when overall timeout trips", async () => { supervisorSpawnMock.mockResolvedValueOnce( createManagedRun({ diff --git a/src/agents/cli-runner.ts b/src/agents/cli-runner.ts index 0ceca9979d0..3dfe728ce31 100644 --- a/src/agents/cli-runner.ts +++ b/src/agents/cli-runner.ts @@ -4,8 +4,11 @@ import type { ThinkLevel } from "../auto-reply/thinking.js"; import type { OpenClawConfig } from "../config/config.js"; import { shouldLogVerbose } from "../globals.js"; import { isTruthyEnvValue } from "../infra/env.js"; +import { requestHeartbeatNow } from "../infra/heartbeat-wake.js"; +import { enqueueSystemEvent } from "../infra/system-events.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; import { getProcessSupervisor } from "../process/supervisor/index.js"; +import { scopedHeartbeatWakeOptions } from "../routing/session-key.js"; import { resolveSessionAgentIds } from "./agent-scope.js"; import { analyzeBootstrapBudget, @@ -341,6 +344,17 @@ export async function runCliAgent(params: { log.warn( `cli watchdog timeout: provider=${params.provider} model=${modelId} session=${resolvedSessionId ?? params.sessionId} noOutputTimeoutMs=${noOutputTimeoutMs} pid=${managedRun.pid ?? "unknown"}`, ); + if (params.sessionKey) { + const stallNotice = [ + `CLI agent (${params.provider}) produced no output for ${Math.round(noOutputTimeoutMs / 1000)}s and was terminated.`, + "It may have been waiting for interactive input or an approval prompt.", + "For Claude Code, prefer --permission-mode bypassPermissions --print.", + ].join(" "); + enqueueSystemEvent(stallNotice, { sessionKey: params.sessionKey }); + requestHeartbeatNow( + scopedHeartbeatWakeOptions(params.sessionKey, { reason: "cli:watchdog:stall" }), + ); + } throw new FailoverError(timeoutReason, { reason: "timeout", provider: params.provider, diff --git a/src/gateway/gateway-cli-backend.live.test.ts b/src/gateway/gateway-cli-backend.live.test.ts index c25463d796d..b0426c59175 100644 --- a/src/gateway/gateway-cli-backend.live.test.ts +++ b/src/gateway/gateway-cli-backend.live.test.ts @@ -20,7 +20,13 @@ const CLI_RESUME = isTruthyEnvValue(process.env.OPENCLAW_LIVE_CLI_BACKEND_RESUME const describeLive = LIVE && CLI_LIVE ? describe : describe.skip; const DEFAULT_MODEL = "claude-cli/claude-sonnet-4-6"; -const DEFAULT_CLAUDE_ARGS = ["-p", "--output-format", "json", "--dangerously-skip-permissions"]; +const DEFAULT_CLAUDE_ARGS = [ + "-p", + "--output-format", + "json", + "--permission-mode", + "bypassPermissions", +]; const DEFAULT_CODEX_ARGS = [ "exec", "--json", From 646817dd808b214eab635599a5c4909202f7bbd3 Mon Sep 17 00:00:00 2001 From: Josh Avant <830519+joshavant@users.noreply.github.com> Date: Wed, 4 Mar 2026 00:20:44 -0600 Subject: [PATCH 02/74] fix(outbound): unify resolved cfg threading across send paths (#33987) --- CHANGELOG.md | 1 + extensions/discord/src/channel.ts | 8 +- .../googlechat/src/resolve-target.test.ts | 129 +++++++++++- extensions/imessage/src/channel.ts | 1 + extensions/irc/src/channel.ts | 6 +- extensions/irc/src/send.test.ts | 116 +++++++++++ extensions/irc/src/send.ts | 3 +- .../line/src/channel.sendPayload.test.ts | 7 +- extensions/line/src/channel.ts | 16 +- extensions/matrix/src/matrix/send.test.ts | 86 +++++++- extensions/matrix/src/matrix/send.ts | 6 +- extensions/matrix/src/matrix/send/client.ts | 14 +- extensions/matrix/src/matrix/send/types.ts | 1 + extensions/matrix/src/outbound.test.ts | 159 +++++++++++++++ extensions/matrix/src/outbound.ts | 9 +- extensions/mattermost/src/channel.test.ts | 31 +++ extensions/mattermost/src/channel.ts | 6 +- .../mattermost/src/mattermost/send.test.ts | 62 +++++- extensions/mattermost/src/mattermost/send.ts | 5 +- extensions/msteams/src/outbound.test.ts | 131 ++++++++++++ extensions/nextcloud-talk/src/channel.ts | 6 +- extensions/nextcloud-talk/src/send.test.ts | 104 ++++++++++ extensions/nextcloud-talk/src/send.ts | 5 +- extensions/nostr/src/channel.outbound.test.ts | 88 ++++++++ extensions/nostr/src/channel.ts | 4 +- .../signal/src/channel.outbound.test.ts | 63 ++++++ extensions/signal/src/channel.ts | 1 + extensions/slack/src/channel.ts | 2 + extensions/telegram/src/channel.ts | 8 +- .../whatsapp/src/channel.outbound.test.ts | 46 +++++ extensions/whatsapp/src/channel.ts | 18 +- src/agents/tools/discord-actions-messaging.ts | 29 ++- src/agents/tools/discord-actions.test.ts | 6 +- src/agents/tools/discord-actions.ts | 2 +- src/channels/plugins/actions/actions.test.ts | 38 ++-- src/channels/plugins/actions/signal.ts | 4 + .../plugins/outbound/direct-text-media.ts | 2 + src/channels/plugins/outbound/discord.test.ts | 13 +- src/channels/plugins/outbound/discord.ts | 12 +- src/channels/plugins/outbound/imessage.ts | 6 +- src/channels/plugins/outbound/signal.ts | 6 +- src/channels/plugins/outbound/slack.test.ts | 8 +- src/channels/plugins/outbound/slack.ts | 7 +- src/channels/plugins/outbound/telegram.ts | 20 +- .../plugins/outbound/whatsapp.poll.test.ts | 41 ++++ src/channels/plugins/outbound/whatsapp.ts | 9 +- src/commands/message.test.ts | 193 ++++++++++++++++++ src/discord/send.components.ts | 7 +- src/discord/send.outbound.ts | 19 +- src/discord/send.reactions.ts | 12 +- src/discord/send.shared.ts | 9 +- src/discord/send.types.ts | 2 + src/discord/send.webhook-activity.test.ts | 19 ++ .../outbound/cfg-threading.guard.test.ts | 179 ++++++++++++++++ src/infra/outbound/deliver.ts | 10 +- src/infra/outbound/message.channels.test.ts | 70 +++++++ src/line/send.ts | 8 +- src/signal/send-reactions.ts | 5 +- src/signal/send.ts | 5 +- src/slack/send.ts | 5 +- src/telegram/send.ts | 2 + src/web/outbound.ts | 7 +- 62 files changed, 1780 insertions(+), 117 deletions(-) create mode 100644 extensions/irc/src/send.test.ts create mode 100644 extensions/matrix/src/outbound.test.ts create mode 100644 extensions/msteams/src/outbound.test.ts create mode 100644 extensions/nextcloud-talk/src/send.test.ts create mode 100644 extensions/nostr/src/channel.outbound.test.ts create mode 100644 extensions/signal/src/channel.outbound.test.ts create mode 100644 extensions/whatsapp/src/channel.outbound.test.ts create mode 100644 src/channels/plugins/outbound/whatsapp.poll.test.ts create mode 100644 src/infra/outbound/cfg-threading.guard.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 42f4d644203..62e54c6d5dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Outbound/send config threading: pass resolved SecretRef config through outbound adapters and helper send paths so send flows do not reload unresolved runtime config. (#33987) Thanks @joshavant. - Sessions/subagent attachments: remove `attachments[].content.maxLength` from `sessions_spawn` schema to avoid llama.cpp GBNF repetition overflow, and preflight UTF-8 byte size before buffer allocation while keeping runtime file-size enforcement unchanged. (#33648) Thanks @anisoptera. - Runtime/tool-state stability: recover from dangling Anthropic `tool_use` after compaction, serialize long-running Discord handler runs without blocking new inbound events, and prevent stale busy snapshots from suppressing stuck-channel recovery. (from #33630, #33583) Thanks @kevinWangSheng and @theotarr. - Extensions/media local-root propagation: consistently forward `mediaLocalRoots` through extension `sendMedia` adapters (Google Chat, Slack, iMessage, Signal, WhatsApp), preserving non-local media behavior while restoring local attachment resolution from configured roots. Synthesis of #33581, #33545, #33540, #33536, #33528. Thanks @bmendonca3. diff --git a/extensions/discord/src/channel.ts b/extensions/discord/src/channel.ts index bfc2b92db74..3abaa82a956 100644 --- a/extensions/discord/src/channel.ts +++ b/extensions/discord/src/channel.ts @@ -302,10 +302,11 @@ export const discordPlugin: ChannelPlugin = { textChunkLimit: 2000, pollMaxOptions: 10, resolveTarget: ({ to }) => normalizeDiscordOutboundTarget(to), - sendText: async ({ to, text, accountId, deps, replyToId, silent }) => { + sendText: async ({ cfg, to, text, accountId, deps, replyToId, silent }) => { const send = deps?.sendDiscord ?? getDiscordRuntime().channel.discord.sendMessageDiscord; const result = await send(to, text, { verbose: false, + cfg, replyTo: replyToId ?? undefined, accountId: accountId ?? undefined, silent: silent ?? undefined, @@ -313,6 +314,7 @@ export const discordPlugin: ChannelPlugin = { return { channel: "discord", ...result }; }, sendMedia: async ({ + cfg, to, text, mediaUrl, @@ -325,6 +327,7 @@ export const discordPlugin: ChannelPlugin = { const send = deps?.sendDiscord ?? getDiscordRuntime().channel.discord.sendMessageDiscord; const result = await send(to, text, { verbose: false, + cfg, mediaUrl, mediaLocalRoots, replyTo: replyToId ?? undefined, @@ -333,8 +336,9 @@ export const discordPlugin: ChannelPlugin = { }); return { channel: "discord", ...result }; }, - sendPoll: async ({ to, poll, accountId, silent }) => + sendPoll: async ({ cfg, to, poll, accountId, silent }) => await getDiscordRuntime().channel.discord.sendPollDiscord(to, poll, { + cfg, accountId: accountId ?? undefined, silent: silent ?? undefined, }), diff --git a/extensions/googlechat/src/resolve-target.test.ts b/extensions/googlechat/src/resolve-target.test.ts index d4b53036f1f..82e340874df 100644 --- a/extensions/googlechat/src/resolve-target.test.ts +++ b/extensions/googlechat/src/resolve-target.test.ts @@ -1,6 +1,11 @@ -import { describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { installCommonResolveTargetErrorCases } from "../../shared/resolve-target-test-helpers.js"; +const runtimeMocks = vi.hoisted(() => ({ + chunkMarkdownText: vi.fn((text: string) => [text]), + fetchRemoteMedia: vi.fn(), +})); + vi.mock("openclaw/plugin-sdk", () => ({ getChatChannelMeta: () => ({ id: "googlechat", label: "Google Chat" }), missingTargetError: (provider: string, hint: string) => @@ -47,7 +52,8 @@ vi.mock("./onboarding.js", () => ({ vi.mock("./runtime.js", () => ({ getGoogleChatRuntime: vi.fn(() => ({ channel: { - text: { chunkMarkdownText: vi.fn() }, + text: { chunkMarkdownText: runtimeMocks.chunkMarkdownText }, + media: { fetchRemoteMedia: runtimeMocks.fetchRemoteMedia }, }, })), })); @@ -66,7 +72,11 @@ vi.mock("./targets.js", () => ({ resolveGoogleChatOutboundSpace: vi.fn(), })); +import { resolveChannelMediaMaxBytes } from "openclaw/plugin-sdk"; +import { resolveGoogleChatAccount } from "./accounts.js"; +import { sendGoogleChatMessage, uploadGoogleChatAttachment } from "./api.js"; import { googlechatPlugin } from "./channel.js"; +import { resolveGoogleChatOutboundSpace } from "./targets.js"; const resolveTarget = googlechatPlugin.outbound!.resolveTarget!; @@ -104,3 +114,118 @@ describe("googlechat resolveTarget", () => { implicitAllowFrom: ["spaces/BBB"], }); }); + +describe("googlechat outbound cfg threading", () => { + beforeEach(() => { + runtimeMocks.fetchRemoteMedia.mockReset(); + runtimeMocks.chunkMarkdownText.mockClear(); + vi.mocked(resolveGoogleChatAccount).mockReset(); + vi.mocked(resolveGoogleChatOutboundSpace).mockReset(); + vi.mocked(resolveChannelMediaMaxBytes).mockReset(); + vi.mocked(uploadGoogleChatAttachment).mockReset(); + vi.mocked(sendGoogleChatMessage).mockReset(); + }); + + it("threads resolved cfg into sendText account resolution", async () => { + const cfg = { + channels: { + googlechat: { + serviceAccount: { + type: "service_account", + }, + }, + }, + }; + const account = { + accountId: "default", + config: {}, + credentialSource: "inline", + }; + vi.mocked(resolveGoogleChatAccount).mockReturnValue(account as any); + vi.mocked(resolveGoogleChatOutboundSpace).mockResolvedValue("spaces/AAA"); + vi.mocked(sendGoogleChatMessage).mockResolvedValue({ + messageName: "spaces/AAA/messages/msg-1", + } as any); + + await googlechatPlugin.outbound!.sendText!({ + cfg: cfg as any, + to: "users/123", + text: "hello", + accountId: "default", + }); + + expect(resolveGoogleChatAccount).toHaveBeenCalledWith({ + cfg, + accountId: "default", + }); + expect(sendGoogleChatMessage).toHaveBeenCalledWith( + expect.objectContaining({ + account, + space: "spaces/AAA", + text: "hello", + }), + ); + }); + + it("threads resolved cfg into sendMedia account and media loading path", async () => { + const cfg = { + channels: { + googlechat: { + serviceAccount: { + type: "service_account", + }, + mediaMaxMb: 8, + }, + }, + }; + const account = { + accountId: "default", + config: { mediaMaxMb: 20 }, + credentialSource: "inline", + }; + vi.mocked(resolveGoogleChatAccount).mockReturnValue(account as any); + vi.mocked(resolveGoogleChatOutboundSpace).mockResolvedValue("spaces/AAA"); + vi.mocked(resolveChannelMediaMaxBytes).mockReturnValue(1024); + runtimeMocks.fetchRemoteMedia.mockResolvedValueOnce({ + buffer: Buffer.from("file"), + fileName: "file.png", + contentType: "image/png", + }); + vi.mocked(uploadGoogleChatAttachment).mockResolvedValue({ + attachmentUploadToken: "token-1", + } as any); + vi.mocked(sendGoogleChatMessage).mockResolvedValue({ + messageName: "spaces/AAA/messages/msg-2", + } as any); + + await googlechatPlugin.outbound!.sendMedia!({ + cfg: cfg as any, + to: "users/123", + text: "photo", + mediaUrl: "https://example.com/file.png", + accountId: "default", + }); + + expect(resolveGoogleChatAccount).toHaveBeenCalledWith({ + cfg, + accountId: "default", + }); + expect(runtimeMocks.fetchRemoteMedia).toHaveBeenCalledWith({ + url: "https://example.com/file.png", + maxBytes: 1024, + }); + expect(uploadGoogleChatAttachment).toHaveBeenCalledWith( + expect.objectContaining({ + account, + space: "spaces/AAA", + filename: "file.png", + }), + ); + expect(sendGoogleChatMessage).toHaveBeenCalledWith( + expect.objectContaining({ + account, + attachments: [{ attachmentUploadToken: "token-1", contentName: "file.png" }], + }), + ); + }); +}); diff --git a/extensions/imessage/src/channel.ts b/extensions/imessage/src/channel.ts index 1a3eee85102..0835f6734ad 100644 --- a/extensions/imessage/src/channel.ts +++ b/extensions/imessage/src/channel.ts @@ -69,6 +69,7 @@ async function sendIMessageOutbound(params: { accountId: params.accountId, }); return await send(params.to, params.text, { + config: params.cfg, ...(params.mediaUrl ? { mediaUrl: params.mediaUrl } : {}), ...(params.mediaLocalRoots?.length ? { mediaLocalRoots: params.mediaLocalRoots } : {}), maxBytes, diff --git a/extensions/irc/src/channel.ts b/extensions/irc/src/channel.ts index 6993baa0ba7..30fd9f9faa5 100644 --- a/extensions/irc/src/channel.ts +++ b/extensions/irc/src/channel.ts @@ -296,16 +296,18 @@ export const ircPlugin: ChannelPlugin = { chunker: (text, limit) => getIrcRuntime().channel.text.chunkMarkdownText(text, limit), chunkerMode: "markdown", textChunkLimit: 350, - sendText: async ({ to, text, accountId, replyToId }) => { + sendText: async ({ cfg, to, text, accountId, replyToId }) => { const result = await sendMessageIrc(to, text, { + cfg: cfg as CoreConfig, accountId: accountId ?? undefined, replyTo: replyToId ?? undefined, }); return { channel: "irc", ...result }; }, - sendMedia: async ({ to, text, mediaUrl, accountId, replyToId }) => { + sendMedia: async ({ cfg, to, text, mediaUrl, accountId, replyToId }) => { const combined = mediaUrl ? `${text}\n\nAttachment: ${mediaUrl}` : text; const result = await sendMessageIrc(to, combined, { + cfg: cfg as CoreConfig, accountId: accountId ?? undefined, replyTo: replyToId ?? undefined, }); diff --git a/extensions/irc/src/send.test.ts b/extensions/irc/src/send.test.ts new file mode 100644 index 00000000000..df7b5e60ddd --- /dev/null +++ b/extensions/irc/src/send.test.ts @@ -0,0 +1,116 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { IrcClient } from "./client.js"; +import type { CoreConfig } from "./types.js"; + +const hoisted = vi.hoisted(() => { + const loadConfig = vi.fn(); + const resolveMarkdownTableMode = vi.fn(() => "preserve"); + const convertMarkdownTables = vi.fn((text: string) => text); + const record = vi.fn(); + return { + loadConfig, + resolveMarkdownTableMode, + convertMarkdownTables, + record, + resolveIrcAccount: vi.fn(() => ({ + configured: true, + accountId: "default", + host: "irc.example.com", + nick: "openclaw", + port: 6697, + tls: true, + })), + normalizeIrcMessagingTarget: vi.fn((value: string) => value.trim()), + connectIrcClient: vi.fn(), + buildIrcConnectOptions: vi.fn(() => ({})), + }; +}); + +vi.mock("./runtime.js", () => ({ + getIrcRuntime: () => ({ + config: { + loadConfig: hoisted.loadConfig, + }, + channel: { + text: { + resolveMarkdownTableMode: hoisted.resolveMarkdownTableMode, + convertMarkdownTables: hoisted.convertMarkdownTables, + }, + activity: { + record: hoisted.record, + }, + }, + }), +})); + +vi.mock("./accounts.js", () => ({ + resolveIrcAccount: hoisted.resolveIrcAccount, +})); + +vi.mock("./normalize.js", () => ({ + normalizeIrcMessagingTarget: hoisted.normalizeIrcMessagingTarget, +})); + +vi.mock("./client.js", () => ({ + connectIrcClient: hoisted.connectIrcClient, +})); + +vi.mock("./connect-options.js", () => ({ + buildIrcConnectOptions: hoisted.buildIrcConnectOptions, +})); + +vi.mock("./protocol.js", async () => { + const actual = await vi.importActual("./protocol.js"); + return { + ...actual, + makeIrcMessageId: () => "irc-msg-1", + }; +}); + +import { sendMessageIrc } from "./send.js"; + +describe("sendMessageIrc cfg threading", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("uses explicitly provided cfg without loading runtime config", async () => { + const providedCfg = { source: "provided" } as unknown as CoreConfig; + const client = { + isReady: vi.fn(() => true), + sendPrivmsg: vi.fn(), + } as unknown as IrcClient; + + const result = await sendMessageIrc("#room", "hello", { + cfg: providedCfg, + client, + accountId: "work", + }); + + expect(hoisted.loadConfig).not.toHaveBeenCalled(); + expect(hoisted.resolveIrcAccount).toHaveBeenCalledWith({ + cfg: providedCfg, + accountId: "work", + }); + expect(client.sendPrivmsg).toHaveBeenCalledWith("#room", "hello"); + expect(result).toEqual({ messageId: "irc-msg-1", target: "#room" }); + }); + + it("falls back to runtime config when cfg is omitted", async () => { + const runtimeCfg = { source: "runtime" } as unknown as CoreConfig; + hoisted.loadConfig.mockReturnValueOnce(runtimeCfg); + const client = { + isReady: vi.fn(() => true), + sendPrivmsg: vi.fn(), + } as unknown as IrcClient; + + await sendMessageIrc("#ops", "ping", { client }); + + expect(hoisted.loadConfig).toHaveBeenCalledTimes(1); + expect(hoisted.resolveIrcAccount).toHaveBeenCalledWith({ + cfg: runtimeCfg, + accountId: undefined, + }); + expect(client.sendPrivmsg).toHaveBeenCalledWith("#ops", "ping"); + }); +}); diff --git a/extensions/irc/src/send.ts b/extensions/irc/src/send.ts index e60859d44e9..544f81f3f47 100644 --- a/extensions/irc/src/send.ts +++ b/extensions/irc/src/send.ts @@ -8,6 +8,7 @@ import { getIrcRuntime } from "./runtime.js"; import type { CoreConfig } from "./types.js"; type SendIrcOptions = { + cfg?: CoreConfig; accountId?: string; replyTo?: string; target?: string; @@ -37,7 +38,7 @@ export async function sendMessageIrc( opts: SendIrcOptions = {}, ): Promise { const runtime = getIrcRuntime(); - const cfg = runtime.config.loadConfig() as CoreConfig; + const cfg = (opts.cfg ?? runtime.config.loadConfig()) as CoreConfig; const account = resolveIrcAccount({ cfg, accountId: opts.accountId, diff --git a/extensions/line/src/channel.sendPayload.test.ts b/extensions/line/src/channel.sendPayload.test.ts index e92551538e9..95dd8e2d4ce 100644 --- a/extensions/line/src/channel.sendPayload.test.ts +++ b/extensions/line/src/channel.sendPayload.test.ts @@ -117,6 +117,7 @@ describe("linePlugin outbound.sendPayload", () => { expect(mocks.pushMessageLine).toHaveBeenCalledWith("line:group:1", "Now playing:", { verbose: false, accountId: "default", + cfg, }); }); @@ -154,6 +155,7 @@ describe("linePlugin outbound.sendPayload", () => { expect(mocks.pushMessageLine).toHaveBeenCalledWith("line:user:1", "Choose one:", { verbose: false, accountId: "default", + cfg, }); }); @@ -193,7 +195,7 @@ describe("linePlugin outbound.sendPayload", () => { quickReply: { items: ["One", "Two"] }, }, ], - { verbose: false, accountId: "default" }, + { verbose: false, accountId: "default", cfg }, ); expect(mocks.createQuickReplyItems).toHaveBeenCalledWith(["One", "Two"]); }); @@ -225,12 +227,13 @@ describe("linePlugin outbound.sendPayload", () => { verbose: false, mediaUrl: "https://example.com/img.jpg", accountId: "default", + cfg, }); expect(mocks.pushTextMessageWithQuickReplies).toHaveBeenCalledWith( "line:user:3", "Hello", ["One", "Two"], - { verbose: false, accountId: "default" }, + { verbose: false, accountId: "default", cfg }, ); const mediaOrder = mocks.sendMessageLine.mock.invocationCallOrder[0]; const quickReplyOrder = mocks.pushTextMessageWithQuickReplies.mock.invocationCallOrder[0]; diff --git a/extensions/line/src/channel.ts b/extensions/line/src/channel.ts index f5a0f9de107..c29046eaaf0 100644 --- a/extensions/line/src/channel.ts +++ b/extensions/line/src/channel.ts @@ -372,6 +372,7 @@ export const linePlugin: ChannelPlugin = { const batch = messages.slice(i, i + 5) as unknown as Parameters[1]; const result = await sendBatch(to, batch, { verbose: false, + cfg, accountId: accountId ?? undefined, }); lastResult = { messageId: result.messageId, chatId: result.chatId }; @@ -399,6 +400,7 @@ export const linePlugin: ChannelPlugin = { const flexContents = lineData.flexMessage.contents as Parameters[2]; lastResult = await sendFlex(to, lineData.flexMessage.altText, flexContents, { verbose: false, + cfg, accountId: accountId ?? undefined, }); } @@ -408,6 +410,7 @@ export const linePlugin: ChannelPlugin = { if (template) { lastResult = await sendTemplate(to, template, { verbose: false, + cfg, accountId: accountId ?? undefined, }); } @@ -416,6 +419,7 @@ export const linePlugin: ChannelPlugin = { if (lineData.location) { lastResult = await sendLocation(to, lineData.location, { verbose: false, + cfg, accountId: accountId ?? undefined, }); } @@ -425,6 +429,7 @@ export const linePlugin: ChannelPlugin = { const flexContents = flexMsg.contents as Parameters[2]; lastResult = await sendFlex(to, flexMsg.altText, flexContents, { verbose: false, + cfg, accountId: accountId ?? undefined, }); } @@ -436,6 +441,7 @@ export const linePlugin: ChannelPlugin = { lastResult = await runtime.channel.line.sendMessageLine(to, "", { verbose: false, mediaUrl: url, + cfg, accountId: accountId ?? undefined, }); } @@ -447,11 +453,13 @@ export const linePlugin: ChannelPlugin = { if (isLast && hasQuickReplies) { lastResult = await sendQuickReplies(to, chunks[i], quickReplies, { verbose: false, + cfg, accountId: accountId ?? undefined, }); } else { lastResult = await sendText(to, chunks[i], { verbose: false, + cfg, accountId: accountId ?? undefined, }); } @@ -513,6 +521,7 @@ export const linePlugin: ChannelPlugin = { lastResult = await runtime.channel.line.sendMessageLine(to, "", { verbose: false, mediaUrl: url, + cfg, accountId: accountId ?? undefined, }); } @@ -523,7 +532,7 @@ export const linePlugin: ChannelPlugin = { } return { channel: "line", messageId: "empty", chatId: to }; }, - sendText: async ({ to, text, accountId }) => { + sendText: async ({ cfg, to, text, accountId }) => { const runtime = getLineRuntime(); const sendText = runtime.channel.line.pushMessageLine; const sendFlex = runtime.channel.line.pushFlexMessage; @@ -536,6 +545,7 @@ export const linePlugin: ChannelPlugin = { if (processed.text.trim()) { result = await sendText(to, processed.text, { verbose: false, + cfg, accountId: accountId ?? undefined, }); } else { @@ -549,17 +559,19 @@ export const linePlugin: ChannelPlugin = { const flexContents = flexMsg.contents as Parameters[2]; await sendFlex(to, flexMsg.altText, flexContents, { verbose: false, + cfg, accountId: accountId ?? undefined, }); } return { channel: "line", ...result }; }, - sendMedia: async ({ to, text, mediaUrl, accountId }) => { + sendMedia: async ({ cfg, to, text, mediaUrl, accountId }) => { const send = getLineRuntime().channel.line.sendMessageLine; const result = await send(to, text, { verbose: false, mediaUrl, + cfg, accountId: accountId ?? undefined, }); return { channel: "line", ...result }; diff --git a/extensions/matrix/src/matrix/send.test.ts b/extensions/matrix/src/matrix/send.test.ts index 8ad67ca2312..234c9950216 100644 --- a/extensions/matrix/src/matrix/send.test.ts +++ b/extensions/matrix/src/matrix/send.test.ts @@ -34,6 +34,7 @@ const loadWebMediaMock = vi.fn().mockResolvedValue({ contentType: "image/png", kind: "image", }); +const runtimeLoadConfigMock = vi.fn(() => ({})); const mediaKindFromMimeMock = vi.fn(() => "image"); const isVoiceCompatibleAudioMock = vi.fn(() => false); const getImageMetadataMock = vi.fn().mockResolvedValue(null); @@ -41,7 +42,7 @@ const resizeToJpegMock = vi.fn(); const runtimeStub = { config: { - loadConfig: () => ({}), + loadConfig: runtimeLoadConfigMock, }, media: { loadWebMedia: loadWebMediaMock as unknown as PluginRuntime["media"]["loadWebMedia"], @@ -65,6 +66,7 @@ const runtimeStub = { } as unknown as PluginRuntime; let sendMessageMatrix: typeof import("./send.js").sendMessageMatrix; +let resolveMediaMaxBytes: typeof import("./send/client.js").resolveMediaMaxBytes; const makeClient = () => { const sendMessage = vi.fn().mockResolvedValue("evt1"); @@ -80,11 +82,14 @@ const makeClient = () => { beforeAll(async () => { setMatrixRuntime(runtimeStub); ({ sendMessageMatrix } = await import("./send.js")); + ({ resolveMediaMaxBytes } = await import("./send/client.js")); }); describe("sendMessageMatrix media", () => { beforeEach(() => { vi.clearAllMocks(); + runtimeLoadConfigMock.mockReset(); + runtimeLoadConfigMock.mockReturnValue({}); mediaKindFromMimeMock.mockReturnValue("image"); isVoiceCompatibleAudioMock.mockReturnValue(false); setMatrixRuntime(runtimeStub); @@ -214,6 +219,8 @@ describe("sendMessageMatrix media", () => { describe("sendMessageMatrix threads", () => { beforeEach(() => { vi.clearAllMocks(); + runtimeLoadConfigMock.mockReset(); + runtimeLoadConfigMock.mockReturnValue({}); setMatrixRuntime(runtimeStub); }); @@ -240,3 +247,80 @@ describe("sendMessageMatrix threads", () => { }); }); }); + +describe("sendMessageMatrix cfg threading", () => { + beforeEach(() => { + vi.clearAllMocks(); + runtimeLoadConfigMock.mockReset(); + runtimeLoadConfigMock.mockReturnValue({ + channels: { + matrix: { + mediaMaxMb: 7, + }, + }, + }); + setMatrixRuntime(runtimeStub); + }); + + it("does not call runtime loadConfig when cfg is provided", async () => { + const { client } = makeClient(); + const providedCfg = { + channels: { + matrix: { + mediaMaxMb: 4, + }, + }, + }; + + await sendMessageMatrix("room:!room:example", "hello cfg", { + client, + cfg: providedCfg as any, + }); + + expect(runtimeLoadConfigMock).not.toHaveBeenCalled(); + }); + + it("falls back to runtime loadConfig when cfg is omitted", async () => { + const { client } = makeClient(); + + await sendMessageMatrix("room:!room:example", "hello runtime", { client }); + + expect(runtimeLoadConfigMock).toHaveBeenCalledTimes(1); + }); +}); + +describe("resolveMediaMaxBytes cfg threading", () => { + beforeEach(() => { + runtimeLoadConfigMock.mockReset(); + runtimeLoadConfigMock.mockReturnValue({ + channels: { + matrix: { + mediaMaxMb: 9, + }, + }, + }); + setMatrixRuntime(runtimeStub); + }); + + it("uses provided cfg and skips runtime loadConfig", () => { + const providedCfg = { + channels: { + matrix: { + mediaMaxMb: 3, + }, + }, + }; + + const maxBytes = resolveMediaMaxBytes(undefined, providedCfg as any); + + expect(maxBytes).toBe(3 * 1024 * 1024); + expect(runtimeLoadConfigMock).not.toHaveBeenCalled(); + }); + + it("falls back to runtime loadConfig when cfg is omitted", () => { + const maxBytes = resolveMediaMaxBytes(); + + expect(maxBytes).toBe(9 * 1024 * 1024); + expect(runtimeLoadConfigMock).toHaveBeenCalledTimes(1); + }); +}); diff --git a/extensions/matrix/src/matrix/send.ts b/extensions/matrix/src/matrix/send.ts index dd72ec2883b..80c1c120333 100644 --- a/extensions/matrix/src/matrix/send.ts +++ b/extensions/matrix/src/matrix/send.ts @@ -47,11 +47,12 @@ export async function sendMessageMatrix( client: opts.client, timeoutMs: opts.timeoutMs, accountId: opts.accountId, + cfg: opts.cfg, }); + const cfg = opts.cfg ?? getCore().config.loadConfig(); try { const roomId = await resolveMatrixRoomId(client, to); return await enqueueSend(roomId, async () => { - const cfg = getCore().config.loadConfig(); const tableMode = getCore().channel.text.resolveMarkdownTableMode({ cfg, channel: "matrix", @@ -81,7 +82,7 @@ export async function sendMessageMatrix( let lastMessageId = ""; if (opts.mediaUrl) { - const maxBytes = resolveMediaMaxBytes(opts.accountId); + const maxBytes = resolveMediaMaxBytes(opts.accountId, cfg); const media = await getCore().media.loadWebMedia(opts.mediaUrl, maxBytes); const uploaded = await uploadMediaMaybeEncrypted(client, roomId, media.buffer, { contentType: media.contentType, @@ -171,6 +172,7 @@ export async function sendPollMatrix( client: opts.client, timeoutMs: opts.timeoutMs, accountId: opts.accountId, + cfg: opts.cfg, }); try { diff --git a/extensions/matrix/src/matrix/send/client.ts b/extensions/matrix/src/matrix/send/client.ts index 9eee35e88ba..e56cf493758 100644 --- a/extensions/matrix/src/matrix/send/client.ts +++ b/extensions/matrix/src/matrix/send/client.ts @@ -32,19 +32,19 @@ function findAccountConfig( return undefined; } -export function resolveMediaMaxBytes(accountId?: string): number | undefined { - const cfg = getCore().config.loadConfig() as CoreConfig; +export function resolveMediaMaxBytes(accountId?: string, cfg?: CoreConfig): number | undefined { + const resolvedCfg = cfg ?? (getCore().config.loadConfig() as CoreConfig); // Check account-specific config first (case-insensitive key matching) const accountConfig = findAccountConfig( - cfg.channels?.matrix?.accounts as Record | undefined, + resolvedCfg.channels?.matrix?.accounts as Record | undefined, accountId ?? "", ); if (typeof accountConfig?.mediaMaxMb === "number") { return (accountConfig.mediaMaxMb as number) * 1024 * 1024; } // Fall back to top-level config - if (typeof cfg.channels?.matrix?.mediaMaxMb === "number") { - return cfg.channels.matrix.mediaMaxMb * 1024 * 1024; + if (typeof resolvedCfg.channels?.matrix?.mediaMaxMb === "number") { + return resolvedCfg.channels.matrix.mediaMaxMb * 1024 * 1024; } return undefined; } @@ -53,6 +53,7 @@ export async function resolveMatrixClient(opts: { client?: MatrixClient; timeoutMs?: number; accountId?: string; + cfg?: CoreConfig; }): Promise<{ client: MatrixClient; stopOnDone: boolean }> { ensureNodeRuntime(); if (opts.client) { @@ -84,10 +85,11 @@ export async function resolveMatrixClient(opts: { const client = await resolveSharedMatrixClient({ timeoutMs: opts.timeoutMs, accountId, + cfg: opts.cfg, }); return { client, stopOnDone: false }; } - const auth = await resolveMatrixAuth({ accountId }); + const auth = await resolveMatrixAuth({ accountId, cfg: opts.cfg }); const client = await createPreparedMatrixClient({ auth, timeoutMs: opts.timeoutMs, diff --git a/extensions/matrix/src/matrix/send/types.ts b/extensions/matrix/src/matrix/send/types.ts index 2b91327aadb..e3aec1dcae7 100644 --- a/extensions/matrix/src/matrix/send/types.ts +++ b/extensions/matrix/src/matrix/send/types.ts @@ -85,6 +85,7 @@ export type MatrixSendResult = { }; export type MatrixSendOpts = { + cfg?: import("../../types.js").CoreConfig; client?: import("@vector-im/matrix-bot-sdk").MatrixClient; mediaUrl?: string; accountId?: string; diff --git a/extensions/matrix/src/outbound.test.ts b/extensions/matrix/src/outbound.test.ts new file mode 100644 index 00000000000..cc70d5cd75b --- /dev/null +++ b/extensions/matrix/src/outbound.test.ts @@ -0,0 +1,159 @@ +import type { OpenClawConfig } from "openclaw/plugin-sdk"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => ({ + sendMessageMatrix: vi.fn(), + sendPollMatrix: vi.fn(), +})); + +vi.mock("./matrix/send.js", () => ({ + sendMessageMatrix: mocks.sendMessageMatrix, + sendPollMatrix: mocks.sendPollMatrix, +})); + +vi.mock("./runtime.js", () => ({ + getMatrixRuntime: () => ({ + channel: { + text: { + chunkMarkdownText: (text: string) => [text], + }, + }, + }), +})); + +import { matrixOutbound } from "./outbound.js"; + +describe("matrixOutbound cfg threading", () => { + beforeEach(() => { + mocks.sendMessageMatrix.mockReset(); + mocks.sendPollMatrix.mockReset(); + mocks.sendMessageMatrix.mockResolvedValue({ messageId: "evt-1", roomId: "!room:example" }); + mocks.sendPollMatrix.mockResolvedValue({ eventId: "$poll", roomId: "!room:example" }); + }); + + it("passes resolved cfg to sendMessageMatrix for text sends", async () => { + const cfg = { + channels: { + matrix: { + accessToken: "resolved-token", + }, + }, + } as OpenClawConfig; + + await matrixOutbound.sendText!({ + cfg, + to: "room:!room:example", + text: "hello", + accountId: "default", + threadId: "$thread", + replyToId: "$reply", + }); + + expect(mocks.sendMessageMatrix).toHaveBeenCalledWith( + "room:!room:example", + "hello", + expect.objectContaining({ + cfg, + accountId: "default", + threadId: "$thread", + replyToId: "$reply", + }), + ); + }); + + it("passes resolved cfg to sendMessageMatrix for media sends", async () => { + const cfg = { + channels: { + matrix: { + accessToken: "resolved-token", + }, + }, + } as OpenClawConfig; + + await matrixOutbound.sendMedia!({ + cfg, + to: "room:!room:example", + text: "caption", + mediaUrl: "file:///tmp/cat.png", + accountId: "default", + }); + + expect(mocks.sendMessageMatrix).toHaveBeenCalledWith( + "room:!room:example", + "caption", + expect.objectContaining({ + cfg, + mediaUrl: "file:///tmp/cat.png", + }), + ); + }); + + it("passes resolved cfg through injected deps.sendMatrix", async () => { + const cfg = { + channels: { + matrix: { + accessToken: "resolved-token", + }, + }, + } as OpenClawConfig; + const sendMatrix = vi.fn(async () => ({ + messageId: "evt-injected", + roomId: "!room:example", + })); + + await matrixOutbound.sendText!({ + cfg, + to: "room:!room:example", + text: "hello via deps", + deps: { sendMatrix }, + accountId: "default", + threadId: "$thread", + replyToId: "$reply", + }); + + expect(sendMatrix).toHaveBeenCalledWith( + "room:!room:example", + "hello via deps", + expect.objectContaining({ + cfg, + accountId: "default", + threadId: "$thread", + replyToId: "$reply", + }), + ); + }); + + it("passes resolved cfg to sendPollMatrix", async () => { + const cfg = { + channels: { + matrix: { + accessToken: "resolved-token", + }, + }, + } as OpenClawConfig; + + await matrixOutbound.sendPoll!({ + cfg, + to: "room:!room:example", + poll: { + question: "Snack?", + options: ["Pizza", "Sushi"], + }, + accountId: "default", + threadId: "$thread", + }); + + expect(mocks.sendPollMatrix).toHaveBeenCalledWith( + "room:!room:example", + expect.objectContaining({ + question: "Snack?", + options: ["Pizza", "Sushi"], + }), + expect.objectContaining({ + cfg, + accountId: "default", + threadId: "$thread", + }), + ); + }); +}); diff --git a/extensions/matrix/src/outbound.ts b/extensions/matrix/src/outbound.ts index 5ad3afbaf03..34d084c609b 100644 --- a/extensions/matrix/src/outbound.ts +++ b/extensions/matrix/src/outbound.ts @@ -7,11 +7,12 @@ export const matrixOutbound: ChannelOutboundAdapter = { chunker: (text, limit) => getMatrixRuntime().channel.text.chunkMarkdownText(text, limit), chunkerMode: "markdown", textChunkLimit: 4000, - sendText: async ({ to, text, deps, replyToId, threadId, accountId }) => { + sendText: async ({ cfg, to, text, deps, replyToId, threadId, accountId }) => { const send = deps?.sendMatrix ?? sendMessageMatrix; const resolvedThreadId = threadId !== undefined && threadId !== null ? String(threadId) : undefined; const result = await send(to, text, { + cfg, replyToId: replyToId ?? undefined, threadId: resolvedThreadId, accountId: accountId ?? undefined, @@ -22,11 +23,12 @@ export const matrixOutbound: ChannelOutboundAdapter = { roomId: result.roomId, }; }, - sendMedia: async ({ to, text, mediaUrl, deps, replyToId, threadId, accountId }) => { + sendMedia: async ({ cfg, to, text, mediaUrl, deps, replyToId, threadId, accountId }) => { const send = deps?.sendMatrix ?? sendMessageMatrix; const resolvedThreadId = threadId !== undefined && threadId !== null ? String(threadId) : undefined; const result = await send(to, text, { + cfg, mediaUrl, replyToId: replyToId ?? undefined, threadId: resolvedThreadId, @@ -38,10 +40,11 @@ export const matrixOutbound: ChannelOutboundAdapter = { roomId: result.roomId, }; }, - sendPoll: async ({ to, poll, threadId, accountId }) => { + sendPoll: async ({ cfg, to, poll, threadId, accountId }) => { const resolvedThreadId = threadId !== undefined && threadId !== null ? String(threadId) : undefined; const result = await sendPollMatrix(to, poll, { + cfg, threadId: resolvedThreadId, accountId: accountId ?? undefined, }); diff --git a/extensions/mattermost/src/channel.test.ts b/extensions/mattermost/src/channel.test.ts index cafc8190d58..c448438278f 100644 --- a/extensions/mattermost/src/channel.test.ts +++ b/extensions/mattermost/src/channel.test.ts @@ -240,6 +240,37 @@ describe("mattermostPlugin", () => { }), ); }); + + it("threads resolved cfg on sendText", async () => { + const sendText = mattermostPlugin.outbound?.sendText; + if (!sendText) { + return; + } + const cfg = { + channels: { + mattermost: { + botToken: "resolved-bot-token", + baseUrl: "https://chat.example.com", + }, + }, + } as OpenClawConfig; + + await sendText({ + cfg, + to: "channel:CHAN1", + text: "hello", + accountId: "default", + } as any); + + expect(sendMessageMattermostMock).toHaveBeenCalledWith( + "channel:CHAN1", + "hello", + expect.objectContaining({ + cfg, + accountId: "default", + }), + ); + }); }); describe("config", () => { diff --git a/extensions/mattermost/src/channel.ts b/extensions/mattermost/src/channel.ts index 0f9ec4c82de..9d28814fc51 100644 --- a/extensions/mattermost/src/channel.ts +++ b/extensions/mattermost/src/channel.ts @@ -273,15 +273,17 @@ export const mattermostPlugin: ChannelPlugin = { } return { ok: true, to: trimmed }; }, - sendText: async ({ to, text, accountId, replyToId }) => { + sendText: async ({ cfg, to, text, accountId, replyToId }) => { const result = await sendMessageMattermost(to, text, { + cfg, accountId: accountId ?? undefined, replyToId: replyToId ?? undefined, }); return { channel: "mattermost", ...result }; }, - sendMedia: async ({ to, text, mediaUrl, mediaLocalRoots, accountId, replyToId }) => { + sendMedia: async ({ cfg, to, text, mediaUrl, mediaLocalRoots, accountId, replyToId }) => { const result = await sendMessageMattermost(to, text, { + cfg, accountId: accountId ?? undefined, mediaUrl, mediaLocalRoots, diff --git a/extensions/mattermost/src/mattermost/send.test.ts b/extensions/mattermost/src/mattermost/send.test.ts index 1176cbfa7d1..d924529517c 100644 --- a/extensions/mattermost/src/mattermost/send.test.ts +++ b/extensions/mattermost/src/mattermost/send.test.ts @@ -2,7 +2,13 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { sendMessageMattermost } from "./send.js"; const mockState = vi.hoisted(() => ({ + loadConfig: vi.fn(() => ({})), loadOutboundMediaFromUrl: vi.fn(), + resolveMattermostAccount: vi.fn(() => ({ + accountId: "default", + botToken: "bot-token", + baseUrl: "https://mattermost.example.com", + })), createMattermostClient: vi.fn(), createMattermostDirectChannel: vi.fn(), createMattermostPost: vi.fn(), @@ -17,11 +23,7 @@ vi.mock("openclaw/plugin-sdk", () => ({ })); vi.mock("./accounts.js", () => ({ - resolveMattermostAccount: () => ({ - accountId: "default", - botToken: "bot-token", - baseUrl: "https://mattermost.example.com", - }), + resolveMattermostAccount: mockState.resolveMattermostAccount, })); vi.mock("./client.js", () => ({ @@ -37,7 +39,7 @@ vi.mock("./client.js", () => ({ vi.mock("../runtime.js", () => ({ getMattermostRuntime: () => ({ config: { - loadConfig: () => ({}), + loadConfig: mockState.loadConfig, }, logging: { shouldLogVerbose: () => false, @@ -57,6 +59,14 @@ vi.mock("../runtime.js", () => ({ describe("sendMessageMattermost", () => { beforeEach(() => { + mockState.loadConfig.mockReset(); + mockState.loadConfig.mockReturnValue({}); + mockState.resolveMattermostAccount.mockReset(); + mockState.resolveMattermostAccount.mockReturnValue({ + accountId: "default", + botToken: "bot-token", + baseUrl: "https://mattermost.example.com", + }); mockState.loadOutboundMediaFromUrl.mockReset(); mockState.createMattermostClient.mockReset(); mockState.createMattermostDirectChannel.mockReset(); @@ -69,6 +79,46 @@ describe("sendMessageMattermost", () => { mockState.uploadMattermostFile.mockResolvedValue({ id: "file-1" }); }); + it("uses provided cfg and skips runtime loadConfig", async () => { + const providedCfg = { + channels: { + mattermost: { + botToken: "provided-token", + }, + }, + }; + + await sendMessageMattermost("channel:town-square", "hello", { + cfg: providedCfg as any, + accountId: "work", + }); + + expect(mockState.loadConfig).not.toHaveBeenCalled(); + expect(mockState.resolveMattermostAccount).toHaveBeenCalledWith({ + cfg: providedCfg, + accountId: "work", + }); + }); + + it("falls back to runtime loadConfig when cfg is omitted", async () => { + const runtimeCfg = { + channels: { + mattermost: { + botToken: "runtime-token", + }, + }, + }; + mockState.loadConfig.mockReturnValueOnce(runtimeCfg); + + await sendMessageMattermost("channel:town-square", "hello"); + + expect(mockState.loadConfig).toHaveBeenCalledTimes(1); + expect(mockState.resolveMattermostAccount).toHaveBeenCalledWith({ + cfg: runtimeCfg, + accountId: undefined, + }); + }); + it("loads outbound media with trusted local roots before upload", async () => { mockState.loadOutboundMediaFromUrl.mockResolvedValueOnce({ buffer: Buffer.from("media-bytes"), diff --git a/extensions/mattermost/src/mattermost/send.ts b/extensions/mattermost/src/mattermost/send.ts index 8732d2400db..b325895e58d 100644 --- a/extensions/mattermost/src/mattermost/send.ts +++ b/extensions/mattermost/src/mattermost/send.ts @@ -1,4 +1,4 @@ -import { loadOutboundMediaFromUrl } from "openclaw/plugin-sdk"; +import { loadOutboundMediaFromUrl, type OpenClawConfig } from "openclaw/plugin-sdk"; import { getMattermostRuntime } from "../runtime.js"; import { resolveMattermostAccount } from "./accounts.js"; import { @@ -13,6 +13,7 @@ import { } from "./client.js"; export type MattermostSendOpts = { + cfg?: OpenClawConfig; botToken?: string; baseUrl?: string; accountId?: string; @@ -146,7 +147,7 @@ export async function sendMessageMattermost( ): Promise { const core = getCore(); const logger = core.logging.getChildLogger({ module: "mattermost" }); - const cfg = core.config.loadConfig(); + const cfg = opts.cfg ?? core.config.loadConfig(); const account = resolveMattermostAccount({ cfg, accountId: opts.accountId, diff --git a/extensions/msteams/src/outbound.test.ts b/extensions/msteams/src/outbound.test.ts new file mode 100644 index 00000000000..950ccd4ece2 --- /dev/null +++ b/extensions/msteams/src/outbound.test.ts @@ -0,0 +1,131 @@ +import type { OpenClawConfig } from "openclaw/plugin-sdk"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => ({ + sendMessageMSTeams: vi.fn(), + sendPollMSTeams: vi.fn(), + createPoll: vi.fn(), +})); + +vi.mock("./send.js", () => ({ + sendMessageMSTeams: mocks.sendMessageMSTeams, + sendPollMSTeams: mocks.sendPollMSTeams, +})); + +vi.mock("./polls.js", () => ({ + createMSTeamsPollStoreFs: () => ({ + createPoll: mocks.createPoll, + }), +})); + +vi.mock("./runtime.js", () => ({ + getMSTeamsRuntime: () => ({ + channel: { + text: { + chunkMarkdownText: (text: string) => [text], + }, + }, + }), +})); + +import { msteamsOutbound } from "./outbound.js"; + +describe("msteamsOutbound cfg threading", () => { + beforeEach(() => { + mocks.sendMessageMSTeams.mockReset(); + mocks.sendPollMSTeams.mockReset(); + mocks.createPoll.mockReset(); + mocks.sendMessageMSTeams.mockResolvedValue({ + messageId: "msg-1", + conversationId: "conv-1", + }); + mocks.sendPollMSTeams.mockResolvedValue({ + pollId: "poll-1", + messageId: "msg-poll-1", + conversationId: "conv-1", + }); + mocks.createPoll.mockResolvedValue(undefined); + }); + + it("passes resolved cfg to sendMessageMSTeams for text sends", async () => { + const cfg = { + channels: { + msteams: { + appId: "resolved-app-id", + }, + }, + } as OpenClawConfig; + + await msteamsOutbound.sendText!({ + cfg, + to: "conversation:abc", + text: "hello", + }); + + expect(mocks.sendMessageMSTeams).toHaveBeenCalledWith({ + cfg, + to: "conversation:abc", + text: "hello", + }); + }); + + it("passes resolved cfg and media roots for media sends", async () => { + const cfg = { + channels: { + msteams: { + appId: "resolved-app-id", + }, + }, + } as OpenClawConfig; + + await msteamsOutbound.sendMedia!({ + cfg, + to: "conversation:abc", + text: "photo", + mediaUrl: "file:///tmp/photo.png", + mediaLocalRoots: ["/tmp"], + }); + + expect(mocks.sendMessageMSTeams).toHaveBeenCalledWith({ + cfg, + to: "conversation:abc", + text: "photo", + mediaUrl: "file:///tmp/photo.png", + mediaLocalRoots: ["/tmp"], + }); + }); + + it("passes resolved cfg to sendPollMSTeams and stores poll metadata", async () => { + const cfg = { + channels: { + msteams: { + appId: "resolved-app-id", + }, + }, + } as OpenClawConfig; + + await msteamsOutbound.sendPoll!({ + cfg, + to: "conversation:abc", + poll: { + question: "Snack?", + options: ["Pizza", "Sushi"], + }, + }); + + expect(mocks.sendPollMSTeams).toHaveBeenCalledWith({ + cfg, + to: "conversation:abc", + question: "Snack?", + options: ["Pizza", "Sushi"], + maxSelections: 1, + }); + expect(mocks.createPoll).toHaveBeenCalledWith( + expect.objectContaining({ + id: "poll-1", + question: "Snack?", + options: ["Pizza", "Sushi"], + }), + ); + }); +}); diff --git a/extensions/nextcloud-talk/src/channel.ts b/extensions/nextcloud-talk/src/channel.ts index e49f057878c..32f4fc9306c 100644 --- a/extensions/nextcloud-talk/src/channel.ts +++ b/extensions/nextcloud-talk/src/channel.ts @@ -262,18 +262,20 @@ export const nextcloudTalkPlugin: ChannelPlugin = chunker: (text, limit) => getNextcloudTalkRuntime().channel.text.chunkMarkdownText(text, limit), chunkerMode: "markdown", textChunkLimit: 4000, - sendText: async ({ to, text, accountId, replyToId }) => { + sendText: async ({ cfg, to, text, accountId, replyToId }) => { const result = await sendMessageNextcloudTalk(to, text, { accountId: accountId ?? undefined, replyTo: replyToId ?? undefined, + cfg: cfg as CoreConfig, }); return { channel: "nextcloud-talk", ...result }; }, - sendMedia: async ({ to, text, mediaUrl, accountId, replyToId }) => { + sendMedia: async ({ cfg, to, text, mediaUrl, accountId, replyToId }) => { const messageWithMedia = mediaUrl ? `${text}\n\nAttachment: ${mediaUrl}` : text; const result = await sendMessageNextcloudTalk(to, messageWithMedia, { accountId: accountId ?? undefined, replyTo: replyToId ?? undefined, + cfg: cfg as CoreConfig, }); return { channel: "nextcloud-talk", ...result }; }, diff --git a/extensions/nextcloud-talk/src/send.test.ts b/extensions/nextcloud-talk/src/send.test.ts new file mode 100644 index 00000000000..3933b13de5a --- /dev/null +++ b/extensions/nextcloud-talk/src/send.test.ts @@ -0,0 +1,104 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const hoisted = vi.hoisted(() => ({ + loadConfig: vi.fn(), + resolveMarkdownTableMode: vi.fn(() => "preserve"), + convertMarkdownTables: vi.fn((text: string) => text), + record: vi.fn(), + resolveNextcloudTalkAccount: vi.fn(() => ({ + accountId: "default", + baseUrl: "https://nextcloud.example.com", + secret: "secret-value", + })), + generateNextcloudTalkSignature: vi.fn(() => ({ + random: "r", + signature: "s", + })), +})); + +vi.mock("./runtime.js", () => ({ + getNextcloudTalkRuntime: () => ({ + config: { + loadConfig: hoisted.loadConfig, + }, + channel: { + text: { + resolveMarkdownTableMode: hoisted.resolveMarkdownTableMode, + convertMarkdownTables: hoisted.convertMarkdownTables, + }, + activity: { + record: hoisted.record, + }, + }, + }), +})); + +vi.mock("./accounts.js", () => ({ + resolveNextcloudTalkAccount: hoisted.resolveNextcloudTalkAccount, +})); + +vi.mock("./signature.js", () => ({ + generateNextcloudTalkSignature: hoisted.generateNextcloudTalkSignature, +})); + +import { sendMessageNextcloudTalk, sendReactionNextcloudTalk } from "./send.js"; + +describe("nextcloud-talk send cfg threading", () => { + const fetchMock = vi.fn(); + + beforeEach(() => { + vi.clearAllMocks(); + fetchMock.mockReset(); + vi.stubGlobal("fetch", fetchMock); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it("uses provided cfg for sendMessage and skips runtime loadConfig", async () => { + const cfg = { source: "provided" } as const; + fetchMock.mockResolvedValueOnce( + new Response( + JSON.stringify({ + ocs: { data: { id: 12345, timestamp: 1_706_000_000 } }, + }), + { status: 200, headers: { "content-type": "application/json" } }, + ), + ); + + const result = await sendMessageNextcloudTalk("room:abc123", "hello", { + cfg, + accountId: "work", + }); + + expect(hoisted.loadConfig).not.toHaveBeenCalled(); + expect(hoisted.resolveNextcloudTalkAccount).toHaveBeenCalledWith({ + cfg, + accountId: "work", + }); + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(result).toEqual({ + messageId: "12345", + roomToken: "abc123", + timestamp: 1_706_000_000, + }); + }); + + it("falls back to runtime cfg for sendReaction when cfg is omitted", async () => { + const runtimeCfg = { source: "runtime" } as const; + hoisted.loadConfig.mockReturnValueOnce(runtimeCfg); + fetchMock.mockResolvedValueOnce(new Response("{}", { status: 200 })); + + const result = await sendReactionNextcloudTalk("room:ops", "m-1", "👍", { + accountId: "default", + }); + + expect(result).toEqual({ ok: true }); + expect(hoisted.loadConfig).toHaveBeenCalledTimes(1); + expect(hoisted.resolveNextcloudTalkAccount).toHaveBeenCalledWith({ + cfg: runtimeCfg, + accountId: "default", + }); + }); +}); diff --git a/extensions/nextcloud-talk/src/send.ts b/extensions/nextcloud-talk/src/send.ts index 6692f7099e9..7cc8f05658c 100644 --- a/extensions/nextcloud-talk/src/send.ts +++ b/extensions/nextcloud-talk/src/send.ts @@ -9,6 +9,7 @@ type NextcloudTalkSendOpts = { accountId?: string; replyTo?: string; verbose?: boolean; + cfg?: CoreConfig; }; function resolveCredentials( @@ -60,7 +61,7 @@ export async function sendMessageNextcloudTalk( text: string, opts: NextcloudTalkSendOpts = {}, ): Promise { - const cfg = getNextcloudTalkRuntime().config.loadConfig() as CoreConfig; + const cfg = (opts.cfg ?? getNextcloudTalkRuntime().config.loadConfig()) as CoreConfig; const account = resolveNextcloudTalkAccount({ cfg, accountId: opts.accountId, @@ -175,7 +176,7 @@ export async function sendReactionNextcloudTalk( reaction: string, opts: Omit = {}, ): Promise<{ ok: true }> { - const cfg = getNextcloudTalkRuntime().config.loadConfig() as CoreConfig; + const cfg = (opts.cfg ?? getNextcloudTalkRuntime().config.loadConfig()) as CoreConfig; const account = resolveNextcloudTalkAccount({ cfg, accountId: opts.accountId, diff --git a/extensions/nostr/src/channel.outbound.test.ts b/extensions/nostr/src/channel.outbound.test.ts new file mode 100644 index 00000000000..9b4717136b0 --- /dev/null +++ b/extensions/nostr/src/channel.outbound.test.ts @@ -0,0 +1,88 @@ +import type { PluginRuntime } from "openclaw/plugin-sdk"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { createStartAccountContext } from "../../test-utils/start-account-context.js"; +import { nostrPlugin } from "./channel.js"; +import { setNostrRuntime } from "./runtime.js"; + +const mocks = vi.hoisted(() => ({ + normalizePubkey: vi.fn((value: string) => `normalized-${value.toLowerCase()}`), + startNostrBus: vi.fn(), +})); + +vi.mock("./nostr-bus.js", () => ({ + DEFAULT_RELAYS: ["wss://relay.example.com"], + getPublicKeyFromPrivate: vi.fn(() => "pubkey"), + normalizePubkey: mocks.normalizePubkey, + startNostrBus: mocks.startNostrBus, +})); + +describe("nostr outbound cfg threading", () => { + afterEach(() => { + mocks.normalizePubkey.mockClear(); + mocks.startNostrBus.mockReset(); + }); + + it("uses resolved cfg when converting markdown tables before send", async () => { + const resolveMarkdownTableMode = vi.fn(() => "off"); + const convertMarkdownTables = vi.fn((text: string) => `converted:${text}`); + setNostrRuntime({ + channel: { + text: { + resolveMarkdownTableMode, + convertMarkdownTables, + }, + }, + reply: {}, + } as unknown as PluginRuntime); + + const sendDm = vi.fn(async () => {}); + const bus = { + sendDm, + close: vi.fn(), + getMetrics: vi.fn(() => ({ counters: {} })), + publishProfile: vi.fn(), + getProfileState: vi.fn(async () => null), + }; + mocks.startNostrBus.mockResolvedValueOnce(bus as any); + + const cleanup = (await nostrPlugin.gateway!.startAccount!( + createStartAccountContext({ + account: { + accountId: "default", + enabled: true, + configured: true, + privateKey: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + publicKey: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789", + relays: ["wss://relay.example.com"], + config: {}, + }, + abortSignal: new AbortController().signal, + }), + )) as { stop: () => void }; + + const cfg = { + channels: { + nostr: { + privateKey: "resolved-nostr-private-key", + }, + }, + }; + await nostrPlugin.outbound!.sendText!({ + cfg: cfg as any, + to: "NPUB123", + text: "|a|b|", + accountId: "default", + }); + + expect(resolveMarkdownTableMode).toHaveBeenCalledWith({ + cfg, + channel: "nostr", + accountId: "default", + }); + expect(convertMarkdownTables).toHaveBeenCalledWith("|a|b|", "off"); + expect(mocks.normalizePubkey).toHaveBeenCalledWith("NPUB123"); + expect(sendDm).toHaveBeenCalledWith("normalized-npub123", "converted:|a|b|"); + + cleanup.stop(); + }); +}); diff --git a/extensions/nostr/src/channel.ts b/extensions/nostr/src/channel.ts index a516f2442eb..b7608953fc9 100644 --- a/extensions/nostr/src/channel.ts +++ b/extensions/nostr/src/channel.ts @@ -135,7 +135,7 @@ export const nostrPlugin: ChannelPlugin = { outbound: { deliveryMode: "direct", textChunkLimit: 4000, - sendText: async ({ to, text, accountId }) => { + sendText: async ({ cfg, to, text, accountId }) => { const core = getNostrRuntime(); const aid = accountId ?? DEFAULT_ACCOUNT_ID; const bus = activeBuses.get(aid); @@ -143,7 +143,7 @@ export const nostrPlugin: ChannelPlugin = { throw new Error(`Nostr bus not running for account ${aid}`); } const tableMode = core.channel.text.resolveMarkdownTableMode({ - cfg: core.config.loadConfig(), + cfg, channel: "nostr", accountId: aid, }); diff --git a/extensions/signal/src/channel.outbound.test.ts b/extensions/signal/src/channel.outbound.test.ts new file mode 100644 index 00000000000..f1ceafbcab2 --- /dev/null +++ b/extensions/signal/src/channel.outbound.test.ts @@ -0,0 +1,63 @@ +import { describe, expect, it, vi } from "vitest"; +import { signalPlugin } from "./channel.js"; + +describe("signal outbound cfg threading", () => { + it("threads provided cfg into sendText deps call", async () => { + const cfg = { + channels: { + signal: { + accounts: { + work: { + mediaMaxMb: 12, + }, + }, + mediaMaxMb: 5, + }, + }, + }; + const sendSignal = vi.fn(async () => ({ messageId: "sig-1" })); + + const result = await signalPlugin.outbound!.sendText!({ + cfg, + to: "+15551230000", + text: "hello", + accountId: "work", + deps: { sendSignal }, + }); + + expect(sendSignal).toHaveBeenCalledWith("+15551230000", "hello", { + cfg, + maxBytes: 12 * 1024 * 1024, + accountId: "work", + }); + expect(result).toEqual({ channel: "signal", messageId: "sig-1" }); + }); + + it("threads cfg + mediaUrl into sendMedia deps call", async () => { + const cfg = { + channels: { + signal: { + mediaMaxMb: 7, + }, + }, + }; + const sendSignal = vi.fn(async () => ({ messageId: "sig-2" })); + + const result = await signalPlugin.outbound!.sendMedia!({ + cfg, + to: "+15559870000", + text: "photo", + mediaUrl: "https://example.com/a.jpg", + accountId: "default", + deps: { sendSignal }, + }); + + expect(sendSignal).toHaveBeenCalledWith("+15559870000", "photo", { + cfg, + mediaUrl: "https://example.com/a.jpg", + maxBytes: 7 * 1024 * 1024, + accountId: "default", + }); + expect(result).toEqual({ channel: "signal", messageId: "sig-2" }); + }); +}); diff --git a/extensions/signal/src/channel.ts b/extensions/signal/src/channel.ts index ff0623705b7..1dc3bbc15cc 100644 --- a/extensions/signal/src/channel.ts +++ b/extensions/signal/src/channel.ts @@ -80,6 +80,7 @@ async function sendSignalOutbound(params: { accountId: params.accountId, }); return await send(params.to, params.text, { + cfg: params.cfg, ...(params.mediaUrl ? { mediaUrl: params.mediaUrl } : {}), ...(params.mediaLocalRoots?.length ? { mediaLocalRoots: params.mediaLocalRoots } : {}), maxBytes, diff --git a/extensions/slack/src/channel.ts b/extensions/slack/src/channel.ts index 5a1364fe8f2..82e29e95b99 100644 --- a/extensions/slack/src/channel.ts +++ b/extensions/slack/src/channel.ts @@ -365,6 +365,7 @@ export const slackPlugin: ChannelPlugin = { threadId, }); const result = await send(to, text, { + cfg, threadTs: threadTsValue != null ? String(threadTsValue) : undefined, accountId: accountId ?? undefined, ...(tokenOverride ? { token: tokenOverride } : {}), @@ -390,6 +391,7 @@ export const slackPlugin: ChannelPlugin = { threadId, }); const result = await send(to, text, { + cfg, mediaUrl, mediaLocalRoots, threadTs: threadTsValue != null ? String(threadTsValue) : undefined, diff --git a/extensions/telegram/src/channel.ts b/extensions/telegram/src/channel.ts index 3564a9719ab..bc8b7e1fcaf 100644 --- a/extensions/telegram/src/channel.ts +++ b/extensions/telegram/src/channel.ts @@ -320,12 +320,13 @@ export const telegramPlugin: ChannelPlugin { + sendText: async ({ cfg, to, text, accountId, deps, replyToId, threadId, silent }) => { const send = deps?.sendTelegram ?? getTelegramRuntime().channel.telegram.sendMessageTelegram; const replyToMessageId = parseTelegramReplyToMessageId(replyToId); const messageThreadId = parseTelegramThreadId(threadId); const result = await send(to, text, { verbose: false, + cfg, messageThreadId, replyToMessageId, accountId: accountId ?? undefined, @@ -334,6 +335,7 @@ export const telegramPlugin: ChannelPlugin + sendPoll: async ({ cfg, to, poll, accountId, threadId, silent, isAnonymous }) => await getTelegramRuntime().channel.telegram.sendPollTelegram(to, poll, { + cfg, accountId: accountId ?? undefined, messageThreadId: parseTelegramThreadId(threadId), silent: silent ?? undefined, diff --git a/extensions/whatsapp/src/channel.outbound.test.ts b/extensions/whatsapp/src/channel.outbound.test.ts new file mode 100644 index 00000000000..3c51e9c1bef --- /dev/null +++ b/extensions/whatsapp/src/channel.outbound.test.ts @@ -0,0 +1,46 @@ +import type { OpenClawConfig } from "openclaw/plugin-sdk"; +import { describe, expect, it, vi } from "vitest"; + +const hoisted = vi.hoisted(() => ({ + sendPollWhatsApp: vi.fn(async () => ({ messageId: "wa-poll-1", toJid: "1555@s.whatsapp.net" })), +})); + +vi.mock("./runtime.js", () => ({ + getWhatsAppRuntime: () => ({ + logging: { + shouldLogVerbose: () => false, + }, + channel: { + whatsapp: { + sendPollWhatsApp: hoisted.sendPollWhatsApp, + }, + }, + }), +})); + +import { whatsappPlugin } from "./channel.js"; + +describe("whatsappPlugin outbound sendPoll", () => { + it("threads cfg into runtime sendPollWhatsApp call", async () => { + const cfg = { marker: "resolved-cfg" } as OpenClawConfig; + const poll = { + question: "Lunch?", + options: ["Pizza", "Sushi"], + maxSelections: 1, + }; + + const result = await whatsappPlugin.outbound!.sendPoll!({ + cfg, + to: "+1555", + poll, + accountId: "work", + }); + + expect(hoisted.sendPollWhatsApp).toHaveBeenCalledWith("+1555", poll, { + verbose: false, + accountId: "work", + cfg, + }); + expect(result).toEqual({ messageId: "wa-poll-1", toJid: "1555@s.whatsapp.net" }); + }); +}); diff --git a/extensions/whatsapp/src/channel.ts b/extensions/whatsapp/src/channel.ts index d45cbe113f2..424c1046c87 100644 --- a/extensions/whatsapp/src/channel.ts +++ b/extensions/whatsapp/src/channel.ts @@ -286,19 +286,30 @@ export const whatsappPlugin: ChannelPlugin = { pollMaxOptions: 12, resolveTarget: ({ to, allowFrom, mode }) => resolveWhatsAppOutboundTarget({ to, allowFrom, mode }), - sendText: async ({ to, text, accountId, deps, gifPlayback }) => { + sendText: async ({ cfg, to, text, accountId, deps, gifPlayback }) => { const send = deps?.sendWhatsApp ?? getWhatsAppRuntime().channel.whatsapp.sendMessageWhatsApp; const result = await send(to, text, { verbose: false, + cfg, accountId: accountId ?? undefined, gifPlayback, }); return { channel: "whatsapp", ...result }; }, - sendMedia: async ({ to, text, mediaUrl, mediaLocalRoots, accountId, deps, gifPlayback }) => { + sendMedia: async ({ + cfg, + to, + text, + mediaUrl, + mediaLocalRoots, + accountId, + deps, + gifPlayback, + }) => { const send = deps?.sendWhatsApp ?? getWhatsAppRuntime().channel.whatsapp.sendMessageWhatsApp; const result = await send(to, text, { verbose: false, + cfg, mediaUrl, mediaLocalRoots, accountId: accountId ?? undefined, @@ -306,10 +317,11 @@ export const whatsappPlugin: ChannelPlugin = { }); return { channel: "whatsapp", ...result }; }, - sendPoll: async ({ to, poll, accountId }) => + sendPoll: async ({ cfg, to, poll, accountId }) => await getWhatsAppRuntime().channel.whatsapp.sendPollWhatsApp(to, poll, { verbose: getWhatsAppRuntime().logging.shouldLogVerbose(), accountId: accountId ?? undefined, + cfg, }), }, auth: { diff --git a/src/agents/tools/discord-actions-messaging.ts b/src/agents/tools/discord-actions-messaging.ts index 9d0b3818334..2846e0879f8 100644 --- a/src/agents/tools/discord-actions-messaging.ts +++ b/src/agents/tools/discord-actions-messaging.ts @@ -1,5 +1,6 @@ import type { AgentToolResult } from "@mariozechner/pi-agent-core"; import type { DiscordActionConfig } from "../../config/config.js"; +import type { OpenClawConfig } from "../../config/config.js"; import { readDiscordComponentSpec } from "../../discord/components.js"; import { createThreadDiscord, @@ -59,6 +60,7 @@ export async function handleDiscordMessagingAction( options?: { mediaLocalRoots?: readonly string[]; }, + cfg?: OpenClawConfig, ): Promise> { const resolveChannelId = () => resolveDiscordChannelId( @@ -67,6 +69,7 @@ export async function handleDiscordMessagingAction( }), ); const accountId = readStringParam(params, "accountId"); + const cfgOptions = cfg ? { cfg } : {}; const normalizeMessage = (message: unknown) => { if (!message || typeof message !== "object") { return message; @@ -90,22 +93,28 @@ export async function handleDiscordMessagingAction( }); if (remove) { if (accountId) { - await removeReactionDiscord(channelId, messageId, emoji, { accountId }); + await removeReactionDiscord(channelId, messageId, emoji, { + ...cfgOptions, + accountId, + }); } else { - await removeReactionDiscord(channelId, messageId, emoji); + await removeReactionDiscord(channelId, messageId, emoji, cfgOptions); } return jsonResult({ ok: true, removed: emoji }); } if (isEmpty) { const removed = accountId - ? await removeOwnReactionsDiscord(channelId, messageId, { accountId }) - : await removeOwnReactionsDiscord(channelId, messageId); + ? await removeOwnReactionsDiscord(channelId, messageId, { ...cfgOptions, accountId }) + : await removeOwnReactionsDiscord(channelId, messageId, cfgOptions); return jsonResult({ ok: true, removed: removed.removed }); } if (accountId) { - await reactMessageDiscord(channelId, messageId, emoji, { accountId }); + await reactMessageDiscord(channelId, messageId, emoji, { + ...cfgOptions, + accountId, + }); } else { - await reactMessageDiscord(channelId, messageId, emoji); + await reactMessageDiscord(channelId, messageId, emoji, cfgOptions); } return jsonResult({ ok: true, added: emoji }); } @@ -121,6 +130,7 @@ export async function handleDiscordMessagingAction( const limit = typeof limitRaw === "number" && Number.isFinite(limitRaw) ? limitRaw : undefined; const reactions = await fetchReactionsDiscord(channelId, messageId, { + ...cfgOptions, ...(accountId ? { accountId } : {}), limit, }); @@ -137,6 +147,7 @@ export async function handleDiscordMessagingAction( label: "stickerIds", }); await sendStickerDiscord(to, stickerIds, { + ...cfgOptions, ...(accountId ? { accountId } : {}), content, }); @@ -165,7 +176,7 @@ export async function handleDiscordMessagingAction( await sendPollDiscord( to, { question, options: answers, maxSelections, durationHours }, - { ...(accountId ? { accountId } : {}), content }, + { ...cfgOptions, ...(accountId ? { accountId } : {}), content }, ); return jsonResult({ ok: true }); } @@ -276,6 +287,7 @@ export async function handleDiscordMessagingAction( ? componentSpec : { ...componentSpec, text: normalizedContent }; const result = await sendDiscordComponentMessage(to, payload, { + ...cfgOptions, ...(accountId ? { accountId } : {}), silent, replyTo: replyTo ?? undefined, @@ -301,6 +313,7 @@ export async function handleDiscordMessagingAction( } assertMediaNotDataUrl(mediaUrl); const result = await sendVoiceMessageDiscord(to, mediaUrl, { + ...cfgOptions, ...(accountId ? { accountId } : {}), replyTo, silent, @@ -309,6 +322,7 @@ export async function handleDiscordMessagingAction( } const result = await sendMessageDiscord(to, content ?? "", { + ...cfgOptions, ...(accountId ? { accountId } : {}), mediaUrl, mediaLocalRoots: options?.mediaLocalRoots, @@ -422,6 +436,7 @@ export async function handleDiscordMessagingAction( const mediaUrl = readStringParam(params, "mediaUrl"); const replyTo = readStringParam(params, "replyTo"); const result = await sendMessageDiscord(`channel:${channelId}`, content, { + ...cfgOptions, ...(accountId ? { accountId } : {}), mediaUrl, mediaLocalRoots: options?.mediaLocalRoots, diff --git a/src/agents/tools/discord-actions.test.ts b/src/agents/tools/discord-actions.test.ts index 87ae04854e9..cbadb77f564 100644 --- a/src/agents/tools/discord-actions.test.ts +++ b/src/agents/tools/discord-actions.test.ts @@ -107,7 +107,7 @@ describe("handleDiscordMessagingAction", () => { expect(reactMessageDiscord).toHaveBeenCalledWith("C1", "M1", "✅", expectedOptions); return; } - expect(reactMessageDiscord).toHaveBeenCalledWith("C1", "M1", "✅"); + expect(reactMessageDiscord).toHaveBeenCalledWith("C1", "M1", "✅", {}); }); it("removes reactions on empty emoji", async () => { @@ -120,7 +120,7 @@ describe("handleDiscordMessagingAction", () => { }, enableAllActions, ); - expect(removeOwnReactionsDiscord).toHaveBeenCalledWith("C1", "M1"); + expect(removeOwnReactionsDiscord).toHaveBeenCalledWith("C1", "M1", {}); }); it("removes reactions when remove flag set", async () => { @@ -134,7 +134,7 @@ describe("handleDiscordMessagingAction", () => { }, enableAllActions, ); - expect(removeReactionDiscord).toHaveBeenCalledWith("C1", "M1", "✅"); + expect(removeReactionDiscord).toHaveBeenCalledWith("C1", "M1", "✅", {}); }); it("rejects removes without emoji", async () => { diff --git a/src/agents/tools/discord-actions.ts b/src/agents/tools/discord-actions.ts index 627d14e40e6..d4533517c8a 100644 --- a/src/agents/tools/discord-actions.ts +++ b/src/agents/tools/discord-actions.ts @@ -67,7 +67,7 @@ export async function handleDiscordAction( const isActionEnabled = createDiscordActionGate({ cfg, accountId }); if (messagingActions.has(action)) { - return await handleDiscordMessagingAction(action, params, isActionEnabled, options); + return await handleDiscordMessagingAction(action, params, isActionEnabled, options, cfg); } if (guildActions.has(action)) { return await handleDiscordGuildAction(action, params, isActionEnabled); diff --git a/src/channels/plugins/actions/actions.test.ts b/src/channels/plugins/actions/actions.test.ts index bd0454bf72d..eda720dfc93 100644 --- a/src/channels/plugins/actions/actions.test.ts +++ b/src/channels/plugins/actions/actions.test.ts @@ -847,7 +847,10 @@ describe("signalMessageActions", () => { cfg: createSignalAccountOverrideCfg(), accountId: "work", params: { to: "+15550001111", messageId: "123", emoji: "👍" }, - expectedArgs: ["+15550001111", 123, "👍", { accountId: "work" }], + expectedRecipient: "+15550001111", + expectedTimestamp: 123, + expectedEmoji: "👍", + expectedOptions: { accountId: "work" }, }, { name: "normalizes uuid recipients", @@ -858,7 +861,10 @@ describe("signalMessageActions", () => { messageId: "123", emoji: "🔥", }, - expectedArgs: ["123e4567-e89b-12d3-a456-426614174000", 123, "🔥", { accountId: undefined }], + expectedRecipient: "123e4567-e89b-12d3-a456-426614174000", + expectedTimestamp: 123, + expectedEmoji: "🔥", + expectedOptions: {}, }, { name: "passes groupId and targetAuthor for group reactions", @@ -870,17 +876,13 @@ describe("signalMessageActions", () => { messageId: "123", emoji: "✅", }, - expectedArgs: [ - "", - 123, - "✅", - { - accountId: undefined, - groupId: "group-id", - targetAuthor: "uuid:123e4567-e89b-12d3-a456-426614174000", - targetAuthorUuid: undefined, - }, - ], + expectedRecipient: "", + expectedTimestamp: 123, + expectedEmoji: "✅", + expectedOptions: { + groupId: "group-id", + targetAuthor: "uuid:123e4567-e89b-12d3-a456-426614174000", + }, }, ] as const; @@ -890,7 +892,15 @@ describe("signalMessageActions", () => { cfg: testCase.cfg, accountId: testCase.accountId, }); - expect(sendReactionSignal, testCase.name).toHaveBeenCalledWith(...testCase.expectedArgs); + expect(sendReactionSignal, testCase.name).toHaveBeenCalledWith( + testCase.expectedRecipient, + testCase.expectedTimestamp, + testCase.expectedEmoji, + expect.objectContaining({ + cfg: testCase.cfg, + ...testCase.expectedOptions, + }), + ); } }); diff --git a/src/channels/plugins/actions/signal.ts b/src/channels/plugins/actions/signal.ts index c934a039f99..c93421489fd 100644 --- a/src/channels/plugins/actions/signal.ts +++ b/src/channels/plugins/actions/signal.ts @@ -40,6 +40,7 @@ function resolveSignalReactionTarget(raw: string): { recipient?: string; groupId } async function mutateSignalReaction(params: { + cfg: Parameters[0]["cfg"]; accountId?: string; target: { recipient?: string; groupId?: string }; timestamp: number; @@ -49,6 +50,7 @@ async function mutateSignalReaction(params: { targetAuthorUuid?: string; }) { const options = { + cfg: params.cfg, accountId: params.accountId, groupId: params.target.groupId, targetAuthor: params.targetAuthor, @@ -153,6 +155,7 @@ export const signalMessageActions: ChannelMessageActionAdapter = { throw new Error("Emoji required to remove reaction."); } return await mutateSignalReaction({ + cfg, accountId: accountId ?? undefined, target, timestamp, @@ -167,6 +170,7 @@ export const signalMessageActions: ChannelMessageActionAdapter = { throw new Error("Emoji required to add reaction."); } return await mutateSignalReaction({ + cfg, accountId: accountId ?? undefined, target, timestamp, diff --git a/src/channels/plugins/outbound/direct-text-media.ts b/src/channels/plugins/outbound/direct-text-media.ts index 3949963dfe8..9617798325d 100644 --- a/src/channels/plugins/outbound/direct-text-media.ts +++ b/src/channels/plugins/outbound/direct-text-media.ts @@ -5,6 +5,7 @@ import { resolveChannelMediaMaxBytes } from "../media-limits.js"; import type { ChannelOutboundAdapter } from "../types.js"; type DirectSendOptions = { + cfg: OpenClawConfig; accountId?: string | null; replyToId?: string | null; mediaUrl?: string; @@ -121,6 +122,7 @@ export function createDirectTextMediaOutbound< sendParams.to, sendParams.text, sendParams.buildOptions({ + cfg: sendParams.cfg, mediaUrl: sendParams.mediaUrl, mediaLocalRoots: sendParams.mediaLocalRoots, accountId: sendParams.accountId, diff --git a/src/channels/plugins/outbound/discord.test.ts b/src/channels/plugins/outbound/discord.test.ts index 70e74da0da5..b6a618f4b5f 100644 --- a/src/channels/plugins/outbound/discord.test.ts +++ b/src/channels/plugins/outbound/discord.test.ts @@ -143,9 +143,16 @@ describe("discordOutbound", () => { it("uses webhook persona delivery for bound thread text replies", async () => { mockBoundThreadManager(); + const cfg = { + channels: { + discord: { + token: "resolved-token", + }, + }, + }; const result = await discordOutbound.sendText?.({ - cfg: {}, + cfg, to: "channel:parent-1", text: "hello from persona", accountId: "default", @@ -169,6 +176,10 @@ describe("discordOutbound", () => { avatarUrl: "https://example.com/avatar.png", }), ); + expect( + (hoisted.sendWebhookMessageDiscordMock.mock.calls[0]?.[1] as { cfg?: unknown } | undefined) + ?.cfg, + ).toBe(cfg); expect(hoisted.sendMessageDiscordMock).not.toHaveBeenCalled(); expect(result).toEqual({ channel: "discord", diff --git a/src/channels/plugins/outbound/discord.ts b/src/channels/plugins/outbound/discord.ts index 4f959d23e38..b88f3cc09ef 100644 --- a/src/channels/plugins/outbound/discord.ts +++ b/src/channels/plugins/outbound/discord.ts @@ -1,3 +1,4 @@ +import type { OpenClawConfig } from "../../../config/config.js"; import { getThreadBindingManager, type ThreadBindingRecord, @@ -38,6 +39,7 @@ function resolveDiscordWebhookIdentity(params: { } async function maybeSendDiscordWebhookText(params: { + cfg?: OpenClawConfig; text: string; threadId?: string | number | null; accountId?: string | null; @@ -68,6 +70,7 @@ async function maybeSendDiscordWebhookText(params: { webhookToken: binding.webhookToken, accountId: binding.accountId, threadId: binding.threadId, + cfg: params.cfg, replyTo: params.replyToId ?? undefined, username: persona.username, avatarUrl: persona.avatarUrl, @@ -83,9 +86,10 @@ export const discordOutbound: ChannelOutboundAdapter = { resolveTarget: ({ to }) => normalizeDiscordOutboundTarget(to), sendPayload: async (ctx) => await sendTextMediaPayload({ channel: "discord", ctx, adapter: discordOutbound }), - sendText: async ({ to, text, accountId, deps, replyToId, threadId, identity, silent }) => { + sendText: async ({ cfg, to, text, accountId, deps, replyToId, threadId, identity, silent }) => { if (!silent) { const webhookResult = await maybeSendDiscordWebhookText({ + cfg, text, threadId, accountId, @@ -103,10 +107,12 @@ export const discordOutbound: ChannelOutboundAdapter = { replyTo: replyToId ?? undefined, accountId: accountId ?? undefined, silent: silent ?? undefined, + cfg, }); return { channel: "discord", ...result }; }, sendMedia: async ({ + cfg, to, text, mediaUrl, @@ -126,14 +132,16 @@ export const discordOutbound: ChannelOutboundAdapter = { replyTo: replyToId ?? undefined, accountId: accountId ?? undefined, silent: silent ?? undefined, + cfg, }); return { channel: "discord", ...result }; }, - sendPoll: async ({ to, poll, accountId, threadId, silent }) => { + sendPoll: async ({ cfg, to, poll, accountId, threadId, silent }) => { const target = resolveDiscordOutboundTarget({ to, threadId }); return await sendPollDiscord(target, poll, { accountId: accountId ?? undefined, silent: silent ?? undefined, + cfg, }); }, }; diff --git a/src/channels/plugins/outbound/imessage.ts b/src/channels/plugins/outbound/imessage.ts index 6a419bc2796..20c92754d28 100644 --- a/src/channels/plugins/outbound/imessage.ts +++ b/src/channels/plugins/outbound/imessage.ts @@ -13,12 +13,14 @@ export const imessageOutbound = createDirectTextMediaOutbound({ channel: "imessage", resolveSender: resolveIMessageSender, resolveMaxBytes: createScopedChannelMediaMaxBytesResolver("imessage"), - buildTextOptions: ({ maxBytes, accountId, replyToId }) => ({ + buildTextOptions: ({ cfg, maxBytes, accountId, replyToId }) => ({ + config: cfg, maxBytes, accountId: accountId ?? undefined, replyToId: replyToId ?? undefined, }), - buildMediaOptions: ({ mediaUrl, maxBytes, accountId, replyToId, mediaLocalRoots }) => ({ + buildMediaOptions: ({ cfg, mediaUrl, maxBytes, accountId, replyToId, mediaLocalRoots }) => ({ + config: cfg, mediaUrl, maxBytes, accountId: accountId ?? undefined, diff --git a/src/channels/plugins/outbound/signal.ts b/src/channels/plugins/outbound/signal.ts index e91feacad64..0ebf8e57670 100644 --- a/src/channels/plugins/outbound/signal.ts +++ b/src/channels/plugins/outbound/signal.ts @@ -13,11 +13,13 @@ export const signalOutbound = createDirectTextMediaOutbound({ channel: "signal", resolveSender: resolveSignalSender, resolveMaxBytes: createScopedChannelMediaMaxBytesResolver("signal"), - buildTextOptions: ({ maxBytes, accountId }) => ({ + buildTextOptions: ({ cfg, maxBytes, accountId }) => ({ + cfg, maxBytes, accountId: accountId ?? undefined, }), - buildMediaOptions: ({ mediaUrl, maxBytes, accountId, mediaLocalRoots }) => ({ + buildMediaOptions: ({ cfg, mediaUrl, maxBytes, accountId, mediaLocalRoots }) => ({ + cfg, mediaUrl, maxBytes, accountId: accountId ?? undefined, diff --git a/src/channels/plugins/outbound/slack.test.ts b/src/channels/plugins/outbound/slack.test.ts index 42583a25b06..18635f0e4a2 100644 --- a/src/channels/plugins/outbound/slack.test.ts +++ b/src/channels/plugins/outbound/slack.test.ts @@ -58,11 +58,13 @@ const expectSlackSendCalledWith = ( }; }, ) => { - expect(sendMessageSlack).toHaveBeenCalledWith("C123", text, { + const expected = { threadTs: "1111.2222", accountId: "default", - ...options, - }); + cfg: expect.any(Object), + ...(options?.identity ? { identity: expect.objectContaining(options.identity) } : {}), + }; + expect(sendMessageSlack).toHaveBeenCalledWith("C123", text, expect.objectContaining(expected)); }; describe("slack outbound hook wiring", () => { diff --git a/src/channels/plugins/outbound/slack.ts b/src/channels/plugins/outbound/slack.ts index 562336776c9..1c14cc3743d 100644 --- a/src/channels/plugins/outbound/slack.ts +++ b/src/channels/plugins/outbound/slack.ts @@ -48,6 +48,7 @@ async function applySlackMessageSendingHooks(params: { } async function sendSlackOutboundMessage(params: { + cfg: NonNullable[2]>["cfg"]; to: string; text: string; mediaUrl?: string; @@ -80,6 +81,7 @@ async function sendSlackOutboundMessage(params: { const slackIdentity = resolveSlackSendIdentity(params.identity); const result = await send(params.to, hookResult.text, { + cfg: params.cfg, threadTs, accountId: params.accountId ?? undefined, ...(params.mediaUrl @@ -96,8 +98,9 @@ export const slackOutbound: ChannelOutboundAdapter = { textChunkLimit: 4000, sendPayload: async (ctx) => await sendTextMediaPayload({ channel: "slack", ctx, adapter: slackOutbound }), - sendText: async ({ to, text, accountId, deps, replyToId, threadId, identity }) => { + sendText: async ({ cfg, to, text, accountId, deps, replyToId, threadId, identity }) => { return await sendSlackOutboundMessage({ + cfg, to, text, accountId, @@ -108,6 +111,7 @@ export const slackOutbound: ChannelOutboundAdapter = { }); }, sendMedia: async ({ + cfg, to, text, mediaUrl, @@ -119,6 +123,7 @@ export const slackOutbound: ChannelOutboundAdapter = { identity, }) => { return await sendSlackOutboundMessage({ + cfg, to, text, mediaUrl, diff --git a/src/channels/plugins/outbound/telegram.ts b/src/channels/plugins/outbound/telegram.ts index 32aadb8fbc1..2a079a6014e 100644 --- a/src/channels/plugins/outbound/telegram.ts +++ b/src/channels/plugins/outbound/telegram.ts @@ -9,6 +9,7 @@ import { sendMessageTelegram } from "../../../telegram/send.js"; import type { ChannelOutboundAdapter } from "../types.js"; function resolveTelegramSendContext(params: { + cfg: NonNullable[2]>["cfg"]; deps?: OutboundSendDeps; accountId?: string | null; replyToId?: string | null; @@ -16,6 +17,7 @@ function resolveTelegramSendContext(params: { }): { send: typeof sendMessageTelegram; baseOpts: { + cfg: NonNullable[2]>["cfg"]; verbose: false; textMode: "html"; messageThreadId?: number; @@ -29,6 +31,7 @@ function resolveTelegramSendContext(params: { baseOpts: { verbose: false, textMode: "html", + cfg: params.cfg, messageThreadId: parseTelegramThreadId(params.threadId), replyToMessageId: parseTelegramReplyToMessageId(params.replyToId), accountId: params.accountId ?? undefined, @@ -41,8 +44,9 @@ export const telegramOutbound: ChannelOutboundAdapter = { chunker: markdownToTelegramHtmlChunks, chunkerMode: "markdown", textChunkLimit: 4000, - sendText: async ({ to, text, accountId, deps, replyToId, threadId }) => { + sendText: async ({ cfg, to, text, accountId, deps, replyToId, threadId }) => { const { send, baseOpts } = resolveTelegramSendContext({ + cfg, deps, accountId, replyToId, @@ -54,6 +58,7 @@ export const telegramOutbound: ChannelOutboundAdapter = { return { channel: "telegram", ...result }; }, sendMedia: async ({ + cfg, to, text, mediaUrl, @@ -64,6 +69,7 @@ export const telegramOutbound: ChannelOutboundAdapter = { threadId, }) => { const { send, baseOpts } = resolveTelegramSendContext({ + cfg, deps, accountId, replyToId, @@ -76,8 +82,18 @@ export const telegramOutbound: ChannelOutboundAdapter = { }); return { channel: "telegram", ...result }; }, - sendPayload: async ({ to, payload, mediaLocalRoots, accountId, deps, replyToId, threadId }) => { + sendPayload: async ({ + cfg, + to, + payload, + mediaLocalRoots, + accountId, + deps, + replyToId, + threadId, + }) => { const { send, baseOpts: contextOpts } = resolveTelegramSendContext({ + cfg, deps, accountId, replyToId, diff --git a/src/channels/plugins/outbound/whatsapp.poll.test.ts b/src/channels/plugins/outbound/whatsapp.poll.test.ts new file mode 100644 index 00000000000..7164a6b152e --- /dev/null +++ b/src/channels/plugins/outbound/whatsapp.poll.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it, vi } from "vitest"; +import type { OpenClawConfig } from "../../../config/config.js"; + +const hoisted = vi.hoisted(() => ({ + sendPollWhatsApp: vi.fn(async () => ({ messageId: "poll-1", toJid: "1555@s.whatsapp.net" })), +})); + +vi.mock("../../../globals.js", () => ({ + shouldLogVerbose: () => false, +})); + +vi.mock("../../../web/outbound.js", () => ({ + sendPollWhatsApp: hoisted.sendPollWhatsApp, +})); + +import { whatsappOutbound } from "./whatsapp.js"; + +describe("whatsappOutbound sendPoll", () => { + it("threads cfg through poll send options", async () => { + const cfg = { marker: "resolved-cfg" } as OpenClawConfig; + const poll = { + question: "Lunch?", + options: ["Pizza", "Sushi"], + maxSelections: 1, + }; + + const result = await whatsappOutbound.sendPoll!({ + cfg, + to: "+1555", + poll, + accountId: "work", + }); + + expect(hoisted.sendPollWhatsApp).toHaveBeenCalledWith("+1555", poll, { + verbose: false, + accountId: "work", + cfg, + }); + expect(result).toEqual({ messageId: "poll-1", toJid: "1555@s.whatsapp.net" }); + }); +}); diff --git a/src/channels/plugins/outbound/whatsapp.ts b/src/channels/plugins/outbound/whatsapp.ts index a314b372e70..e5de15241ae 100644 --- a/src/channels/plugins/outbound/whatsapp.ts +++ b/src/channels/plugins/outbound/whatsapp.ts @@ -15,21 +15,23 @@ export const whatsappOutbound: ChannelOutboundAdapter = { resolveWhatsAppOutboundTarget({ to, allowFrom, mode }), sendPayload: async (ctx) => await sendTextMediaPayload({ channel: "whatsapp", ctx, adapter: whatsappOutbound }), - sendText: async ({ to, text, accountId, deps, gifPlayback }) => { + sendText: async ({ cfg, to, text, accountId, deps, gifPlayback }) => { const send = deps?.sendWhatsApp ?? (await import("../../../web/outbound.js")).sendMessageWhatsApp; const result = await send(to, text, { verbose: false, + cfg, accountId: accountId ?? undefined, gifPlayback, }); return { channel: "whatsapp", ...result }; }, - sendMedia: async ({ to, text, mediaUrl, mediaLocalRoots, accountId, deps, gifPlayback }) => { + sendMedia: async ({ cfg, to, text, mediaUrl, mediaLocalRoots, accountId, deps, gifPlayback }) => { const send = deps?.sendWhatsApp ?? (await import("../../../web/outbound.js")).sendMessageWhatsApp; const result = await send(to, text, { verbose: false, + cfg, mediaUrl, mediaLocalRoots, accountId: accountId ?? undefined, @@ -37,9 +39,10 @@ export const whatsappOutbound: ChannelOutboundAdapter = { }); return { channel: "whatsapp", ...result }; }, - sendPoll: async ({ to, poll, accountId }) => + sendPoll: async ({ cfg, to, poll, accountId }) => await sendPollWhatsApp(to, poll, { verbose: shouldLogVerbose(), accountId: accountId ?? undefined, + cfg, }), }; diff --git a/src/commands/message.test.ts b/src/commands/message.test.ts index 6c805574778..f5a23298b1a 100644 --- a/src/commands/message.test.ts +++ b/src/commands/message.test.ts @@ -169,6 +169,199 @@ const createTelegramSendPluginRegistration = () => ({ const { messageCommand } = await import("./message.js"); describe("messageCommand", () => { + it("threads resolved SecretRef config into outbound send actions", async () => { + const rawConfig = { + channels: { + telegram: { + token: { $secret: "vault://telegram/token" }, + }, + }, + }; + const resolvedConfig = { + channels: { + telegram: { + token: "12345:resolved-token", + }, + }, + }; + testConfig = rawConfig; + resolveCommandSecretRefsViaGateway.mockResolvedValueOnce({ + resolvedConfig: resolvedConfig as unknown as Record, + diagnostics: ["resolved channels.telegram.token"], + }); + await setRegistry( + createTestRegistry([ + { + ...createTelegramSendPluginRegistration(), + }, + ]), + ); + + const deps = makeDeps(); + await messageCommand( + { + action: "send", + channel: "telegram", + target: "123456", + message: "hi", + }, + deps, + runtime, + ); + + expect(resolveCommandSecretRefsViaGateway).toHaveBeenCalledWith( + expect.objectContaining({ + config: rawConfig, + commandName: "message", + }), + ); + expect(handleTelegramAction).toHaveBeenCalledWith( + expect.objectContaining({ action: "send", to: "123456", accountId: undefined }), + resolvedConfig, + ); + }); + + it("threads resolved SecretRef config into outbound adapter sends", async () => { + const rawConfig = { + channels: { + telegram: { + token: { $secret: "vault://telegram/token" }, + }, + }, + }; + const resolvedConfig = { + channels: { + telegram: { + token: "12345:resolved-token", + }, + }, + }; + testConfig = rawConfig; + resolveCommandSecretRefsViaGateway.mockResolvedValueOnce({ + resolvedConfig: resolvedConfig as unknown as Record, + diagnostics: ["resolved channels.telegram.token"], + }); + const sendText = vi.fn(async (_ctx: { cfg?: unknown; to: string; text: string }) => ({ + channel: "telegram" as const, + messageId: "msg-1", + chatId: "123456", + })); + const sendMedia = vi.fn(async (_ctx: { cfg?: unknown }) => ({ + channel: "telegram" as const, + messageId: "msg-2", + chatId: "123456", + })); + await setRegistry( + createTestRegistry([ + { + pluginId: "telegram", + source: "test", + plugin: createStubPlugin({ + id: "telegram", + label: "Telegram", + outbound: { + deliveryMode: "direct", + sendText, + sendMedia, + }, + }), + }, + ]), + ); + + const deps = makeDeps(); + await messageCommand( + { + action: "send", + channel: "telegram", + target: "123456", + message: "hi", + }, + deps, + runtime, + ); + + expect(sendText).toHaveBeenCalledWith( + expect.objectContaining({ + cfg: resolvedConfig, + to: "123456", + text: "hi", + }), + ); + expect(sendText.mock.calls[0]?.[0]?.cfg).not.toBe(rawConfig); + }); + + it("keeps local-fallback resolved cfg in outbound adapter sends", async () => { + const rawConfig = { + channels: { + telegram: { + token: { source: "env", provider: "default", id: "TELEGRAM_BOT_TOKEN" }, + }, + }, + }; + const locallyResolvedConfig = { + channels: { + telegram: { + token: "12345:local-fallback-token", + }, + }, + }; + testConfig = rawConfig; + resolveCommandSecretRefsViaGateway.mockResolvedValueOnce({ + resolvedConfig: locallyResolvedConfig as unknown as Record, + diagnostics: ["gateway secrets.resolve unavailable; used local resolver fallback."], + }); + const sendText = vi.fn(async (_ctx: { cfg?: unknown }) => ({ + channel: "telegram" as const, + messageId: "msg-3", + chatId: "123456", + })); + const sendMedia = vi.fn(async (_ctx: { cfg?: unknown }) => ({ + channel: "telegram" as const, + messageId: "msg-4", + chatId: "123456", + })); + await setRegistry( + createTestRegistry([ + { + pluginId: "telegram", + source: "test", + plugin: createStubPlugin({ + id: "telegram", + label: "Telegram", + outbound: { + deliveryMode: "direct", + sendText, + sendMedia, + }, + }), + }, + ]), + ); + + const deps = makeDeps(); + await messageCommand( + { + action: "send", + channel: "telegram", + target: "123456", + message: "hi", + }, + deps, + runtime, + ); + + expect(sendText).toHaveBeenCalledWith( + expect.objectContaining({ + cfg: locallyResolvedConfig, + }), + ); + expect(sendText.mock.calls[0]?.[0]?.cfg).not.toBe(rawConfig); + expect(runtime.log).toHaveBeenCalledWith( + expect.stringContaining("[secrets] gateway secrets.resolve unavailable"), + ); + }); + it("defaults channel when only one configured", async () => { process.env.TELEGRAM_BOT_TOKEN = "token-abc"; await setRegistry( diff --git a/src/discord/send.components.ts b/src/discord/send.components.ts index e2c87fd5f3f..5cdbee1b90c 100644 --- a/src/discord/send.components.ts +++ b/src/discord/send.components.ts @@ -5,7 +5,7 @@ import { type RequestClient, } from "@buape/carbon"; import { ChannelType, Routes } from "discord-api-types/v10"; -import { loadConfig } from "../config/config.js"; +import { loadConfig, type OpenClawConfig } from "../config/config.js"; import { recordChannelActivity } from "../infra/channel-activity.js"; import { loadWebMedia } from "../web/media.js"; import { resolveDiscordAccount } from "./accounts.js"; @@ -41,6 +41,7 @@ function extractComponentAttachmentNames(spec: DiscordComponentMessageSpec): str } type DiscordComponentSendOpts = { + cfg?: OpenClawConfig; accountId?: string; token?: string; rest?: RequestClient; @@ -58,10 +59,10 @@ export async function sendDiscordComponentMessage( spec: DiscordComponentMessageSpec, opts: DiscordComponentSendOpts = {}, ): Promise { - const cfg = loadConfig(); + const cfg = opts.cfg ?? loadConfig(); const accountInfo = resolveDiscordAccount({ cfg, accountId: opts.accountId }); const { token, rest, request } = createDiscordClient(opts, cfg); - const recipient = await parseAndResolveRecipient(to, opts.accountId); + const recipient = await parseAndResolveRecipient(to, opts.accountId, cfg); const { channelId } = await resolveChannelId(rest, recipient, request); const channelType = await resolveDiscordChannelType(rest, channelId); diff --git a/src/discord/send.outbound.ts b/src/discord/send.outbound.ts index 3e261f4a278..533d4060ed5 100644 --- a/src/discord/send.outbound.ts +++ b/src/discord/send.outbound.ts @@ -4,7 +4,7 @@ import path from "node:path"; import { serializePayload, type MessagePayloadObject, type RequestClient } from "@buape/carbon"; import { ChannelType, Routes } from "discord-api-types/v10"; import { resolveChunkMode } from "../auto-reply/chunk.js"; -import { loadConfig } from "../config/config.js"; +import { loadConfig, type OpenClawConfig } from "../config/config.js"; import { resolveMarkdownTableMode } from "../config/markdown-tables.js"; import { recordChannelActivity } from "../infra/channel-activity.js"; import type { RetryConfig } from "../infra/retry.js"; @@ -44,6 +44,7 @@ import { } from "./voice-message.js"; type DiscordSendOpts = { + cfg?: OpenClawConfig; token?: string; accountId?: string; mediaUrl?: string; @@ -121,9 +122,9 @@ async function resolveDiscordSendTarget( to: string, opts: DiscordSendOpts, ): Promise<{ rest: RequestClient; request: DiscordClientRequest; channelId: string }> { - const cfg = loadConfig(); + const cfg = opts.cfg ?? loadConfig(); const { rest, request } = createDiscordClient(opts, cfg); - const recipient = await parseAndResolveRecipient(to, opts.accountId); + const recipient = await parseAndResolveRecipient(to, opts.accountId, cfg); const { channelId } = await resolveChannelId(rest, recipient, request); return { rest, request, channelId }; } @@ -133,7 +134,7 @@ export async function sendMessageDiscord( text: string, opts: DiscordSendOpts = {}, ): Promise { - const cfg = loadConfig(); + const cfg = opts.cfg ?? loadConfig(); const accountInfo = resolveDiscordAccount({ cfg, accountId: opts.accountId, @@ -149,7 +150,7 @@ export async function sendMessageDiscord( accountId: accountInfo.accountId, }); const { token, rest, request } = createDiscordClient(opts, cfg); - const recipient = await parseAndResolveRecipient(to, opts.accountId); + const recipient = await parseAndResolveRecipient(to, opts.accountId, cfg); const { channelId } = await resolveChannelId(rest, recipient, request); // Forum/Media channels reject POST /messages; auto-create a thread post instead. @@ -310,6 +311,7 @@ export async function sendMessageDiscord( } type DiscordWebhookSendOpts = { + cfg?: OpenClawConfig; webhookId: string; webhookToken: string; accountId?: string; @@ -385,7 +387,7 @@ export async function sendWebhookMessageDiscord( }; try { const account = resolveDiscordAccount({ - cfg: loadConfig(), + cfg: opts.cfg ?? loadConfig(), accountId: opts.accountId, }); recordChannelActivity({ @@ -464,6 +466,7 @@ export async function sendPollDiscord( } type VoiceMessageOpts = { + cfg?: OpenClawConfig; token?: string; accountId?: string; verbose?: boolean; @@ -509,7 +512,7 @@ export async function sendVoiceMessageDiscord( let channelId: string | undefined; try { - const cfg = loadConfig(); + const cfg = opts.cfg ?? loadConfig(); const accountInfo = resolveDiscordAccount({ cfg, accountId: opts.accountId, @@ -518,7 +521,7 @@ export async function sendVoiceMessageDiscord( token = client.token; rest = client.rest; const request = client.request; - const recipient = await parseAndResolveRecipient(to, opts.accountId); + const recipient = await parseAndResolveRecipient(to, opts.accountId, cfg); channelId = (await resolveChannelId(rest, recipient, request)).channelId; // Convert to OGG/Opus if needed diff --git a/src/discord/send.reactions.ts b/src/discord/send.reactions.ts index 89dd9b9070e..436d64ac5b2 100644 --- a/src/discord/send.reactions.ts +++ b/src/discord/send.reactions.ts @@ -5,7 +5,6 @@ import { createDiscordClient, formatReactionEmoji, normalizeReactionEmoji, - resolveDiscordRest, } from "./send.shared.js"; import type { DiscordReactionSummary, DiscordReactOpts } from "./send.types.js"; @@ -15,7 +14,7 @@ export async function reactMessageDiscord( emoji: string, opts: DiscordReactOpts = {}, ) { - const cfg = loadConfig(); + const cfg = opts.cfg ?? loadConfig(); const { rest, request } = createDiscordClient(opts, cfg); const encoded = normalizeReactionEmoji(emoji); await request( @@ -31,7 +30,8 @@ export async function removeReactionDiscord( emoji: string, opts: DiscordReactOpts = {}, ) { - const rest = resolveDiscordRest(opts); + const cfg = opts.cfg ?? loadConfig(); + const { rest } = createDiscordClient(opts, cfg); const encoded = normalizeReactionEmoji(emoji); await rest.delete(Routes.channelMessageOwnReaction(channelId, messageId, encoded)); return { ok: true }; @@ -42,7 +42,8 @@ export async function removeOwnReactionsDiscord( messageId: string, opts: DiscordReactOpts = {}, ): Promise<{ ok: true; removed: string[] }> { - const rest = resolveDiscordRest(opts); + const cfg = opts.cfg ?? loadConfig(); + const { rest } = createDiscordClient(opts, cfg); const message = (await rest.get(Routes.channelMessage(channelId, messageId))) as { reactions?: Array<{ emoji: { id?: string | null; name?: string | null } }>; }; @@ -73,7 +74,8 @@ export async function fetchReactionsDiscord( messageId: string, opts: DiscordReactOpts & { limit?: number } = {}, ): Promise { - const rest = resolveDiscordRest(opts); + const cfg = opts.cfg ?? loadConfig(); + const { rest } = createDiscordClient(opts, cfg); const message = (await rest.get(Routes.channelMessage(channelId, messageId))) as { reactions?: Array<{ count: number; diff --git a/src/discord/send.shared.ts b/src/discord/send.shared.ts index 3a5d71f03e4..fddc276fccf 100644 --- a/src/discord/send.shared.ts +++ b/src/discord/send.shared.ts @@ -10,7 +10,7 @@ import { PollLayoutType } from "discord-api-types/payloads/v10"; import type { RESTAPIPoll } from "discord-api-types/rest/v10"; import { Routes, type APIChannel, type APIEmbed } from "discord-api-types/v10"; import type { ChunkMode } from "../auto-reply/chunk.js"; -import { loadConfig } from "../config/config.js"; +import { loadConfig, type OpenClawConfig } from "../config/config.js"; import type { RetryRunner } from "../infra/retry-policy.js"; import { buildOutboundMediaLoadOptions } from "../media/load-options.js"; import { normalizePollDurationHours, normalizePollInput, type PollInput } from "../polls.js"; @@ -80,9 +80,10 @@ function parseRecipient(raw: string): DiscordRecipient { export async function parseAndResolveRecipient( raw: string, accountId?: string, + cfg?: OpenClawConfig, ): Promise { - const cfg = loadConfig(); - const accountInfo = resolveDiscordAccount({ cfg, accountId }); + const resolvedCfg = cfg ?? loadConfig(); + const accountInfo = resolveDiscordAccount({ cfg: resolvedCfg, accountId }); // First try to resolve using directory lookup (handles usernames) const trimmed = raw.trim(); @@ -93,7 +94,7 @@ export async function parseAndResolveRecipient( const resolved = await resolveDiscordTarget( raw, { - cfg, + cfg: resolvedCfg, accountId: accountInfo.accountId, }, parseOptions, diff --git a/src/discord/send.types.ts b/src/discord/send.types.ts index c69058f8687..2dc29921f7e 100644 --- a/src/discord/send.types.ts +++ b/src/discord/send.types.ts @@ -1,4 +1,5 @@ import type { RequestClient } from "@buape/carbon"; +import type { OpenClawConfig } from "../config/config.js"; import type { RetryConfig } from "../infra/retry.js"; export class DiscordSendError extends Error { @@ -28,6 +29,7 @@ export type DiscordSendResult = { }; export type DiscordReactOpts = { + cfg?: OpenClawConfig; token?: string; accountId?: string; rest?: RequestClient; diff --git a/src/discord/send.webhook-activity.test.ts b/src/discord/send.webhook-activity.test.ts index 0d92e16de3f..c51ba3b814d 100644 --- a/src/discord/send.webhook-activity.test.ts +++ b/src/discord/send.webhook-activity.test.ts @@ -2,6 +2,15 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { sendWebhookMessageDiscord } from "./send.js"; const recordChannelActivityMock = vi.hoisted(() => vi.fn()); +const loadConfigMock = vi.hoisted(() => vi.fn(() => ({ channels: { discord: {} } }))); + +vi.mock("../config/config.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + loadConfig: () => loadConfigMock(), + }; +}); vi.mock("../infra/channel-activity.js", async (importOriginal) => { const actual = await importOriginal(); @@ -14,6 +23,7 @@ vi.mock("../infra/channel-activity.js", async (importOriginal) => { describe("sendWebhookMessageDiscord activity", () => { beforeEach(() => { recordChannelActivityMock.mockClear(); + loadConfigMock.mockClear(); vi.stubGlobal( "fetch", vi.fn(async () => { @@ -30,7 +40,15 @@ describe("sendWebhookMessageDiscord activity", () => { }); it("records outbound channel activity for webhook sends", async () => { + const cfg = { + channels: { + discord: { + token: "resolved-token", + }, + }, + }; const result = await sendWebhookMessageDiscord("hello world", { + cfg, webhookId: "wh-1", webhookToken: "tok-1", accountId: "runtime", @@ -46,5 +64,6 @@ describe("sendWebhookMessageDiscord activity", () => { accountId: "runtime", direction: "outbound", }); + expect(loadConfigMock).not.toHaveBeenCalled(); }); }); diff --git a/src/infra/outbound/cfg-threading.guard.test.ts b/src/infra/outbound/cfg-threading.guard.test.ts new file mode 100644 index 00000000000..306170281c8 --- /dev/null +++ b/src/infra/outbound/cfg-threading.guard.test.ts @@ -0,0 +1,179 @@ +import { existsSync, readdirSync, readFileSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; + +const thisFilePath = fileURLToPath(import.meta.url); +const thisDir = path.dirname(thisFilePath); +const repoRoot = path.resolve(thisDir, "../../.."); +const loadConfigPattern = /\b(?:loadConfig|config\.loadConfig)\s*\(/; + +function toPosix(relativePath: string): string { + return relativePath.split(path.sep).join("/"); +} + +function readRepoFile(relativePath: string): string { + const absolute = path.join(repoRoot, relativePath); + return readFileSync(absolute, "utf8"); +} + +function listCoreOutboundEntryFiles(): string[] { + const outboundDir = path.join(repoRoot, "src/channels/plugins/outbound"); + return readdirSync(outboundDir) + .filter((name) => name.endsWith(".ts") && !name.endsWith(".test.ts")) + .map((name) => toPosix(path.join("src/channels/plugins/outbound", name))) + .toSorted(); +} + +function listExtensionFiles(): { + adapterEntrypoints: string[]; + inlineChannelEntrypoints: string[]; +} { + const extensionsRoot = path.join(repoRoot, "extensions"); + const adapterEntrypoints: string[] = []; + const inlineChannelEntrypoints: string[] = []; + + for (const entry of readdirSync(extensionsRoot, { withFileTypes: true })) { + if (!entry.isDirectory()) { + continue; + } + const srcDir = path.join(extensionsRoot, entry.name, "src"); + const outboundPath = path.join(srcDir, "outbound.ts"); + if (existsSync(outboundPath)) { + adapterEntrypoints.push(toPosix(path.join("extensions", entry.name, "src/outbound.ts"))); + } + + const channelPath = path.join(srcDir, "channel.ts"); + if (!existsSync(channelPath)) { + continue; + } + const source = readFileSync(channelPath, "utf8"); + if (source.includes("outbound:")) { + inlineChannelEntrypoints.push(toPosix(path.join("extensions", entry.name, "src/channel.ts"))); + } + } + + return { + adapterEntrypoints: adapterEntrypoints.toSorted(), + inlineChannelEntrypoints: inlineChannelEntrypoints.toSorted(), + }; +} + +function extractOutboundBlock(source: string, file: string): string { + const outboundKeyIndex = source.indexOf("outbound:"); + expect(outboundKeyIndex, `${file} should define outbound:`).toBeGreaterThanOrEqual(0); + const braceStart = source.indexOf("{", outboundKeyIndex); + expect(braceStart, `${file} should define outbound object`).toBeGreaterThanOrEqual(0); + + let depth = 0; + let state: "code" | "single" | "double" | "template" | "lineComment" | "blockComment" = "code"; + for (let i = braceStart; i < source.length; i += 1) { + const current = source[i]; + const next = source[i + 1]; + + if (state === "lineComment") { + if (current === "\n") { + state = "code"; + } + continue; + } + if (state === "blockComment") { + if (current === "*" && next === "/") { + state = "code"; + i += 1; + } + continue; + } + if (state === "single") { + if (current === "\\" && next) { + i += 1; + continue; + } + if (current === "'") { + state = "code"; + } + continue; + } + if (state === "double") { + if (current === "\\" && next) { + i += 1; + continue; + } + if (current === '"') { + state = "code"; + } + continue; + } + if (state === "template") { + if (current === "\\" && next) { + i += 1; + continue; + } + if (current === "`") { + state = "code"; + } + continue; + } + + if (current === "/" && next === "/") { + state = "lineComment"; + i += 1; + continue; + } + if (current === "/" && next === "*") { + state = "blockComment"; + i += 1; + continue; + } + if (current === "'") { + state = "single"; + continue; + } + if (current === '"') { + state = "double"; + continue; + } + if (current === "`") { + state = "template"; + continue; + } + if (current === "{") { + depth += 1; + continue; + } + if (current === "}") { + depth -= 1; + if (depth === 0) { + return source.slice(braceStart, i + 1); + } + } + } + + throw new Error(`Unable to parse outbound block in ${file}`); +} + +describe("outbound cfg-threading guard", () => { + it("keeps outbound adapter entrypoints free of loadConfig calls", () => { + const coreAdapterFiles = listCoreOutboundEntryFiles(); + const extensionAdapterFiles = listExtensionFiles().adapterEntrypoints; + const adapterFiles = [...coreAdapterFiles, ...extensionAdapterFiles]; + + for (const file of adapterFiles) { + const source = readRepoFile(file); + expect(source, `${file} must not call loadConfig in outbound entrypoint`).not.toMatch( + loadConfigPattern, + ); + } + }); + + it("keeps inline channel outbound blocks free of loadConfig calls", () => { + const inlineFiles = listExtensionFiles().inlineChannelEntrypoints; + for (const file of inlineFiles) { + const source = readRepoFile(file); + const outboundBlock = extractOutboundBlock(source, file); + expect(outboundBlock, `${file} outbound block must not call loadConfig`).not.toMatch( + loadConfigPattern, + ); + } + }); +}); diff --git a/src/infra/outbound/deliver.ts b/src/infra/outbound/deliver.ts index ac1e957c73d..45bff297065 100644 --- a/src/infra/outbound/deliver.ts +++ b/src/infra/outbound/deliver.ts @@ -53,7 +53,13 @@ const TELEGRAM_TEXT_LIMIT = 4096; type SendMatrixMessage = ( to: string, text: string, - opts?: { mediaUrl?: string; replyToId?: string; threadId?: string; timeoutMs?: number }, + opts?: { + cfg?: OpenClawConfig; + mediaUrl?: string; + replyToId?: string; + threadId?: string; + timeoutMs?: number; + }, ) => Promise<{ messageId: string; roomId: string }>; export type OutboundSendDeps = { @@ -600,6 +606,7 @@ async function deliverOutboundPayloadsCore( return { channel: "signal" as const, ...(await sendSignal(to, text, { + cfg, maxBytes: signalMaxBytes, accountId: accountId ?? undefined, textMode: "plain", @@ -636,6 +643,7 @@ async function deliverOutboundPayloadsCore( return { channel: "signal" as const, ...(await sendSignal(to, formatted.text, { + cfg, mediaUrl, maxBytes: signalMaxBytes, accountId: accountId ?? undefined, diff --git a/src/infra/outbound/message.channels.test.ts b/src/infra/outbound/message.channels.test.ts index af10cb9faf3..0a21264b43e 100644 --- a/src/infra/outbound/message.channels.test.ts +++ b/src/infra/outbound/message.channels.test.ts @@ -27,6 +27,76 @@ afterEach(() => { }); describe("sendMessage channel normalization", () => { + it("threads resolved cfg through alias + target normalization in outbound dispatch", async () => { + const resolvedCfg = { + __resolvedCfgMarker: "cfg-from-secret-resolution", + channels: {}, + } as Record; + const seen: { + resolveCfg?: unknown; + sendCfg?: unknown; + to?: string; + } = {}; + const imessageAliasPlugin: ChannelPlugin = { + id: "imessage", + meta: { + id: "imessage", + label: "iMessage", + selectionLabel: "iMessage", + docsPath: "/channels/imessage", + blurb: "iMessage test stub.", + aliases: ["imsg"], + }, + capabilities: { chatTypes: ["direct"] }, + config: { + listAccountIds: () => [], + resolveAccount: () => ({}), + }, + outbound: { + deliveryMode: "direct", + resolveTarget: ({ to, cfg }) => { + seen.resolveCfg = cfg; + const normalized = String(to ?? "") + .trim() + .replace(/^imessage:/i, ""); + return { ok: true, to: normalized }; + }, + sendText: async ({ cfg, to }) => { + seen.sendCfg = cfg; + seen.to = to; + return { channel: "imessage", messageId: "i-resolved" }; + }, + sendMedia: async ({ cfg, to }) => { + seen.sendCfg = cfg; + seen.to = to; + return { channel: "imessage", messageId: "i-resolved-media" }; + }, + }, + }; + + setRegistry( + createTestRegistry([ + { + pluginId: "imessage", + source: "test", + plugin: imessageAliasPlugin, + }, + ]), + ); + + const result = await sendMessage({ + cfg: resolvedCfg, + to: " imessage:+15551234567 ", + content: "hi", + channel: "imsg", + }); + + expect(result.channel).toBe("imessage"); + expect(seen.resolveCfg).toBe(resolvedCfg); + expect(seen.sendCfg).toBe(resolvedCfg); + expect(seen.to).toBe("+15551234567"); + }); + it("normalizes Teams alias", async () => { const sendMSTeams = vi.fn(async () => ({ messageId: "m1", diff --git a/src/line/send.ts b/src/line/send.ts index 7b6f4ac936e..1e97f247f70 100644 --- a/src/line/send.ts +++ b/src/line/send.ts @@ -1,5 +1,6 @@ import { messagingApi } from "@line/bot-sdk"; import { loadConfig } from "../config/config.js"; +import type { OpenClawConfig } from "../config/types.openclaw.js"; import { logVerbose } from "../globals.js"; import { recordChannelActivity } from "../infra/channel-activity.js"; import { resolveLineAccount } from "./accounts.js"; @@ -25,6 +26,7 @@ const userProfileCache = new Map< const PROFILE_CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes interface LineSendOpts { + cfg?: OpenClawConfig; channelAccessToken?: string; accountId?: string; verbose?: boolean; @@ -32,8 +34,8 @@ interface LineSendOpts { replyToken?: string; } -type LineClientOpts = Pick; -type LinePushOpts = Pick; +type LineClientOpts = Pick; +type LinePushOpts = Pick; interface LinePushBehavior { errorContext?: string; @@ -68,7 +70,7 @@ function createLineMessagingClient(opts: LineClientOpts): { account: ReturnType; client: messagingApi.MessagingApiClient; } { - const cfg = loadConfig(); + const cfg = opts.cfg ?? loadConfig(); const account = resolveLineAccount({ cfg, accountId: opts.accountId, diff --git a/src/signal/send-reactions.ts b/src/signal/send-reactions.ts index 3f252635da7..dba41bb8b7d 100644 --- a/src/signal/send-reactions.ts +++ b/src/signal/send-reactions.ts @@ -3,11 +3,13 @@ */ import { loadConfig } from "../config/config.js"; +import type { OpenClawConfig } from "../config/config.js"; import { resolveSignalAccount } from "./accounts.js"; import { signalRpcRequest } from "./client.js"; import { resolveSignalRpcContext } from "./rpc-context.js"; export type SignalReactionOpts = { + cfg?: OpenClawConfig; baseUrl?: string; account?: string; accountId?: string; @@ -75,8 +77,9 @@ async function sendReactionSignalCore(params: { opts: SignalReactionOpts; errors: SignalReactionErrorMessages; }): Promise { + const cfg = params.opts.cfg ?? loadConfig(); const accountInfo = resolveSignalAccount({ - cfg: loadConfig(), + cfg, accountId: params.opts.accountId, }); const { baseUrl, account } = resolveSignalRpcContext(params.opts, accountInfo); diff --git a/src/signal/send.ts b/src/signal/send.ts index 8bcd385e2e8..9dc4ef97917 100644 --- a/src/signal/send.ts +++ b/src/signal/send.ts @@ -1,4 +1,4 @@ -import { loadConfig } from "../config/config.js"; +import { loadConfig, type OpenClawConfig } from "../config/config.js"; import { resolveMarkdownTableMode } from "../config/markdown-tables.js"; import { kindFromMime } from "../media/mime.js"; import { resolveOutboundAttachmentFromUrl } from "../media/outbound-attachment.js"; @@ -8,6 +8,7 @@ import { markdownToSignalText, type SignalTextStyleRange } from "./format.js"; import { resolveSignalRpcContext } from "./rpc-context.js"; export type SignalSendOpts = { + cfg?: OpenClawConfig; baseUrl?: string; account?: string; accountId?: string; @@ -100,7 +101,7 @@ export async function sendMessageSignal( text: string, opts: SignalSendOpts = {}, ): Promise { - const cfg = loadConfig(); + const cfg = opts.cfg ?? loadConfig(); const accountInfo = resolveSignalAccount({ cfg, accountId: opts.accountId, diff --git a/src/slack/send.ts b/src/slack/send.ts index fcfe230f7dc..8ce7fd3c3f3 100644 --- a/src/slack/send.ts +++ b/src/slack/send.ts @@ -5,7 +5,7 @@ import { resolveTextChunkLimit, } from "../auto-reply/chunk.js"; import { isSilentReplyText } from "../auto-reply/tokens.js"; -import { loadConfig } from "../config/config.js"; +import { loadConfig, type OpenClawConfig } from "../config/config.js"; import { resolveMarkdownTableMode } from "../config/markdown-tables.js"; import { logVerbose } from "../globals.js"; import { @@ -45,6 +45,7 @@ export type SlackSendIdentity = { }; type SlackSendOpts = { + cfg?: OpenClawConfig; token?: string; accountId?: string; mediaUrl?: string; @@ -262,7 +263,7 @@ export async function sendMessageSlack( if (!trimmedMessage && !opts.mediaUrl && !blocks) { throw new Error("Slack send requires text, blocks, or media"); } - const cfg = loadConfig(); + const cfg = opts.cfg ?? loadConfig(); const account = resolveSlackAccount({ cfg, accountId: opts.accountId, diff --git a/src/telegram/send.ts b/src/telegram/send.ts index 6fa00740572..b04bd792529 100644 --- a/src/telegram/send.ts +++ b/src/telegram/send.ts @@ -42,6 +42,7 @@ type TelegramApi = Bot["api"]; type TelegramApiOverride = Partial; type TelegramSendOpts = { + cfg?: ReturnType; token?: string; accountId?: string; verbose?: boolean; @@ -1038,6 +1039,7 @@ export async function sendStickerTelegram( } type TelegramPollOpts = { + cfg?: ReturnType; token?: string; accountId?: string; verbose?: boolean; diff --git a/src/web/outbound.ts b/src/web/outbound.ts index da1428a6980..95cc84b1f11 100644 --- a/src/web/outbound.ts +++ b/src/web/outbound.ts @@ -1,4 +1,4 @@ -import { loadConfig } from "../config/config.js"; +import { loadConfig, type OpenClawConfig } from "../config/config.js"; import { resolveMarkdownTableMode } from "../config/markdown-tables.js"; import { generateSecureUuid } from "../infra/secure-random.js"; import { getChildLogger } from "../logging/logger.js"; @@ -18,6 +18,7 @@ export async function sendMessageWhatsApp( body: string, options: { verbose: boolean; + cfg?: OpenClawConfig; mediaUrl?: string; mediaLocalRoots?: readonly string[]; gifPlayback?: boolean; @@ -30,7 +31,7 @@ export async function sendMessageWhatsApp( const { listener: active, accountId: resolvedAccountId } = requireActiveWebListener( options.accountId, ); - const cfg = loadConfig(); + const cfg = options.cfg ?? loadConfig(); const tableMode = resolveMarkdownTableMode({ cfg, channel: "whatsapp", @@ -150,7 +151,7 @@ export async function sendReactionWhatsApp( export async function sendPollWhatsApp( to: string, poll: PollInput, - options: { verbose: boolean; accountId?: string }, + options: { verbose: boolean; accountId?: string; cfg?: OpenClawConfig }, ): Promise<{ messageId: string; toJid: string }> { const correlationId = generateSecureUuid(); const startedAt = Date.now(); From 802b9f6b191078f4168c689370be4c319bab8b80 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 4 Mar 2026 01:19:17 -0500 Subject: [PATCH 03/74] Plugins: add root-alias shim and cache/docs updates --- CHANGELOG.md | 1 + docs/tools/plugin.md | 14 +- package.json | 8 +- ...-no-monolithic-plugin-sdk-entry-imports.ts | 70 ++++++++- scripts/check-plugin-sdk-exports.mjs | 11 ++ scripts/copy-plugin-sdk-root-alias.mjs | 10 ++ scripts/release-check.ts | 3 + scripts/write-plugin-sdk-entry-dts.ts | 1 + src/plugin-sdk/compat.ts | 1 + src/plugin-sdk/root-alias.cjs | 145 ++++++++++++++++++ src/plugin-sdk/root-alias.test.ts | 44 ++++++ src/plugin-sdk/subpaths.test.ts | 6 + src/plugin-sdk/telegram.ts | 10 +- src/plugins/discovery.test.ts | 39 ++++- src/plugins/discovery.ts | 75 ++++++++- src/plugins/loader.test.ts | 116 +++++++++++++- src/plugins/loader.ts | 28 +++- src/plugins/manifest-registry.ts | 3 +- tsconfig.plugin-sdk.dts.json | 1 + tsdown.config.ts | 7 + vitest.config.ts | 4 + 21 files changed, 576 insertions(+), 21 deletions(-) create mode 100644 scripts/copy-plugin-sdk-root-alias.mjs create mode 100644 src/plugin-sdk/compat.ts create mode 100644 src/plugin-sdk/root-alias.cjs create mode 100644 src/plugin-sdk/root-alias.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 62e54c6d5dc..57307903afd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ Docs: https://docs.openclaw.ai - Extensions/media local-root propagation: consistently forward `mediaLocalRoots` through extension `sendMedia` adapters (Google Chat, Slack, iMessage, Signal, WhatsApp), preserving non-local media behavior while restoring local attachment resolution from configured roots. Synthesis of #33581, #33545, #33540, #33536, #33528. Thanks @bmendonca3. - Gateway/security default response headers: add `Permissions-Policy: camera=(), microphone=(), geolocation=()` to baseline gateway HTTP security headers for all responses. (#30186) thanks @habakan. - Plugins/startup loading: lazily initialize plugin runtime, split startup-critical plugin SDK imports into `openclaw/plugin-sdk/core` and `openclaw/plugin-sdk/telegram`, and preserve `api.runtime` reflection semantics for plugin compatibility. (#28620) thanks @hmemcpy. +- Plugins/startup performance: reduce bursty plugin discovery/manifest overhead with short in-process caches, skip importing bundled memory plugins that are disabled by slot selection, and speed legacy root `openclaw/plugin-sdk` compatibility via runtime root-alias routing while preserving backward compatibility. Thanks @gumadeiras. - Build/lazy runtime boundaries: replace ineffective dynamic import sites with dedicated lazy runtime boundaries across Slack slash handling, Telegram audit, CLI send deps, memory fallback, and outbound delivery paths while preserving behavior. (#33690) thanks @gumadeiras. - Config/heartbeat legacy-path handling: auto-migrate top-level `heartbeat` into `agents.defaults.heartbeat` (with merge semantics that preserve explicit defaults), and keep startup failures on non-migratable legacy entries in the detailed invalid-config path instead of generic migration-failed errors. (#32706) thanks @xiwan. - Plugins/SDK subpath parity: add channel-specific plugin SDK subpaths for Discord, Slack, Signal, iMessage, WhatsApp, and LINE; migrate bundled plugin entrypoints to scoped subpaths/core with CI guardrails; and keep `openclaw/plugin-sdk` root import compatibility for existing external plugins. (#33737) thanks @gumadeiras. diff --git a/docs/tools/plugin.md b/docs/tools/plugin.md index f5fd5a34ab6..60d5aa61c37 100644 --- a/docs/tools/plugin.md +++ b/docs/tools/plugin.md @@ -112,6 +112,7 @@ Use SDK subpaths instead of the monolithic `openclaw/plugin-sdk` import when authoring plugins: - `openclaw/plugin-sdk/core` for generic plugin APIs, provider auth types, and shared helpers. +- `openclaw/plugin-sdk/compat` for bundled/internal plugin code that needs broader shared runtime helpers than `core`. - `openclaw/plugin-sdk/telegram` for Telegram channel plugins. - `openclaw/plugin-sdk/discord` for Discord channel plugins. - `openclaw/plugin-sdk/slack` for Slack channel plugins. @@ -123,8 +124,17 @@ authoring plugins: Compatibility note: - `openclaw/plugin-sdk` remains supported for existing external plugins. -- New and migrated bundled plugins should use channel subpaths (or `core`) to - keep startup imports scoped. +- New and migrated bundled plugins should use channel subpaths and `core`; use + `compat` only when broader shared helpers are required. + +Performance note: + +- Plugin discovery and manifest metadata use short in-process caches to reduce + bursty startup/reload work. +- Set `OPENCLAW_DISABLE_PLUGIN_DISCOVERY_CACHE=1` or + `OPENCLAW_DISABLE_PLUGIN_MANIFEST_CACHE=1` to disable these caches. +- Tune cache windows with `OPENCLAW_PLUGIN_DISCOVERY_CACHE_MS` and + `OPENCLAW_PLUGIN_MANIFEST_CACHE_MS`. ## Discovery & precedence diff --git a/package.json b/package.json index 60c1ebaf263..590f2b4e9a4 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,10 @@ "types": "./dist/plugin-sdk/core.d.ts", "default": "./dist/plugin-sdk/core.js" }, + "./plugin-sdk/compat": { + "types": "./dist/plugin-sdk/compat.d.ts", + "default": "./dist/plugin-sdk/compat.js" + }, "./plugin-sdk/telegram": { "types": "./dist/plugin-sdk/telegram.d.ts", "default": "./dist/plugin-sdk/telegram.js" @@ -91,9 +95,9 @@ "android:run": "cd apps/android && ./gradlew :app:installDebug && adb shell am start -n ai.openclaw.android/.MainActivity", "android:test": "cd apps/android && ./gradlew :app:testDebugUnitTest", "android:test:integration": "OPENCLAW_LIVE_TEST=1 OPENCLAW_LIVE_ANDROID_NODE=1 vitest run --config vitest.live.config.ts src/gateway/android-node.capabilities.live.test.ts", - "build": "pnpm canvas:a2ui:bundle && tsdown && pnpm build:plugin-sdk:dts && node --import tsx scripts/write-plugin-sdk-entry-dts.ts && node --import tsx scripts/canvas-a2ui-copy.ts && node --import tsx scripts/copy-hook-metadata.ts && node --import tsx scripts/copy-export-html-templates.ts && node --import tsx scripts/write-build-info.ts && node --import tsx scripts/write-cli-startup-metadata.ts && node --import tsx scripts/write-cli-compat.ts", + "build": "pnpm canvas:a2ui:bundle && tsdown && node scripts/copy-plugin-sdk-root-alias.mjs && pnpm build:plugin-sdk:dts && node --import tsx scripts/write-plugin-sdk-entry-dts.ts && node --import tsx scripts/canvas-a2ui-copy.ts && node --import tsx scripts/copy-hook-metadata.ts && node --import tsx scripts/copy-export-html-templates.ts && node --import tsx scripts/write-build-info.ts && node --import tsx scripts/write-cli-startup-metadata.ts && node --import tsx scripts/write-cli-compat.ts", "build:plugin-sdk:dts": "tsc -p tsconfig.plugin-sdk.dts.json", - "build:strict-smoke": "pnpm canvas:a2ui:bundle && tsdown && pnpm build:plugin-sdk:dts", + "build:strict-smoke": "pnpm canvas:a2ui:bundle && tsdown && node scripts/copy-plugin-sdk-root-alias.mjs && pnpm build:plugin-sdk:dts", "canvas:a2ui:bundle": "bash scripts/bundle-a2ui.sh", "check": "pnpm format:check && pnpm tsgo && pnpm lint && pnpm lint:tmp:no-random-messaging && pnpm lint:tmp:channel-agnostic-boundaries && pnpm lint:tmp:no-raw-channel-fetch && pnpm lint:agent:ingress-owner && pnpm lint:plugins:no-register-http-handler && pnpm lint:plugins:no-monolithic-plugin-sdk-entry-imports && pnpm lint:webhook:no-low-level-body-read && pnpm lint:auth:no-pairing-store-group && pnpm lint:auth:pairing-account-scope && pnpm check:host-env-policy:swift", "check:docs": "pnpm format:docs:check && pnpm lint:docs && pnpm docs:check-links", diff --git a/scripts/check-no-monolithic-plugin-sdk-entry-imports.ts b/scripts/check-no-monolithic-plugin-sdk-entry-imports.ts index 3c41add7ab6..bde974d5154 100644 --- a/scripts/check-no-monolithic-plugin-sdk-entry-imports.ts +++ b/scripts/check-no-monolithic-plugin-sdk-entry-imports.ts @@ -13,14 +13,68 @@ function hasMonolithicRootImport(content: string): boolean { return ROOT_IMPORT_PATTERNS.some((pattern) => pattern.test(content)); } +function isSourceFile(filePath: string): boolean { + if (filePath.endsWith(".d.ts")) { + return false; + } + return /\.(?:[cm]?ts|[cm]?js|tsx|jsx)$/u.test(filePath); +} + +function collectPluginSourceFiles(rootDir: string): string[] { + const srcDir = path.join(rootDir, "src"); + if (!fs.existsSync(srcDir)) { + return []; + } + + const files: string[] = []; + const stack: string[] = [srcDir]; + while (stack.length > 0) { + const current = stack.pop(); + if (!current) { + continue; + } + let entries: fs.Dirent[] = []; + try { + entries = fs.readdirSync(current, { withFileTypes: true }); + } catch { + continue; + } + for (const entry of entries) { + const fullPath = path.join(current, entry.name); + if (entry.isDirectory()) { + if ( + entry.name === "node_modules" || + entry.name === "dist" || + entry.name === ".git" || + entry.name === "coverage" + ) { + continue; + } + stack.push(fullPath); + continue; + } + if (entry.isFile() && isSourceFile(fullPath)) { + files.push(fullPath); + } + } + } + + return files; +} + function main() { const discovery = discoverOpenClawPlugins({}); - const bundledEntryFiles = [ - ...new Set(discovery.candidates.filter((c) => c.origin === "bundled").map((c) => c.source)), - ]; + const bundledCandidates = discovery.candidates.filter((c) => c.origin === "bundled"); + const filesToCheck = new Set(); + for (const candidate of bundledCandidates) { + filesToCheck.add(candidate.source); + for (const srcFile of collectPluginSourceFiles(candidate.rootDir)) { + filesToCheck.add(srcFile); + } + } const offenders: string[] = []; - for (const entryFile of bundledEntryFiles) { + for (const entryFile of filesToCheck) { let content = ""; try { content = fs.readFileSync(entryFile, "utf8"); @@ -33,17 +87,19 @@ function main() { } if (offenders.length > 0) { - console.error("Bundled plugin entrypoints must not import monolithic openclaw/plugin-sdk."); + console.error("Bundled plugin source files must not import monolithic openclaw/plugin-sdk."); for (const file of offenders.toSorted()) { const relative = path.relative(process.cwd(), file) || file; console.error(`- ${relative}`); } - console.error("Use openclaw/plugin-sdk/ for channel plugins or /core for others."); + console.error( + "Use openclaw/plugin-sdk/ for channel plugins, /core for startup surfaces, or /compat for broader internals.", + ); process.exit(1); } console.log( - `OK: bundled entrypoints use scoped plugin-sdk subpaths (${bundledEntryFiles.length} checked).`, + `OK: bundled plugin source files use scoped plugin-sdk subpaths (${filesToCheck.size} checked).`, ); } diff --git a/scripts/check-plugin-sdk-exports.mjs b/scripts/check-plugin-sdk-exports.mjs index 993c92e33c3..87d7826945f 100755 --- a/scripts/check-plugin-sdk-exports.mjs +++ b/scripts/check-plugin-sdk-exports.mjs @@ -43,6 +43,7 @@ const exportSet = new Set(exportedNames); const requiredSubpathEntries = [ "core", + "compat", "telegram", "discord", "slack", @@ -53,6 +54,8 @@ const requiredSubpathEntries = [ "account-id", ]; +const requiredRuntimeShimEntries = ["root-alias.cjs"]; + // Critical functions that channel extension plugins import from openclaw/plugin-sdk. // If any of these are missing, plugins will fail at runtime with: // TypeError: (0 , _pluginSdk.) is not a function @@ -101,6 +104,14 @@ for (const entry of requiredSubpathEntries) { } } +for (const entry of requiredRuntimeShimEntries) { + const shimPath = resolve(__dirname, "..", "dist", "plugin-sdk", entry); + if (!existsSync(shimPath)) { + console.error(`MISSING RUNTIME SHIM: dist/plugin-sdk/${entry}`); + missing += 1; + } +} + if (missing > 0) { console.error( `\nERROR: ${missing} required plugin-sdk artifact(s) missing (named exports or subpath files).`, diff --git a/scripts/copy-plugin-sdk-root-alias.mjs b/scripts/copy-plugin-sdk-root-alias.mjs new file mode 100644 index 00000000000..b1bf80b6312 --- /dev/null +++ b/scripts/copy-plugin-sdk-root-alias.mjs @@ -0,0 +1,10 @@ +#!/usr/bin/env node + +import { copyFileSync, mkdirSync } from "node:fs"; +import { dirname, resolve } from "node:path"; + +const source = resolve("src/plugin-sdk/root-alias.cjs"); +const target = resolve("dist/plugin-sdk/root-alias.cjs"); + +mkdirSync(dirname(target), { recursive: true }); +copyFileSync(source, target); diff --git a/scripts/release-check.ts b/scripts/release-check.ts index d4f302a824b..9b2848e8ead 100755 --- a/scripts/release-check.ts +++ b/scripts/release-check.ts @@ -16,6 +16,9 @@ const requiredPathGroups = [ "dist/plugin-sdk/index.d.ts", "dist/plugin-sdk/core.js", "dist/plugin-sdk/core.d.ts", + "dist/plugin-sdk/root-alias.cjs", + "dist/plugin-sdk/compat.js", + "dist/plugin-sdk/compat.d.ts", "dist/plugin-sdk/telegram.js", "dist/plugin-sdk/telegram.d.ts", "dist/plugin-sdk/discord.js", diff --git a/scripts/write-plugin-sdk-entry-dts.ts b/scripts/write-plugin-sdk-entry-dts.ts index 611ec4dfe86..197b36004a8 100644 --- a/scripts/write-plugin-sdk-entry-dts.ts +++ b/scripts/write-plugin-sdk-entry-dts.ts @@ -9,6 +9,7 @@ import path from "node:path"; const entrypoints = [ "index", "core", + "compat", "telegram", "discord", "slack", diff --git a/src/plugin-sdk/compat.ts b/src/plugin-sdk/compat.ts new file mode 100644 index 00000000000..8e893de15df --- /dev/null +++ b/src/plugin-sdk/compat.ts @@ -0,0 +1 @@ +export * from "./index.js"; diff --git a/src/plugin-sdk/root-alias.cjs b/src/plugin-sdk/root-alias.cjs new file mode 100644 index 00000000000..37626deebaf --- /dev/null +++ b/src/plugin-sdk/root-alias.cjs @@ -0,0 +1,145 @@ +"use strict"; + +const path = require("node:path"); +const fs = require("node:fs"); + +let monolithicSdk = null; + +function emptyPluginConfigSchema() { + function error(message) { + return { success: false, error: { issues: [{ path: [], message }] } }; + } + + return { + safeParse(value) { + if (value === undefined) { + return { success: true, data: undefined }; + } + if (!value || typeof value !== "object" || Array.isArray(value)) { + return error("expected config object"); + } + if (Object.keys(value).length > 0) { + return error("config must be empty"); + } + return { success: true, data: value }; + }, + jsonSchema: { + type: "object", + additionalProperties: false, + properties: {}, + }, + }; +} + +function loadMonolithicSdk() { + if (monolithicSdk) { + return monolithicSdk; + } + + const { createJiti } = require("jiti"); + const jiti = createJiti(__filename, { + interopDefault: true, + extensions: [".ts", ".tsx", ".mts", ".cts", ".mtsx", ".ctsx", ".js", ".mjs", ".cjs", ".json"], + }); + + const distCandidate = path.resolve(__dirname, "..", "..", "dist", "plugin-sdk", "index.js"); + if (fs.existsSync(distCandidate)) { + try { + monolithicSdk = jiti(distCandidate); + return monolithicSdk; + } catch { + // Fall through to source alias if dist is unavailable or stale. + } + } + + monolithicSdk = jiti(path.join(__dirname, "index.ts")); + return monolithicSdk; +} + +const fastExports = { + emptyPluginConfigSchema, +}; + +const rootProxy = new Proxy(fastExports, { + get(target, prop, receiver) { + if (prop === "__esModule") { + return true; + } + if (prop === "default") { + return rootProxy; + } + if (Reflect.has(target, prop)) { + return Reflect.get(target, prop, receiver); + } + return loadMonolithicSdk()[prop]; + }, + has(target, prop) { + if (prop === "__esModule" || prop === "default") { + return true; + } + if (Reflect.has(target, prop)) { + return true; + } + return prop in loadMonolithicSdk(); + }, + ownKeys(target) { + const keys = new Set([ + ...Reflect.ownKeys(target), + ...Reflect.ownKeys(loadMonolithicSdk()), + "default", + "__esModule", + ]); + return [...keys]; + }, + getOwnPropertyDescriptor(target, prop) { + if (prop === "__esModule") { + return { + configurable: true, + enumerable: false, + writable: false, + value: true, + }; + } + if (prop === "default") { + return { + configurable: true, + enumerable: false, + writable: false, + value: rootProxy, + }; + } + const own = Object.getOwnPropertyDescriptor(target, prop); + if (own) { + return own; + } + const descriptor = Object.getOwnPropertyDescriptor(loadMonolithicSdk(), prop); + if (!descriptor) { + return undefined; + } + if (descriptor.get || descriptor.set) { + const monolithic = loadMonolithicSdk(); + return { + configurable: true, + enumerable: descriptor.enumerable ?? true, + get: descriptor.get + ? function getLegacyValue() { + return descriptor.get.call(monolithic); + } + : undefined, + set: descriptor.set + ? function setLegacyValue(value) { + return descriptor.set.call(monolithic, value); + } + : undefined, + }; + } + return { + configurable: true, + enumerable: descriptor.enumerable ?? true, + value: descriptor.value, + writable: descriptor.writable, + }; + }, +}); + +module.exports = rootProxy; diff --git a/src/plugin-sdk/root-alias.test.ts b/src/plugin-sdk/root-alias.test.ts new file mode 100644 index 00000000000..dd2cc10b1bb --- /dev/null +++ b/src/plugin-sdk/root-alias.test.ts @@ -0,0 +1,44 @@ +import { createRequire } from "node:module"; +import { describe, expect, it } from "vitest"; + +const require = createRequire(import.meta.url); +const rootSdk = require("./root-alias.cjs") as Record; + +type EmptySchema = { + safeParse: (value: unknown) => + | { success: true; data?: unknown } + | { + success: false; + error: { issues: Array<{ path: Array; message: string }> }; + }; +}; + +describe("plugin-sdk root alias", () => { + it("exposes the fast empty config schema helper", () => { + const factory = rootSdk.emptyPluginConfigSchema as (() => EmptySchema) | undefined; + expect(typeof factory).toBe("function"); + if (!factory) { + return; + } + const schema = factory(); + expect(schema.safeParse(undefined)).toEqual({ success: true, data: undefined }); + expect(schema.safeParse({})).toEqual({ success: true, data: {} }); + const parsed = schema.safeParse({ invalid: true }); + expect(parsed.success).toBe(false); + }); + + it("loads legacy root exports lazily through the proxy", () => { + expect(typeof rootSdk.resolveControlCommandGate).toBe("function"); + expect(typeof rootSdk.default).toBe("object"); + expect(rootSdk.default).toBe(rootSdk); + expect(rootSdk.__esModule).toBe(true); + }); + + it("preserves reflection semantics for lazily resolved exports", () => { + expect("resolveControlCommandGate" in rootSdk).toBe(true); + const keys = Object.keys(rootSdk); + expect(keys).toContain("resolveControlCommandGate"); + const descriptor = Object.getOwnPropertyDescriptor(rootSdk, "resolveControlCommandGate"); + expect(descriptor).toBeDefined(); + }); +}); diff --git a/src/plugin-sdk/subpaths.test.ts b/src/plugin-sdk/subpaths.test.ts index 80a2d2ffaf1..9065712d235 100644 --- a/src/plugin-sdk/subpaths.test.ts +++ b/src/plugin-sdk/subpaths.test.ts @@ -1,3 +1,4 @@ +import * as compatSdk from "openclaw/plugin-sdk/compat"; import * as discordSdk from "openclaw/plugin-sdk/discord"; import * as imessageSdk from "openclaw/plugin-sdk/imessage"; import * as lineSdk from "openclaw/plugin-sdk/line"; @@ -7,6 +8,11 @@ import * as whatsappSdk from "openclaw/plugin-sdk/whatsapp"; import { describe, expect, it } from "vitest"; describe("plugin-sdk subpath exports", () => { + it("exports compat helpers", () => { + expect(typeof compatSdk.emptyPluginConfigSchema).toBe("function"); + expect(typeof compatSdk.resolveControlCommandGate).toBe("function"); + }); + it("exports Discord helpers", () => { expect(typeof discordSdk.resolveDiscordAccount).toBe("function"); expect(typeof discordSdk.discordOnboardingAdapter).toBe("object"); diff --git a/src/plugin-sdk/telegram.ts b/src/plugin-sdk/telegram.ts index aae6a429080..75dfc920c29 100644 --- a/src/plugin-sdk/telegram.ts +++ b/src/plugin-sdk/telegram.ts @@ -1,9 +1,17 @@ -export type { ChannelMessageActionAdapter } from "../channels/plugins/types.js"; +export type { + ChannelAccountSnapshot, + ChannelGatewayContext, + ChannelMessageActionAdapter, +} from "../channels/plugins/types.js"; export type { ChannelPlugin } from "../channels/plugins/types.plugin.js"; export type { OpenClawConfig } from "../config/config.js"; +export type { PluginRuntime } from "../plugins/runtime/types.js"; +export type { OpenClawPluginApi } from "../plugins/types.js"; export type { ResolvedTelegramAccount } from "../telegram/accounts.js"; export type { TelegramProbe } from "../telegram/probe.js"; +export { emptyPluginConfigSchema } from "../plugins/config-schema.js"; + export { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js"; export { diff --git a/src/plugins/discovery.test.ts b/src/plugins/discovery.test.ts index 5a760161f41..aa33803c2ab 100644 --- a/src/plugins/discovery.test.ts +++ b/src/plugins/discovery.test.ts @@ -4,7 +4,7 @@ import os from "node:os"; import path from "node:path"; import { afterEach, describe, expect, it } from "vitest"; import { withEnvAsync } from "../test-utils/env.js"; -import { discoverOpenClawPlugins } from "./discovery.js"; +import { clearPluginDiscoveryCache, discoverOpenClawPlugins } from "./discovery.js"; const tempDirs: string[] = []; @@ -57,6 +57,7 @@ function expectEscapesPackageDiagnostic(diagnostics: Array<{ message: string }>) } afterEach(() => { + clearPluginDiscoveryCache(); for (const dir of tempDirs.splice(0)) { try { fs.rmSync(dir, { recursive: true, force: true }); @@ -350,4 +351,40 @@ describe("discoverOpenClawPlugins", () => { ); }, ); + + it("reuses discovery results from cache until cleared", async () => { + const stateDir = makeTempDir(); + const globalExt = path.join(stateDir, "extensions"); + fs.mkdirSync(globalExt, { recursive: true }); + const pluginPath = path.join(globalExt, "cached.ts"); + fs.writeFileSync(pluginPath, "export default function () {}", "utf-8"); + + const first = await withEnvAsync( + { + OPENCLAW_PLUGIN_DISCOVERY_CACHE_MS: "5000", + }, + async () => withStateDir(stateDir, async () => discoverOpenClawPlugins({})), + ); + expect(first.candidates.some((candidate) => candidate.idHint === "cached")).toBe(true); + + fs.rmSync(pluginPath, { force: true }); + + const second = await withEnvAsync( + { + OPENCLAW_PLUGIN_DISCOVERY_CACHE_MS: "5000", + }, + async () => withStateDir(stateDir, async () => discoverOpenClawPlugins({})), + ); + expect(second.candidates.some((candidate) => candidate.idHint === "cached")).toBe(true); + + clearPluginDiscoveryCache(); + + const third = await withEnvAsync( + { + OPENCLAW_PLUGIN_DISCOVERY_CACHE_MS: "5000", + }, + async () => withStateDir(stateDir, async () => discoverOpenClawPlugins({})), + ); + expect(third.candidates.some((candidate) => candidate.idHint === "cached")).toBe(false); + }); }); diff --git a/src/plugins/discovery.ts b/src/plugins/discovery.ts index 5d4fb48c6bf..c03b0fe01bf 100644 --- a/src/plugins/discovery.ts +++ b/src/plugins/discovery.ts @@ -33,6 +33,56 @@ export type PluginDiscoveryResult = { diagnostics: PluginDiagnostic[]; }; +const discoveryCache = new Map(); + +// Keep a short cache window to collapse bursty reloads during startup flows. +const DEFAULT_DISCOVERY_CACHE_MS = 1000; + +export function clearPluginDiscoveryCache(): void { + discoveryCache.clear(); +} + +function resolveDiscoveryCacheMs(env: NodeJS.ProcessEnv): number { + const raw = env.OPENCLAW_PLUGIN_DISCOVERY_CACHE_MS?.trim(); + if (raw === "" || raw === "0") { + return 0; + } + if (!raw) { + return DEFAULT_DISCOVERY_CACHE_MS; + } + const parsed = Number.parseInt(raw, 10); + if (!Number.isFinite(parsed)) { + return DEFAULT_DISCOVERY_CACHE_MS; + } + return Math.max(0, parsed); +} + +function shouldUseDiscoveryCache(env: NodeJS.ProcessEnv): boolean { + const disabled = env.OPENCLAW_DISABLE_PLUGIN_DISCOVERY_CACHE?.trim(); + if (disabled) { + return false; + } + return resolveDiscoveryCacheMs(env) > 0; +} + +function buildDiscoveryCacheKey(params: { + workspaceDir?: string; + extraPaths?: string[]; + ownershipUid?: number | null; +}): string { + const workspaceKey = params.workspaceDir ? resolveUserPath(params.workspaceDir) : ""; + const configExtensionsRoot = path.join(resolveConfigDir(), "extensions"); + const bundledRoot = resolveBundledPluginsDir() ?? ""; + const normalizedExtraPaths = (params.extraPaths ?? []) + .filter((entry): entry is string => typeof entry === "string") + .map((entry) => entry.trim()) + .filter(Boolean) + .map((entry) => resolveUserPath(entry)) + .toSorted(); + const ownershipUid = params.ownershipUid ?? currentUid(); + return `${workspaceKey}::${ownershipUid ?? "none"}::${configExtensionsRoot}::${bundledRoot}::${JSON.stringify(normalizedExtraPaths)}`; +} + function currentUid(overrideUid?: number | null): number | null { if (overrideUid !== undefined) { return overrideUid; @@ -569,7 +619,23 @@ export function discoverOpenClawPlugins(params: { workspaceDir?: string; extraPaths?: string[]; ownershipUid?: number | null; + cache?: boolean; + env?: NodeJS.ProcessEnv; }): PluginDiscoveryResult { + const env = params.env ?? process.env; + const cacheEnabled = params.cache !== false && shouldUseDiscoveryCache(env); + const cacheKey = buildDiscoveryCacheKey({ + workspaceDir: params.workspaceDir, + extraPaths: params.extraPaths, + ownershipUid: params.ownershipUid, + }); + if (cacheEnabled) { + const cached = discoveryCache.get(cacheKey); + if (cached && cached.expiresAt > Date.now()) { + return cached.result; + } + } + const candidates: PluginCandidate[] = []; const diagnostics: PluginDiagnostic[] = []; const seen = new Set(); @@ -634,5 +700,12 @@ export function discoverOpenClawPlugins(params: { seen, }); - return { candidates, diagnostics }; + const result = { candidates, diagnostics }; + if (cacheEnabled) { + const ttl = resolveDiscoveryCacheMs(env); + if (ttl > 0) { + discoveryCache.set(cacheKey, { expiresAt: Date.now() + ttl, result }); + } + } + return result; } diff --git a/src/plugins/loader.test.ts b/src/plugins/loader.test.ts index 1f9a6ebd5a5..5e61d3e3270 100644 --- a/src/plugins/loader.test.ts +++ b/src/plugins/loader.test.ts @@ -211,14 +211,19 @@ function createEscapingEntryFixture(params: { id: string; sourceBody: string }) return { pluginDir, outsideEntry, linkedEntry }; } -function createPluginSdkAliasFixture() { +function createPluginSdkAliasFixture(params?: { + srcFile?: string; + distFile?: string; + srcBody?: string; + distBody?: string; +}) { const root = makeTempDir(); - const srcFile = path.join(root, "src", "plugin-sdk", "index.ts"); - const distFile = path.join(root, "dist", "plugin-sdk", "index.js"); + const srcFile = path.join(root, "src", "plugin-sdk", params?.srcFile ?? "index.ts"); + const distFile = path.join(root, "dist", "plugin-sdk", params?.distFile ?? "index.js"); fs.mkdirSync(path.dirname(srcFile), { recursive: true }); fs.mkdirSync(path.dirname(distFile), { recursive: true }); - fs.writeFileSync(srcFile, "export {};\n", "utf-8"); - fs.writeFileSync(distFile, "export {};\n", "utf-8"); + fs.writeFileSync(srcFile, params?.srcBody ?? "export {};\n", "utf-8"); + fs.writeFileSync(distFile, params?.distBody ?? "export {};\n", "utf-8"); return { root, srcFile, distFile }; } @@ -707,6 +712,73 @@ describe("loadOpenClawPlugins", () => { expect(a?.status).toBe("disabled"); }); + it("skips importing bundled memory plugins that are disabled by memory slot", () => { + const bundledDir = makeTempDir(); + const memoryADir = path.join(bundledDir, "memory-a"); + const memoryBDir = path.join(bundledDir, "memory-b"); + fs.mkdirSync(memoryADir, { recursive: true }); + fs.mkdirSync(memoryBDir, { recursive: true }); + writePlugin({ + id: "memory-a", + dir: memoryADir, + filename: "index.cjs", + body: `throw new Error("memory-a should not be imported when slot selects memory-b");`, + }); + writePlugin({ + id: "memory-b", + dir: memoryBDir, + filename: "index.cjs", + body: `module.exports = { id: "memory-b", kind: "memory", register() {} };`, + }); + fs.writeFileSync( + path.join(memoryADir, "openclaw.plugin.json"), + JSON.stringify( + { + id: "memory-a", + kind: "memory", + configSchema: EMPTY_PLUGIN_SCHEMA, + }, + null, + 2, + ), + "utf-8", + ); + fs.writeFileSync( + path.join(memoryBDir, "openclaw.plugin.json"), + JSON.stringify( + { + id: "memory-b", + kind: "memory", + configSchema: EMPTY_PLUGIN_SCHEMA, + }, + null, + 2, + ), + "utf-8", + ); + process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = bundledDir; + + const registry = loadOpenClawPlugins({ + cache: false, + config: { + plugins: { + allow: ["memory-a", "memory-b"], + slots: { memory: "memory-b" }, + entries: { + "memory-a": { enabled: true }, + "memory-b": { enabled: true }, + }, + }, + }, + }); + + const a = registry.plugins.find((entry) => entry.id === "memory-a"); + const b = registry.plugins.find((entry) => entry.id === "memory-b"); + expect(a?.status).toBe("disabled"); + expect(String(a?.error ?? "")).toContain('memory slot set to "memory-b"'); + expect(b?.status).toBe("loaded"); + }); + it("disables memory plugins when slot is none", () => { process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = "/nonexistent/bundled/plugins"; const memory = writePlugin({ @@ -1051,4 +1123,38 @@ describe("loadOpenClawPlugins", () => { ); expect(resolved).toBe(srcFile); }); + + it("prefers dist root-alias shim when loader runs from dist", () => { + const { root, distFile } = createPluginSdkAliasFixture({ + srcFile: "root-alias.cjs", + distFile: "root-alias.cjs", + srcBody: "module.exports = {};\n", + distBody: "module.exports = {};\n", + }); + + const resolved = __testing.resolvePluginSdkAliasFile({ + srcFile: "root-alias.cjs", + distFile: "root-alias.cjs", + modulePath: path.join(root, "dist", "plugins", "loader.js"), + }); + expect(resolved).toBe(distFile); + }); + + it("prefers src root-alias shim when loader runs from src in non-production", () => { + const { root, srcFile } = createPluginSdkAliasFixture({ + srcFile: "root-alias.cjs", + distFile: "root-alias.cjs", + srcBody: "module.exports = {};\n", + distBody: "module.exports = {};\n", + }); + + const resolved = withEnv({ NODE_ENV: undefined }, () => + __testing.resolvePluginSdkAliasFile({ + srcFile: "root-alias.cjs", + distFile: "root-alias.cjs", + modulePath: path.join(root, "src", "plugins", "loader.ts"), + }), + ); + expect(resolved).toBe(srcFile); + }); }); diff --git a/src/plugins/loader.ts b/src/plugins/loader.ts index 8df588d6b87..953592d1b59 100644 --- a/src/plugins/loader.ts +++ b/src/plugins/loader.ts @@ -86,7 +86,7 @@ const resolvePluginSdkAliasFile = (params: { }; const resolvePluginSdkAlias = (): string | null => - resolvePluginSdkAliasFile({ srcFile: "index.ts", distFile: "index.js" }); + resolvePluginSdkAliasFile({ srcFile: "root-alias.cjs", distFile: "root-alias.cjs" }); const resolvePluginSdkAccountIdAlias = (): string | null => { return resolvePluginSdkAliasFile({ srcFile: "account-id.ts", distFile: "account-id.js" }); @@ -96,6 +96,10 @@ const resolvePluginSdkCoreAlias = (): string | null => { return resolvePluginSdkAliasFile({ srcFile: "core.ts", distFile: "core.js" }); }; +const resolvePluginSdkCompatAlias = (): string | null => { + return resolvePluginSdkAliasFile({ srcFile: "compat.ts", distFile: "compat.js" }); +}; + const resolvePluginSdkTelegramAlias = (): string | null => { return resolvePluginSdkAliasFile({ srcFile: "telegram.ts", distFile: "telegram.js" }); }; @@ -468,6 +472,7 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi const discovery = discoverOpenClawPlugins({ workspaceDir: options.workspaceDir, extraPaths: normalized.loadPaths, + cache: options.cache, }); const manifestRegistry = loadPluginManifestRegistry({ config: cfg, @@ -501,6 +506,7 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi const pluginSdkAlias = resolvePluginSdkAlias(); const pluginSdkAccountIdAlias = resolvePluginSdkAccountIdAlias(); const pluginSdkCoreAlias = resolvePluginSdkCoreAlias(); + const pluginSdkCompatAlias = resolvePluginSdkCompatAlias(); const pluginSdkTelegramAlias = resolvePluginSdkTelegramAlias(); const pluginSdkDiscordAlias = resolvePluginSdkDiscordAlias(); const pluginSdkSlackAlias = resolvePluginSdkSlackAlias(); @@ -511,6 +517,7 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi const aliasMap = { ...(pluginSdkAlias ? { "openclaw/plugin-sdk": pluginSdkAlias } : {}), ...(pluginSdkCoreAlias ? { "openclaw/plugin-sdk/core": pluginSdkCoreAlias } : {}), + ...(pluginSdkCompatAlias ? { "openclaw/plugin-sdk/compat": pluginSdkCompatAlias } : {}), ...(pluginSdkTelegramAlias ? { "openclaw/plugin-sdk/telegram": pluginSdkTelegramAlias } : {}), ...(pluginSdkDiscordAlias ? { "openclaw/plugin-sdk/discord": pluginSdkDiscordAlias } : {}), ...(pluginSdkSlackAlias ? { "openclaw/plugin-sdk/slack": pluginSdkSlackAlias } : {}), @@ -610,6 +617,25 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi continue; } + // Fast-path bundled memory plugins that are guaranteed disabled by slot policy. + // This avoids opening/importing heavy memory plugin modules that will never register. + if (candidate.origin === "bundled" && manifestRecord.kind === "memory") { + const earlyMemoryDecision = resolveMemorySlotDecision({ + id: record.id, + kind: "memory", + slot: memorySlot, + selectedId: selectedMemoryPluginId, + }); + if (!earlyMemoryDecision.enabled) { + record.enabled = false; + record.status = "disabled"; + record.error = earlyMemoryDecision.reason; + registry.plugins.push(record); + seenIds.set(pluginId, candidate.origin); + continue; + } + } + if (!manifestRecord.configSchema) { pushPluginLoadError("missing config schema"); continue; diff --git a/src/plugins/manifest-registry.ts b/src/plugins/manifest-registry.ts index 6176f9ee18f..d392144f925 100644 --- a/src/plugins/manifest-registry.ts +++ b/src/plugins/manifest-registry.ts @@ -46,7 +46,8 @@ export type PluginManifestRegistry = { const registryCache = new Map(); -const DEFAULT_MANIFEST_CACHE_MS = 200; +// Keep a short cache window to collapse bursty reloads during startup flows. +const DEFAULT_MANIFEST_CACHE_MS = 1000; export function clearPluginManifestRegistryCache(): void { registryCache.clear(); diff --git a/tsconfig.plugin-sdk.dts.json b/tsconfig.plugin-sdk.dts.json index 3e5be344b80..c3efae99617 100644 --- a/tsconfig.plugin-sdk.dts.json +++ b/tsconfig.plugin-sdk.dts.json @@ -13,6 +13,7 @@ "include": [ "src/plugin-sdk/index.ts", "src/plugin-sdk/core.ts", + "src/plugin-sdk/compat.ts", "src/plugin-sdk/telegram.ts", "src/plugin-sdk/discord.ts", "src/plugin-sdk/slack.ts", diff --git a/tsdown.config.ts b/tsdown.config.ts index ef5fd70dbb9..a69be542d08 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -62,6 +62,13 @@ export default defineConfig([ fixedExtension: false, platform: "node", }, + { + entry: "src/plugin-sdk/compat.ts", + outDir: "dist/plugin-sdk", + env, + fixedExtension: false, + platform: "node", + }, { entry: "src/plugin-sdk/telegram.ts", outDir: "dist/plugin-sdk", diff --git a/vitest.config.ts b/vitest.config.ts index 026b1a618f2..2094476eff1 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -21,6 +21,10 @@ export default defineConfig({ find: "openclaw/plugin-sdk/core", replacement: path.join(repoRoot, "src", "plugin-sdk", "core.ts"), }, + { + find: "openclaw/plugin-sdk/compat", + replacement: path.join(repoRoot, "src", "plugin-sdk", "compat.ts"), + }, { find: "openclaw/plugin-sdk/telegram", replacement: path.join(repoRoot, "src", "plugin-sdk", "telegram.ts"), From ff38bc7649ec7cfb1eb7a4cc8d7a49aa8b1cdfc6 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 4 Mar 2026 01:20:02 -0500 Subject: [PATCH 04/74] Extensions: migrate bluebubbles plugin-sdk imports --- extensions/bluebubbles/src/account-resolve.ts | 2 +- extensions/bluebubbles/src/accounts.ts | 2 +- extensions/bluebubbles/src/actions.test.ts | 2 +- extensions/bluebubbles/src/actions.ts | 2 +- extensions/bluebubbles/src/attachments.test.ts | 2 +- extensions/bluebubbles/src/attachments.ts | 2 +- extensions/bluebubbles/src/channel.ts | 8 ++++++-- extensions/bluebubbles/src/chat.ts | 2 +- extensions/bluebubbles/src/config-schema.ts | 2 +- extensions/bluebubbles/src/history.ts | 2 +- extensions/bluebubbles/src/media-send.test.ts | 2 +- extensions/bluebubbles/src/media-send.ts | 2 +- extensions/bluebubbles/src/monitor-debounce.ts | 2 +- extensions/bluebubbles/src/monitor-processing.ts | 4 ++-- extensions/bluebubbles/src/monitor-shared.ts | 2 +- extensions/bluebubbles/src/monitor.test.ts | 2 +- extensions/bluebubbles/src/monitor.ts | 2 +- extensions/bluebubbles/src/monitor.webhook-auth.test.ts | 2 +- extensions/bluebubbles/src/monitor.webhook-route.test.ts | 2 +- .../bluebubbles/src/onboarding.secret-input.test.ts | 2 +- extensions/bluebubbles/src/onboarding.ts | 4 ++-- extensions/bluebubbles/src/probe.ts | 2 +- extensions/bluebubbles/src/reactions.ts | 2 +- extensions/bluebubbles/src/runtime.ts | 2 +- extensions/bluebubbles/src/secret-input.ts | 2 +- extensions/bluebubbles/src/send.test.ts | 2 +- extensions/bluebubbles/src/send.ts | 4 ++-- extensions/bluebubbles/src/targets.ts | 2 +- extensions/bluebubbles/src/types.ts | 4 ++-- 29 files changed, 38 insertions(+), 34 deletions(-) diff --git a/extensions/bluebubbles/src/account-resolve.ts b/extensions/bluebubbles/src/account-resolve.ts index ebdf7a7bc46..b69c613e548 100644 --- a/extensions/bluebubbles/src/account-resolve.ts +++ b/extensions/bluebubbles/src/account-resolve.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig } from "openclaw/plugin-sdk"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; import { resolveBlueBubblesAccount } from "./accounts.js"; import { normalizeResolvedSecretInputString } from "./secret-input.js"; diff --git a/extensions/bluebubbles/src/accounts.ts b/extensions/bluebubbles/src/accounts.ts index 142e2d8fef9..bbde1cf4656 100644 --- a/extensions/bluebubbles/src/accounts.ts +++ b/extensions/bluebubbles/src/accounts.ts @@ -1,9 +1,9 @@ -import type { OpenClawConfig } from "openclaw/plugin-sdk"; import { DEFAULT_ACCOUNT_ID, normalizeAccountId, normalizeOptionalAccountId, } from "openclaw/plugin-sdk/account-id"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; import { hasConfiguredSecretInput, normalizeSecretInputString } from "./secret-input.js"; import { normalizeBlueBubblesServerUrl, type BlueBubblesAccountConfig } from "./types.js"; diff --git a/extensions/bluebubbles/src/actions.test.ts b/extensions/bluebubbles/src/actions.test.ts index 5db42331207..e958035bd4b 100644 --- a/extensions/bluebubbles/src/actions.test.ts +++ b/extensions/bluebubbles/src/actions.test.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig } from "openclaw/plugin-sdk"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; import { describe, expect, it, vi, beforeEach } from "vitest"; import { bluebubblesMessageActions } from "./actions.js"; import { getCachedBlueBubblesPrivateApiStatus } from "./probe.js"; diff --git a/extensions/bluebubbles/src/actions.ts b/extensions/bluebubbles/src/actions.ts index e85400748a9..daf9dd0c872 100644 --- a/extensions/bluebubbles/src/actions.ts +++ b/extensions/bluebubbles/src/actions.ts @@ -10,7 +10,7 @@ import { readStringParam, type ChannelMessageActionAdapter, type ChannelMessageActionName, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { resolveBlueBubblesAccount } from "./accounts.js"; import { sendBlueBubblesAttachment } from "./attachments.js"; import { diff --git a/extensions/bluebubbles/src/attachments.test.ts b/extensions/bluebubbles/src/attachments.test.ts index da431c7325f..169f79f6b43 100644 --- a/extensions/bluebubbles/src/attachments.test.ts +++ b/extensions/bluebubbles/src/attachments.test.ts @@ -1,4 +1,4 @@ -import type { PluginRuntime } from "openclaw/plugin-sdk"; +import type { PluginRuntime } from "openclaw/plugin-sdk/compat"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import "./test-mocks.js"; import { downloadBlueBubblesAttachment, sendBlueBubblesAttachment } from "./attachments.js"; diff --git a/extensions/bluebubbles/src/attachments.ts b/extensions/bluebubbles/src/attachments.ts index ca7ce69a89c..582c5f92abd 100644 --- a/extensions/bluebubbles/src/attachments.ts +++ b/extensions/bluebubbles/src/attachments.ts @@ -1,6 +1,6 @@ import crypto from "node:crypto"; import path from "node:path"; -import type { OpenClawConfig } from "openclaw/plugin-sdk"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; import { resolveBlueBubblesServerAccount } from "./account-resolve.js"; import { postMultipartFormData } from "./multipart.js"; import { diff --git a/extensions/bluebubbles/src/channel.ts b/extensions/bluebubbles/src/channel.ts index fbaa5ce39fc..7fc3b97de19 100644 --- a/extensions/bluebubbles/src/channel.ts +++ b/extensions/bluebubbles/src/channel.ts @@ -1,4 +1,8 @@ -import type { ChannelAccountSnapshot, ChannelPlugin, OpenClawConfig } from "openclaw/plugin-sdk"; +import type { + ChannelAccountSnapshot, + ChannelPlugin, + OpenClawConfig, +} from "openclaw/plugin-sdk/compat"; import { applyAccountNameToChannelSection, buildChannelConfigSchema, @@ -13,7 +17,7 @@ import { resolveBlueBubblesGroupRequireMention, resolveBlueBubblesGroupToolPolicy, setAccountEnabledInConfigSection, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { listBlueBubblesAccountIds, type ResolvedBlueBubblesAccount, diff --git a/extensions/bluebubbles/src/chat.ts b/extensions/bluebubbles/src/chat.ts index f5f83b1b6ae..17a782c9312 100644 --- a/extensions/bluebubbles/src/chat.ts +++ b/extensions/bluebubbles/src/chat.ts @@ -1,6 +1,6 @@ import crypto from "node:crypto"; import path from "node:path"; -import type { OpenClawConfig } from "openclaw/plugin-sdk"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; import { resolveBlueBubblesServerAccount } from "./account-resolve.js"; import { postMultipartFormData } from "./multipart.js"; import { getCachedBlueBubblesPrivateApiStatus } from "./probe.js"; diff --git a/extensions/bluebubbles/src/config-schema.ts b/extensions/bluebubbles/src/config-schema.ts index f4b6991441c..54f9b175ce5 100644 --- a/extensions/bluebubbles/src/config-schema.ts +++ b/extensions/bluebubbles/src/config-schema.ts @@ -1,4 +1,4 @@ -import { MarkdownConfigSchema, ToolPolicySchema } from "openclaw/plugin-sdk"; +import { MarkdownConfigSchema, ToolPolicySchema } from "openclaw/plugin-sdk/compat"; import { z } from "zod"; import { buildSecretInputSchema, hasConfiguredSecretInput } from "./secret-input.js"; diff --git a/extensions/bluebubbles/src/history.ts b/extensions/bluebubbles/src/history.ts index 672e2c48c80..82c6de6b635 100644 --- a/extensions/bluebubbles/src/history.ts +++ b/extensions/bluebubbles/src/history.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig } from "openclaw/plugin-sdk"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; import { resolveBlueBubblesServerAccount } from "./account-resolve.js"; import { blueBubblesFetchWithTimeout, buildBlueBubblesApiUrl } from "./types.js"; diff --git a/extensions/bluebubbles/src/media-send.test.ts b/extensions/bluebubbles/src/media-send.test.ts index 901c90f2d4f..2129fd2c252 100644 --- a/extensions/bluebubbles/src/media-send.test.ts +++ b/extensions/bluebubbles/src/media-send.test.ts @@ -2,7 +2,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { pathToFileURL } from "node:url"; -import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk"; +import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk/compat"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { sendBlueBubblesMedia } from "./media-send.js"; import { setBlueBubblesRuntime } from "./runtime.js"; diff --git a/extensions/bluebubbles/src/media-send.ts b/extensions/bluebubbles/src/media-send.ts index 797b2b92fae..4b52aa6aa72 100644 --- a/extensions/bluebubbles/src/media-send.ts +++ b/extensions/bluebubbles/src/media-send.ts @@ -3,7 +3,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; -import { resolveChannelMediaMaxBytes, type OpenClawConfig } from "openclaw/plugin-sdk"; +import { resolveChannelMediaMaxBytes, type OpenClawConfig } from "openclaw/plugin-sdk/compat"; import { resolveBlueBubblesAccount } from "./accounts.js"; import { sendBlueBubblesAttachment } from "./attachments.js"; import { resolveBlueBubblesMessageId } from "./monitor.js"; diff --git a/extensions/bluebubbles/src/monitor-debounce.ts b/extensions/bluebubbles/src/monitor-debounce.ts index 952c591e847..2c4daa55028 100644 --- a/extensions/bluebubbles/src/monitor-debounce.ts +++ b/extensions/bluebubbles/src/monitor-debounce.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig } from "openclaw/plugin-sdk"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; import type { NormalizedWebhookMessage } from "./monitor-normalize.js"; import type { BlueBubblesCoreRuntime, WebhookTarget } from "./monitor-shared.js"; diff --git a/extensions/bluebubbles/src/monitor-processing.ts b/extensions/bluebubbles/src/monitor-processing.ts index de26a7d0c54..62a8564ae0c 100644 --- a/extensions/bluebubbles/src/monitor-processing.ts +++ b/extensions/bluebubbles/src/monitor-processing.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig } from "openclaw/plugin-sdk"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; import { DM_GROUP_ACCESS_REASON, createScopedPairingAccess, @@ -14,7 +14,7 @@ import { resolveControlCommandGate, stripMarkdown, type HistoryEntry, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { downloadBlueBubblesAttachment } from "./attachments.js"; import { markBlueBubblesChatRead, sendBlueBubblesTyping } from "./chat.js"; import { fetchBlueBubblesHistory } from "./history.js"; diff --git a/extensions/bluebubbles/src/monitor-shared.ts b/extensions/bluebubbles/src/monitor-shared.ts index c768385e03a..00477a020c5 100644 --- a/extensions/bluebubbles/src/monitor-shared.ts +++ b/extensions/bluebubbles/src/monitor-shared.ts @@ -1,4 +1,4 @@ -import { normalizeWebhookPath, type OpenClawConfig } from "openclaw/plugin-sdk"; +import { normalizeWebhookPath, type OpenClawConfig } from "openclaw/plugin-sdk/compat"; import type { ResolvedBlueBubblesAccount } from "./accounts.js"; import { getBlueBubblesRuntime } from "./runtime.js"; import type { BlueBubblesAccountConfig } from "./types.js"; diff --git a/extensions/bluebubbles/src/monitor.test.ts b/extensions/bluebubbles/src/monitor.test.ts index c914050616d..9dc41fa26b9 100644 --- a/extensions/bluebubbles/src/monitor.test.ts +++ b/extensions/bluebubbles/src/monitor.test.ts @@ -1,6 +1,6 @@ import { EventEmitter } from "node:events"; import type { IncomingMessage, ServerResponse } from "node:http"; -import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk"; +import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk/compat"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { createPluginRuntimeMock } from "../../test-utils/plugin-runtime-mock.js"; import type { ResolvedBlueBubblesAccount } from "./accounts.js"; diff --git a/extensions/bluebubbles/src/monitor.ts b/extensions/bluebubbles/src/monitor.ts index a0e06bce6d8..97c47dc0118 100644 --- a/extensions/bluebubbles/src/monitor.ts +++ b/extensions/bluebubbles/src/monitor.ts @@ -7,7 +7,7 @@ import { readWebhookBodyOrReject, resolveWebhookTargetWithAuthOrRejectSync, resolveWebhookTargets, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { createBlueBubblesDebounceRegistry } from "./monitor-debounce.js"; import { normalizeWebhookMessage, normalizeWebhookReaction } from "./monitor-normalize.js"; import { logVerbose, processMessage, processReaction } from "./monitor-processing.js"; diff --git a/extensions/bluebubbles/src/monitor.webhook-auth.test.ts b/extensions/bluebubbles/src/monitor.webhook-auth.test.ts index 72e765fcd57..271b9521987 100644 --- a/extensions/bluebubbles/src/monitor.webhook-auth.test.ts +++ b/extensions/bluebubbles/src/monitor.webhook-auth.test.ts @@ -1,6 +1,6 @@ import { EventEmitter } from "node:events"; import type { IncomingMessage, ServerResponse } from "node:http"; -import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk"; +import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk/compat"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { createPluginRuntimeMock } from "../../test-utils/plugin-runtime-mock.js"; import type { ResolvedBlueBubblesAccount } from "./accounts.js"; diff --git a/extensions/bluebubbles/src/monitor.webhook-route.test.ts b/extensions/bluebubbles/src/monitor.webhook-route.test.ts index 8499ea56b3d..322e8a76377 100644 --- a/extensions/bluebubbles/src/monitor.webhook-route.test.ts +++ b/extensions/bluebubbles/src/monitor.webhook-route.test.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig } from "openclaw/plugin-sdk"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; import { afterEach, describe, expect, it } from "vitest"; import { createEmptyPluginRegistry } from "../../../src/plugins/registry.js"; import { setActivePluginRegistry } from "../../../src/plugins/runtime.js"; diff --git a/extensions/bluebubbles/src/onboarding.secret-input.test.ts b/extensions/bluebubbles/src/onboarding.secret-input.test.ts index 7452ae3c2d4..a7ca9e1d3eb 100644 --- a/extensions/bluebubbles/src/onboarding.secret-input.test.ts +++ b/extensions/bluebubbles/src/onboarding.secret-input.test.ts @@ -1,4 +1,4 @@ -import type { WizardPrompter } from "openclaw/plugin-sdk"; +import type { WizardPrompter } from "openclaw/plugin-sdk/compat"; import { describe, expect, it, vi } from "vitest"; vi.mock("openclaw/plugin-sdk", () => ({ diff --git a/extensions/bluebubbles/src/onboarding.ts b/extensions/bluebubbles/src/onboarding.ts index 5eb0d6e4066..1e716c73b75 100644 --- a/extensions/bluebubbles/src/onboarding.ts +++ b/extensions/bluebubbles/src/onboarding.ts @@ -4,7 +4,7 @@ import type { OpenClawConfig, DmPolicy, WizardPrompter, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { DEFAULT_ACCOUNT_ID, addWildcardAllowFrom, @@ -12,7 +12,7 @@ import { mergeAllowFromEntries, normalizeAccountId, promptAccountId, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { listBlueBubblesAccountIds, resolveBlueBubblesAccount, diff --git a/extensions/bluebubbles/src/probe.ts b/extensions/bluebubbles/src/probe.ts index eeeba033ee2..b0af1252d06 100644 --- a/extensions/bluebubbles/src/probe.ts +++ b/extensions/bluebubbles/src/probe.ts @@ -1,4 +1,4 @@ -import type { BaseProbeResult } from "openclaw/plugin-sdk"; +import type { BaseProbeResult } from "openclaw/plugin-sdk/compat"; import { normalizeSecretInputString } from "./secret-input.js"; import { buildBlueBubblesApiUrl, blueBubblesFetchWithTimeout } from "./types.js"; diff --git a/extensions/bluebubbles/src/reactions.ts b/extensions/bluebubbles/src/reactions.ts index 69d5b2055cc..411fcc759ec 100644 --- a/extensions/bluebubbles/src/reactions.ts +++ b/extensions/bluebubbles/src/reactions.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig } from "openclaw/plugin-sdk"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; import { resolveBlueBubblesServerAccount } from "./account-resolve.js"; import { getCachedBlueBubblesPrivateApiStatus } from "./probe.js"; import { blueBubblesFetchWithTimeout, buildBlueBubblesApiUrl } from "./types.js"; diff --git a/extensions/bluebubbles/src/runtime.ts b/extensions/bluebubbles/src/runtime.ts index c9468234d3e..6f82c5916e3 100644 --- a/extensions/bluebubbles/src/runtime.ts +++ b/extensions/bluebubbles/src/runtime.ts @@ -1,4 +1,4 @@ -import type { PluginRuntime } from "openclaw/plugin-sdk"; +import type { PluginRuntime } from "openclaw/plugin-sdk/compat"; let runtime: PluginRuntime | null = null; type LegacyRuntimeLogShape = { log?: (message: string) => void }; diff --git a/extensions/bluebubbles/src/secret-input.ts b/extensions/bluebubbles/src/secret-input.ts index f90d41c6fb9..3fc82b3ac91 100644 --- a/extensions/bluebubbles/src/secret-input.ts +++ b/extensions/bluebubbles/src/secret-input.ts @@ -2,7 +2,7 @@ import { hasConfiguredSecretInput, normalizeResolvedSecretInputString, normalizeSecretInputString, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { z } from "zod"; export { hasConfiguredSecretInput, normalizeResolvedSecretInputString, normalizeSecretInputString }; diff --git a/extensions/bluebubbles/src/send.test.ts b/extensions/bluebubbles/src/send.test.ts index 3de22b4d714..0895da0e4bb 100644 --- a/extensions/bluebubbles/src/send.test.ts +++ b/extensions/bluebubbles/src/send.test.ts @@ -1,4 +1,4 @@ -import type { PluginRuntime } from "openclaw/plugin-sdk"; +import type { PluginRuntime } from "openclaw/plugin-sdk/compat"; import { beforeEach, describe, expect, it, vi } from "vitest"; import "./test-mocks.js"; import { getCachedBlueBubblesPrivateApiStatus } from "./probe.js"; diff --git a/extensions/bluebubbles/src/send.ts b/extensions/bluebubbles/src/send.ts index ccd932f3e47..307e5e4d839 100644 --- a/extensions/bluebubbles/src/send.ts +++ b/extensions/bluebubbles/src/send.ts @@ -1,6 +1,6 @@ import crypto from "node:crypto"; -import type { OpenClawConfig } from "openclaw/plugin-sdk"; -import { stripMarkdown } from "openclaw/plugin-sdk"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; +import { stripMarkdown } from "openclaw/plugin-sdk/compat"; import { resolveBlueBubblesAccount } from "./accounts.js"; import { getCachedBlueBubblesPrivateApiStatus, diff --git a/extensions/bluebubbles/src/targets.ts b/extensions/bluebubbles/src/targets.ts index 11d8faf1f76..da62f1f8445 100644 --- a/extensions/bluebubbles/src/targets.ts +++ b/extensions/bluebubbles/src/targets.ts @@ -5,7 +5,7 @@ import { type ParsedChatTarget, resolveServicePrefixedAllowTarget, resolveServicePrefixedTarget, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; export type BlueBubblesService = "imessage" | "sms" | "auto"; diff --git a/extensions/bluebubbles/src/types.ts b/extensions/bluebubbles/src/types.ts index d3dc46bd692..a310f02f86a 100644 --- a/extensions/bluebubbles/src/types.ts +++ b/extensions/bluebubbles/src/types.ts @@ -1,6 +1,6 @@ -import type { DmPolicy, GroupPolicy } from "openclaw/plugin-sdk"; +import type { DmPolicy, GroupPolicy } from "openclaw/plugin-sdk/compat"; -export type { DmPolicy, GroupPolicy } from "openclaw/plugin-sdk"; +export type { DmPolicy, GroupPolicy } from "openclaw/plugin-sdk/compat"; export type BlueBubblesGroupConfig = { /** If true, only respond in this group when mentioned. */ From 2bb63868c6278d1f94bb92267cc10b7b19343809 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 4 Mar 2026 01:20:03 -0500 Subject: [PATCH 05/74] Extensions: migrate device-pair plugin-sdk imports --- extensions/device-pair/notify.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/device-pair/notify.ts b/extensions/device-pair/notify.ts index da43d2dc273..dbdf483ed73 100644 --- a/extensions/device-pair/notify.ts +++ b/extensions/device-pair/notify.ts @@ -1,7 +1,7 @@ import { promises as fs } from "node:fs"; import path from "node:path"; -import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core"; -import { listDevicePairing } from "openclaw/plugin-sdk/core"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/compat"; +import { listDevicePairing } from "openclaw/plugin-sdk/compat"; const NOTIFY_STATE_FILE = "device-pair-notify.json"; const NOTIFY_POLL_INTERVAL_MS = 10_000; From 56d98a50cfff2aee9b8e36bde12b9537783b7692 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 4 Mar 2026 01:20:03 -0500 Subject: [PATCH 06/74] Extensions: migrate diagnostics-otel plugin-sdk imports --- extensions/diagnostics-otel/src/service.test.ts | 10 ++++++---- extensions/diagnostics-otel/src/service.ts | 8 ++++++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/extensions/diagnostics-otel/src/service.test.ts b/extensions/diagnostics-otel/src/service.test.ts index ab3fb57e15a..031922f2c5d 100644 --- a/extensions/diagnostics-otel/src/service.test.ts +++ b/extensions/diagnostics-otel/src/service.test.ts @@ -98,16 +98,18 @@ vi.mock("@opentelemetry/semantic-conventions", () => ({ ATTR_SERVICE_NAME: "service.name", })); -vi.mock("openclaw/plugin-sdk", async () => { - const actual = await vi.importActual("openclaw/plugin-sdk"); +vi.mock("openclaw/plugin-sdk/compat", async () => { + const actual = await vi.importActual( + "openclaw/plugin-sdk/compat", + ); return { ...actual, registerLogTransport: registerLogTransportMock, }; }); -import type { OpenClawPluginServiceContext } from "openclaw/plugin-sdk"; -import { emitDiagnosticEvent } from "openclaw/plugin-sdk"; +import type { OpenClawPluginServiceContext } from "openclaw/plugin-sdk/compat"; +import { emitDiagnosticEvent } from "openclaw/plugin-sdk/compat"; import { createDiagnosticsOtelService } from "./service.js"; const OTEL_TEST_STATE_DIR = "/tmp/openclaw-diagnostics-otel-test"; diff --git a/extensions/diagnostics-otel/src/service.ts b/extensions/diagnostics-otel/src/service.ts index be9a547963f..a63620a3e9a 100644 --- a/extensions/diagnostics-otel/src/service.ts +++ b/extensions/diagnostics-otel/src/service.ts @@ -9,8 +9,12 @@ import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics"; import { NodeSDK } from "@opentelemetry/sdk-node"; import { ParentBasedSampler, TraceIdRatioBasedSampler } from "@opentelemetry/sdk-trace-base"; import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions"; -import type { DiagnosticEventPayload, OpenClawPluginService } from "openclaw/plugin-sdk"; -import { onDiagnosticEvent, redactSensitiveText, registerLogTransport } from "openclaw/plugin-sdk"; +import type { DiagnosticEventPayload, OpenClawPluginService } from "openclaw/plugin-sdk/compat"; +import { + onDiagnosticEvent, + redactSensitiveText, + registerLogTransport, +} from "openclaw/plugin-sdk/compat"; const DEFAULT_SERVICE_NAME = "openclaw"; From 73de1d038e6d7ce762294c03448b5b4c80e816b1 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 4 Mar 2026 01:20:04 -0500 Subject: [PATCH 07/74] Extensions: migrate diffs plugin-sdk imports --- extensions/diffs/src/browser.test.ts | 2 +- extensions/diffs/src/browser.ts | 2 +- extensions/diffs/src/config.ts | 2 +- extensions/diffs/src/http.ts | 2 +- extensions/diffs/src/store.ts | 2 +- extensions/diffs/src/tool.test.ts | 2 +- extensions/diffs/src/tool.ts | 2 +- extensions/diffs/src/url.ts | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/extensions/diffs/src/browser.test.ts b/extensions/diffs/src/browser.test.ts index 1498561cfa3..11f4befe122 100644 --- a/extensions/diffs/src/browser.test.ts +++ b/extensions/diffs/src/browser.test.ts @@ -1,7 +1,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import type { OpenClawConfig } from "openclaw/plugin-sdk"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; const { launchMock } = vi.hoisted(() => ({ diff --git a/extensions/diffs/src/browser.ts b/extensions/diffs/src/browser.ts index d0afa23bb8b..caa8319a237 100644 --- a/extensions/diffs/src/browser.ts +++ b/extensions/diffs/src/browser.ts @@ -1,7 +1,7 @@ import { constants as fsConstants } from "node:fs"; import fs from "node:fs/promises"; import path from "node:path"; -import type { OpenClawConfig } from "openclaw/plugin-sdk"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; import { chromium } from "playwright-core"; import type { DiffRenderOptions, DiffTheme } from "./types.js"; import { VIEWER_ASSET_PREFIX, getServedViewerAsset } from "./viewer-assets.js"; diff --git a/extensions/diffs/src/config.ts b/extensions/diffs/src/config.ts index 153cf27bb10..066c9ed0948 100644 --- a/extensions/diffs/src/config.ts +++ b/extensions/diffs/src/config.ts @@ -1,4 +1,4 @@ -import type { OpenClawPluginConfigSchema } from "openclaw/plugin-sdk"; +import type { OpenClawPluginConfigSchema } from "openclaw/plugin-sdk/compat"; import { DIFF_IMAGE_QUALITY_PRESETS, DIFF_INDICATORS, diff --git a/extensions/diffs/src/http.ts b/extensions/diffs/src/http.ts index f2cb4433ed2..f53b6e0c386 100644 --- a/extensions/diffs/src/http.ts +++ b/extensions/diffs/src/http.ts @@ -1,5 +1,5 @@ import type { IncomingMessage, ServerResponse } from "node:http"; -import type { PluginLogger } from "openclaw/plugin-sdk"; +import type { PluginLogger } from "openclaw/plugin-sdk/compat"; import type { DiffArtifactStore } from "./store.js"; import { DIFF_ARTIFACT_ID_PATTERN, DIFF_ARTIFACT_TOKEN_PATTERN } from "./types.js"; import { VIEWER_ASSET_PREFIX, getServedViewerAsset } from "./viewer-assets.js"; diff --git a/extensions/diffs/src/store.ts b/extensions/diffs/src/store.ts index 26a0784ca7a..36d3d9f45a0 100644 --- a/extensions/diffs/src/store.ts +++ b/extensions/diffs/src/store.ts @@ -1,7 +1,7 @@ import crypto from "node:crypto"; import fs from "node:fs/promises"; import path from "node:path"; -import type { PluginLogger } from "openclaw/plugin-sdk"; +import type { PluginLogger } from "openclaw/plugin-sdk/compat"; import type { DiffArtifactMeta, DiffOutputFormat } from "./types.js"; const DEFAULT_TTL_MS = 30 * 60 * 1000; diff --git a/extensions/diffs/src/tool.test.ts b/extensions/diffs/src/tool.test.ts index f623599f1dd..a7a3b29261f 100644 --- a/extensions/diffs/src/tool.test.ts +++ b/extensions/diffs/src/tool.test.ts @@ -1,7 +1,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import type { OpenClawPluginApi } from "openclaw/plugin-sdk"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/compat"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { DiffScreenshotter } from "./browser.js"; import { DEFAULT_DIFFS_TOOL_DEFAULTS } from "./config.js"; diff --git a/extensions/diffs/src/tool.ts b/extensions/diffs/src/tool.ts index 1578c6e1e36..2b4d5885033 100644 --- a/extensions/diffs/src/tool.ts +++ b/extensions/diffs/src/tool.ts @@ -1,6 +1,6 @@ import fs from "node:fs/promises"; import { Static, Type } from "@sinclair/typebox"; -import type { AnyAgentTool, OpenClawPluginApi } from "openclaw/plugin-sdk"; +import type { AnyAgentTool, OpenClawPluginApi } from "openclaw/plugin-sdk/compat"; import { PlaywrightDiffScreenshotter, type DiffScreenshotter } from "./browser.js"; import { resolveDiffImageRenderOptions } from "./config.js"; import { renderDiffDocument } from "./render.js"; diff --git a/extensions/diffs/src/url.ts b/extensions/diffs/src/url.ts index 43dca97ff72..516bcf2e1aa 100644 --- a/extensions/diffs/src/url.ts +++ b/extensions/diffs/src/url.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig } from "openclaw/plugin-sdk"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; const DEFAULT_GATEWAY_PORT = 18789; From 1ebd1fdb2d4006f9ac8384a81e440e29b45bbfca Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 4 Mar 2026 01:20:05 -0500 Subject: [PATCH 08/74] Extensions: migrate feishu plugin-sdk imports --- extensions/feishu/src/accounts.ts | 2 +- extensions/feishu/src/bitable.ts | 2 +- extensions/feishu/src/bot.test.ts | 2 +- extensions/feishu/src/bot.ts | 4 ++-- extensions/feishu/src/card-action.ts | 2 +- extensions/feishu/src/channel.test.ts | 2 +- extensions/feishu/src/channel.ts | 4 ++-- extensions/feishu/src/chat.ts | 2 +- extensions/feishu/src/dedup.ts | 2 +- extensions/feishu/src/directory.ts | 2 +- extensions/feishu/src/docx.account-selection.test.ts | 2 +- extensions/feishu/src/docx.ts | 2 +- extensions/feishu/src/drive.ts | 2 +- extensions/feishu/src/dynamic-agent.ts | 2 +- extensions/feishu/src/media.ts | 2 +- extensions/feishu/src/monitor.account.ts | 2 +- extensions/feishu/src/monitor.reaction.test.ts | 2 +- extensions/feishu/src/monitor.startup.test.ts | 2 +- extensions/feishu/src/monitor.startup.ts | 2 +- extensions/feishu/src/monitor.state.ts | 2 +- extensions/feishu/src/monitor.transport.ts | 2 +- extensions/feishu/src/monitor.ts | 2 +- extensions/feishu/src/monitor.webhook-security.test.ts | 2 +- extensions/feishu/src/onboarding.status.test.ts | 2 +- extensions/feishu/src/onboarding.ts | 4 ++-- extensions/feishu/src/outbound.ts | 2 +- extensions/feishu/src/perm.ts | 2 +- extensions/feishu/src/policy.ts | 2 +- extensions/feishu/src/reactions.ts | 2 +- extensions/feishu/src/reply-dispatcher.ts | 2 +- extensions/feishu/src/runtime.ts | 2 +- extensions/feishu/src/secret-input.ts | 2 +- extensions/feishu/src/send-target.test.ts | 2 +- extensions/feishu/src/send-target.ts | 2 +- extensions/feishu/src/send.test.ts | 2 +- extensions/feishu/src/send.ts | 2 +- extensions/feishu/src/streaming-card.ts | 2 +- extensions/feishu/src/tool-account-routing.test.ts | 2 +- extensions/feishu/src/tool-account.ts | 2 +- extensions/feishu/src/tool-factory-test-harness.ts | 2 +- extensions/feishu/src/types.ts | 2 +- extensions/feishu/src/typing.ts | 2 +- extensions/feishu/src/wiki.ts | 2 +- 43 files changed, 46 insertions(+), 46 deletions(-) diff --git a/extensions/feishu/src/accounts.ts b/extensions/feishu/src/accounts.ts index 39194cda066..4f84a477b91 100644 --- a/extensions/feishu/src/accounts.ts +++ b/extensions/feishu/src/accounts.ts @@ -1,5 +1,5 @@ -import type { ClawdbotConfig } from "openclaw/plugin-sdk"; import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id"; +import type { ClawdbotConfig } from "openclaw/plugin-sdk/compat"; import { normalizeResolvedSecretInputString, normalizeSecretInputString } from "./secret-input.js"; import type { FeishuConfig, diff --git a/extensions/feishu/src/bitable.ts b/extensions/feishu/src/bitable.ts index 8617282bb0a..891cbb8dd19 100644 --- a/extensions/feishu/src/bitable.ts +++ b/extensions/feishu/src/bitable.ts @@ -1,6 +1,6 @@ import type * as Lark from "@larksuiteoapi/node-sdk"; import { Type } from "@sinclair/typebox"; -import type { OpenClawPluginApi } from "openclaw/plugin-sdk"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/compat"; import { listEnabledFeishuAccounts } from "./accounts.js"; import { createFeishuToolClient } from "./tool-account.js"; diff --git a/extensions/feishu/src/bot.test.ts b/extensions/feishu/src/bot.test.ts index 1c0fe5e998a..271bf2a618b 100644 --- a/extensions/feishu/src/bot.test.ts +++ b/extensions/feishu/src/bot.test.ts @@ -1,4 +1,4 @@ -import type { ClawdbotConfig, PluginRuntime, RuntimeEnv } from "openclaw/plugin-sdk"; +import type { ClawdbotConfig, PluginRuntime, RuntimeEnv } from "openclaw/plugin-sdk/compat"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { createPluginRuntimeMock } from "../../test-utils/plugin-runtime-mock.js"; import type { FeishuMessageEvent } from "./bot.js"; diff --git a/extensions/feishu/src/bot.ts b/extensions/feishu/src/bot.ts index 2a4ac9a3063..0aa9cbb15a2 100644 --- a/extensions/feishu/src/bot.ts +++ b/extensions/feishu/src/bot.ts @@ -1,4 +1,4 @@ -import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk"; +import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk/compat"; import { buildAgentMediaPayload, buildPendingHistoryContextFromMap, @@ -11,7 +11,7 @@ import { resolveOpenProviderRuntimeGroupPolicy, resolveDefaultGroupPolicy, warnMissingProviderGroupPolicyFallbackOnce, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { resolveFeishuAccount } from "./accounts.js"; import { createFeishuClient } from "./client.js"; import { tryRecordMessage, tryRecordMessagePersistent } from "./dedup.js"; diff --git a/extensions/feishu/src/card-action.ts b/extensions/feishu/src/card-action.ts index 9dfb2759066..d0c2dbdde5e 100644 --- a/extensions/feishu/src/card-action.ts +++ b/extensions/feishu/src/card-action.ts @@ -1,4 +1,4 @@ -import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk"; +import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk/compat"; import { resolveFeishuAccount } from "./accounts.js"; import { handleFeishuMessage, type FeishuMessageEvent } from "./bot.js"; diff --git a/extensions/feishu/src/channel.test.ts b/extensions/feishu/src/channel.test.ts index affc25fae5d..8a59efed263 100644 --- a/extensions/feishu/src/channel.test.ts +++ b/extensions/feishu/src/channel.test.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig } from "openclaw/plugin-sdk"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; import { describe, expect, it, vi } from "vitest"; const probeFeishuMock = vi.hoisted(() => vi.fn()); diff --git a/extensions/feishu/src/channel.ts b/extensions/feishu/src/channel.ts index 69befba3371..64e64f5e6b3 100644 --- a/extensions/feishu/src/channel.ts +++ b/extensions/feishu/src/channel.ts @@ -1,4 +1,4 @@ -import type { ChannelMeta, ChannelPlugin, ClawdbotConfig } from "openclaw/plugin-sdk"; +import type { ChannelMeta, ChannelPlugin, ClawdbotConfig } from "openclaw/plugin-sdk/compat"; import { buildBaseChannelStatusSummary, createDefaultChannelRuntimeState, @@ -6,7 +6,7 @@ import { PAIRING_APPROVED_MESSAGE, resolveAllowlistProviderRuntimeGroupPolicy, resolveDefaultGroupPolicy, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { resolveFeishuAccount, resolveFeishuCredentials, diff --git a/extensions/feishu/src/chat.ts b/extensions/feishu/src/chat.ts index a2430be9adc..7aae3683978 100644 --- a/extensions/feishu/src/chat.ts +++ b/extensions/feishu/src/chat.ts @@ -1,5 +1,5 @@ import type * as Lark from "@larksuiteoapi/node-sdk"; -import type { OpenClawPluginApi } from "openclaw/plugin-sdk"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/compat"; import { listEnabledFeishuAccounts } from "./accounts.js"; import { FeishuChatSchema, type FeishuChatParams } from "./chat-schema.js"; import { createFeishuClient } from "./client.js"; diff --git a/extensions/feishu/src/dedup.ts b/extensions/feishu/src/dedup.ts index 408a53d5d1a..55f983ebd90 100644 --- a/extensions/feishu/src/dedup.ts +++ b/extensions/feishu/src/dedup.ts @@ -4,7 +4,7 @@ import { createDedupeCache, createPersistentDedupe, readJsonFileWithFallback, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; // Persistent TTL: 24 hours — survives restarts & WebSocket reconnects. const DEDUP_TTL_MS = 24 * 60 * 60 * 1000; diff --git a/extensions/feishu/src/directory.ts b/extensions/feishu/src/directory.ts index c87c23513d0..464c11fa6e1 100644 --- a/extensions/feishu/src/directory.ts +++ b/extensions/feishu/src/directory.ts @@ -1,4 +1,4 @@ -import type { ClawdbotConfig } from "openclaw/plugin-sdk"; +import type { ClawdbotConfig } from "openclaw/plugin-sdk/compat"; import { resolveFeishuAccount } from "./accounts.js"; import { createFeishuClient } from "./client.js"; import { normalizeFeishuTarget } from "./targets.js"; diff --git a/extensions/feishu/src/docx.account-selection.test.ts b/extensions/feishu/src/docx.account-selection.test.ts index 562f5cbe45b..324bccacbcf 100644 --- a/extensions/feishu/src/docx.account-selection.test.ts +++ b/extensions/feishu/src/docx.account-selection.test.ts @@ -1,4 +1,4 @@ -import type { OpenClawPluginApi } from "openclaw/plugin-sdk"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/compat"; import { describe, expect, test, vi } from "vitest"; import { registerFeishuDocTools } from "./docx.js"; import { createToolFactoryHarness } from "./tool-factory-test-harness.js"; diff --git a/extensions/feishu/src/docx.ts b/extensions/feishu/src/docx.ts index db14e8a91ba..b1ad52ca131 100644 --- a/extensions/feishu/src/docx.ts +++ b/extensions/feishu/src/docx.ts @@ -4,7 +4,7 @@ import { isAbsolute } from "node:path"; import { basename } from "node:path"; import type * as Lark from "@larksuiteoapi/node-sdk"; import { Type } from "@sinclair/typebox"; -import type { OpenClawPluginApi } from "openclaw/plugin-sdk"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/compat"; import { listEnabledFeishuAccounts } from "./accounts.js"; import { FeishuDocSchema, type FeishuDocParams } from "./doc-schema.js"; import { BATCH_SIZE, insertBlocksInBatches } from "./docx-batch-insert.js"; diff --git a/extensions/feishu/src/drive.ts b/extensions/feishu/src/drive.ts index d4bde43aff3..35f3c798054 100644 --- a/extensions/feishu/src/drive.ts +++ b/extensions/feishu/src/drive.ts @@ -1,5 +1,5 @@ import type * as Lark from "@larksuiteoapi/node-sdk"; -import type { OpenClawPluginApi } from "openclaw/plugin-sdk"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/compat"; import { listEnabledFeishuAccounts } from "./accounts.js"; import { FeishuDriveSchema, type FeishuDriveParams } from "./drive-schema.js"; import { createFeishuToolClient, resolveAnyEnabledFeishuToolsConfig } from "./tool-account.js"; diff --git a/extensions/feishu/src/dynamic-agent.ts b/extensions/feishu/src/dynamic-agent.ts index d62c3f2a43e..f7341dd94db 100644 --- a/extensions/feishu/src/dynamic-agent.ts +++ b/extensions/feishu/src/dynamic-agent.ts @@ -1,7 +1,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk"; +import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk/compat"; import type { DynamicAgentCreationConfig } from "./types.js"; export type MaybeCreateDynamicAgentResult = { diff --git a/extensions/feishu/src/media.ts b/extensions/feishu/src/media.ts index 05f8c59a0ce..1945d672367 100644 --- a/extensions/feishu/src/media.ts +++ b/extensions/feishu/src/media.ts @@ -1,7 +1,7 @@ import fs from "fs"; import path from "path"; import { Readable } from "stream"; -import { withTempDownloadPath, type ClawdbotConfig } from "openclaw/plugin-sdk"; +import { withTempDownloadPath, type ClawdbotConfig } from "openclaw/plugin-sdk/compat"; import { resolveFeishuAccount } from "./accounts.js"; import { createFeishuClient } from "./client.js"; import { normalizeFeishuExternalKey } from "./external-keys.js"; diff --git a/extensions/feishu/src/monitor.account.ts b/extensions/feishu/src/monitor.account.ts index 4e8d30b2359..c8c0d908cb9 100644 --- a/extensions/feishu/src/monitor.account.ts +++ b/extensions/feishu/src/monitor.account.ts @@ -1,6 +1,6 @@ import * as crypto from "crypto"; import * as Lark from "@larksuiteoapi/node-sdk"; -import type { ClawdbotConfig, RuntimeEnv, HistoryEntry } from "openclaw/plugin-sdk"; +import type { ClawdbotConfig, RuntimeEnv, HistoryEntry } from "openclaw/plugin-sdk/compat"; import { resolveFeishuAccount } from "./accounts.js"; import { raceWithTimeoutAndAbort } from "./async.js"; import { diff --git a/extensions/feishu/src/monitor.reaction.test.ts b/extensions/feishu/src/monitor.reaction.test.ts index 5de88065b0e..7cef897e559 100644 --- a/extensions/feishu/src/monitor.reaction.test.ts +++ b/extensions/feishu/src/monitor.reaction.test.ts @@ -1,4 +1,4 @@ -import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk"; +import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk/compat"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { hasControlCommand } from "../../../src/auto-reply/command-detection.js"; import { diff --git a/extensions/feishu/src/monitor.startup.test.ts b/extensions/feishu/src/monitor.startup.test.ts index 2c142e85e5e..62f5aea41be 100644 --- a/extensions/feishu/src/monitor.startup.test.ts +++ b/extensions/feishu/src/monitor.startup.test.ts @@ -1,4 +1,4 @@ -import type { ClawdbotConfig } from "openclaw/plugin-sdk"; +import type { ClawdbotConfig } from "openclaw/plugin-sdk/compat"; import { afterEach, describe, expect, it, vi } from "vitest"; import { monitorFeishuProvider, stopFeishuMonitor } from "./monitor.js"; diff --git a/extensions/feishu/src/monitor.startup.ts b/extensions/feishu/src/monitor.startup.ts index aab61bca933..f3f59a7c5f6 100644 --- a/extensions/feishu/src/monitor.startup.ts +++ b/extensions/feishu/src/monitor.startup.ts @@ -1,4 +1,4 @@ -import type { RuntimeEnv } from "openclaw/plugin-sdk"; +import type { RuntimeEnv } from "openclaw/plugin-sdk/compat"; import { probeFeishu } from "./probe.js"; import type { ResolvedFeishuAccount } from "./types.js"; diff --git a/extensions/feishu/src/monitor.state.ts b/extensions/feishu/src/monitor.state.ts index 150a9adc2a5..a41b5ab2034 100644 --- a/extensions/feishu/src/monitor.state.ts +++ b/extensions/feishu/src/monitor.state.ts @@ -6,7 +6,7 @@ import { type RuntimeEnv, WEBHOOK_ANOMALY_COUNTER_DEFAULTS as WEBHOOK_ANOMALY_COUNTER_DEFAULTS_FROM_SDK, WEBHOOK_RATE_LIMIT_DEFAULTS as WEBHOOK_RATE_LIMIT_DEFAULTS_FROM_SDK, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; export const wsClients = new Map(); export const httpServers = new Map(); diff --git a/extensions/feishu/src/monitor.transport.ts b/extensions/feishu/src/monitor.transport.ts index 9fcb2783f39..f7c21cc1e23 100644 --- a/extensions/feishu/src/monitor.transport.ts +++ b/extensions/feishu/src/monitor.transport.ts @@ -4,7 +4,7 @@ import { applyBasicWebhookRequestGuards, type RuntimeEnv, installRequestBodyLimitGuard, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { createFeishuWSClient } from "./client.js"; import { botOpenIds, diff --git a/extensions/feishu/src/monitor.ts b/extensions/feishu/src/monitor.ts index b7156fd238d..87ae8db7884 100644 --- a/extensions/feishu/src/monitor.ts +++ b/extensions/feishu/src/monitor.ts @@ -1,4 +1,4 @@ -import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk"; +import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk/compat"; import { listEnabledFeishuAccounts, resolveFeishuAccount } from "./accounts.js"; import { monitorSingleAccount, diff --git a/extensions/feishu/src/monitor.webhook-security.test.ts b/extensions/feishu/src/monitor.webhook-security.test.ts index bca56edb598..eddbae01db5 100644 --- a/extensions/feishu/src/monitor.webhook-security.test.ts +++ b/extensions/feishu/src/monitor.webhook-security.test.ts @@ -1,6 +1,6 @@ import { createServer } from "node:http"; import type { AddressInfo } from "node:net"; -import type { ClawdbotConfig } from "openclaw/plugin-sdk"; +import type { ClawdbotConfig } from "openclaw/plugin-sdk/compat"; import { afterEach, describe, expect, it, vi } from "vitest"; const probeFeishuMock = vi.hoisted(() => vi.fn()); diff --git a/extensions/feishu/src/onboarding.status.test.ts b/extensions/feishu/src/onboarding.status.test.ts index 61eeb0d1a66..1a5bae00080 100644 --- a/extensions/feishu/src/onboarding.status.test.ts +++ b/extensions/feishu/src/onboarding.status.test.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig } from "openclaw/plugin-sdk"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; import { describe, expect, it } from "vitest"; import { feishuOnboardingAdapter } from "./onboarding.js"; diff --git a/extensions/feishu/src/onboarding.ts b/extensions/feishu/src/onboarding.ts index 00a4165b480..255f4ff7134 100644 --- a/extensions/feishu/src/onboarding.ts +++ b/extensions/feishu/src/onboarding.ts @@ -5,14 +5,14 @@ import type { DmPolicy, SecretInput, WizardPrompter, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { addWildcardAllowFrom, DEFAULT_ACCOUNT_ID, formatDocsLink, hasConfiguredSecretInput, promptSingleChannelSecretInput, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { resolveFeishuCredentials } from "./accounts.js"; import { probeFeishu } from "./probe.js"; import type { FeishuConfig } from "./types.js"; diff --git a/extensions/feishu/src/outbound.ts b/extensions/feishu/src/outbound.ts index b9867c496f4..5aeb71514a1 100644 --- a/extensions/feishu/src/outbound.ts +++ b/extensions/feishu/src/outbound.ts @@ -1,6 +1,6 @@ import fs from "fs"; import path from "path"; -import type { ChannelOutboundAdapter } from "openclaw/plugin-sdk"; +import type { ChannelOutboundAdapter } from "openclaw/plugin-sdk/compat"; import { resolveFeishuAccount } from "./accounts.js"; import { sendMediaFeishu } from "./media.js"; import { getFeishuRuntime } from "./runtime.js"; diff --git a/extensions/feishu/src/perm.ts b/extensions/feishu/src/perm.ts index 92c3bb8cdd9..332b3992640 100644 --- a/extensions/feishu/src/perm.ts +++ b/extensions/feishu/src/perm.ts @@ -1,5 +1,5 @@ import type * as Lark from "@larksuiteoapi/node-sdk"; -import type { OpenClawPluginApi } from "openclaw/plugin-sdk"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/compat"; import { listEnabledFeishuAccounts } from "./accounts.js"; import { FeishuPermSchema, type FeishuPermParams } from "./perm-schema.js"; import { createFeishuToolClient, resolveAnyEnabledFeishuToolsConfig } from "./tool-account.js"; diff --git a/extensions/feishu/src/policy.ts b/extensions/feishu/src/policy.ts index 430fa7005ec..c0f480e449f 100644 --- a/extensions/feishu/src/policy.ts +++ b/extensions/feishu/src/policy.ts @@ -2,7 +2,7 @@ import type { AllowlistMatch, ChannelGroupContext, GroupToolPolicyConfig, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { normalizeFeishuTarget } from "./targets.js"; import type { FeishuConfig, FeishuGroupConfig } from "./types.js"; diff --git a/extensions/feishu/src/reactions.ts b/extensions/feishu/src/reactions.ts index 93937186072..73a515cd037 100644 --- a/extensions/feishu/src/reactions.ts +++ b/extensions/feishu/src/reactions.ts @@ -1,4 +1,4 @@ -import type { ClawdbotConfig } from "openclaw/plugin-sdk"; +import type { ClawdbotConfig } from "openclaw/plugin-sdk/compat"; import { resolveFeishuAccount } from "./accounts.js"; import { createFeishuClient } from "./client.js"; diff --git a/extensions/feishu/src/reply-dispatcher.ts b/extensions/feishu/src/reply-dispatcher.ts index 88c31c66260..23aa835347e 100644 --- a/extensions/feishu/src/reply-dispatcher.ts +++ b/extensions/feishu/src/reply-dispatcher.ts @@ -5,7 +5,7 @@ import { type ClawdbotConfig, type ReplyPayload, type RuntimeEnv, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { resolveFeishuAccount } from "./accounts.js"; import { createFeishuClient } from "./client.js"; import { sendMediaFeishu } from "./media.js"; diff --git a/extensions/feishu/src/runtime.ts b/extensions/feishu/src/runtime.ts index f1148c5e7df..9f3b8ed9d58 100644 --- a/extensions/feishu/src/runtime.ts +++ b/extensions/feishu/src/runtime.ts @@ -1,4 +1,4 @@ -import type { PluginRuntime } from "openclaw/plugin-sdk"; +import type { PluginRuntime } from "openclaw/plugin-sdk/compat"; let runtime: PluginRuntime | null = null; diff --git a/extensions/feishu/src/secret-input.ts b/extensions/feishu/src/secret-input.ts index f90d41c6fb9..3fc82b3ac91 100644 --- a/extensions/feishu/src/secret-input.ts +++ b/extensions/feishu/src/secret-input.ts @@ -2,7 +2,7 @@ import { hasConfiguredSecretInput, normalizeResolvedSecretInputString, normalizeSecretInputString, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { z } from "zod"; export { hasConfiguredSecretInput, normalizeResolvedSecretInputString, normalizeSecretInputString }; diff --git a/extensions/feishu/src/send-target.test.ts b/extensions/feishu/src/send-target.test.ts index 617c2aa051e..123e0d020ca 100644 --- a/extensions/feishu/src/send-target.test.ts +++ b/extensions/feishu/src/send-target.test.ts @@ -1,4 +1,4 @@ -import type { ClawdbotConfig } from "openclaw/plugin-sdk"; +import type { ClawdbotConfig } from "openclaw/plugin-sdk/compat"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { resolveFeishuSendTarget } from "./send-target.js"; diff --git a/extensions/feishu/src/send-target.ts b/extensions/feishu/src/send-target.ts index caf02f9cf8a..15d0e7ae6e4 100644 --- a/extensions/feishu/src/send-target.ts +++ b/extensions/feishu/src/send-target.ts @@ -1,4 +1,4 @@ -import type { ClawdbotConfig } from "openclaw/plugin-sdk"; +import type { ClawdbotConfig } from "openclaw/plugin-sdk/compat"; import { resolveFeishuAccount } from "./accounts.js"; import { createFeishuClient } from "./client.js"; import { resolveReceiveIdType, normalizeFeishuTarget } from "./targets.js"; diff --git a/extensions/feishu/src/send.test.ts b/extensions/feishu/src/send.test.ts index a58a347a438..fab23a64829 100644 --- a/extensions/feishu/src/send.test.ts +++ b/extensions/feishu/src/send.test.ts @@ -1,4 +1,4 @@ -import type { ClawdbotConfig } from "openclaw/plugin-sdk"; +import type { ClawdbotConfig } from "openclaw/plugin-sdk/compat"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { getMessageFeishu } from "./send.js"; diff --git a/extensions/feishu/src/send.ts b/extensions/feishu/src/send.ts index 7cb53e79f4c..7703fde77cb 100644 --- a/extensions/feishu/src/send.ts +++ b/extensions/feishu/src/send.ts @@ -1,4 +1,4 @@ -import type { ClawdbotConfig } from "openclaw/plugin-sdk"; +import type { ClawdbotConfig } from "openclaw/plugin-sdk/compat"; import { resolveFeishuAccount } from "./accounts.js"; import { createFeishuClient } from "./client.js"; import type { MentionTarget } from "./mention.js"; diff --git a/extensions/feishu/src/streaming-card.ts b/extensions/feishu/src/streaming-card.ts index 615636467a9..d93fc9937dd 100644 --- a/extensions/feishu/src/streaming-card.ts +++ b/extensions/feishu/src/streaming-card.ts @@ -3,7 +3,7 @@ */ import type { Client } from "@larksuiteoapi/node-sdk"; -import { fetchWithSsrFGuard } from "openclaw/plugin-sdk"; +import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/compat"; import type { FeishuDomain } from "./types.js"; type Credentials = { appId: string; appSecret: string; domain?: FeishuDomain }; diff --git a/extensions/feishu/src/tool-account-routing.test.ts b/extensions/feishu/src/tool-account-routing.test.ts index bceb069def9..aebc9fe71ad 100644 --- a/extensions/feishu/src/tool-account-routing.test.ts +++ b/extensions/feishu/src/tool-account-routing.test.ts @@ -1,4 +1,4 @@ -import type { OpenClawPluginApi } from "openclaw/plugin-sdk"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/compat"; import { beforeEach, describe, expect, test, vi } from "vitest"; import { registerFeishuBitableTools } from "./bitable.js"; import { registerFeishuDriveTools } from "./drive.js"; diff --git a/extensions/feishu/src/tool-account.ts b/extensions/feishu/src/tool-account.ts index 33cb82503aa..37b069d8c57 100644 --- a/extensions/feishu/src/tool-account.ts +++ b/extensions/feishu/src/tool-account.ts @@ -1,5 +1,5 @@ import type * as Lark from "@larksuiteoapi/node-sdk"; -import type { OpenClawPluginApi } from "openclaw/plugin-sdk"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/compat"; import { resolveFeishuAccount } from "./accounts.js"; import { createFeishuClient } from "./client.js"; import { resolveToolsConfig } from "./tools-config.js"; diff --git a/extensions/feishu/src/tool-factory-test-harness.ts b/extensions/feishu/src/tool-factory-test-harness.ts index a945e063900..9f3e801b070 100644 --- a/extensions/feishu/src/tool-factory-test-harness.ts +++ b/extensions/feishu/src/tool-factory-test-harness.ts @@ -1,4 +1,4 @@ -import type { AnyAgentTool, OpenClawPluginApi } from "openclaw/plugin-sdk"; +import type { AnyAgentTool, OpenClawPluginApi } from "openclaw/plugin-sdk/compat"; type ToolContextLike = { agentAccountId?: string; diff --git a/extensions/feishu/src/types.ts b/extensions/feishu/src/types.ts index 40287ac7983..3bfba9fe168 100644 --- a/extensions/feishu/src/types.ts +++ b/extensions/feishu/src/types.ts @@ -1,4 +1,4 @@ -import type { BaseProbeResult } from "openclaw/plugin-sdk"; +import type { BaseProbeResult } from "openclaw/plugin-sdk/compat"; import type { FeishuConfigSchema, FeishuGroupSchema, diff --git a/extensions/feishu/src/typing.ts b/extensions/feishu/src/typing.ts index 5e47a0085ac..65abb9ae832 100644 --- a/extensions/feishu/src/typing.ts +++ b/extensions/feishu/src/typing.ts @@ -1,4 +1,4 @@ -import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk"; +import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk/compat"; import { resolveFeishuAccount } from "./accounts.js"; import { createFeishuClient } from "./client.js"; import { getFeishuRuntime } from "./runtime.js"; diff --git a/extensions/feishu/src/wiki.ts b/extensions/feishu/src/wiki.ts index 0c4383b0647..66e2c291af0 100644 --- a/extensions/feishu/src/wiki.ts +++ b/extensions/feishu/src/wiki.ts @@ -1,5 +1,5 @@ import type * as Lark from "@larksuiteoapi/node-sdk"; -import type { OpenClawPluginApi } from "openclaw/plugin-sdk"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/compat"; import { listEnabledFeishuAccounts } from "./accounts.js"; import { createFeishuToolClient, resolveAnyEnabledFeishuToolsConfig } from "./tool-account.js"; import { FeishuWikiSchema, type FeishuWikiParams } from "./wiki-schema.js"; From b4f60d900be298ba9886b70b14a9d2f45ea38046 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 4 Mar 2026 01:20:06 -0500 Subject: [PATCH 09/74] Extensions: migrate google-gemini-cli-auth plugin-sdk imports --- extensions/google-gemini-cli-auth/oauth.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/google-gemini-cli-auth/oauth.ts b/extensions/google-gemini-cli-auth/oauth.ts index 1b0d2232833..0730127bf2e 100644 --- a/extensions/google-gemini-cli-auth/oauth.ts +++ b/extensions/google-gemini-cli-auth/oauth.ts @@ -2,7 +2,7 @@ import { createHash, randomBytes } from "node:crypto"; import { existsSync, readFileSync, readdirSync, realpathSync } from "node:fs"; import { createServer } from "node:http"; import { delimiter, dirname, join } from "node:path"; -import { fetchWithSsrFGuard, isWSL2Sync } from "openclaw/plugin-sdk"; +import { fetchWithSsrFGuard, isWSL2Sync } from "openclaw/plugin-sdk/compat"; const CLIENT_ID_KEYS = ["OPENCLAW_GEMINI_OAUTH_CLIENT_ID", "GEMINI_CLI_OAUTH_CLIENT_ID"]; const CLIENT_SECRET_KEYS = [ From 39a55844bc5dc02da225adad0b37c00f5d5f53ff Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 4 Mar 2026 01:20:07 -0500 Subject: [PATCH 10/74] Extensions: migrate googlechat plugin-sdk imports --- extensions/googlechat/src/accounts.ts | 4 ++-- extensions/googlechat/src/actions.ts | 4 ++-- extensions/googlechat/src/api.ts | 2 +- extensions/googlechat/src/channel.startup.test.ts | 2 +- extensions/googlechat/src/channel.ts | 4 ++-- extensions/googlechat/src/monitor-access.ts | 4 ++-- extensions/googlechat/src/monitor-types.ts | 2 +- extensions/googlechat/src/monitor-webhook.ts | 2 +- extensions/googlechat/src/monitor.ts | 4 ++-- extensions/googlechat/src/monitor.webhook-routing.test.ts | 2 +- extensions/googlechat/src/onboarding.ts | 4 ++-- extensions/googlechat/src/runtime.ts | 2 +- extensions/googlechat/src/types.config.ts | 2 +- 13 files changed, 19 insertions(+), 19 deletions(-) diff --git a/extensions/googlechat/src/accounts.ts b/extensions/googlechat/src/accounts.ts index a50ef0b2a74..494e1481b6d 100644 --- a/extensions/googlechat/src/accounts.ts +++ b/extensions/googlechat/src/accounts.ts @@ -1,10 +1,10 @@ -import { isSecretRef } from "openclaw/plugin-sdk"; -import type { OpenClawConfig } from "openclaw/plugin-sdk"; import { DEFAULT_ACCOUNT_ID, normalizeAccountId, normalizeOptionalAccountId, } from "openclaw/plugin-sdk/account-id"; +import { isSecretRef } from "openclaw/plugin-sdk/compat"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; import type { GoogleChatAccountConfig } from "./types.config.js"; export type GoogleChatCredentialSource = "file" | "inline" | "env" | "none"; diff --git a/extensions/googlechat/src/actions.ts b/extensions/googlechat/src/actions.ts index 85a3e3d383d..41f94593c67 100644 --- a/extensions/googlechat/src/actions.ts +++ b/extensions/googlechat/src/actions.ts @@ -2,7 +2,7 @@ import type { ChannelMessageActionAdapter, ChannelMessageActionName, OpenClawConfig, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { createActionGate, extractToolSend, @@ -10,7 +10,7 @@ import { readNumberParam, readReactionParams, readStringParam, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { listEnabledGoogleChatAccounts, resolveGoogleChatAccount } from "./accounts.js"; import { createGoogleChatReaction, diff --git a/extensions/googlechat/src/api.ts b/extensions/googlechat/src/api.ts index de611f66af5..e7db6b76022 100644 --- a/extensions/googlechat/src/api.ts +++ b/extensions/googlechat/src/api.ts @@ -1,5 +1,5 @@ import crypto from "node:crypto"; -import { fetchWithSsrFGuard } from "openclaw/plugin-sdk"; +import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/compat"; import type { ResolvedGoogleChatAccount } from "./accounts.js"; import { getGoogleChatAccessToken } from "./auth.js"; import type { GoogleChatReaction } from "./types.js"; diff --git a/extensions/googlechat/src/channel.startup.test.ts b/extensions/googlechat/src/channel.startup.test.ts index 4735ae811e4..421aa474328 100644 --- a/extensions/googlechat/src/channel.startup.test.ts +++ b/extensions/googlechat/src/channel.startup.test.ts @@ -1,4 +1,4 @@ -import type { ChannelAccountSnapshot } from "openclaw/plugin-sdk"; +import type { ChannelAccountSnapshot } from "openclaw/plugin-sdk/compat"; import { afterEach, describe, expect, it, vi } from "vitest"; import { createStartAccountContext } from "../../test-utils/start-account-context.js"; import type { ResolvedGoogleChatAccount } from "./accounts.js"; diff --git a/extensions/googlechat/src/channel.ts b/extensions/googlechat/src/channel.ts index f79d2212ec7..bfb2aabe356 100644 --- a/extensions/googlechat/src/channel.ts +++ b/extensions/googlechat/src/channel.ts @@ -19,8 +19,8 @@ import { type ChannelPlugin, type ChannelStatusIssue, type OpenClawConfig, -} from "openclaw/plugin-sdk"; -import { GoogleChatConfigSchema } from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; +import { GoogleChatConfigSchema } from "openclaw/plugin-sdk/compat"; import { listGoogleChatAccountIds, resolveDefaultGoogleChatAccountId, diff --git a/extensions/googlechat/src/monitor-access.ts b/extensions/googlechat/src/monitor-access.ts index f057c645de9..58c13ab9b9c 100644 --- a/extensions/googlechat/src/monitor-access.ts +++ b/extensions/googlechat/src/monitor-access.ts @@ -7,8 +7,8 @@ import { resolveDmGroupAccessWithLists, resolveMentionGatingWithBypass, warnMissingProviderGroupPolicyFallbackOnce, -} from "openclaw/plugin-sdk"; -import type { OpenClawConfig } from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; import type { ResolvedGoogleChatAccount } from "./accounts.js"; import { sendGoogleChatMessage } from "./api.js"; import type { GoogleChatCoreRuntime } from "./monitor-types.js"; diff --git a/extensions/googlechat/src/monitor-types.ts b/extensions/googlechat/src/monitor-types.ts index 6a0f6d8f847..5cc43d2eedf 100644 --- a/extensions/googlechat/src/monitor-types.ts +++ b/extensions/googlechat/src/monitor-types.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig } from "openclaw/plugin-sdk"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; import type { ResolvedGoogleChatAccount } from "./accounts.js"; import type { GoogleChatAudienceType } from "./auth.js"; import { getGoogleChatRuntime } from "./runtime.js"; diff --git a/extensions/googlechat/src/monitor-webhook.ts b/extensions/googlechat/src/monitor-webhook.ts index c2978566198..e7ba9107e0f 100644 --- a/extensions/googlechat/src/monitor-webhook.ts +++ b/extensions/googlechat/src/monitor-webhook.ts @@ -5,7 +5,7 @@ import { resolveWebhookTargetWithAuthOrReject, resolveWebhookTargets, type WebhookInFlightLimiter, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { verifyGoogleChatRequest } from "./auth.js"; import type { WebhookTarget } from "./monitor-types.js"; import type { diff --git a/extensions/googlechat/src/monitor.ts b/extensions/googlechat/src/monitor.ts index f0079b5c0f8..138de4b1882 100644 --- a/extensions/googlechat/src/monitor.ts +++ b/extensions/googlechat/src/monitor.ts @@ -1,12 +1,12 @@ import type { IncomingMessage, ServerResponse } from "node:http"; -import type { OpenClawConfig } from "openclaw/plugin-sdk"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; import { createWebhookInFlightLimiter, createReplyPrefixOptions, registerWebhookTargetWithPluginRoute, resolveInboundRouteEnvelopeBuilderWithRuntime, resolveWebhookPath, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { type ResolvedGoogleChatAccount } from "./accounts.js"; import { downloadGoogleChatMedia, diff --git a/extensions/googlechat/src/monitor.webhook-routing.test.ts b/extensions/googlechat/src/monitor.webhook-routing.test.ts index 0aafa77e09f..d35077d2167 100644 --- a/extensions/googlechat/src/monitor.webhook-routing.test.ts +++ b/extensions/googlechat/src/monitor.webhook-routing.test.ts @@ -1,6 +1,6 @@ import { EventEmitter } from "node:events"; import type { IncomingMessage } from "node:http"; -import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk"; +import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk/compat"; import { afterEach, describe, expect, it, vi } from "vitest"; import { createEmptyPluginRegistry } from "../../../src/plugins/registry.js"; import { setActivePluginRegistry } from "../../../src/plugins/runtime.js"; diff --git a/extensions/googlechat/src/onboarding.ts b/extensions/googlechat/src/onboarding.ts index 1b7e82f6951..da0c647698b 100644 --- a/extensions/googlechat/src/onboarding.ts +++ b/extensions/googlechat/src/onboarding.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig, DmPolicy } from "openclaw/plugin-sdk"; +import type { OpenClawConfig, DmPolicy } from "openclaw/plugin-sdk/compat"; import { addWildcardAllowFrom, formatDocsLink, @@ -10,7 +10,7 @@ import { DEFAULT_ACCOUNT_ID, normalizeAccountId, migrateBaseNameToDefaultAccount, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { listGoogleChatAccountIds, resolveDefaultGoogleChatAccountId, diff --git a/extensions/googlechat/src/runtime.ts b/extensions/googlechat/src/runtime.ts index 67a1917a888..67a21a21e9c 100644 --- a/extensions/googlechat/src/runtime.ts +++ b/extensions/googlechat/src/runtime.ts @@ -1,4 +1,4 @@ -import type { PluginRuntime } from "openclaw/plugin-sdk"; +import type { PluginRuntime } from "openclaw/plugin-sdk/compat"; let runtime: PluginRuntime | null = null; diff --git a/extensions/googlechat/src/types.config.ts b/extensions/googlechat/src/types.config.ts index 17fe1dc67d9..09062a2661d 100644 --- a/extensions/googlechat/src/types.config.ts +++ b/extensions/googlechat/src/types.config.ts @@ -1,3 +1,3 @@ -import type { GoogleChatAccountConfig, GoogleChatConfig } from "openclaw/plugin-sdk"; +import type { GoogleChatAccountConfig, GoogleChatConfig } from "openclaw/plugin-sdk/compat"; export type { GoogleChatAccountConfig, GoogleChatConfig }; From 9b6101e382671fe5c8693d85e5fe28a2ad7e63bc Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 4 Mar 2026 01:20:07 -0500 Subject: [PATCH 11/74] Extensions: migrate irc plugin-sdk imports --- extensions/irc/src/accounts.ts | 2 +- extensions/irc/src/channel.ts | 2 +- extensions/irc/src/config-schema.ts | 2 +- extensions/irc/src/inbound.ts | 2 +- extensions/irc/src/monitor.ts | 2 +- extensions/irc/src/onboarding.test.ts | 2 +- extensions/irc/src/onboarding.ts | 2 +- extensions/irc/src/runtime.ts | 2 +- extensions/irc/src/types.ts | 4 ++-- 9 files changed, 10 insertions(+), 10 deletions(-) diff --git a/extensions/irc/src/accounts.ts b/extensions/irc/src/accounts.ts index 8d47957ab7b..78f15bbc6ec 100644 --- a/extensions/irc/src/accounts.ts +++ b/extensions/irc/src/accounts.ts @@ -1,10 +1,10 @@ import { readFileSync } from "node:fs"; -import { normalizeResolvedSecretInputString } from "openclaw/plugin-sdk"; import { DEFAULT_ACCOUNT_ID, normalizeAccountId, normalizeOptionalAccountId, } from "openclaw/plugin-sdk/account-id"; +import { normalizeResolvedSecretInputString } from "openclaw/plugin-sdk/compat"; import type { CoreConfig, IrcAccountConfig, IrcNickServConfig } from "./types.js"; const TRUTHY_ENV = new Set(["true", "1", "yes", "on"]); diff --git a/extensions/irc/src/channel.ts b/extensions/irc/src/channel.ts index 30fd9f9faa5..c29186cb700 100644 --- a/extensions/irc/src/channel.ts +++ b/extensions/irc/src/channel.ts @@ -11,7 +11,7 @@ import { resolveDefaultGroupPolicy, setAccountEnabledInConfigSection, type ChannelPlugin, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { listIrcAccountIds, resolveDefaultIrcAccountId, diff --git a/extensions/irc/src/config-schema.ts b/extensions/irc/src/config-schema.ts index f08fd0585fd..373e3c79ba4 100644 --- a/extensions/irc/src/config-schema.ts +++ b/extensions/irc/src/config-schema.ts @@ -7,7 +7,7 @@ import { ReplyRuntimeConfigSchemaShape, ToolPolicySchema, requireOpenAllowFrom, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { z } from "zod"; const IrcGroupSchema = z diff --git a/extensions/irc/src/inbound.ts b/extensions/irc/src/inbound.ts index cb21b92c361..b0139c853c7 100644 --- a/extensions/irc/src/inbound.ts +++ b/extensions/irc/src/inbound.ts @@ -16,7 +16,7 @@ import { type OutboundReplyPayload, type OpenClawConfig, type RuntimeEnv, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import type { ResolvedIrcAccount } from "./accounts.js"; import { normalizeIrcAllowlist, resolveIrcAllowlistMatch } from "./normalize.js"; import { diff --git a/extensions/irc/src/monitor.ts b/extensions/irc/src/monitor.ts index 4e07fa28abd..8ebd6766ed3 100644 --- a/extensions/irc/src/monitor.ts +++ b/extensions/irc/src/monitor.ts @@ -1,4 +1,4 @@ -import { createLoggerBackedRuntime, type RuntimeEnv } from "openclaw/plugin-sdk"; +import { createLoggerBackedRuntime, type RuntimeEnv } from "openclaw/plugin-sdk/compat"; import { resolveIrcAccount } from "./accounts.js"; import { connectIrcClient, type IrcClient } from "./client.js"; import { buildIrcConnectOptions } from "./connect-options.js"; diff --git a/extensions/irc/src/onboarding.test.ts b/extensions/irc/src/onboarding.test.ts index 1a0f79b21ae..d597ccdb9e9 100644 --- a/extensions/irc/src/onboarding.test.ts +++ b/extensions/irc/src/onboarding.test.ts @@ -1,4 +1,4 @@ -import type { RuntimeEnv, WizardPrompter } from "openclaw/plugin-sdk"; +import type { RuntimeEnv, WizardPrompter } from "openclaw/plugin-sdk/compat"; import { describe, expect, it, vi } from "vitest"; import { ircOnboardingAdapter } from "./onboarding.js"; import type { CoreConfig } from "./types.js"; diff --git a/extensions/irc/src/onboarding.ts b/extensions/irc/src/onboarding.ts index 2b2cecf8e41..6e6cf707fc0 100644 --- a/extensions/irc/src/onboarding.ts +++ b/extensions/irc/src/onboarding.ts @@ -8,7 +8,7 @@ import { type ChannelOnboardingDmPolicy, type DmPolicy, type WizardPrompter, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { listIrcAccountIds, resolveDefaultIrcAccountId, resolveIrcAccount } from "./accounts.js"; import { isChannelTarget, diff --git a/extensions/irc/src/runtime.ts b/extensions/irc/src/runtime.ts index 547525cea4f..305a0b8bdf4 100644 --- a/extensions/irc/src/runtime.ts +++ b/extensions/irc/src/runtime.ts @@ -1,4 +1,4 @@ -import type { PluginRuntime } from "openclaw/plugin-sdk"; +import type { PluginRuntime } from "openclaw/plugin-sdk/compat"; let runtime: PluginRuntime | null = null; diff --git a/extensions/irc/src/types.ts b/extensions/irc/src/types.ts index 59dd21ef270..4add1357bce 100644 --- a/extensions/irc/src/types.ts +++ b/extensions/irc/src/types.ts @@ -1,4 +1,4 @@ -import type { BaseProbeResult } from "openclaw/plugin-sdk"; +import type { BaseProbeResult } from "openclaw/plugin-sdk/compat"; import type { BlockStreamingCoalesceConfig, DmConfig, @@ -8,7 +8,7 @@ import type { GroupToolPolicyConfig, MarkdownConfig, OpenClawConfig, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; export type IrcChannelConfig = { requireMention?: boolean; From d9b8ec5afa7e87a7298ca0f4050e128bc3fd182a Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 4 Mar 2026 01:20:08 -0500 Subject: [PATCH 12/74] Extensions: migrate llm-task plugin-sdk imports --- extensions/llm-task/src/llm-task-tool.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/llm-task/src/llm-task-tool.ts b/extensions/llm-task/src/llm-task-tool.ts index 6a58118618c..34e7607c378 100644 --- a/extensions/llm-task/src/llm-task-tool.ts +++ b/extensions/llm-task/src/llm-task-tool.ts @@ -2,7 +2,7 @@ import fs from "node:fs/promises"; import path from "node:path"; import { Type } from "@sinclair/typebox"; import Ajv from "ajv"; -import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk"; +import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/compat"; // NOTE: This extension is intended to be bundled with OpenClaw. // When running from source (tests/dev), OpenClaw internals live under src/. // When running from a built install, internals live under dist/ (no src/ tree). From b7df821372623ed8d0376c5be190f77e1bea19ab Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 4 Mar 2026 01:20:09 -0500 Subject: [PATCH 13/74] Extensions: migrate lobster plugin-sdk imports --- extensions/lobster/src/windows-spawn.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/lobster/src/windows-spawn.ts b/extensions/lobster/src/windows-spawn.ts index 6e42dfec41c..a28252a6536 100644 --- a/extensions/lobster/src/windows-spawn.ts +++ b/extensions/lobster/src/windows-spawn.ts @@ -2,7 +2,7 @@ import { applyWindowsSpawnProgramPolicy, materializeWindowsSpawnProgram, resolveWindowsSpawnProgramCandidate, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; type SpawnTarget = { command: string; From 15f7e329c24e4e51feb95f8b66b407cf06b02ef3 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 4 Mar 2026 01:20:10 -0500 Subject: [PATCH 14/74] Extensions: migrate matrix plugin-sdk imports --- extensions/matrix/src/actions.ts | 2 +- extensions/matrix/src/channel.directory.test.ts | 2 +- extensions/matrix/src/channel.ts | 2 +- extensions/matrix/src/config-schema.ts | 2 +- extensions/matrix/src/directory-live.ts | 2 +- extensions/matrix/src/group-mentions.ts | 2 +- extensions/matrix/src/matrix/client/config.ts | 2 +- extensions/matrix/src/matrix/deps.ts | 2 +- extensions/matrix/src/matrix/monitor/access-policy.ts | 2 +- extensions/matrix/src/matrix/monitor/allowlist.ts | 2 +- extensions/matrix/src/matrix/monitor/auto-join.ts | 2 +- extensions/matrix/src/matrix/monitor/events.test.ts | 2 +- extensions/matrix/src/matrix/monitor/events.ts | 2 +- .../matrix/src/matrix/monitor/handler.body-for-agent.test.ts | 2 +- extensions/matrix/src/matrix/monitor/handler.ts | 2 +- extensions/matrix/src/matrix/monitor/index.ts | 2 +- extensions/matrix/src/matrix/monitor/location.ts | 2 +- extensions/matrix/src/matrix/monitor/media.test.ts | 2 +- extensions/matrix/src/matrix/monitor/replies.test.ts | 2 +- extensions/matrix/src/matrix/monitor/replies.ts | 2 +- extensions/matrix/src/matrix/monitor/rooms.ts | 2 +- extensions/matrix/src/matrix/poll-types.ts | 2 +- extensions/matrix/src/matrix/probe.ts | 2 +- extensions/matrix/src/matrix/send.test.ts | 2 +- extensions/matrix/src/matrix/send.ts | 2 +- extensions/matrix/src/onboarding.ts | 4 ++-- extensions/matrix/src/outbound.ts | 2 +- extensions/matrix/src/resolve-targets.test.ts | 2 +- extensions/matrix/src/resolve-targets.ts | 2 +- extensions/matrix/src/runtime.ts | 2 +- extensions/matrix/src/secret-input.ts | 2 +- extensions/matrix/src/tool-actions.ts | 2 +- extensions/matrix/src/types.ts | 2 +- 33 files changed, 34 insertions(+), 34 deletions(-) diff --git a/extensions/matrix/src/actions.ts b/extensions/matrix/src/actions.ts index 868d46632c9..a2b9e9560fe 100644 --- a/extensions/matrix/src/actions.ts +++ b/extensions/matrix/src/actions.ts @@ -6,7 +6,7 @@ import { type ChannelMessageActionContext, type ChannelMessageActionName, type ChannelToolSend, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { resolveMatrixAccount } from "./matrix/accounts.js"; import { handleMatrixAction } from "./tool-actions.js"; import type { CoreConfig } from "./types.js"; diff --git a/extensions/matrix/src/channel.directory.test.ts b/extensions/matrix/src/channel.directory.test.ts index 5fc6bbe28fb..f790152d761 100644 --- a/extensions/matrix/src/channel.directory.test.ts +++ b/extensions/matrix/src/channel.directory.test.ts @@ -1,4 +1,4 @@ -import type { PluginRuntime, RuntimeEnv } from "openclaw/plugin-sdk"; +import type { PluginRuntime, RuntimeEnv } from "openclaw/plugin-sdk/compat"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { matrixPlugin } from "./channel.js"; import { setMatrixRuntime } from "./runtime.js"; diff --git a/extensions/matrix/src/channel.ts b/extensions/matrix/src/channel.ts index b85f12085a4..73569223eac 100644 --- a/extensions/matrix/src/channel.ts +++ b/extensions/matrix/src/channel.ts @@ -11,7 +11,7 @@ import { resolveDefaultGroupPolicy, setAccountEnabledInConfigSection, type ChannelPlugin, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { matrixMessageActions } from "./actions.js"; import { MatrixConfigSchema } from "./config-schema.js"; import { listMatrixDirectoryGroupsLive, listMatrixDirectoryPeersLive } from "./directory-live.js"; diff --git a/extensions/matrix/src/config-schema.ts b/extensions/matrix/src/config-schema.ts index a1070b1448a..88f18d4e0fb 100644 --- a/extensions/matrix/src/config-schema.ts +++ b/extensions/matrix/src/config-schema.ts @@ -1,4 +1,4 @@ -import { MarkdownConfigSchema, ToolPolicySchema } from "openclaw/plugin-sdk"; +import { MarkdownConfigSchema, ToolPolicySchema } from "openclaw/plugin-sdk/compat"; import { z } from "zod"; import { buildSecretInputSchema } from "./secret-input.js"; diff --git a/extensions/matrix/src/directory-live.ts b/extensions/matrix/src/directory-live.ts index 6ac2fc26c6a..59afbfb0a7e 100644 --- a/extensions/matrix/src/directory-live.ts +++ b/extensions/matrix/src/directory-live.ts @@ -1,4 +1,4 @@ -import type { ChannelDirectoryEntry } from "openclaw/plugin-sdk"; +import type { ChannelDirectoryEntry } from "openclaw/plugin-sdk/compat"; import { resolveMatrixAuth } from "./matrix/client.js"; type MatrixUserResult = { diff --git a/extensions/matrix/src/group-mentions.ts b/extensions/matrix/src/group-mentions.ts index b324b4197a7..18b3da2fd1b 100644 --- a/extensions/matrix/src/group-mentions.ts +++ b/extensions/matrix/src/group-mentions.ts @@ -1,4 +1,4 @@ -import type { ChannelGroupContext, GroupToolPolicyConfig } from "openclaw/plugin-sdk"; +import type { ChannelGroupContext, GroupToolPolicyConfig } from "openclaw/plugin-sdk/compat"; import { resolveMatrixAccountConfig } from "./matrix/accounts.js"; import { resolveMatrixRoomConfig } from "./matrix/monitor/rooms.js"; import type { CoreConfig } from "./types.js"; diff --git a/extensions/matrix/src/matrix/client/config.ts b/extensions/matrix/src/matrix/client/config.ts index de7041b9403..7d3a0812d04 100644 --- a/extensions/matrix/src/matrix/client/config.ts +++ b/extensions/matrix/src/matrix/client/config.ts @@ -1,5 +1,5 @@ -import { fetchWithSsrFGuard } from "openclaw/plugin-sdk"; import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id"; +import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/compat"; import { getMatrixRuntime } from "../../runtime.js"; import { normalizeResolvedSecretInputString, diff --git a/extensions/matrix/src/matrix/deps.ts b/extensions/matrix/src/matrix/deps.ts index c1e9957fe23..27bdd3e03af 100644 --- a/extensions/matrix/src/matrix/deps.ts +++ b/extensions/matrix/src/matrix/deps.ts @@ -2,7 +2,7 @@ import fs from "node:fs"; import { createRequire } from "node:module"; import path from "node:path"; import { fileURLToPath } from "node:url"; -import { runPluginCommandWithTimeout, type RuntimeEnv } from "openclaw/plugin-sdk"; +import { runPluginCommandWithTimeout, type RuntimeEnv } from "openclaw/plugin-sdk/compat"; const MATRIX_SDK_PACKAGE = "@vector-im/matrix-bot-sdk"; const MATRIX_CRYPTO_DOWNLOAD_HELPER = "@matrix-org/matrix-sdk-crypto-nodejs/download-lib.js"; diff --git a/extensions/matrix/src/matrix/monitor/access-policy.ts b/extensions/matrix/src/matrix/monitor/access-policy.ts index e937ba81848..77cc1c86d05 100644 --- a/extensions/matrix/src/matrix/monitor/access-policy.ts +++ b/extensions/matrix/src/matrix/monitor/access-policy.ts @@ -3,7 +3,7 @@ import { issuePairingChallenge, readStoreAllowFromForDmPolicy, resolveDmGroupAccessWithLists, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { normalizeMatrixAllowList, resolveMatrixAllowListMatch, diff --git a/extensions/matrix/src/matrix/monitor/allowlist.ts b/extensions/matrix/src/matrix/monitor/allowlist.ts index 165268616ad..76e193c1b6a 100644 --- a/extensions/matrix/src/matrix/monitor/allowlist.ts +++ b/extensions/matrix/src/matrix/monitor/allowlist.ts @@ -1,4 +1,4 @@ -import { resolveAllowlistMatchByCandidates, type AllowlistMatch } from "openclaw/plugin-sdk"; +import { resolveAllowlistMatchByCandidates, type AllowlistMatch } from "openclaw/plugin-sdk/compat"; function normalizeAllowList(list?: Array) { return (list ?? []).map((entry) => String(entry).trim()).filter(Boolean); diff --git a/extensions/matrix/src/matrix/monitor/auto-join.ts b/extensions/matrix/src/matrix/monitor/auto-join.ts index 58121a95f86..ee5f3a5b95d 100644 --- a/extensions/matrix/src/matrix/monitor/auto-join.ts +++ b/extensions/matrix/src/matrix/monitor/auto-join.ts @@ -1,5 +1,5 @@ import type { MatrixClient } from "@vector-im/matrix-bot-sdk"; -import type { RuntimeEnv } from "openclaw/plugin-sdk"; +import type { RuntimeEnv } from "openclaw/plugin-sdk/compat"; import { getMatrixRuntime } from "../../runtime.js"; import type { CoreConfig } from "../../types.js"; import { loadMatrixSdk } from "../sdk-runtime.js"; diff --git a/extensions/matrix/src/matrix/monitor/events.test.ts b/extensions/matrix/src/matrix/monitor/events.test.ts index eeedb8195c6..4f28a8a42e9 100644 --- a/extensions/matrix/src/matrix/monitor/events.test.ts +++ b/extensions/matrix/src/matrix/monitor/events.test.ts @@ -1,5 +1,5 @@ import type { MatrixClient } from "@vector-im/matrix-bot-sdk"; -import type { PluginRuntime, RuntimeLogger } from "openclaw/plugin-sdk"; +import type { PluginRuntime, RuntimeLogger } from "openclaw/plugin-sdk/compat"; import { beforeEach, describe, expect, it, vi } from "vitest"; import type { MatrixAuth } from "../client.js"; import { registerMatrixMonitorEvents } from "./events.js"; diff --git a/extensions/matrix/src/matrix/monitor/events.ts b/extensions/matrix/src/matrix/monitor/events.ts index 76d2168a14d..3e4e40e8637 100644 --- a/extensions/matrix/src/matrix/monitor/events.ts +++ b/extensions/matrix/src/matrix/monitor/events.ts @@ -1,5 +1,5 @@ import type { MatrixClient } from "@vector-im/matrix-bot-sdk"; -import type { PluginRuntime, RuntimeLogger } from "openclaw/plugin-sdk"; +import type { PluginRuntime, RuntimeLogger } from "openclaw/plugin-sdk/compat"; import type { MatrixAuth } from "../client.js"; import { sendReadReceiptMatrix } from "../send.js"; import type { MatrixRawEvent } from "./types.js"; diff --git a/extensions/matrix/src/matrix/monitor/handler.body-for-agent.test.ts b/extensions/matrix/src/matrix/monitor/handler.body-for-agent.test.ts index 49ae7323317..cfd2c314b91 100644 --- a/extensions/matrix/src/matrix/monitor/handler.body-for-agent.test.ts +++ b/extensions/matrix/src/matrix/monitor/handler.body-for-agent.test.ts @@ -1,5 +1,5 @@ import type { MatrixClient } from "@vector-im/matrix-bot-sdk"; -import type { PluginRuntime, RuntimeEnv, RuntimeLogger } from "openclaw/plugin-sdk"; +import type { PluginRuntime, RuntimeEnv, RuntimeLogger } from "openclaw/plugin-sdk/compat"; import { describe, expect, it, vi } from "vitest"; import { createMatrixRoomMessageHandler } from "./handler.js"; import { EventType, type MatrixRawEvent } from "./types.js"; diff --git a/extensions/matrix/src/matrix/monitor/handler.ts b/extensions/matrix/src/matrix/monitor/handler.ts index fc441b83f9a..5fe935821e3 100644 --- a/extensions/matrix/src/matrix/monitor/handler.ts +++ b/extensions/matrix/src/matrix/monitor/handler.ts @@ -11,7 +11,7 @@ import { type PluginRuntime, type RuntimeEnv, type RuntimeLogger, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import type { CoreConfig, MatrixRoomConfig, ReplyToMode } from "../../types.js"; import { fetchEventSummary } from "../actions/summary.js"; import { diff --git a/extensions/matrix/src/matrix/monitor/index.ts b/extensions/matrix/src/matrix/monitor/index.ts index 4f7df2a7a08..c9e5f835907 100644 --- a/extensions/matrix/src/matrix/monitor/index.ts +++ b/extensions/matrix/src/matrix/monitor/index.ts @@ -7,7 +7,7 @@ import { summarizeMapping, warnMissingProviderGroupPolicyFallbackOnce, type RuntimeEnv, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { resolveMatrixTargets } from "../../resolve-targets.js"; import { getMatrixRuntime } from "../../runtime.js"; import type { CoreConfig, MatrixConfig, MatrixRoomConfig, ReplyToMode } from "../../types.js"; diff --git a/extensions/matrix/src/matrix/monitor/location.ts b/extensions/matrix/src/matrix/monitor/location.ts index 41c91aecc16..5f999ce121d 100644 --- a/extensions/matrix/src/matrix/monitor/location.ts +++ b/extensions/matrix/src/matrix/monitor/location.ts @@ -3,7 +3,7 @@ import { formatLocationText, toLocationContext, type NormalizedLocation, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { EventType } from "./types.js"; export type MatrixLocationPayload = { diff --git a/extensions/matrix/src/matrix/monitor/media.test.ts b/extensions/matrix/src/matrix/monitor/media.test.ts index 11b045609a9..3f8fbc1d2d3 100644 --- a/extensions/matrix/src/matrix/monitor/media.test.ts +++ b/extensions/matrix/src/matrix/monitor/media.test.ts @@ -1,4 +1,4 @@ -import type { PluginRuntime } from "openclaw/plugin-sdk"; +import type { PluginRuntime } from "openclaw/plugin-sdk/compat"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { setMatrixRuntime } from "../../runtime.js"; import { downloadMatrixMedia } from "./media.js"; diff --git a/extensions/matrix/src/matrix/monitor/replies.test.ts b/extensions/matrix/src/matrix/monitor/replies.test.ts index dfbfbabb8af..56c35623db4 100644 --- a/extensions/matrix/src/matrix/monitor/replies.test.ts +++ b/extensions/matrix/src/matrix/monitor/replies.test.ts @@ -1,5 +1,5 @@ import type { MatrixClient } from "@vector-im/matrix-bot-sdk"; -import type { PluginRuntime, RuntimeEnv } from "openclaw/plugin-sdk"; +import type { PluginRuntime, RuntimeEnv } from "openclaw/plugin-sdk/compat"; import { beforeEach, describe, expect, it, vi } from "vitest"; const sendMessageMatrixMock = vi.hoisted(() => vi.fn().mockResolvedValue({ messageId: "mx-1" })); diff --git a/extensions/matrix/src/matrix/monitor/replies.ts b/extensions/matrix/src/matrix/monitor/replies.ts index c86c7dde688..bef1757cf04 100644 --- a/extensions/matrix/src/matrix/monitor/replies.ts +++ b/extensions/matrix/src/matrix/monitor/replies.ts @@ -1,5 +1,5 @@ import type { MatrixClient } from "@vector-im/matrix-bot-sdk"; -import type { MarkdownTableMode, ReplyPayload, RuntimeEnv } from "openclaw/plugin-sdk"; +import type { MarkdownTableMode, ReplyPayload, RuntimeEnv } from "openclaw/plugin-sdk/compat"; import { getMatrixRuntime } from "../../runtime.js"; import { sendMessageMatrix } from "../send.js"; diff --git a/extensions/matrix/src/matrix/monitor/rooms.ts b/extensions/matrix/src/matrix/monitor/rooms.ts index 2200ad0c1e4..30e813c6f49 100644 --- a/extensions/matrix/src/matrix/monitor/rooms.ts +++ b/extensions/matrix/src/matrix/monitor/rooms.ts @@ -1,4 +1,4 @@ -import { buildChannelKeyCandidates, resolveChannelEntryMatch } from "openclaw/plugin-sdk"; +import { buildChannelKeyCandidates, resolveChannelEntryMatch } from "openclaw/plugin-sdk/compat"; import type { MatrixRoomConfig } from "../../types.js"; export type MatrixRoomConfigResolved = { diff --git a/extensions/matrix/src/matrix/poll-types.ts b/extensions/matrix/src/matrix/poll-types.ts index aa55a83d681..2bf1fb87f7b 100644 --- a/extensions/matrix/src/matrix/poll-types.ts +++ b/extensions/matrix/src/matrix/poll-types.ts @@ -7,7 +7,7 @@ * - m.poll.end - Closes a poll */ -import type { PollInput } from "openclaw/plugin-sdk"; +import type { PollInput } from "openclaw/plugin-sdk/compat"; export const M_POLL_START = "m.poll.start" as const; export const M_POLL_RESPONSE = "m.poll.response" as const; diff --git a/extensions/matrix/src/matrix/probe.ts b/extensions/matrix/src/matrix/probe.ts index 5681b242c24..42d2273b8fe 100644 --- a/extensions/matrix/src/matrix/probe.ts +++ b/extensions/matrix/src/matrix/probe.ts @@ -1,4 +1,4 @@ -import type { BaseProbeResult } from "openclaw/plugin-sdk"; +import type { BaseProbeResult } from "openclaw/plugin-sdk/compat"; import { createMatrixClient, isBunRuntime } from "./client.js"; export type MatrixProbe = BaseProbeResult & { diff --git a/extensions/matrix/src/matrix/send.test.ts b/extensions/matrix/src/matrix/send.test.ts index 234c9950216..74af672c9d2 100644 --- a/extensions/matrix/src/matrix/send.test.ts +++ b/extensions/matrix/src/matrix/send.test.ts @@ -1,4 +1,4 @@ -import type { PluginRuntime } from "openclaw/plugin-sdk"; +import type { PluginRuntime } from "openclaw/plugin-sdk/compat"; import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { setMatrixRuntime } from "../runtime.js"; diff --git a/extensions/matrix/src/matrix/send.ts b/extensions/matrix/src/matrix/send.ts index 80c1c120333..31cc2bdab52 100644 --- a/extensions/matrix/src/matrix/send.ts +++ b/extensions/matrix/src/matrix/send.ts @@ -1,5 +1,5 @@ import type { MatrixClient } from "@vector-im/matrix-bot-sdk"; -import type { PollInput } from "openclaw/plugin-sdk"; +import type { PollInput } from "openclaw/plugin-sdk/compat"; import { getMatrixRuntime } from "../runtime.js"; import { buildPollStartContent, M_POLL_START } from "./poll-types.js"; import { enqueueSend } from "./send-queue.js"; diff --git a/extensions/matrix/src/onboarding.ts b/extensions/matrix/src/onboarding.ts index 1b2b9cf5ca3..a55cc5676f5 100644 --- a/extensions/matrix/src/onboarding.ts +++ b/extensions/matrix/src/onboarding.ts @@ -1,4 +1,4 @@ -import type { DmPolicy } from "openclaw/plugin-sdk"; +import type { DmPolicy } from "openclaw/plugin-sdk/compat"; import { addWildcardAllowFrom, formatResolvedUnresolvedNote, @@ -11,7 +11,7 @@ import { type ChannelOnboardingAdapter, type ChannelOnboardingDmPolicy, type WizardPrompter, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { listMatrixDirectoryGroupsLive } from "./directory-live.js"; import { resolveMatrixAccount } from "./matrix/accounts.js"; import { ensureMatrixSdkInstalled, isMatrixSdkAvailable } from "./matrix/deps.js"; diff --git a/extensions/matrix/src/outbound.ts b/extensions/matrix/src/outbound.ts index 34d084c609b..547f053c1d3 100644 --- a/extensions/matrix/src/outbound.ts +++ b/extensions/matrix/src/outbound.ts @@ -1,4 +1,4 @@ -import type { ChannelOutboundAdapter } from "openclaw/plugin-sdk"; +import type { ChannelOutboundAdapter } from "openclaw/plugin-sdk/compat"; import { sendMessageMatrix, sendPollMatrix } from "./matrix/send.js"; import { getMatrixRuntime } from "./runtime.js"; diff --git a/extensions/matrix/src/resolve-targets.test.ts b/extensions/matrix/src/resolve-targets.test.ts index 3d6310534f8..750ee73a7b1 100644 --- a/extensions/matrix/src/resolve-targets.test.ts +++ b/extensions/matrix/src/resolve-targets.test.ts @@ -1,4 +1,4 @@ -import type { ChannelDirectoryEntry } from "openclaw/plugin-sdk"; +import type { ChannelDirectoryEntry } from "openclaw/plugin-sdk/compat"; import { describe, expect, it, vi, beforeEach } from "vitest"; import { listMatrixDirectoryGroupsLive, listMatrixDirectoryPeersLive } from "./directory-live.js"; import { resolveMatrixTargets } from "./resolve-targets.js"; diff --git a/extensions/matrix/src/resolve-targets.ts b/extensions/matrix/src/resolve-targets.ts index fb111da0c74..2da1eb12c1c 100644 --- a/extensions/matrix/src/resolve-targets.ts +++ b/extensions/matrix/src/resolve-targets.ts @@ -3,7 +3,7 @@ import type { ChannelResolveKind, ChannelResolveResult, RuntimeEnv, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { listMatrixDirectoryGroupsLive, listMatrixDirectoryPeersLive } from "./directory-live.js"; function findExactDirectoryMatches( diff --git a/extensions/matrix/src/runtime.ts b/extensions/matrix/src/runtime.ts index 62eff71ad17..504566a0868 100644 --- a/extensions/matrix/src/runtime.ts +++ b/extensions/matrix/src/runtime.ts @@ -1,4 +1,4 @@ -import type { PluginRuntime } from "openclaw/plugin-sdk"; +import type { PluginRuntime } from "openclaw/plugin-sdk/compat"; let runtime: PluginRuntime | null = null; diff --git a/extensions/matrix/src/secret-input.ts b/extensions/matrix/src/secret-input.ts index f90d41c6fb9..3fc82b3ac91 100644 --- a/extensions/matrix/src/secret-input.ts +++ b/extensions/matrix/src/secret-input.ts @@ -2,7 +2,7 @@ import { hasConfiguredSecretInput, normalizeResolvedSecretInputString, normalizeSecretInputString, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { z } from "zod"; export { hasConfiguredSecretInput, normalizeResolvedSecretInputString, normalizeSecretInputString }; diff --git a/extensions/matrix/src/tool-actions.ts b/extensions/matrix/src/tool-actions.ts index 7105058a44e..5f4f2830312 100644 --- a/extensions/matrix/src/tool-actions.ts +++ b/extensions/matrix/src/tool-actions.ts @@ -5,7 +5,7 @@ import { readNumberParam, readReactionParams, readStringParam, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { deleteMatrixMessage, editMatrixMessage, diff --git a/extensions/matrix/src/types.ts b/extensions/matrix/src/types.ts index d7501f80b50..28e4711319f 100644 --- a/extensions/matrix/src/types.ts +++ b/extensions/matrix/src/types.ts @@ -1,4 +1,4 @@ -import type { DmPolicy, GroupPolicy, SecretInput } from "openclaw/plugin-sdk"; +import type { DmPolicy, GroupPolicy, SecretInput } from "openclaw/plugin-sdk/compat"; export type { DmPolicy, GroupPolicy }; export type ReplyToMode = "off" | "first" | "all"; From 009d4d115ada6825e6b6a8ac0b08d24d50d3da16 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 4 Mar 2026 01:20:11 -0500 Subject: [PATCH 15/74] Extensions: migrate mattermost plugin-sdk imports --- extensions/mattermost/src/channel.test.ts | 4 ++-- extensions/mattermost/src/channel.ts | 2 +- extensions/mattermost/src/config-schema.ts | 2 +- extensions/mattermost/src/group-mentions.ts | 2 +- extensions/mattermost/src/mattermost/accounts.test.ts | 2 +- extensions/mattermost/src/mattermost/accounts.ts | 2 +- extensions/mattermost/src/mattermost/monitor-auth.ts | 5 ++++- extensions/mattermost/src/mattermost/monitor-helpers.ts | 4 ++-- .../mattermost/src/mattermost/monitor-websocket.test.ts | 2 +- extensions/mattermost/src/mattermost/monitor-websocket.ts | 2 +- extensions/mattermost/src/mattermost/monitor.authz.test.ts | 2 +- extensions/mattermost/src/mattermost/monitor.ts | 4 ++-- extensions/mattermost/src/mattermost/probe.ts | 2 +- .../mattermost/src/mattermost/reactions.test-helpers.ts | 2 +- extensions/mattermost/src/mattermost/reactions.ts | 2 +- extensions/mattermost/src/mattermost/send.ts | 2 +- extensions/mattermost/src/mattermost/slash-http.test.ts | 2 +- extensions/mattermost/src/mattermost/slash-http.ts | 4 ++-- extensions/mattermost/src/mattermost/slash-state.ts | 6 +++--- extensions/mattermost/src/onboarding-helpers.ts | 2 +- extensions/mattermost/src/onboarding.status.test.ts | 2 +- extensions/mattermost/src/onboarding.ts | 4 ++-- extensions/mattermost/src/runtime.ts | 2 +- extensions/mattermost/src/secret-input.ts | 2 +- extensions/mattermost/src/types.ts | 2 +- 25 files changed, 35 insertions(+), 32 deletions(-) diff --git a/extensions/mattermost/src/channel.test.ts b/extensions/mattermost/src/channel.test.ts index c448438278f..b0a24137c4d 100644 --- a/extensions/mattermost/src/channel.test.ts +++ b/extensions/mattermost/src/channel.test.ts @@ -1,5 +1,5 @@ -import type { OpenClawConfig } from "openclaw/plugin-sdk"; -import { createReplyPrefixOptions } from "openclaw/plugin-sdk"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; +import { createReplyPrefixOptions } from "openclaw/plugin-sdk/compat"; import { beforeEach, describe, expect, it, vi } from "vitest"; const { sendMessageMattermostMock } = vi.hoisted(() => ({ sendMessageMattermostMock: vi.fn(), diff --git a/extensions/mattermost/src/channel.ts b/extensions/mattermost/src/channel.ts index 9d28814fc51..a45e7b57e5b 100644 --- a/extensions/mattermost/src/channel.ts +++ b/extensions/mattermost/src/channel.ts @@ -12,7 +12,7 @@ import { type ChannelMessageActionAdapter, type ChannelMessageActionName, type ChannelPlugin, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { MattermostConfigSchema } from "./config-schema.js"; import { resolveMattermostGroupRequireMention } from "./group-mentions.js"; import { diff --git a/extensions/mattermost/src/config-schema.ts b/extensions/mattermost/src/config-schema.ts index 837facb5587..a3dd07900e2 100644 --- a/extensions/mattermost/src/config-schema.ts +++ b/extensions/mattermost/src/config-schema.ts @@ -4,7 +4,7 @@ import { GroupPolicySchema, MarkdownConfigSchema, requireOpenAllowFrom, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { z } from "zod"; import { buildSecretInputSchema } from "./secret-input.js"; diff --git a/extensions/mattermost/src/group-mentions.ts b/extensions/mattermost/src/group-mentions.ts index c92da2000c0..6f57c8f7970 100644 --- a/extensions/mattermost/src/group-mentions.ts +++ b/extensions/mattermost/src/group-mentions.ts @@ -1,4 +1,4 @@ -import type { ChannelGroupContext } from "openclaw/plugin-sdk"; +import type { ChannelGroupContext } from "openclaw/plugin-sdk/compat"; import { resolveMattermostAccount } from "./mattermost/accounts.js"; export function resolveMattermostGroupRequireMention( diff --git a/extensions/mattermost/src/mattermost/accounts.test.ts b/extensions/mattermost/src/mattermost/accounts.test.ts index 2fd6b253163..29011495398 100644 --- a/extensions/mattermost/src/mattermost/accounts.test.ts +++ b/extensions/mattermost/src/mattermost/accounts.test.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig } from "openclaw/plugin-sdk"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; import { describe, expect, it } from "vitest"; import { resolveDefaultMattermostAccountId } from "./accounts.js"; diff --git a/extensions/mattermost/src/mattermost/accounts.ts b/extensions/mattermost/src/mattermost/accounts.ts index ca120d08c6b..521bab481df 100644 --- a/extensions/mattermost/src/mattermost/accounts.ts +++ b/extensions/mattermost/src/mattermost/accounts.ts @@ -1,9 +1,9 @@ -import type { OpenClawConfig } from "openclaw/plugin-sdk"; import { DEFAULT_ACCOUNT_ID, normalizeAccountId, normalizeOptionalAccountId, } from "openclaw/plugin-sdk/account-id"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; import { normalizeResolvedSecretInputString, normalizeSecretInputString } from "../secret-input.js"; import type { MattermostAccountConfig, MattermostChatMode } from "../types.js"; import { normalizeMattermostBaseUrl } from "./client.js"; diff --git a/extensions/mattermost/src/mattermost/monitor-auth.ts b/extensions/mattermost/src/mattermost/monitor-auth.ts index 2b968c5f117..edf945dbd16 100644 --- a/extensions/mattermost/src/mattermost/monitor-auth.ts +++ b/extensions/mattermost/src/mattermost/monitor-auth.ts @@ -1,4 +1,7 @@ -import { resolveAllowlistMatchSimple, resolveEffectiveAllowFromLists } from "openclaw/plugin-sdk"; +import { + resolveAllowlistMatchSimple, + resolveEffectiveAllowFromLists, +} from "openclaw/plugin-sdk/compat"; export function normalizeMattermostAllowEntry(entry: string): string { const trimmed = entry.trim(); diff --git a/extensions/mattermost/src/mattermost/monitor-helpers.ts b/extensions/mattermost/src/mattermost/monitor-helpers.ts index d645d563d38..74d6f7689df 100644 --- a/extensions/mattermost/src/mattermost/monitor-helpers.ts +++ b/extensions/mattermost/src/mattermost/monitor-helpers.ts @@ -2,8 +2,8 @@ import { formatInboundFromLabel as formatInboundFromLabelShared, resolveThreadSessionKeys as resolveThreadSessionKeysShared, type OpenClawConfig, -} from "openclaw/plugin-sdk"; -export { createDedupeCache, rawDataToString } from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; +export { createDedupeCache, rawDataToString } from "openclaw/plugin-sdk/compat"; export type ResponsePrefixContext = { model?: string; diff --git a/extensions/mattermost/src/mattermost/monitor-websocket.test.ts b/extensions/mattermost/src/mattermost/monitor-websocket.test.ts index 8311092ff94..02b9f6a2742 100644 --- a/extensions/mattermost/src/mattermost/monitor-websocket.test.ts +++ b/extensions/mattermost/src/mattermost/monitor-websocket.test.ts @@ -1,4 +1,4 @@ -import type { RuntimeEnv } from "openclaw/plugin-sdk"; +import type { RuntimeEnv } from "openclaw/plugin-sdk/compat"; import { describe, expect, it, vi } from "vitest"; import { createMattermostConnectOnce, diff --git a/extensions/mattermost/src/mattermost/monitor-websocket.ts b/extensions/mattermost/src/mattermost/monitor-websocket.ts index 19494c1a01b..cfe5ab96fdc 100644 --- a/extensions/mattermost/src/mattermost/monitor-websocket.ts +++ b/extensions/mattermost/src/mattermost/monitor-websocket.ts @@ -1,4 +1,4 @@ -import type { ChannelAccountSnapshot, RuntimeEnv } from "openclaw/plugin-sdk"; +import type { ChannelAccountSnapshot, RuntimeEnv } from "openclaw/plugin-sdk/compat"; import WebSocket from "ws"; import type { MattermostPost } from "./client.js"; import { rawDataToString } from "./monitor-helpers.js"; diff --git a/extensions/mattermost/src/mattermost/monitor.authz.test.ts b/extensions/mattermost/src/mattermost/monitor.authz.test.ts index 9b6a296a34e..9ee7071dac5 100644 --- a/extensions/mattermost/src/mattermost/monitor.authz.test.ts +++ b/extensions/mattermost/src/mattermost/monitor.authz.test.ts @@ -1,4 +1,4 @@ -import { resolveControlCommandGate } from "openclaw/plugin-sdk"; +import { resolveControlCommandGate } from "openclaw/plugin-sdk/compat"; import { describe, expect, it } from "vitest"; import { resolveMattermostEffectiveAllowFromLists } from "./monitor-auth.js"; diff --git a/extensions/mattermost/src/mattermost/monitor.ts b/extensions/mattermost/src/mattermost/monitor.ts index 6ad677cf131..d62ddd80896 100644 --- a/extensions/mattermost/src/mattermost/monitor.ts +++ b/extensions/mattermost/src/mattermost/monitor.ts @@ -4,7 +4,7 @@ import type { OpenClawConfig, ReplyPayload, RuntimeEnv, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { buildAgentMediaPayload, DM_GROUP_ACCESS_REASON, @@ -27,7 +27,7 @@ import { warnMissingProviderGroupPolicyFallbackOnce, listSkillCommandsForAgents, type HistoryEntry, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { getMattermostRuntime } from "../runtime.js"; import { resolveMattermostAccount } from "./accounts.js"; import { diff --git a/extensions/mattermost/src/mattermost/probe.ts b/extensions/mattermost/src/mattermost/probe.ts index eda98b21c0e..f9ec2005af6 100644 --- a/extensions/mattermost/src/mattermost/probe.ts +++ b/extensions/mattermost/src/mattermost/probe.ts @@ -1,4 +1,4 @@ -import type { BaseProbeResult } from "openclaw/plugin-sdk"; +import type { BaseProbeResult } from "openclaw/plugin-sdk/compat"; import { normalizeMattermostBaseUrl, readMattermostError, type MattermostUser } from "./client.js"; export type MattermostProbe = BaseProbeResult & { diff --git a/extensions/mattermost/src/mattermost/reactions.test-helpers.ts b/extensions/mattermost/src/mattermost/reactions.test-helpers.ts index 3556067167f..a19f9b00222 100644 --- a/extensions/mattermost/src/mattermost/reactions.test-helpers.ts +++ b/extensions/mattermost/src/mattermost/reactions.test-helpers.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig } from "openclaw/plugin-sdk"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; import { expect, vi } from "vitest"; export function createMattermostTestConfig(): OpenClawConfig { diff --git a/extensions/mattermost/src/mattermost/reactions.ts b/extensions/mattermost/src/mattermost/reactions.ts index cc67e639851..7bb3d5eca08 100644 --- a/extensions/mattermost/src/mattermost/reactions.ts +++ b/extensions/mattermost/src/mattermost/reactions.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig } from "openclaw/plugin-sdk"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; import { resolveMattermostAccount } from "./accounts.js"; import { createMattermostClient, fetchMattermostMe, type MattermostClient } from "./client.js"; diff --git a/extensions/mattermost/src/mattermost/send.ts b/extensions/mattermost/src/mattermost/send.ts index b325895e58d..e7805f39308 100644 --- a/extensions/mattermost/src/mattermost/send.ts +++ b/extensions/mattermost/src/mattermost/send.ts @@ -1,4 +1,4 @@ -import { loadOutboundMediaFromUrl, type OpenClawConfig } from "openclaw/plugin-sdk"; +import { loadOutboundMediaFromUrl, type OpenClawConfig } from "openclaw/plugin-sdk/compat"; import { getMattermostRuntime } from "../runtime.js"; import { resolveMattermostAccount } from "./accounts.js"; import { diff --git a/extensions/mattermost/src/mattermost/slash-http.test.ts b/extensions/mattermost/src/mattermost/slash-http.test.ts index c4469b9cad9..7592de3c4dd 100644 --- a/extensions/mattermost/src/mattermost/slash-http.test.ts +++ b/extensions/mattermost/src/mattermost/slash-http.test.ts @@ -1,6 +1,6 @@ import type { IncomingMessage, ServerResponse } from "node:http"; import { PassThrough } from "node:stream"; -import type { OpenClawConfig, RuntimeEnv } from "openclaw/plugin-sdk"; +import type { OpenClawConfig, RuntimeEnv } from "openclaw/plugin-sdk/compat"; import { describe, expect, it } from "vitest"; import type { ResolvedMattermostAccount } from "./accounts.js"; import { createSlashCommandHttpHandler } from "./slash-http.js"; diff --git a/extensions/mattermost/src/mattermost/slash-http.ts b/extensions/mattermost/src/mattermost/slash-http.ts index a454b5c670a..49f0e89c1ea 100644 --- a/extensions/mattermost/src/mattermost/slash-http.ts +++ b/extensions/mattermost/src/mattermost/slash-http.ts @@ -6,14 +6,14 @@ */ import type { IncomingMessage, ServerResponse } from "node:http"; -import type { OpenClawConfig, ReplyPayload, RuntimeEnv } from "openclaw/plugin-sdk"; +import type { OpenClawConfig, ReplyPayload, RuntimeEnv } from "openclaw/plugin-sdk/compat"; import { createReplyPrefixOptions, createTypingCallbacks, isDangerousNameMatchingEnabled, logTypingFailure, resolveControlCommandGate, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import type { ResolvedMattermostAccount } from "../mattermost/accounts.js"; import { getMattermostRuntime } from "../runtime.js"; import { diff --git a/extensions/mattermost/src/mattermost/slash-state.ts b/extensions/mattermost/src/mattermost/slash-state.ts index 26a2ed029c6..2ecf9eba2bf 100644 --- a/extensions/mattermost/src/mattermost/slash-state.ts +++ b/extensions/mattermost/src/mattermost/slash-state.ts @@ -10,7 +10,7 @@ */ import type { IncomingMessage, ServerResponse } from "node:http"; -import type { OpenClawPluginApi } from "openclaw/plugin-sdk"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/compat"; import type { ResolvedMattermostAccount } from "./accounts.js"; import { resolveSlashCommandConfig, type MattermostRegisteredCommand } from "./slash-commands.js"; import { createSlashCommandHttpHandler } from "./slash-http.js"; @@ -86,8 +86,8 @@ export function activateSlashCommands(params: { registeredCommands: MattermostRegisteredCommand[]; triggerMap?: Map; api: { - cfg: import("openclaw/plugin-sdk").OpenClawConfig; - runtime: import("openclaw/plugin-sdk").RuntimeEnv; + cfg: import("openclaw/plugin-sdk/compat").OpenClawConfig; + runtime: import("openclaw/plugin-sdk/compat").RuntimeEnv; }; log?: (msg: string) => void; }) { diff --git a/extensions/mattermost/src/onboarding-helpers.ts b/extensions/mattermost/src/onboarding-helpers.ts index 796de0f1cb1..f3797c608ad 100644 --- a/extensions/mattermost/src/onboarding-helpers.ts +++ b/extensions/mattermost/src/onboarding-helpers.ts @@ -1 +1 @@ -export { promptAccountId } from "openclaw/plugin-sdk"; +export { promptAccountId } from "openclaw/plugin-sdk/compat"; diff --git a/extensions/mattermost/src/onboarding.status.test.ts b/extensions/mattermost/src/onboarding.status.test.ts index 03cb2844782..fa1cc13c382 100644 --- a/extensions/mattermost/src/onboarding.status.test.ts +++ b/extensions/mattermost/src/onboarding.status.test.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig } from "openclaw/plugin-sdk"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; import { describe, expect, it } from "vitest"; import { mattermostOnboardingAdapter } from "./onboarding.js"; diff --git a/extensions/mattermost/src/onboarding.ts b/extensions/mattermost/src/onboarding.ts index a76145213e4..c1f01b4622c 100644 --- a/extensions/mattermost/src/onboarding.ts +++ b/extensions/mattermost/src/onboarding.ts @@ -1,3 +1,4 @@ +import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id"; import { hasConfiguredSecretInput, promptSingleChannelSecretInput, @@ -5,8 +6,7 @@ import { type OpenClawConfig, type SecretInput, type WizardPrompter, -} from "openclaw/plugin-sdk"; -import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id"; +} from "openclaw/plugin-sdk/compat"; import { listMattermostAccountIds, resolveDefaultMattermostAccountId, diff --git a/extensions/mattermost/src/runtime.ts b/extensions/mattermost/src/runtime.ts index 10ae1698a05..e2cb807f30f 100644 --- a/extensions/mattermost/src/runtime.ts +++ b/extensions/mattermost/src/runtime.ts @@ -1,4 +1,4 @@ -import type { PluginRuntime } from "openclaw/plugin-sdk"; +import type { PluginRuntime } from "openclaw/plugin-sdk/compat"; let runtime: PluginRuntime | null = null; diff --git a/extensions/mattermost/src/secret-input.ts b/extensions/mattermost/src/secret-input.ts index f90d41c6fb9..3fc82b3ac91 100644 --- a/extensions/mattermost/src/secret-input.ts +++ b/extensions/mattermost/src/secret-input.ts @@ -2,7 +2,7 @@ import { hasConfiguredSecretInput, normalizeResolvedSecretInputString, normalizeSecretInputString, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { z } from "zod"; export { hasConfiguredSecretInput, normalizeResolvedSecretInputString, normalizeSecretInputString }; diff --git a/extensions/mattermost/src/types.ts b/extensions/mattermost/src/types.ts index f141695ff73..d2b50e3a510 100644 --- a/extensions/mattermost/src/types.ts +++ b/extensions/mattermost/src/types.ts @@ -3,7 +3,7 @@ import type { DmPolicy, GroupPolicy, SecretInput, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; export type MattermostChatMode = "oncall" | "onmessage" | "onchar"; From b2188092a198b600aa2321e00cf32088adf3a74d Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 4 Mar 2026 01:20:11 -0500 Subject: [PATCH 16/74] Extensions: migrate minimax-portal-auth plugin-sdk imports --- extensions/minimax-portal-auth/oauth.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/minimax-portal-auth/oauth.ts b/extensions/minimax-portal-auth/oauth.ts index ac387f72d14..016af72dbd5 100644 --- a/extensions/minimax-portal-auth/oauth.ts +++ b/extensions/minimax-portal-auth/oauth.ts @@ -1,5 +1,5 @@ import { randomBytes, randomUUID } from "node:crypto"; -import { generatePkceVerifierChallenge, toFormUrlEncoded } from "openclaw/plugin-sdk"; +import { generatePkceVerifierChallenge, toFormUrlEncoded } from "openclaw/plugin-sdk/compat"; export type MiniMaxRegion = "cn" | "global"; From 10bd6ae3c8e110027208aa3ff0b1d6d0b5363afb Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 4 Mar 2026 01:20:12 -0500 Subject: [PATCH 17/74] Extensions: migrate msteams plugin-sdk imports --- extensions/msteams/src/attachments.test.ts | 2 +- extensions/msteams/src/attachments/graph.ts | 2 +- extensions/msteams/src/attachments/payload.ts | 2 +- extensions/msteams/src/attachments/remote-media.ts | 2 +- extensions/msteams/src/attachments/shared.ts | 4 ++-- extensions/msteams/src/channel.directory.test.ts | 2 +- extensions/msteams/src/channel.ts | 8 ++++++-- extensions/msteams/src/directory-live.ts | 2 +- extensions/msteams/src/file-lock.ts | 2 +- extensions/msteams/src/graph.ts | 2 +- extensions/msteams/src/media-helpers.ts | 2 +- extensions/msteams/src/messenger.test.ts | 2 +- extensions/msteams/src/messenger.ts | 2 +- .../msteams/src/monitor-handler.file-consent.test.ts | 2 +- extensions/msteams/src/monitor-handler.ts | 2 +- .../src/monitor-handler/message-handler.authz.test.ts | 2 +- extensions/msteams/src/monitor-handler/message-handler.ts | 2 +- extensions/msteams/src/monitor.lifecycle.test.ts | 2 +- extensions/msteams/src/monitor.ts | 2 +- extensions/msteams/src/onboarding.ts | 4 ++-- extensions/msteams/src/outbound.ts | 2 +- extensions/msteams/src/policy.test.ts | 2 +- extensions/msteams/src/policy.ts | 4 ++-- extensions/msteams/src/probe.test.ts | 2 +- extensions/msteams/src/probe.ts | 2 +- extensions/msteams/src/reply-dispatcher.ts | 2 +- extensions/msteams/src/runtime.ts | 2 +- extensions/msteams/src/secret-input.ts | 2 +- extensions/msteams/src/send-context.ts | 2 +- extensions/msteams/src/send.test.ts | 2 +- extensions/msteams/src/send.ts | 4 ++-- extensions/msteams/src/store-fs.ts | 2 +- extensions/msteams/src/test-runtime.ts | 2 +- extensions/msteams/src/token.ts | 2 +- 34 files changed, 43 insertions(+), 39 deletions(-) diff --git a/extensions/msteams/src/attachments.test.ts b/extensions/msteams/src/attachments.test.ts index 97ace8819c9..9f0de10992f 100644 --- a/extensions/msteams/src/attachments.test.ts +++ b/extensions/msteams/src/attachments.test.ts @@ -1,4 +1,4 @@ -import type { PluginRuntime, SsrFPolicy } from "openclaw/plugin-sdk"; +import type { PluginRuntime, SsrFPolicy } from "openclaw/plugin-sdk/compat"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { createPluginRuntimeMock } from "../../test-utils/plugin-runtime-mock.js"; import { diff --git a/extensions/msteams/src/attachments/graph.ts b/extensions/msteams/src/attachments/graph.ts index a50356e3ced..c9f632c7f38 100644 --- a/extensions/msteams/src/attachments/graph.ts +++ b/extensions/msteams/src/attachments/graph.ts @@ -1,4 +1,4 @@ -import { fetchWithSsrFGuard, type SsrFPolicy } from "openclaw/plugin-sdk"; +import { fetchWithSsrFGuard, type SsrFPolicy } from "openclaw/plugin-sdk/compat"; import { getMSTeamsRuntime } from "../runtime.js"; import { downloadMSTeamsAttachments } from "./download.js"; import { downloadAndStoreMSTeamsRemoteMedia } from "./remote-media.js"; diff --git a/extensions/msteams/src/attachments/payload.ts b/extensions/msteams/src/attachments/payload.ts index 2049609d894..2134a382f0c 100644 --- a/extensions/msteams/src/attachments/payload.ts +++ b/extensions/msteams/src/attachments/payload.ts @@ -1,4 +1,4 @@ -import { buildMediaPayload } from "openclaw/plugin-sdk"; +import { buildMediaPayload } from "openclaw/plugin-sdk/compat"; export function buildMSTeamsMediaPayload( mediaList: Array<{ path: string; contentType?: string }>, diff --git a/extensions/msteams/src/attachments/remote-media.ts b/extensions/msteams/src/attachments/remote-media.ts index 162a797b57f..b31b47723b9 100644 --- a/extensions/msteams/src/attachments/remote-media.ts +++ b/extensions/msteams/src/attachments/remote-media.ts @@ -1,4 +1,4 @@ -import type { SsrFPolicy } from "openclaw/plugin-sdk"; +import type { SsrFPolicy } from "openclaw/plugin-sdk/compat"; import { getMSTeamsRuntime } from "../runtime.js"; import { inferPlaceholder } from "./shared.js"; import type { MSTeamsInboundMedia } from "./types.js"; diff --git a/extensions/msteams/src/attachments/shared.ts b/extensions/msteams/src/attachments/shared.ts index 7897b52803e..3222be248bc 100644 --- a/extensions/msteams/src/attachments/shared.ts +++ b/extensions/msteams/src/attachments/shared.ts @@ -4,8 +4,8 @@ import { isHttpsUrlAllowedByHostnameSuffixAllowlist, isPrivateIpAddress, normalizeHostnameSuffixAllowlist, -} from "openclaw/plugin-sdk"; -import type { SsrFPolicy } from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; +import type { SsrFPolicy } from "openclaw/plugin-sdk/compat"; import type { MSTeamsAttachmentLike } from "./types.js"; type InlineImageCandidate = diff --git a/extensions/msteams/src/channel.directory.test.ts b/extensions/msteams/src/channel.directory.test.ts index 26a9bec2f5d..97bfd227f5f 100644 --- a/extensions/msteams/src/channel.directory.test.ts +++ b/extensions/msteams/src/channel.directory.test.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig, RuntimeEnv } from "openclaw/plugin-sdk"; +import type { OpenClawConfig, RuntimeEnv } from "openclaw/plugin-sdk/compat"; import { describe, expect, it } from "vitest"; import { msteamsPlugin } from "./channel.js"; diff --git a/extensions/msteams/src/channel.ts b/extensions/msteams/src/channel.ts index 16c7ad0fb49..057f37c83a4 100644 --- a/extensions/msteams/src/channel.ts +++ b/extensions/msteams/src/channel.ts @@ -1,4 +1,8 @@ -import type { ChannelMessageActionName, ChannelPlugin, OpenClawConfig } from "openclaw/plugin-sdk"; +import type { + ChannelMessageActionName, + ChannelPlugin, + OpenClawConfig, +} from "openclaw/plugin-sdk/compat"; import { buildBaseChannelStatusSummary, buildChannelConfigSchema, @@ -8,7 +12,7 @@ import { PAIRING_APPROVED_MESSAGE, resolveAllowlistProviderRuntimeGroupPolicy, resolveDefaultGroupPolicy, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { listMSTeamsDirectoryGroupsLive, listMSTeamsDirectoryPeersLive } from "./directory-live.js"; import { msteamsOnboardingAdapter } from "./onboarding.js"; import { msteamsOutbound } from "./outbound.js"; diff --git a/extensions/msteams/src/directory-live.ts b/extensions/msteams/src/directory-live.ts index 06b2485eb3b..0e2464aa0ce 100644 --- a/extensions/msteams/src/directory-live.ts +++ b/extensions/msteams/src/directory-live.ts @@ -1,4 +1,4 @@ -import type { ChannelDirectoryEntry } from "openclaw/plugin-sdk"; +import type { ChannelDirectoryEntry } from "openclaw/plugin-sdk/compat"; import { searchGraphUsers } from "./graph-users.js"; import { type GraphChannel, diff --git a/extensions/msteams/src/file-lock.ts b/extensions/msteams/src/file-lock.ts index 02bf9aa5b43..9c5782b9ff5 100644 --- a/extensions/msteams/src/file-lock.ts +++ b/extensions/msteams/src/file-lock.ts @@ -1 +1 @@ -export { withFileLock } from "openclaw/plugin-sdk"; +export { withFileLock } from "openclaw/plugin-sdk/compat"; diff --git a/extensions/msteams/src/graph.ts b/extensions/msteams/src/graph.ts index d2c21015361..983bfe9ed64 100644 --- a/extensions/msteams/src/graph.ts +++ b/extensions/msteams/src/graph.ts @@ -1,4 +1,4 @@ -import type { MSTeamsConfig } from "openclaw/plugin-sdk"; +import type { MSTeamsConfig } from "openclaw/plugin-sdk/compat"; import { GRAPH_ROOT } from "./attachments/shared.js"; import { loadMSTeamsSdkWithAuth } from "./sdk.js"; import { readAccessToken } from "./token-response.js"; diff --git a/extensions/msteams/src/media-helpers.ts b/extensions/msteams/src/media-helpers.ts index bfe113d40e9..f8a36e55f81 100644 --- a/extensions/msteams/src/media-helpers.ts +++ b/extensions/msteams/src/media-helpers.ts @@ -8,7 +8,7 @@ import { extensionForMime, extractOriginalFilename, getFileExtension, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; /** * Detect MIME type from URL extension or data URL. diff --git a/extensions/msteams/src/messenger.test.ts b/extensions/msteams/src/messenger.test.ts index 0857f8d5c3f..973bbb67973 100644 --- a/extensions/msteams/src/messenger.test.ts +++ b/extensions/msteams/src/messenger.test.ts @@ -1,7 +1,7 @@ import { mkdtemp, rm, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import { SILENT_REPLY_TOKEN, type PluginRuntime } from "openclaw/plugin-sdk"; +import { SILENT_REPLY_TOKEN, type PluginRuntime } from "openclaw/plugin-sdk/compat"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { createPluginRuntimeMock } from "../../test-utils/plugin-runtime-mock.js"; import type { StoredConversationReference } from "./conversation-store.js"; diff --git a/extensions/msteams/src/messenger.ts b/extensions/msteams/src/messenger.ts index 4a913192944..c412c47d048 100644 --- a/extensions/msteams/src/messenger.ts +++ b/extensions/msteams/src/messenger.ts @@ -7,7 +7,7 @@ import { type ReplyPayload, SILENT_REPLY_TOKEN, sleep, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import type { MSTeamsAccessTokenProvider } from "./attachments/types.js"; import type { StoredConversationReference } from "./conversation-store.js"; import { classifyMSTeamsSendError } from "./errors.js"; diff --git a/extensions/msteams/src/monitor-handler.file-consent.test.ts b/extensions/msteams/src/monitor-handler.file-consent.test.ts index 386ffc34853..8288668ba67 100644 --- a/extensions/msteams/src/monitor-handler.file-consent.test.ts +++ b/extensions/msteams/src/monitor-handler.file-consent.test.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig, PluginRuntime, RuntimeEnv } from "openclaw/plugin-sdk"; +import type { OpenClawConfig, PluginRuntime, RuntimeEnv } from "openclaw/plugin-sdk/compat"; import { beforeEach, describe, expect, it, vi } from "vitest"; import type { MSTeamsConversationStore } from "./conversation-store.js"; import type { MSTeamsAdapter } from "./messenger.js"; diff --git a/extensions/msteams/src/monitor-handler.ts b/extensions/msteams/src/monitor-handler.ts index ac1b469e8be..b64fdee6d67 100644 --- a/extensions/msteams/src/monitor-handler.ts +++ b/extensions/msteams/src/monitor-handler.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig, RuntimeEnv } from "openclaw/plugin-sdk"; +import type { OpenClawConfig, RuntimeEnv } from "openclaw/plugin-sdk/compat"; import type { MSTeamsConversationStore } from "./conversation-store.js"; import { buildFileInfoCard, parseFileConsentInvoke, uploadToConsentUrl } from "./file-consent.js"; import { normalizeMSTeamsConversationId } from "./inbound.js"; diff --git a/extensions/msteams/src/monitor-handler/message-handler.authz.test.ts b/extensions/msteams/src/monitor-handler/message-handler.authz.test.ts index 2be36f89732..7e8118b5629 100644 --- a/extensions/msteams/src/monitor-handler/message-handler.authz.test.ts +++ b/extensions/msteams/src/monitor-handler/message-handler.authz.test.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig, PluginRuntime, RuntimeEnv } from "openclaw/plugin-sdk"; +import type { OpenClawConfig, PluginRuntime, RuntimeEnv } from "openclaw/plugin-sdk/compat"; import { describe, expect, it, vi } from "vitest"; import type { MSTeamsMessageHandlerDeps } from "../monitor-handler.js"; import { setMSTeamsRuntime } from "../runtime.js"; diff --git a/extensions/msteams/src/monitor-handler/message-handler.ts b/extensions/msteams/src/monitor-handler/message-handler.ts index a85e06348b0..0bdb9142641 100644 --- a/extensions/msteams/src/monitor-handler/message-handler.ts +++ b/extensions/msteams/src/monitor-handler/message-handler.ts @@ -15,7 +15,7 @@ import { resolveEffectiveAllowFromLists, resolveDmGroupAccessWithLists, type HistoryEntry, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { buildMSTeamsAttachmentPlaceholder, buildMSTeamsMediaPayload, diff --git a/extensions/msteams/src/monitor.lifecycle.test.ts b/extensions/msteams/src/monitor.lifecycle.test.ts index 980b0871bc5..560b2839efe 100644 --- a/extensions/msteams/src/monitor.lifecycle.test.ts +++ b/extensions/msteams/src/monitor.lifecycle.test.ts @@ -1,5 +1,5 @@ import { EventEmitter } from "node:events"; -import type { OpenClawConfig, RuntimeEnv } from "openclaw/plugin-sdk"; +import type { OpenClawConfig, RuntimeEnv } from "openclaw/plugin-sdk/compat"; import { afterEach, describe, expect, it, vi } from "vitest"; import type { MSTeamsConversationStore } from "./conversation-store.js"; import type { MSTeamsPollStore } from "./polls.js"; diff --git a/extensions/msteams/src/monitor.ts b/extensions/msteams/src/monitor.ts index f2adba52139..432af67ad8d 100644 --- a/extensions/msteams/src/monitor.ts +++ b/extensions/msteams/src/monitor.ts @@ -7,7 +7,7 @@ import { summarizeMapping, type OpenClawConfig, type RuntimeEnv, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { createMSTeamsConversationStoreFs } from "./conversation-store-fs.js"; import type { MSTeamsConversationStore } from "./conversation-store.js"; import { formatUnknownError } from "./errors.js"; diff --git a/extensions/msteams/src/onboarding.ts b/extensions/msteams/src/onboarding.ts index c40d88b2bc4..e5836cc73e6 100644 --- a/extensions/msteams/src/onboarding.ts +++ b/extensions/msteams/src/onboarding.ts @@ -5,14 +5,14 @@ import type { DmPolicy, WizardPrompter, MSTeamsTeamConfig, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { addWildcardAllowFrom, DEFAULT_ACCOUNT_ID, formatDocsLink, mergeAllowFromEntries, promptChannelAccessConfig, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { parseMSTeamsTeamEntry, resolveMSTeamsChannelAllowlist, diff --git a/extensions/msteams/src/outbound.ts b/extensions/msteams/src/outbound.ts index 3a401f13d9c..ca2edc3985f 100644 --- a/extensions/msteams/src/outbound.ts +++ b/extensions/msteams/src/outbound.ts @@ -1,4 +1,4 @@ -import type { ChannelOutboundAdapter } from "openclaw/plugin-sdk"; +import type { ChannelOutboundAdapter } from "openclaw/plugin-sdk/compat"; import { createMSTeamsPollStoreFs } from "./polls.js"; import { getMSTeamsRuntime } from "./runtime.js"; import { sendMessageMSTeams, sendPollMSTeams } from "./send.js"; diff --git a/extensions/msteams/src/policy.test.ts b/extensions/msteams/src/policy.test.ts index 3c7daa58b3f..81582cb857a 100644 --- a/extensions/msteams/src/policy.test.ts +++ b/extensions/msteams/src/policy.test.ts @@ -1,4 +1,4 @@ -import type { MSTeamsConfig } from "openclaw/plugin-sdk"; +import type { MSTeamsConfig } from "openclaw/plugin-sdk/compat"; import { describe, expect, it } from "vitest"; import { isMSTeamsGroupAllowed, diff --git a/extensions/msteams/src/policy.ts b/extensions/msteams/src/policy.ts index a3545c0594f..c55a8433b17 100644 --- a/extensions/msteams/src/policy.ts +++ b/extensions/msteams/src/policy.ts @@ -7,7 +7,7 @@ import type { MSTeamsConfig, MSTeamsReplyStyle, MSTeamsTeamConfig, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { buildChannelKeyCandidates, normalizeChannelSlug, @@ -15,7 +15,7 @@ import { resolveToolsBySender, resolveChannelEntryMatchWithFallback, resolveNestedAllowlistDecision, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; export type MSTeamsResolvedRouteConfig = { teamConfig?: MSTeamsTeamConfig; diff --git a/extensions/msteams/src/probe.test.ts b/extensions/msteams/src/probe.test.ts index b9c18019ac5..9ab758b2709 100644 --- a/extensions/msteams/src/probe.test.ts +++ b/extensions/msteams/src/probe.test.ts @@ -1,4 +1,4 @@ -import type { MSTeamsConfig } from "openclaw/plugin-sdk"; +import type { MSTeamsConfig } from "openclaw/plugin-sdk/compat"; import { describe, expect, it, vi } from "vitest"; const hostMockState = vi.hoisted(() => ({ diff --git a/extensions/msteams/src/probe.ts b/extensions/msteams/src/probe.ts index 8434fa50416..46dd2747785 100644 --- a/extensions/msteams/src/probe.ts +++ b/extensions/msteams/src/probe.ts @@ -1,4 +1,4 @@ -import type { BaseProbeResult, MSTeamsConfig } from "openclaw/plugin-sdk"; +import type { BaseProbeResult, MSTeamsConfig } from "openclaw/plugin-sdk/compat"; import { formatUnknownError } from "./errors.js"; import { loadMSTeamsSdkWithAuth } from "./sdk.js"; import { readAccessToken } from "./token-response.js"; diff --git a/extensions/msteams/src/reply-dispatcher.ts b/extensions/msteams/src/reply-dispatcher.ts index 3ddf7b18c5e..e940fd23738 100644 --- a/extensions/msteams/src/reply-dispatcher.ts +++ b/extensions/msteams/src/reply-dispatcher.ts @@ -6,7 +6,7 @@ import { type OpenClawConfig, type MSTeamsReplyStyle, type RuntimeEnv, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import type { MSTeamsAccessTokenProvider } from "./attachments/types.js"; import type { StoredConversationReference } from "./conversation-store.js"; import { diff --git a/extensions/msteams/src/runtime.ts b/extensions/msteams/src/runtime.ts index deb09f3ebc8..86c8f9a34a3 100644 --- a/extensions/msteams/src/runtime.ts +++ b/extensions/msteams/src/runtime.ts @@ -1,4 +1,4 @@ -import type { PluginRuntime } from "openclaw/plugin-sdk"; +import type { PluginRuntime } from "openclaw/plugin-sdk/compat"; let runtime: PluginRuntime | null = null; diff --git a/extensions/msteams/src/secret-input.ts b/extensions/msteams/src/secret-input.ts index 0e24edc05b3..fc64ca00fd1 100644 --- a/extensions/msteams/src/secret-input.ts +++ b/extensions/msteams/src/secret-input.ts @@ -2,6 +2,6 @@ import { hasConfiguredSecretInput, normalizeResolvedSecretInputString, normalizeSecretInputString, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; export { hasConfiguredSecretInput, normalizeResolvedSecretInputString, normalizeSecretInputString }; diff --git a/extensions/msteams/src/send-context.ts b/extensions/msteams/src/send-context.ts index af617a7150f..389aed43b91 100644 --- a/extensions/msteams/src/send-context.ts +++ b/extensions/msteams/src/send-context.ts @@ -2,7 +2,7 @@ import { resolveChannelMediaMaxBytes, type OpenClawConfig, type PluginRuntime, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import type { MSTeamsAccessTokenProvider } from "./attachments/types.js"; import { createMSTeamsConversationStoreFs } from "./conversation-store-fs.js"; import type { diff --git a/extensions/msteams/src/send.test.ts b/extensions/msteams/src/send.test.ts index cbab8459dd9..3c826310c58 100644 --- a/extensions/msteams/src/send.test.ts +++ b/extensions/msteams/src/send.test.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig } from "openclaw/plugin-sdk"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { sendMessageMSTeams } from "./send.js"; diff --git a/extensions/msteams/src/send.ts b/extensions/msteams/src/send.ts index 2ddb12df116..3adb3a1436c 100644 --- a/extensions/msteams/src/send.ts +++ b/extensions/msteams/src/send.ts @@ -1,5 +1,5 @@ -import type { OpenClawConfig } from "openclaw/plugin-sdk"; -import { loadOutboundMediaFromUrl } from "openclaw/plugin-sdk"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; +import { loadOutboundMediaFromUrl } from "openclaw/plugin-sdk/compat"; import { createMSTeamsConversationStoreFs } from "./conversation-store-fs.js"; import { classifyMSTeamsSendError, diff --git a/extensions/msteams/src/store-fs.ts b/extensions/msteams/src/store-fs.ts index c13c7dd55e1..c96e96d898c 100644 --- a/extensions/msteams/src/store-fs.ts +++ b/extensions/msteams/src/store-fs.ts @@ -1,5 +1,5 @@ import fs from "node:fs"; -import { readJsonFileWithFallback, writeJsonFileAtomically } from "openclaw/plugin-sdk"; +import { readJsonFileWithFallback, writeJsonFileAtomically } from "openclaw/plugin-sdk/compat"; import { withFileLock as withPathLock } from "./file-lock.js"; const STORE_LOCK_OPTIONS = { diff --git a/extensions/msteams/src/test-runtime.ts b/extensions/msteams/src/test-runtime.ts index e32a8288ac2..6cc4800350a 100644 --- a/extensions/msteams/src/test-runtime.ts +++ b/extensions/msteams/src/test-runtime.ts @@ -1,6 +1,6 @@ import os from "node:os"; import path from "node:path"; -import type { PluginRuntime } from "openclaw/plugin-sdk"; +import type { PluginRuntime } from "openclaw/plugin-sdk/compat"; export const msteamsRuntimeStub = { state: { diff --git a/extensions/msteams/src/token.ts b/extensions/msteams/src/token.ts index c5514699375..862030ba086 100644 --- a/extensions/msteams/src/token.ts +++ b/extensions/msteams/src/token.ts @@ -1,4 +1,4 @@ -import type { MSTeamsConfig } from "openclaw/plugin-sdk"; +import type { MSTeamsConfig } from "openclaw/plugin-sdk/compat"; import { hasConfiguredSecretInput, normalizeResolvedSecretInputString, From ed29472af6176f4ef3705c9e6e85c132e5a95e6a Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 4 Mar 2026 01:20:13 -0500 Subject: [PATCH 18/74] Extensions: migrate nextcloud-talk plugin-sdk imports --- extensions/nextcloud-talk/src/accounts.ts | 8 ++++---- extensions/nextcloud-talk/src/channel.ts | 2 +- extensions/nextcloud-talk/src/config-schema.ts | 2 +- extensions/nextcloud-talk/src/inbound.authz.test.ts | 2 +- extensions/nextcloud-talk/src/inbound.ts | 2 +- extensions/nextcloud-talk/src/monitor.ts | 2 +- extensions/nextcloud-talk/src/onboarding.ts | 2 +- extensions/nextcloud-talk/src/policy.ts | 4 ++-- extensions/nextcloud-talk/src/replay-guard.ts | 2 +- extensions/nextcloud-talk/src/room-info.ts | 4 ++-- extensions/nextcloud-talk/src/runtime.ts | 2 +- extensions/nextcloud-talk/src/secret-input.ts | 2 +- extensions/nextcloud-talk/src/types.ts | 2 +- 13 files changed, 18 insertions(+), 18 deletions(-) diff --git a/extensions/nextcloud-talk/src/accounts.ts b/extensions/nextcloud-talk/src/accounts.ts index 14d71ca5109..9ec61f59384 100644 --- a/extensions/nextcloud-talk/src/accounts.ts +++ b/extensions/nextcloud-talk/src/accounts.ts @@ -1,13 +1,13 @@ import { readFileSync } from "node:fs"; -import { - listConfiguredAccountIds as listConfiguredAccountIdsFromSection, - resolveAccountWithDefaultFallback, -} from "openclaw/plugin-sdk"; import { DEFAULT_ACCOUNT_ID, normalizeAccountId, normalizeOptionalAccountId, } from "openclaw/plugin-sdk/account-id"; +import { + listConfiguredAccountIds as listConfiguredAccountIdsFromSection, + resolveAccountWithDefaultFallback, +} from "openclaw/plugin-sdk/compat"; import { normalizeResolvedSecretInputString } from "./secret-input.js"; import type { CoreConfig, NextcloudTalkAccountConfig } from "./types.js"; diff --git a/extensions/nextcloud-talk/src/channel.ts b/extensions/nextcloud-talk/src/channel.ts index 32f4fc9306c..ac3591e3806 100644 --- a/extensions/nextcloud-talk/src/channel.ts +++ b/extensions/nextcloud-talk/src/channel.ts @@ -11,7 +11,7 @@ import { type ChannelPlugin, type OpenClawConfig, type ChannelSetupInput, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { waitForAbortSignal } from "../../../src/infra/abort-signal.js"; import { listNextcloudTalkAccountIds, diff --git a/extensions/nextcloud-talk/src/config-schema.ts b/extensions/nextcloud-talk/src/config-schema.ts index 52fab42c47c..c683fb6b562 100644 --- a/extensions/nextcloud-talk/src/config-schema.ts +++ b/extensions/nextcloud-talk/src/config-schema.ts @@ -7,7 +7,7 @@ import { ReplyRuntimeConfigSchemaShape, ToolPolicySchema, requireOpenAllowFrom, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { z } from "zod"; import { buildSecretInputSchema } from "./secret-input.js"; diff --git a/extensions/nextcloud-talk/src/inbound.authz.test.ts b/extensions/nextcloud-talk/src/inbound.authz.test.ts index 6ceca861ad8..be64f0968c0 100644 --- a/extensions/nextcloud-talk/src/inbound.authz.test.ts +++ b/extensions/nextcloud-talk/src/inbound.authz.test.ts @@ -1,4 +1,4 @@ -import type { PluginRuntime, RuntimeEnv } from "openclaw/plugin-sdk"; +import type { PluginRuntime, RuntimeEnv } from "openclaw/plugin-sdk/compat"; import { describe, expect, it, vi } from "vitest"; import type { ResolvedNextcloudTalkAccount } from "./accounts.js"; import { handleNextcloudTalkInbound } from "./inbound.js"; diff --git a/extensions/nextcloud-talk/src/inbound.ts b/extensions/nextcloud-talk/src/inbound.ts index 69b983b68cd..b9f9c6f98da 100644 --- a/extensions/nextcloud-talk/src/inbound.ts +++ b/extensions/nextcloud-talk/src/inbound.ts @@ -14,7 +14,7 @@ import { type OutboundReplyPayload, type OpenClawConfig, type RuntimeEnv, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import type { ResolvedNextcloudTalkAccount } from "./accounts.js"; import { normalizeNextcloudTalkAllowlist, diff --git a/extensions/nextcloud-talk/src/monitor.ts b/extensions/nextcloud-talk/src/monitor.ts index 2de886864b7..fc5c0955f45 100644 --- a/extensions/nextcloud-talk/src/monitor.ts +++ b/extensions/nextcloud-talk/src/monitor.ts @@ -6,7 +6,7 @@ import { isRequestBodyLimitError, readRequestBodyWithLimit, requestBodyErrorToText, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { resolveNextcloudTalkAccount } from "./accounts.js"; import { handleNextcloudTalkInbound } from "./inbound.js"; import { createNextcloudTalkReplayGuard } from "./replay-guard.js"; diff --git a/extensions/nextcloud-talk/src/onboarding.ts b/extensions/nextcloud-talk/src/onboarding.ts index a05a3c27ad1..a5f819d9b6a 100644 --- a/extensions/nextcloud-talk/src/onboarding.ts +++ b/extensions/nextcloud-talk/src/onboarding.ts @@ -12,7 +12,7 @@ import { type ChannelOnboardingDmPolicy, type OpenClawConfig, type WizardPrompter, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { listNextcloudTalkAccountIds, resolveDefaultNextcloudTalkAccountId, diff --git a/extensions/nextcloud-talk/src/policy.ts b/extensions/nextcloud-talk/src/policy.ts index f68d7e6989d..ae1c9b1cb73 100644 --- a/extensions/nextcloud-talk/src/policy.ts +++ b/extensions/nextcloud-talk/src/policy.ts @@ -3,14 +3,14 @@ import type { ChannelGroupContext, GroupPolicy, GroupToolPolicyConfig, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { buildChannelKeyCandidates, normalizeChannelSlug, resolveChannelEntryMatchWithFallback, resolveMentionGatingWithBypass, resolveNestedAllowlistDecision, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import type { NextcloudTalkRoomConfig } from "./types.js"; function normalizeAllowEntry(raw: string): string { diff --git a/extensions/nextcloud-talk/src/replay-guard.ts b/extensions/nextcloud-talk/src/replay-guard.ts index 14b074ed2ab..3291e80ed6a 100644 --- a/extensions/nextcloud-talk/src/replay-guard.ts +++ b/extensions/nextcloud-talk/src/replay-guard.ts @@ -1,5 +1,5 @@ import path from "node:path"; -import { createPersistentDedupe } from "openclaw/plugin-sdk"; +import { createPersistentDedupe } from "openclaw/plugin-sdk/compat"; const DEFAULT_REPLAY_TTL_MS = 24 * 60 * 60 * 1000; const DEFAULT_MEMORY_MAX_SIZE = 1_000; diff --git a/extensions/nextcloud-talk/src/room-info.ts b/extensions/nextcloud-talk/src/room-info.ts index 14b6e2dba73..a59195690e8 100644 --- a/extensions/nextcloud-talk/src/room-info.ts +++ b/extensions/nextcloud-talk/src/room-info.ts @@ -1,6 +1,6 @@ import { readFileSync } from "node:fs"; -import { fetchWithSsrFGuard } from "openclaw/plugin-sdk"; -import type { RuntimeEnv } from "openclaw/plugin-sdk"; +import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/compat"; +import type { RuntimeEnv } from "openclaw/plugin-sdk/compat"; import type { ResolvedNextcloudTalkAccount } from "./accounts.js"; import { normalizeResolvedSecretInputString } from "./secret-input.js"; diff --git a/extensions/nextcloud-talk/src/runtime.ts b/extensions/nextcloud-talk/src/runtime.ts index 61b0ea61b8f..1a56f24de10 100644 --- a/extensions/nextcloud-talk/src/runtime.ts +++ b/extensions/nextcloud-talk/src/runtime.ts @@ -1,4 +1,4 @@ -import type { PluginRuntime } from "openclaw/plugin-sdk"; +import type { PluginRuntime } from "openclaw/plugin-sdk/compat"; let runtime: PluginRuntime | null = null; diff --git a/extensions/nextcloud-talk/src/secret-input.ts b/extensions/nextcloud-talk/src/secret-input.ts index f90d41c6fb9..3fc82b3ac91 100644 --- a/extensions/nextcloud-talk/src/secret-input.ts +++ b/extensions/nextcloud-talk/src/secret-input.ts @@ -2,7 +2,7 @@ import { hasConfiguredSecretInput, normalizeResolvedSecretInputString, normalizeSecretInputString, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { z } from "zod"; export { hasConfiguredSecretInput, normalizeResolvedSecretInputString, normalizeSecretInputString }; diff --git a/extensions/nextcloud-talk/src/types.ts b/extensions/nextcloud-talk/src/types.ts index 718136f2d4b..3f7a9905399 100644 --- a/extensions/nextcloud-talk/src/types.ts +++ b/extensions/nextcloud-talk/src/types.ts @@ -4,7 +4,7 @@ import type { DmPolicy, GroupPolicy, SecretInput, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; export type { DmPolicy, GroupPolicy }; From 612ca670da55cc27b1206bc97d1a25f041510595 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 4 Mar 2026 01:20:14 -0500 Subject: [PATCH 19/74] Extensions: migrate nostr plugin-sdk imports --- extensions/nostr/src/channel.ts | 2 +- extensions/nostr/src/config-schema.ts | 2 +- extensions/nostr/src/nostr-profile-http.ts | 2 +- extensions/nostr/src/nostr-state-store.test.ts | 2 +- extensions/nostr/src/runtime.ts | 2 +- extensions/nostr/src/types.ts | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/extensions/nostr/src/channel.ts b/extensions/nostr/src/channel.ts index b7608953fc9..fef181810f2 100644 --- a/extensions/nostr/src/channel.ts +++ b/extensions/nostr/src/channel.ts @@ -5,7 +5,7 @@ import { DEFAULT_ACCOUNT_ID, formatPairingApproveHint, type ChannelPlugin, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import type { NostrProfile } from "./config-schema.js"; import { NostrConfigSchema } from "./config-schema.js"; import type { MetricEvent, MetricsSnapshot } from "./metrics.js"; diff --git a/extensions/nostr/src/config-schema.ts b/extensions/nostr/src/config-schema.ts index 45afce68163..0f94c099dca 100644 --- a/extensions/nostr/src/config-schema.ts +++ b/extensions/nostr/src/config-schema.ts @@ -1,4 +1,4 @@ -import { MarkdownConfigSchema, buildChannelConfigSchema } from "openclaw/plugin-sdk"; +import { MarkdownConfigSchema, buildChannelConfigSchema } from "openclaw/plugin-sdk/compat"; import { z } from "zod"; const allowFromEntry = z.union([z.string(), z.number()]); diff --git a/extensions/nostr/src/nostr-profile-http.ts b/extensions/nostr/src/nostr-profile-http.ts index d42d8e52ee1..d1367b0ddab 100644 --- a/extensions/nostr/src/nostr-profile-http.ts +++ b/extensions/nostr/src/nostr-profile-http.ts @@ -13,7 +13,7 @@ import { isBlockedHostnameOrIp, readJsonBodyWithLimit, requestBodyErrorToText, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { z } from "zod"; import { publishNostrProfile, getNostrProfileState } from "./channel.js"; import { NostrProfileSchema, type NostrProfile } from "./config-schema.js"; diff --git a/extensions/nostr/src/nostr-state-store.test.ts b/extensions/nostr/src/nostr-state-store.test.ts index 2dcb9d2d494..beb5caa0048 100644 --- a/extensions/nostr/src/nostr-state-store.test.ts +++ b/extensions/nostr/src/nostr-state-store.test.ts @@ -1,7 +1,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import type { PluginRuntime } from "openclaw/plugin-sdk"; +import type { PluginRuntime } from "openclaw/plugin-sdk/compat"; import { describe, expect, it } from "vitest"; import { readNostrBusState, diff --git a/extensions/nostr/src/runtime.ts b/extensions/nostr/src/runtime.ts index 902fb9b1205..e3e2e7028b0 100644 --- a/extensions/nostr/src/runtime.ts +++ b/extensions/nostr/src/runtime.ts @@ -1,4 +1,4 @@ -import type { PluginRuntime } from "openclaw/plugin-sdk"; +import type { PluginRuntime } from "openclaw/plugin-sdk/compat"; let runtime: PluginRuntime | null = null; diff --git a/extensions/nostr/src/types.ts b/extensions/nostr/src/types.ts index 9dd8d6a8c0e..9e25fb6f392 100644 --- a/extensions/nostr/src/types.ts +++ b/extensions/nostr/src/types.ts @@ -1,9 +1,9 @@ -import type { OpenClawConfig } from "openclaw/plugin-sdk"; import { DEFAULT_ACCOUNT_ID, normalizeAccountId, normalizeOptionalAccountId, } from "openclaw/plugin-sdk/account-id"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; import type { NostrProfile } from "./config-schema.js"; import { getPublicKeyFromPrivate } from "./nostr-bus.js"; import { DEFAULT_RELAYS } from "./nostr-bus.js"; From de05186ad7d4c8d8c4cd9ae743271f288efe55a0 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 4 Mar 2026 01:20:15 -0500 Subject: [PATCH 20/74] Extensions: migrate qwen-portal-auth plugin-sdk imports --- extensions/qwen-portal-auth/oauth.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/qwen-portal-auth/oauth.ts b/extensions/qwen-portal-auth/oauth.ts index b75a8639a4d..9b5129bb45e 100644 --- a/extensions/qwen-portal-auth/oauth.ts +++ b/extensions/qwen-portal-auth/oauth.ts @@ -1,5 +1,5 @@ import { randomUUID } from "node:crypto"; -import { generatePkceVerifierChallenge, toFormUrlEncoded } from "openclaw/plugin-sdk"; +import { generatePkceVerifierChallenge, toFormUrlEncoded } from "openclaw/plugin-sdk/compat"; const QWEN_OAUTH_BASE_URL = "https://chat.qwen.ai"; const QWEN_OAUTH_DEVICE_CODE_ENDPOINT = `${QWEN_OAUTH_BASE_URL}/api/v1/oauth2/device/code`; From 96b0fce27c03f974dd05cf451635d3416f57d426 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 4 Mar 2026 01:20:15 -0500 Subject: [PATCH 21/74] Extensions: migrate synology-chat plugin-sdk imports --- extensions/synology-chat/src/channel.integration.test.ts | 4 ++-- extensions/synology-chat/src/channel.test.ts | 4 ++-- extensions/synology-chat/src/channel.ts | 2 +- extensions/synology-chat/src/runtime.ts | 2 +- extensions/synology-chat/src/security.ts | 5 ++++- extensions/synology-chat/src/webhook-handler.ts | 2 +- 6 files changed, 11 insertions(+), 8 deletions(-) diff --git a/extensions/synology-chat/src/channel.integration.test.ts b/extensions/synology-chat/src/channel.integration.test.ts index 34f03567465..338efbc7676 100644 --- a/extensions/synology-chat/src/channel.integration.test.ts +++ b/extensions/synology-chat/src/channel.integration.test.ts @@ -11,8 +11,8 @@ type RegisteredRoute = { const registerPluginHttpRouteMock = vi.fn<(params: RegisteredRoute) => () => void>(() => vi.fn()); const dispatchReplyWithBufferedBlockDispatcher = vi.fn().mockResolvedValue({ counts: {} }); -vi.mock("openclaw/plugin-sdk", async (importOriginal) => { - const actual = await importOriginal(); +vi.mock("openclaw/plugin-sdk/compat", async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, DEFAULT_ACCOUNT_ID: "default", diff --git a/extensions/synology-chat/src/channel.test.ts b/extensions/synology-chat/src/channel.test.ts index 2d9935c604a..af5d1ed78b0 100644 --- a/extensions/synology-chat/src/channel.test.ts +++ b/extensions/synology-chat/src/channel.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; // Mock external dependencies -vi.mock("openclaw/plugin-sdk", () => ({ +vi.mock("openclaw/plugin-sdk/compat", () => ({ DEFAULT_ACCOUNT_ID: "default", setAccountEnabledInConfigSection: vi.fn((_opts: any) => ({})), registerPluginHttpRoute: vi.fn(() => vi.fn()), @@ -44,7 +44,7 @@ vi.mock("zod", () => ({ })); const { createSynologyChatPlugin } = await import("./channel.js"); -const { registerPluginHttpRoute } = await import("openclaw/plugin-sdk"); +const { registerPluginHttpRoute } = await import("openclaw/plugin-sdk/compat"); describe("createSynologyChatPlugin", () => { it("returns a plugin object with all required sections", () => { diff --git a/extensions/synology-chat/src/channel.ts b/extensions/synology-chat/src/channel.ts index 142f39d7f45..ed003d69a9d 100644 --- a/extensions/synology-chat/src/channel.ts +++ b/extensions/synology-chat/src/channel.ts @@ -9,7 +9,7 @@ import { setAccountEnabledInConfigSection, registerPluginHttpRoute, buildChannelConfigSchema, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { z } from "zod"; import { listAccountIds, resolveAccount } from "./accounts.js"; import { sendMessage, sendFileUrl } from "./client.js"; diff --git a/extensions/synology-chat/src/runtime.ts b/extensions/synology-chat/src/runtime.ts index 9257d4d3f73..a27e67d77ec 100644 --- a/extensions/synology-chat/src/runtime.ts +++ b/extensions/synology-chat/src/runtime.ts @@ -4,7 +4,7 @@ * Used by channel.ts to access dispatch functions. */ -import type { PluginRuntime } from "openclaw/plugin-sdk"; +import type { PluginRuntime } from "openclaw/plugin-sdk/compat"; let runtime: PluginRuntime | null = null; diff --git a/extensions/synology-chat/src/security.ts b/extensions/synology-chat/src/security.ts index 7c4f646b60e..dd4bed35b29 100644 --- a/extensions/synology-chat/src/security.ts +++ b/extensions/synology-chat/src/security.ts @@ -3,7 +3,10 @@ */ import * as crypto from "node:crypto"; -import { createFixedWindowRateLimiter, type FixedWindowRateLimiter } from "openclaw/plugin-sdk"; +import { + createFixedWindowRateLimiter, + type FixedWindowRateLimiter, +} from "openclaw/plugin-sdk/compat"; export type DmAuthorizationResult = | { allowed: true } diff --git a/extensions/synology-chat/src/webhook-handler.ts b/extensions/synology-chat/src/webhook-handler.ts index 197ec2ceefd..aa0e3187bc1 100644 --- a/extensions/synology-chat/src/webhook-handler.ts +++ b/extensions/synology-chat/src/webhook-handler.ts @@ -9,7 +9,7 @@ import { isRequestBodyLimitError, readRequestBodyWithLimit, requestBodyErrorToText, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { sendMessage, resolveChatUserId } from "./client.js"; import { validateToken, authorizeUserForDm, sanitizeInput, RateLimiter } from "./security.js"; import type { SynologyWebhookPayload, ResolvedSynologyChatAccount } from "./types.js"; From 7a9754c9271dcb80da8ef39a2c1d85110dfea8ea Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 4 Mar 2026 01:20:16 -0500 Subject: [PATCH 22/74] Extensions: migrate telegram plugin-sdk imports --- extensions/telegram/index.ts | 4 ++-- extensions/telegram/src/channel.test.ts | 2 +- extensions/telegram/src/runtime.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/extensions/telegram/index.ts b/extensions/telegram/index.ts index d47ae46b6ce..37367c5280c 100644 --- a/extensions/telegram/index.ts +++ b/extensions/telegram/index.ts @@ -1,5 +1,5 @@ -import type { ChannelPlugin, OpenClawPluginApi } from "openclaw/plugin-sdk/core"; -import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/core"; +import type { ChannelPlugin, OpenClawPluginApi } from "openclaw/plugin-sdk/telegram"; +import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/telegram"; import { telegramPlugin } from "./src/channel.js"; import { setTelegramRuntime } from "./src/runtime.js"; diff --git a/extensions/telegram/src/channel.test.ts b/extensions/telegram/src/channel.test.ts index a856502e60b..5f755a7284b 100644 --- a/extensions/telegram/src/channel.test.ts +++ b/extensions/telegram/src/channel.test.ts @@ -4,7 +4,7 @@ import type { OpenClawConfig, PluginRuntime, ResolvedTelegramAccount, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/telegram"; import { describe, expect, it, vi } from "vitest"; import { createRuntimeEnv } from "../../test-utils/runtime-env.js"; import { telegramPlugin } from "./channel.js"; diff --git a/extensions/telegram/src/runtime.ts b/extensions/telegram/src/runtime.ts index 491f7f7d956..dd1e3f9f2b8 100644 --- a/extensions/telegram/src/runtime.ts +++ b/extensions/telegram/src/runtime.ts @@ -1,4 +1,4 @@ -import type { PluginRuntime } from "openclaw/plugin-sdk/core"; +import type { PluginRuntime } from "openclaw/plugin-sdk/telegram"; let runtime: PluginRuntime | null = null; From 9bf08c926b70fe2e51925025ba98d17d74638f9b Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 4 Mar 2026 01:20:17 -0500 Subject: [PATCH 23/74] Extensions: migrate test-utils plugin-sdk imports --- extensions/test-utils/plugin-runtime-mock.ts | 4 ++-- extensions/test-utils/runtime-env.ts | 2 +- extensions/test-utils/start-account-context.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/extensions/test-utils/plugin-runtime-mock.ts b/extensions/test-utils/plugin-runtime-mock.ts index 166f5df5c49..bc2d97c4bac 100644 --- a/extensions/test-utils/plugin-runtime-mock.ts +++ b/extensions/test-utils/plugin-runtime-mock.ts @@ -1,5 +1,5 @@ -import type { PluginRuntime } from "openclaw/plugin-sdk"; -import { removeAckReactionAfterReply, shouldAckReaction } from "openclaw/plugin-sdk"; +import type { PluginRuntime } from "openclaw/plugin-sdk/compat"; +import { removeAckReactionAfterReply, shouldAckReaction } from "openclaw/plugin-sdk/compat"; import { vi } from "vitest"; type DeepPartial = { diff --git a/extensions/test-utils/runtime-env.ts b/extensions/test-utils/runtime-env.ts index 747ad5f5f3a..ef67c61429a 100644 --- a/extensions/test-utils/runtime-env.ts +++ b/extensions/test-utils/runtime-env.ts @@ -1,4 +1,4 @@ -import type { RuntimeEnv } from "openclaw/plugin-sdk"; +import type { RuntimeEnv } from "openclaw/plugin-sdk/compat"; import { vi } from "vitest"; export function createRuntimeEnv(): RuntimeEnv { diff --git a/extensions/test-utils/start-account-context.ts b/extensions/test-utils/start-account-context.ts index 99d76dd7c81..179444b445c 100644 --- a/extensions/test-utils/start-account-context.ts +++ b/extensions/test-utils/start-account-context.ts @@ -2,7 +2,7 @@ import type { ChannelAccountSnapshot, ChannelGatewayContext, OpenClawConfig, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { vi } from "vitest"; import { createRuntimeEnv } from "./runtime-env.js"; From b0bca8d6e95c97156c72185e030a29ab115b2654 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 4 Mar 2026 01:20:18 -0500 Subject: [PATCH 24/74] Extensions: migrate tlon plugin-sdk imports --- extensions/tlon/src/channel.ts | 8 ++++---- extensions/tlon/src/config-schema.ts | 2 +- extensions/tlon/src/monitor/discovery.ts | 2 +- extensions/tlon/src/monitor/history.ts | 2 +- extensions/tlon/src/monitor/index.ts | 4 ++-- extensions/tlon/src/monitor/media.ts | 2 +- extensions/tlon/src/monitor/processed-messages.ts | 2 +- extensions/tlon/src/onboarding.ts | 4 ++-- extensions/tlon/src/runtime.ts | 2 +- extensions/tlon/src/types.ts | 2 +- extensions/tlon/src/urbit/auth.ssrf.test.ts | 4 ++-- extensions/tlon/src/urbit/auth.ts | 2 +- extensions/tlon/src/urbit/base-url.ts | 2 +- extensions/tlon/src/urbit/channel-ops.ts | 2 +- extensions/tlon/src/urbit/context.ts | 2 +- extensions/tlon/src/urbit/fetch.ts | 4 ++-- extensions/tlon/src/urbit/sse-client.ts | 2 +- extensions/tlon/src/urbit/upload.test.ts | 14 +++++++------- extensions/tlon/src/urbit/upload.ts | 2 +- 19 files changed, 32 insertions(+), 32 deletions(-) diff --git a/extensions/tlon/src/channel.ts b/extensions/tlon/src/channel.ts index 3b2dd73f388..3432973c7d5 100644 --- a/extensions/tlon/src/channel.ts +++ b/extensions/tlon/src/channel.ts @@ -5,12 +5,12 @@ import type { ChannelPlugin, ChannelSetupInput, OpenClawConfig, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { applyAccountNameToChannelSection, DEFAULT_ACCOUNT_ID, normalizeAccountId, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { buildTlonAccountFields } from "./account-fields.js"; import { tlonChannelConfigSchema } from "./config-schema.js"; import { monitorTlonProvider } from "./monitor/index.js"; @@ -497,7 +497,7 @@ export const tlonPlugin: ChannelPlugin = { lastError: runtime?.lastError ?? null, probe, }; - return snapshot as import("openclaw/plugin-sdk").ChannelAccountSnapshot; + return snapshot as import("openclaw/plugin-sdk/compat").ChannelAccountSnapshot; }, }, gateway: { @@ -507,7 +507,7 @@ export const tlonPlugin: ChannelPlugin = { accountId: account.accountId, ship: account.ship, url: account.url, - } as import("openclaw/plugin-sdk").ChannelAccountSnapshot); + } as import("openclaw/plugin-sdk/compat").ChannelAccountSnapshot); ctx.log?.info(`[${account.accountId}] starting Tlon provider for ${account.ship ?? "tlon"}`); return monitorTlonProvider({ runtime: ctx.runtime, diff --git a/extensions/tlon/src/config-schema.ts b/extensions/tlon/src/config-schema.ts index 4a091c8f650..8bcf8300069 100644 --- a/extensions/tlon/src/config-schema.ts +++ b/extensions/tlon/src/config-schema.ts @@ -1,4 +1,4 @@ -import { buildChannelConfigSchema } from "openclaw/plugin-sdk"; +import { buildChannelConfigSchema } from "openclaw/plugin-sdk/compat"; import { z } from "zod"; const ShipSchema = z.string().min(1); diff --git a/extensions/tlon/src/monitor/discovery.ts b/extensions/tlon/src/monitor/discovery.ts index cce767ea4db..ae0ea47d7b9 100644 --- a/extensions/tlon/src/monitor/discovery.ts +++ b/extensions/tlon/src/monitor/discovery.ts @@ -1,4 +1,4 @@ -import type { RuntimeEnv } from "openclaw/plugin-sdk"; +import type { RuntimeEnv } from "openclaw/plugin-sdk/compat"; import type { Foreigns } from "../urbit/foreigns.js"; import { formatChangesDate } from "./utils.js"; diff --git a/extensions/tlon/src/monitor/history.ts b/extensions/tlon/src/monitor/history.ts index 3674b175b3c..0636c102f7f 100644 --- a/extensions/tlon/src/monitor/history.ts +++ b/extensions/tlon/src/monitor/history.ts @@ -1,4 +1,4 @@ -import type { RuntimeEnv } from "openclaw/plugin-sdk"; +import type { RuntimeEnv } from "openclaw/plugin-sdk/compat"; import { extractMessageText } from "./utils.js"; /** diff --git a/extensions/tlon/src/monitor/index.ts b/extensions/tlon/src/monitor/index.ts index b3a0e092970..3d12393cd90 100644 --- a/extensions/tlon/src/monitor/index.ts +++ b/extensions/tlon/src/monitor/index.ts @@ -1,5 +1,5 @@ -import type { RuntimeEnv, ReplyPayload, OpenClawConfig } from "openclaw/plugin-sdk"; -import { createLoggerBackedRuntime, createReplyPrefixOptions } from "openclaw/plugin-sdk"; +import type { RuntimeEnv, ReplyPayload, OpenClawConfig } from "openclaw/plugin-sdk/compat"; +import { createLoggerBackedRuntime, createReplyPrefixOptions } from "openclaw/plugin-sdk/compat"; import { getTlonRuntime } from "../runtime.js"; import { createSettingsManager, type TlonSettingsStore } from "../settings.js"; import { normalizeShip, parseChannelNest } from "../targets.js"; diff --git a/extensions/tlon/src/monitor/media.ts b/extensions/tlon/src/monitor/media.ts index fabf7697795..e8301976a85 100644 --- a/extensions/tlon/src/monitor/media.ts +++ b/extensions/tlon/src/monitor/media.ts @@ -5,7 +5,7 @@ import { homedir } from "node:os"; import * as path from "node:path"; import { Readable } from "node:stream"; import { pipeline } from "node:stream/promises"; -import { fetchWithSsrFGuard } from "openclaw/plugin-sdk"; +import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/compat"; import { getDefaultSsrFPolicy } from "../urbit/context.js"; // Default to OpenClaw workspace media directory diff --git a/extensions/tlon/src/monitor/processed-messages.ts b/extensions/tlon/src/monitor/processed-messages.ts index 560db28575a..e2a533ee7da 100644 --- a/extensions/tlon/src/monitor/processed-messages.ts +++ b/extensions/tlon/src/monitor/processed-messages.ts @@ -1,4 +1,4 @@ -import { createDedupeCache } from "openclaw/plugin-sdk"; +import { createDedupeCache } from "openclaw/plugin-sdk/compat"; export type ProcessedMessageTracker = { mark: (id?: string | null) => boolean; diff --git a/extensions/tlon/src/onboarding.ts b/extensions/tlon/src/onboarding.ts index 11b1ceccbd1..f0b84ab5bef 100644 --- a/extensions/tlon/src/onboarding.ts +++ b/extensions/tlon/src/onboarding.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig } from "openclaw/plugin-sdk"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; import { formatDocsLink, promptAccountId, @@ -6,7 +6,7 @@ import { normalizeAccountId, type ChannelOnboardingAdapter, type WizardPrompter, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { buildTlonAccountFields } from "./account-fields.js"; import type { TlonResolvedAccount } from "./types.js"; import { listTlonAccountIds, resolveTlonAccount } from "./types.js"; diff --git a/extensions/tlon/src/runtime.ts b/extensions/tlon/src/runtime.ts index 0ffa71c9b4f..79ad7a872b9 100644 --- a/extensions/tlon/src/runtime.ts +++ b/extensions/tlon/src/runtime.ts @@ -1,4 +1,4 @@ -import type { PluginRuntime } from "openclaw/plugin-sdk"; +import type { PluginRuntime } from "openclaw/plugin-sdk/compat"; let runtime: PluginRuntime | null = null; diff --git a/extensions/tlon/src/types.ts b/extensions/tlon/src/types.ts index 81f38adc76b..4352e88bb63 100644 --- a/extensions/tlon/src/types.ts +++ b/extensions/tlon/src/types.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig } from "openclaw/plugin-sdk"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; export type TlonResolvedAccount = { accountId: string; diff --git a/extensions/tlon/src/urbit/auth.ssrf.test.ts b/extensions/tlon/src/urbit/auth.ssrf.test.ts index f67891589cc..f28e7e217e1 100644 --- a/extensions/tlon/src/urbit/auth.ssrf.test.ts +++ b/extensions/tlon/src/urbit/auth.ssrf.test.ts @@ -1,5 +1,5 @@ -import type { LookupFn } from "openclaw/plugin-sdk"; -import { SsrFBlockedError } from "openclaw/plugin-sdk"; +import type { LookupFn } from "openclaw/plugin-sdk/compat"; +import { SsrFBlockedError } from "openclaw/plugin-sdk/compat"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { authenticate } from "./auth.js"; diff --git a/extensions/tlon/src/urbit/auth.ts b/extensions/tlon/src/urbit/auth.ts index 0f11a5859f2..7ae150980a1 100644 --- a/extensions/tlon/src/urbit/auth.ts +++ b/extensions/tlon/src/urbit/auth.ts @@ -1,4 +1,4 @@ -import type { LookupFn, SsrFPolicy } from "openclaw/plugin-sdk"; +import type { LookupFn, SsrFPolicy } from "openclaw/plugin-sdk/compat"; import { UrbitAuthError } from "./errors.js"; import { urbitFetch } from "./fetch.js"; diff --git a/extensions/tlon/src/urbit/base-url.ts b/extensions/tlon/src/urbit/base-url.ts index d18832bdd1a..46619449315 100644 --- a/extensions/tlon/src/urbit/base-url.ts +++ b/extensions/tlon/src/urbit/base-url.ts @@ -1,4 +1,4 @@ -import { isBlockedHostnameOrIp } from "openclaw/plugin-sdk"; +import { isBlockedHostnameOrIp } from "openclaw/plugin-sdk/compat"; export type UrbitBaseUrlValidation = | { ok: true; baseUrl: string; hostname: string } diff --git a/extensions/tlon/src/urbit/channel-ops.ts b/extensions/tlon/src/urbit/channel-ops.ts index 077e8d01816..c58652b62eb 100644 --- a/extensions/tlon/src/urbit/channel-ops.ts +++ b/extensions/tlon/src/urbit/channel-ops.ts @@ -1,4 +1,4 @@ -import type { LookupFn, SsrFPolicy } from "openclaw/plugin-sdk"; +import type { LookupFn, SsrFPolicy } from "openclaw/plugin-sdk/compat"; import { UrbitHttpError } from "./errors.js"; import { urbitFetch } from "./fetch.js"; diff --git a/extensions/tlon/src/urbit/context.ts b/extensions/tlon/src/urbit/context.ts index e5c78aeee7f..25381df8e75 100644 --- a/extensions/tlon/src/urbit/context.ts +++ b/extensions/tlon/src/urbit/context.ts @@ -1,4 +1,4 @@ -import type { SsrFPolicy } from "openclaw/plugin-sdk"; +import type { SsrFPolicy } from "openclaw/plugin-sdk/compat"; import { validateUrbitBaseUrl } from "./base-url.js"; import { UrbitUrlError } from "./errors.js"; diff --git a/extensions/tlon/src/urbit/fetch.ts b/extensions/tlon/src/urbit/fetch.ts index 08032a028ef..1c60a0b6dd2 100644 --- a/extensions/tlon/src/urbit/fetch.ts +++ b/extensions/tlon/src/urbit/fetch.ts @@ -1,5 +1,5 @@ -import type { LookupFn, SsrFPolicy } from "openclaw/plugin-sdk"; -import { fetchWithSsrFGuard } from "openclaw/plugin-sdk"; +import type { LookupFn, SsrFPolicy } from "openclaw/plugin-sdk/compat"; +import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/compat"; import { validateUrbitBaseUrl } from "./base-url.js"; import { UrbitUrlError } from "./errors.js"; diff --git a/extensions/tlon/src/urbit/sse-client.ts b/extensions/tlon/src/urbit/sse-client.ts index 897859d2fcd..94056e523e3 100644 --- a/extensions/tlon/src/urbit/sse-client.ts +++ b/extensions/tlon/src/urbit/sse-client.ts @@ -1,6 +1,6 @@ import { randomUUID } from "node:crypto"; import { Readable } from "node:stream"; -import type { LookupFn, SsrFPolicy } from "openclaw/plugin-sdk"; +import type { LookupFn, SsrFPolicy } from "openclaw/plugin-sdk/compat"; import { ensureUrbitChannelOpen, pokeUrbitChannel, scryUrbitPath } from "./channel-ops.js"; import { getUrbitContext, normalizeUrbitCookie } from "./context.js"; import { urbitFetch } from "./fetch.js"; diff --git a/extensions/tlon/src/urbit/upload.test.ts b/extensions/tlon/src/urbit/upload.test.ts index 3ff0e9fd1a0..0f078669859 100644 --- a/extensions/tlon/src/urbit/upload.test.ts +++ b/extensions/tlon/src/urbit/upload.test.ts @@ -1,8 +1,8 @@ import { describe, expect, it, vi, afterEach, beforeEach } from "vitest"; // Mock fetchWithSsrFGuard from plugin-sdk -vi.mock("openclaw/plugin-sdk", async (importOriginal) => { - const actual = await importOriginal(); +vi.mock("openclaw/plugin-sdk/compat", async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, fetchWithSsrFGuard: vi.fn(), @@ -24,7 +24,7 @@ describe("uploadImageFromUrl", () => { }); it("fetches image and calls uploadFile, returns uploaded URL", async () => { - const { fetchWithSsrFGuard } = await import("openclaw/plugin-sdk"); + const { fetchWithSsrFGuard } = await import("openclaw/plugin-sdk/compat"); const mockFetch = vi.mocked(fetchWithSsrFGuard); const { uploadFile } = await import("@tloncorp/api"); @@ -59,7 +59,7 @@ describe("uploadImageFromUrl", () => { }); it("returns original URL if fetch fails", async () => { - const { fetchWithSsrFGuard } = await import("openclaw/plugin-sdk"); + const { fetchWithSsrFGuard } = await import("openclaw/plugin-sdk/compat"); const mockFetch = vi.mocked(fetchWithSsrFGuard); // Mock fetchWithSsrFGuard to return a failed response @@ -79,7 +79,7 @@ describe("uploadImageFromUrl", () => { }); it("returns original URL if upload fails", async () => { - const { fetchWithSsrFGuard } = await import("openclaw/plugin-sdk"); + const { fetchWithSsrFGuard } = await import("openclaw/plugin-sdk/compat"); const mockFetch = vi.mocked(fetchWithSsrFGuard); const { uploadFile } = await import("@tloncorp/api"); @@ -127,7 +127,7 @@ describe("uploadImageFromUrl", () => { }); it("extracts filename from URL path", async () => { - const { fetchWithSsrFGuard } = await import("openclaw/plugin-sdk"); + const { fetchWithSsrFGuard } = await import("openclaw/plugin-sdk/compat"); const mockFetch = vi.mocked(fetchWithSsrFGuard); const { uploadFile } = await import("@tloncorp/api"); @@ -157,7 +157,7 @@ describe("uploadImageFromUrl", () => { }); it("uses default filename when URL has no path", async () => { - const { fetchWithSsrFGuard } = await import("openclaw/plugin-sdk"); + const { fetchWithSsrFGuard } = await import("openclaw/plugin-sdk/compat"); const mockFetch = vi.mocked(fetchWithSsrFGuard); const { uploadFile } = await import("@tloncorp/api"); diff --git a/extensions/tlon/src/urbit/upload.ts b/extensions/tlon/src/urbit/upload.ts index 0c01483991b..78c9c706e7c 100644 --- a/extensions/tlon/src/urbit/upload.ts +++ b/extensions/tlon/src/urbit/upload.ts @@ -2,7 +2,7 @@ * Upload an image from a URL to Tlon storage. */ import { uploadFile } from "@tloncorp/api"; -import { fetchWithSsrFGuard } from "openclaw/plugin-sdk"; +import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/compat"; import { getDefaultSsrFPolicy } from "./context.js"; /** From 9d102b762e24558c0584ed84b33efa33c6e913a1 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 4 Mar 2026 01:20:19 -0500 Subject: [PATCH 25/74] Extensions: migrate twitch plugin-sdk imports --- extensions/twitch/src/config-schema.ts | 2 +- extensions/twitch/src/config.ts | 2 +- extensions/twitch/src/monitor.ts | 4 ++-- extensions/twitch/src/onboarding.test.ts | 2 +- extensions/twitch/src/onboarding.ts | 4 ++-- extensions/twitch/src/plugin.test.ts | 2 +- extensions/twitch/src/plugin.ts | 4 ++-- extensions/twitch/src/probe.ts | 2 +- extensions/twitch/src/runtime.ts | 2 +- extensions/twitch/src/send.ts | 2 +- extensions/twitch/src/status.ts | 2 +- extensions/twitch/src/test-fixtures.ts | 2 +- extensions/twitch/src/token.test.ts | 2 +- extensions/twitch/src/twitch-client.ts | 2 +- 14 files changed, 17 insertions(+), 17 deletions(-) diff --git a/extensions/twitch/src/config-schema.ts b/extensions/twitch/src/config-schema.ts index 73ddb5eaab7..2542591d8f9 100644 --- a/extensions/twitch/src/config-schema.ts +++ b/extensions/twitch/src/config-schema.ts @@ -1,4 +1,4 @@ -import { MarkdownConfigSchema } from "openclaw/plugin-sdk"; +import { MarkdownConfigSchema } from "openclaw/plugin-sdk/compat"; import { z } from "zod"; /** diff --git a/extensions/twitch/src/config.ts b/extensions/twitch/src/config.ts index 39a1a9c4ca9..7b65cce7e9a 100644 --- a/extensions/twitch/src/config.ts +++ b/extensions/twitch/src/config.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig } from "openclaw/plugin-sdk"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; import type { TwitchAccountConfig } from "./types.js"; /** diff --git a/extensions/twitch/src/monitor.ts b/extensions/twitch/src/monitor.ts index 9f0c0df5b88..d70c04cc2d8 100644 --- a/extensions/twitch/src/monitor.ts +++ b/extensions/twitch/src/monitor.ts @@ -5,8 +5,8 @@ * resolves agent routes, and handles replies. */ -import type { ReplyPayload, OpenClawConfig } from "openclaw/plugin-sdk"; -import { createReplyPrefixOptions } from "openclaw/plugin-sdk"; +import type { ReplyPayload, OpenClawConfig } from "openclaw/plugin-sdk/compat"; +import { createReplyPrefixOptions } from "openclaw/plugin-sdk/compat"; import { checkTwitchAccessControl } from "./access-control.js"; import { getOrCreateClientManager } from "./client-manager-registry.js"; import { getTwitchRuntime } from "./runtime.js"; diff --git a/extensions/twitch/src/onboarding.test.ts b/extensions/twitch/src/onboarding.test.ts index d57e2e2de4d..4df95f39fb3 100644 --- a/extensions/twitch/src/onboarding.test.ts +++ b/extensions/twitch/src/onboarding.test.ts @@ -11,7 +11,7 @@ * - setTwitchAccount config updates */ -import type { WizardPrompter } from "openclaw/plugin-sdk"; +import type { WizardPrompter } from "openclaw/plugin-sdk/compat"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { TwitchAccountConfig } from "./types.js"; diff --git a/extensions/twitch/src/onboarding.ts b/extensions/twitch/src/onboarding.ts index adfa8b9e4d7..6148f165fb4 100644 --- a/extensions/twitch/src/onboarding.ts +++ b/extensions/twitch/src/onboarding.ts @@ -2,14 +2,14 @@ * Twitch onboarding adapter for CLI setup wizard. */ -import type { OpenClawConfig } from "openclaw/plugin-sdk"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; import { formatDocsLink, promptChannelAccessConfig, type ChannelOnboardingAdapter, type ChannelOnboardingDmPolicy, type WizardPrompter, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { DEFAULT_ACCOUNT_ID, getAccountConfig } from "./config.js"; import type { TwitchAccountConfig, TwitchRole } from "./types.js"; import { isAccountConfigured } from "./utils/twitch.js"; diff --git a/extensions/twitch/src/plugin.test.ts b/extensions/twitch/src/plugin.test.ts index 1e76d2e620c..fa1a9a51d39 100644 --- a/extensions/twitch/src/plugin.test.ts +++ b/extensions/twitch/src/plugin.test.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig } from "openclaw/plugin-sdk"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; import { describe, expect, it } from "vitest"; import { twitchPlugin } from "./plugin.js"; diff --git a/extensions/twitch/src/plugin.ts b/extensions/twitch/src/plugin.ts index 15624e38f31..25776af9c7b 100644 --- a/extensions/twitch/src/plugin.ts +++ b/extensions/twitch/src/plugin.ts @@ -5,8 +5,8 @@ * This is the primary entry point for the Twitch channel integration. */ -import type { OpenClawConfig } from "openclaw/plugin-sdk"; -import { buildChannelConfigSchema } from "openclaw/plugin-sdk"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; +import { buildChannelConfigSchema } from "openclaw/plugin-sdk/compat"; import { twitchMessageActions } from "./actions.js"; import { removeClientManager } from "./client-manager-registry.js"; import { TwitchConfigSchema } from "./config-schema.js"; diff --git a/extensions/twitch/src/probe.ts b/extensions/twitch/src/probe.ts index 0f421ff2981..8a55f2425c8 100644 --- a/extensions/twitch/src/probe.ts +++ b/extensions/twitch/src/probe.ts @@ -1,6 +1,6 @@ import { StaticAuthProvider } from "@twurple/auth"; import { ChatClient } from "@twurple/chat"; -import type { BaseProbeResult } from "openclaw/plugin-sdk"; +import type { BaseProbeResult } from "openclaw/plugin-sdk/compat"; import type { TwitchAccountConfig } from "./types.js"; import { normalizeToken } from "./utils/twitch.js"; diff --git a/extensions/twitch/src/runtime.ts b/extensions/twitch/src/runtime.ts index 1c0c16cfcb4..f20cbbf475d 100644 --- a/extensions/twitch/src/runtime.ts +++ b/extensions/twitch/src/runtime.ts @@ -1,4 +1,4 @@ -import type { PluginRuntime } from "openclaw/plugin-sdk"; +import type { PluginRuntime } from "openclaw/plugin-sdk/compat"; let runtime: PluginRuntime | null = null; diff --git a/extensions/twitch/src/send.ts b/extensions/twitch/src/send.ts index d8a9cc3b0c9..67f8452296b 100644 --- a/extensions/twitch/src/send.ts +++ b/extensions/twitch/src/send.ts @@ -5,7 +5,7 @@ * They support dependency injection via the `deps` parameter for testability. */ -import type { OpenClawConfig } from "openclaw/plugin-sdk"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; import { getClientManager as getRegistryClientManager } from "./client-manager-registry.js"; import { DEFAULT_ACCOUNT_ID, getAccountConfig } from "./config.js"; import { resolveTwitchToken } from "./token.js"; diff --git a/extensions/twitch/src/status.ts b/extensions/twitch/src/status.ts index 33a62d09acf..12f220c897b 100644 --- a/extensions/twitch/src/status.ts +++ b/extensions/twitch/src/status.ts @@ -4,7 +4,7 @@ * Detects and reports configuration issues for Twitch accounts. */ -import type { ChannelStatusIssue } from "openclaw/plugin-sdk"; +import type { ChannelStatusIssue } from "openclaw/plugin-sdk/compat"; import { getAccountConfig } from "./config.js"; import { resolveTwitchToken } from "./token.js"; import type { ChannelAccountSnapshot } from "./types.js"; diff --git a/extensions/twitch/src/test-fixtures.ts b/extensions/twitch/src/test-fixtures.ts index c2eb4df28f2..f6c59f6f2df 100644 --- a/extensions/twitch/src/test-fixtures.ts +++ b/extensions/twitch/src/test-fixtures.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig } from "openclaw/plugin-sdk"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; import { afterEach, beforeEach, vi } from "vitest"; export const BASE_TWITCH_TEST_ACCOUNT = { diff --git a/extensions/twitch/src/token.test.ts b/extensions/twitch/src/token.test.ts index 7935d582b50..f5b702ea9a6 100644 --- a/extensions/twitch/src/token.test.ts +++ b/extensions/twitch/src/token.test.ts @@ -8,7 +8,7 @@ * - Account ID normalization */ -import type { OpenClawConfig } from "openclaw/plugin-sdk"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { resolveTwitchToken, type TwitchTokenSource } from "./token.js"; diff --git a/extensions/twitch/src/twitch-client.ts b/extensions/twitch/src/twitch-client.ts index 86697719946..4dd1ea8495c 100644 --- a/extensions/twitch/src/twitch-client.ts +++ b/extensions/twitch/src/twitch-client.ts @@ -1,6 +1,6 @@ import { RefreshingAuthProvider, StaticAuthProvider } from "@twurple/auth"; import { ChatClient, LogLevel } from "@twurple/chat"; -import type { OpenClawConfig } from "openclaw/plugin-sdk"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; import { resolveTwitchToken } from "./token.js"; import type { ChannelLogSink, TwitchAccountConfig, TwitchChatMessage } from "./types.js"; import { normalizeToken } from "./utils/twitch.js"; From b361cac7534782d52fcb582138e8dbe64b2679f3 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 4 Mar 2026 01:20:19 -0500 Subject: [PATCH 26/74] Extensions: migrate voice-call plugin-sdk imports --- extensions/voice-call/src/cli.ts | 2 +- extensions/voice-call/src/config.ts | 2 +- extensions/voice-call/src/providers/shared/guarded-json-api.ts | 2 +- extensions/voice-call/src/webhook.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/extensions/voice-call/src/cli.ts b/extensions/voice-call/src/cli.ts index 4e7ad96a90f..82b459c336c 100644 --- a/extensions/voice-call/src/cli.ts +++ b/extensions/voice-call/src/cli.ts @@ -2,7 +2,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import type { Command } from "commander"; -import { sleep } from "openclaw/plugin-sdk"; +import { sleep } from "openclaw/plugin-sdk/compat"; import type { VoiceCallConfig } from "./config.js"; import type { VoiceCallRuntime } from "./runtime.js"; import { resolveUserPath } from "./utils.js"; diff --git a/extensions/voice-call/src/config.ts b/extensions/voice-call/src/config.ts index 36b77778e9f..fc572a6b426 100644 --- a/extensions/voice-call/src/config.ts +++ b/extensions/voice-call/src/config.ts @@ -3,7 +3,7 @@ import { TtsConfigSchema, TtsModeSchema, TtsProviderSchema, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { z } from "zod"; // ----------------------------------------------------------------------------- diff --git a/extensions/voice-call/src/providers/shared/guarded-json-api.ts b/extensions/voice-call/src/providers/shared/guarded-json-api.ts index 6790cae5d76..39ca5b73625 100644 --- a/extensions/voice-call/src/providers/shared/guarded-json-api.ts +++ b/extensions/voice-call/src/providers/shared/guarded-json-api.ts @@ -1,4 +1,4 @@ -import { fetchWithSsrFGuard } from "openclaw/plugin-sdk"; +import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/compat"; type GuardedJsonApiRequestParams = { url: string; diff --git a/extensions/voice-call/src/webhook.ts b/extensions/voice-call/src/webhook.ts index 6dda99edd88..38fca1db0d2 100644 --- a/extensions/voice-call/src/webhook.ts +++ b/extensions/voice-call/src/webhook.ts @@ -4,7 +4,7 @@ import { isRequestBodyLimitError, readRequestBodyWithLimit, requestBodyErrorToText, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import type { VoiceCallConfig } from "./config.js"; import type { CoreConfig } from "./core-bridge.js"; import type { CallManager } from "./manager.js"; From dda86af8664f30e7d37ffa359849984a583ec47d Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 4 Mar 2026 01:20:20 -0500 Subject: [PATCH 27/74] Extensions: migrate zalo plugin-sdk imports --- extensions/zalo/src/accounts.ts | 2 +- extensions/zalo/src/actions.ts | 4 ++-- extensions/zalo/src/channel.directory.test.ts | 2 +- extensions/zalo/src/channel.sendpayload.test.ts | 2 +- extensions/zalo/src/channel.ts | 4 ++-- extensions/zalo/src/config-schema.ts | 2 +- extensions/zalo/src/group-access.ts | 4 ++-- extensions/zalo/src/monitor.ts | 8 ++++++-- extensions/zalo/src/monitor.webhook.test.ts | 2 +- extensions/zalo/src/monitor.webhook.ts | 4 ++-- extensions/zalo/src/onboarding.status.test.ts | 2 +- extensions/zalo/src/onboarding.ts | 4 ++-- extensions/zalo/src/probe.ts | 2 +- extensions/zalo/src/runtime.ts | 2 +- extensions/zalo/src/secret-input.ts | 2 +- extensions/zalo/src/send.ts | 2 +- extensions/zalo/src/status-issues.ts | 2 +- extensions/zalo/src/token.ts | 2 +- extensions/zalo/src/types.ts | 2 +- 19 files changed, 29 insertions(+), 25 deletions(-) diff --git a/extensions/zalo/src/accounts.ts b/extensions/zalo/src/accounts.ts index a39a166c24d..d74d906fce6 100644 --- a/extensions/zalo/src/accounts.ts +++ b/extensions/zalo/src/accounts.ts @@ -1,9 +1,9 @@ -import type { OpenClawConfig } from "openclaw/plugin-sdk"; import { DEFAULT_ACCOUNT_ID, normalizeAccountId, normalizeOptionalAccountId, } from "openclaw/plugin-sdk/account-id"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; import { resolveZaloToken } from "./token.js"; import type { ResolvedZaloAccount, ZaloAccountConfig, ZaloConfig } from "./types.js"; diff --git a/extensions/zalo/src/actions.ts b/extensions/zalo/src/actions.ts index a5fca946ca7..e3fe5d22fdf 100644 --- a/extensions/zalo/src/actions.ts +++ b/extensions/zalo/src/actions.ts @@ -2,8 +2,8 @@ import type { ChannelMessageActionAdapter, ChannelMessageActionName, OpenClawConfig, -} from "openclaw/plugin-sdk"; -import { extractToolSend, jsonResult, readStringParam } from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; +import { extractToolSend, jsonResult, readStringParam } from "openclaw/plugin-sdk/compat"; import { listEnabledZaloAccounts } from "./accounts.js"; import { sendMessageZalo } from "./send.js"; diff --git a/extensions/zalo/src/channel.directory.test.ts b/extensions/zalo/src/channel.directory.test.ts index 61b446a50fb..5159ae3a6ac 100644 --- a/extensions/zalo/src/channel.directory.test.ts +++ b/extensions/zalo/src/channel.directory.test.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig, RuntimeEnv } from "openclaw/plugin-sdk"; +import type { OpenClawConfig, RuntimeEnv } from "openclaw/plugin-sdk/compat"; import { describe, expect, it } from "vitest"; import { zaloPlugin } from "./channel.js"; diff --git a/extensions/zalo/src/channel.sendpayload.test.ts b/extensions/zalo/src/channel.sendpayload.test.ts index 5bac81dc54e..d51eb4660fb 100644 --- a/extensions/zalo/src/channel.sendpayload.test.ts +++ b/extensions/zalo/src/channel.sendpayload.test.ts @@ -1,4 +1,4 @@ -import type { ReplyPayload } from "openclaw/plugin-sdk"; +import type { ReplyPayload } from "openclaw/plugin-sdk/compat"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { zaloPlugin } from "./channel.js"; diff --git a/extensions/zalo/src/channel.ts b/extensions/zalo/src/channel.ts index 74fe92ee01e..a5d743c3efd 100644 --- a/extensions/zalo/src/channel.ts +++ b/extensions/zalo/src/channel.ts @@ -3,7 +3,7 @@ import type { ChannelDock, ChannelPlugin, OpenClawConfig, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { applyAccountNameToChannelSection, buildChannelConfigSchema, @@ -20,7 +20,7 @@ import { resolveOpenProviderRuntimeGroupPolicy, resolveChannelAccountConfigBasePath, setAccountEnabledInConfigSection, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { listZaloAccountIds, resolveDefaultZaloAccountId, diff --git a/extensions/zalo/src/config-schema.ts b/extensions/zalo/src/config-schema.ts index ec0b038a8d1..0786429755b 100644 --- a/extensions/zalo/src/config-schema.ts +++ b/extensions/zalo/src/config-schema.ts @@ -1,4 +1,4 @@ -import { MarkdownConfigSchema } from "openclaw/plugin-sdk"; +import { MarkdownConfigSchema } from "openclaw/plugin-sdk/compat"; import { z } from "zod"; import { buildSecretInputSchema } from "./secret-input.js"; diff --git a/extensions/zalo/src/group-access.ts b/extensions/zalo/src/group-access.ts index 7acd1997096..48292e5ac80 100644 --- a/extensions/zalo/src/group-access.ts +++ b/extensions/zalo/src/group-access.ts @@ -1,9 +1,9 @@ -import type { GroupPolicy, SenderGroupAccessDecision } from "openclaw/plugin-sdk"; +import type { GroupPolicy, SenderGroupAccessDecision } from "openclaw/plugin-sdk/compat"; import { evaluateSenderGroupAccess, isNormalizedSenderAllowed, resolveOpenProviderRuntimeGroupPolicy, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; const ZALO_ALLOW_FROM_PREFIX_RE = /^(zalo|zl):/i; diff --git a/extensions/zalo/src/monitor.ts b/extensions/zalo/src/monitor.ts index e3087e6ad00..aa3b37f463d 100644 --- a/extensions/zalo/src/monitor.ts +++ b/extensions/zalo/src/monitor.ts @@ -1,5 +1,9 @@ import type { IncomingMessage, ServerResponse } from "node:http"; -import type { MarkdownTableMode, OpenClawConfig, OutboundReplyPayload } from "openclaw/plugin-sdk"; +import type { + MarkdownTableMode, + OpenClawConfig, + OutboundReplyPayload, +} from "openclaw/plugin-sdk/compat"; import { createScopedPairingAccess, createReplyPrefixOptions, @@ -11,7 +15,7 @@ import { sendMediaWithLeadingCaption, resolveWebhookPath, warnMissingProviderGroupPolicyFallbackOnce, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import type { ResolvedZaloAccount } from "./accounts.js"; import { ZaloApiError, diff --git a/extensions/zalo/src/monitor.webhook.test.ts b/extensions/zalo/src/monitor.webhook.test.ts index 2a297e3a722..33fa7530f6b 100644 --- a/extensions/zalo/src/monitor.webhook.test.ts +++ b/extensions/zalo/src/monitor.webhook.test.ts @@ -1,6 +1,6 @@ import { createServer, type RequestListener } from "node:http"; import type { AddressInfo } from "node:net"; -import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk"; +import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk/compat"; import { afterEach, describe, expect, it, vi } from "vitest"; import { createEmptyPluginRegistry } from "../../../src/plugins/registry.js"; import { setActivePluginRegistry } from "../../../src/plugins/runtime.js"; diff --git a/extensions/zalo/src/monitor.webhook.ts b/extensions/zalo/src/monitor.webhook.ts index b699d986de4..82f09811c9d 100644 --- a/extensions/zalo/src/monitor.webhook.ts +++ b/extensions/zalo/src/monitor.webhook.ts @@ -1,6 +1,6 @@ import { timingSafeEqual } from "node:crypto"; import type { IncomingMessage, ServerResponse } from "node:http"; -import type { OpenClawConfig } from "openclaw/plugin-sdk"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; import { createDedupeCache, createFixedWindowRateLimiter, @@ -15,7 +15,7 @@ import { resolveWebhookTargets, WEBHOOK_ANOMALY_COUNTER_DEFAULTS, WEBHOOK_RATE_LIMIT_DEFAULTS, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import type { ResolvedZaloAccount } from "./accounts.js"; import type { ZaloFetch, ZaloUpdate } from "./api.js"; import type { ZaloRuntimeEnv } from "./monitor.js"; diff --git a/extensions/zalo/src/onboarding.status.test.ts b/extensions/zalo/src/onboarding.status.test.ts index 7bc4b7f845b..6282b7eaf67 100644 --- a/extensions/zalo/src/onboarding.status.test.ts +++ b/extensions/zalo/src/onboarding.status.test.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig } from "openclaw/plugin-sdk"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; import { describe, expect, it } from "vitest"; import { zaloOnboardingAdapter } from "./onboarding.js"; diff --git a/extensions/zalo/src/onboarding.ts b/extensions/zalo/src/onboarding.ts index c249e094ba6..0f68bd4f36d 100644 --- a/extensions/zalo/src/onboarding.ts +++ b/extensions/zalo/src/onboarding.ts @@ -4,7 +4,7 @@ import type { OpenClawConfig, SecretInput, WizardPrompter, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { addWildcardAllowFrom, DEFAULT_ACCOUNT_ID, @@ -13,7 +13,7 @@ import { normalizeAccountId, promptAccountId, promptSingleChannelSecretInput, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { listZaloAccountIds, resolveDefaultZaloAccountId, resolveZaloAccount } from "./accounts.js"; const channel = "zalo" as const; diff --git a/extensions/zalo/src/probe.ts b/extensions/zalo/src/probe.ts index c2d95fa1d28..f8fa6b87943 100644 --- a/extensions/zalo/src/probe.ts +++ b/extensions/zalo/src/probe.ts @@ -1,4 +1,4 @@ -import type { BaseProbeResult } from "openclaw/plugin-sdk"; +import type { BaseProbeResult } from "openclaw/plugin-sdk/compat"; import { getMe, ZaloApiError, type ZaloBotInfo, type ZaloFetch } from "./api.js"; export type ZaloProbeResult = BaseProbeResult & { diff --git a/extensions/zalo/src/runtime.ts b/extensions/zalo/src/runtime.ts index 08ed58572e1..706fe2587d5 100644 --- a/extensions/zalo/src/runtime.ts +++ b/extensions/zalo/src/runtime.ts @@ -1,4 +1,4 @@ -import type { PluginRuntime } from "openclaw/plugin-sdk"; +import type { PluginRuntime } from "openclaw/plugin-sdk/compat"; let runtime: PluginRuntime | null = null; diff --git a/extensions/zalo/src/secret-input.ts b/extensions/zalo/src/secret-input.ts index f90d41c6fb9..3fc82b3ac91 100644 --- a/extensions/zalo/src/secret-input.ts +++ b/extensions/zalo/src/secret-input.ts @@ -2,7 +2,7 @@ import { hasConfiguredSecretInput, normalizeResolvedSecretInputString, normalizeSecretInputString, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { z } from "zod"; export { hasConfiguredSecretInput, normalizeResolvedSecretInputString, normalizeSecretInputString }; diff --git a/extensions/zalo/src/send.ts b/extensions/zalo/src/send.ts index e2ac8b4bcb9..29120cfce02 100644 --- a/extensions/zalo/src/send.ts +++ b/extensions/zalo/src/send.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig } from "openclaw/plugin-sdk"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; import { resolveZaloAccount } from "./accounts.js"; import type { ZaloFetch } from "./api.js"; import { sendMessage, sendPhoto } from "./api.js"; diff --git a/extensions/zalo/src/status-issues.ts b/extensions/zalo/src/status-issues.ts index ba217570eb4..ecff1af2b14 100644 --- a/extensions/zalo/src/status-issues.ts +++ b/extensions/zalo/src/status-issues.ts @@ -1,4 +1,4 @@ -import type { ChannelAccountSnapshot, ChannelStatusIssue } from "openclaw/plugin-sdk"; +import type { ChannelAccountSnapshot, ChannelStatusIssue } from "openclaw/plugin-sdk/compat"; type ZaloAccountStatus = { accountId?: unknown; diff --git a/extensions/zalo/src/token.ts b/extensions/zalo/src/token.ts index 50d3c5557bb..992d4b1b2ad 100644 --- a/extensions/zalo/src/token.ts +++ b/extensions/zalo/src/token.ts @@ -1,6 +1,6 @@ import { readFileSync } from "node:fs"; -import type { BaseTokenResolution } from "openclaw/plugin-sdk"; import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id"; +import type { BaseTokenResolution } from "openclaw/plugin-sdk/compat"; import { normalizeResolvedSecretInputString, normalizeSecretInputString } from "./secret-input.js"; import type { ZaloConfig } from "./types.js"; diff --git a/extensions/zalo/src/types.ts b/extensions/zalo/src/types.ts index 0e2952552a8..3ad17ef5b88 100644 --- a/extensions/zalo/src/types.ts +++ b/extensions/zalo/src/types.ts @@ -1,4 +1,4 @@ -import type { SecretInput } from "openclaw/plugin-sdk"; +import type { SecretInput } from "openclaw/plugin-sdk/compat"; export type ZaloAccountConfig = { /** Optional display name for this account (used in CLI/UI lists). */ From 37a8caee42bdb94292f6e00480d896e96764e407 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 4 Mar 2026 01:20:21 -0500 Subject: [PATCH 28/74] Extensions: migrate zalouser plugin-sdk imports --- extensions/zalouser/src/accounts.test.ts | 2 +- extensions/zalouser/src/accounts.ts | 2 +- extensions/zalouser/src/channel.sendpayload.test.ts | 2 +- extensions/zalouser/src/channel.ts | 4 ++-- extensions/zalouser/src/config-schema.ts | 2 +- extensions/zalouser/src/monitor.account-scope.test.ts | 2 +- extensions/zalouser/src/monitor.group-gating.test.ts | 2 +- extensions/zalouser/src/monitor.ts | 4 ++-- extensions/zalouser/src/onboarding.ts | 4 ++-- extensions/zalouser/src/probe.ts | 2 +- extensions/zalouser/src/runtime.ts | 2 +- extensions/zalouser/src/status-issues.ts | 2 +- extensions/zalouser/src/zalo-js.ts | 2 +- 13 files changed, 16 insertions(+), 16 deletions(-) diff --git a/extensions/zalouser/src/accounts.test.ts b/extensions/zalouser/src/accounts.test.ts index f1ce6509358..672a3618431 100644 --- a/extensions/zalouser/src/accounts.test.ts +++ b/extensions/zalouser/src/accounts.test.ts @@ -1,5 +1,5 @@ -import type { OpenClawConfig } from "openclaw/plugin-sdk"; import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { getZcaUserInfo, diff --git a/extensions/zalouser/src/accounts.ts b/extensions/zalouser/src/accounts.ts index 4797ec0416a..860c1202155 100644 --- a/extensions/zalouser/src/accounts.ts +++ b/extensions/zalouser/src/accounts.ts @@ -1,9 +1,9 @@ -import type { OpenClawConfig } from "openclaw/plugin-sdk"; import { DEFAULT_ACCOUNT_ID, normalizeAccountId, normalizeOptionalAccountId, } from "openclaw/plugin-sdk/account-id"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; import type { ResolvedZalouserAccount, ZalouserAccountConfig, ZalouserConfig } from "./types.js"; import { checkZaloAuthenticated, getZaloUserInfo } from "./zalo-js.js"; diff --git a/extensions/zalouser/src/channel.sendpayload.test.ts b/extensions/zalouser/src/channel.sendpayload.test.ts index cdf478411f0..9ca29d8bea3 100644 --- a/extensions/zalouser/src/channel.sendpayload.test.ts +++ b/extensions/zalouser/src/channel.sendpayload.test.ts @@ -1,4 +1,4 @@ -import type { ReplyPayload } from "openclaw/plugin-sdk"; +import type { ReplyPayload } from "openclaw/plugin-sdk/compat"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { zalouserPlugin } from "./channel.js"; diff --git a/extensions/zalouser/src/channel.ts b/extensions/zalouser/src/channel.ts index 2c1770b6ebd..fa5411e2ccc 100644 --- a/extensions/zalouser/src/channel.ts +++ b/extensions/zalouser/src/channel.ts @@ -9,7 +9,7 @@ import type { ChannelPlugin, OpenClawConfig, GroupToolPolicyConfig, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { applyAccountNameToChannelSection, buildChannelConfigSchema, @@ -23,7 +23,7 @@ import { resolvePreferredOpenClawTmpDir, resolveChannelAccountConfigBasePath, setAccountEnabledInConfigSection, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { listZalouserAccountIds, resolveDefaultZalouserAccountId, diff --git a/extensions/zalouser/src/config-schema.ts b/extensions/zalouser/src/config-schema.ts index 795c5b6da42..0f4b505d38e 100644 --- a/extensions/zalouser/src/config-schema.ts +++ b/extensions/zalouser/src/config-schema.ts @@ -1,4 +1,4 @@ -import { MarkdownConfigSchema, ToolPolicySchema } from "openclaw/plugin-sdk"; +import { MarkdownConfigSchema, ToolPolicySchema } from "openclaw/plugin-sdk/compat"; import { z } from "zod"; const allowFromEntry = z.union([z.string(), z.number()]); diff --git a/extensions/zalouser/src/monitor.account-scope.test.ts b/extensions/zalouser/src/monitor.account-scope.test.ts index a5a6e8967e9..eca0cff6c8c 100644 --- a/extensions/zalouser/src/monitor.account-scope.test.ts +++ b/extensions/zalouser/src/monitor.account-scope.test.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig, PluginRuntime, RuntimeEnv } from "openclaw/plugin-sdk"; +import type { OpenClawConfig, PluginRuntime, RuntimeEnv } from "openclaw/plugin-sdk/compat"; import { describe, expect, it, vi } from "vitest"; import { __testing } from "./monitor.js"; import { setZalouserRuntime } from "./runtime.js"; diff --git a/extensions/zalouser/src/monitor.group-gating.test.ts b/extensions/zalouser/src/monitor.group-gating.test.ts index 25ef0e54594..146ae563589 100644 --- a/extensions/zalouser/src/monitor.group-gating.test.ts +++ b/extensions/zalouser/src/monitor.group-gating.test.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig, PluginRuntime, RuntimeEnv } from "openclaw/plugin-sdk"; +import type { OpenClawConfig, PluginRuntime, RuntimeEnv } from "openclaw/plugin-sdk/compat"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { __testing } from "./monitor.js"; import { setZalouserRuntime } from "./runtime.js"; diff --git a/extensions/zalouser/src/monitor.ts b/extensions/zalouser/src/monitor.ts index c6cb79a9d9f..9382bdb9e7f 100644 --- a/extensions/zalouser/src/monitor.ts +++ b/extensions/zalouser/src/monitor.ts @@ -3,7 +3,7 @@ import type { OpenClawConfig, OutboundReplyPayload, RuntimeEnv, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { createTypingCallbacks, createScopedPairingAccess, @@ -17,7 +17,7 @@ import { sendMediaWithLeadingCaption, summarizeMapping, warnMissingProviderGroupPolicyFallbackOnce, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { buildZalouserGroupCandidates, findZalouserGroupEntry, diff --git a/extensions/zalouser/src/onboarding.ts b/extensions/zalouser/src/onboarding.ts index 8c702efeb7d..22039413085 100644 --- a/extensions/zalouser/src/onboarding.ts +++ b/extensions/zalouser/src/onboarding.ts @@ -5,7 +5,7 @@ import type { ChannelOnboardingDmPolicy, OpenClawConfig, WizardPrompter, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { addWildcardAllowFrom, DEFAULT_ACCOUNT_ID, @@ -15,7 +15,7 @@ import { promptAccountId, promptChannelAccessConfig, resolvePreferredOpenClawTmpDir, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { listZalouserAccountIds, resolveDefaultZalouserAccountId, diff --git a/extensions/zalouser/src/probe.ts b/extensions/zalouser/src/probe.ts index 2285c46feaf..cfa33b0c645 100644 --- a/extensions/zalouser/src/probe.ts +++ b/extensions/zalouser/src/probe.ts @@ -1,4 +1,4 @@ -import type { BaseProbeResult } from "openclaw/plugin-sdk"; +import type { BaseProbeResult } from "openclaw/plugin-sdk/compat"; import type { ZcaUserInfo } from "./types.js"; import { getZaloUserInfo } from "./zalo-js.js"; diff --git a/extensions/zalouser/src/runtime.ts b/extensions/zalouser/src/runtime.ts index 2ab0f243cb3..66287f1280f 100644 --- a/extensions/zalouser/src/runtime.ts +++ b/extensions/zalouser/src/runtime.ts @@ -1,4 +1,4 @@ -import type { PluginRuntime } from "openclaw/plugin-sdk"; +import type { PluginRuntime } from "openclaw/plugin-sdk/compat"; let runtime: PluginRuntime | null = null; diff --git a/extensions/zalouser/src/status-issues.ts b/extensions/zalouser/src/status-issues.ts index 34ebdc2e330..d9f47361d30 100644 --- a/extensions/zalouser/src/status-issues.ts +++ b/extensions/zalouser/src/status-issues.ts @@ -1,4 +1,4 @@ -import type { ChannelAccountSnapshot, ChannelStatusIssue } from "openclaw/plugin-sdk"; +import type { ChannelAccountSnapshot, ChannelStatusIssue } from "openclaw/plugin-sdk/compat"; type ZalouserAccountStatus = { accountId?: unknown; diff --git a/extensions/zalouser/src/zalo-js.ts b/extensions/zalouser/src/zalo-js.ts index c7e036cf8c7..2e230c81ec1 100644 --- a/extensions/zalouser/src/zalo-js.ts +++ b/extensions/zalouser/src/zalo-js.ts @@ -3,7 +3,7 @@ import fs from "node:fs"; import fsp from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import { loadOutboundMediaFromUrl } from "openclaw/plugin-sdk"; +import { loadOutboundMediaFromUrl } from "openclaw/plugin-sdk/compat"; import { normalizeZaloReactionIcon } from "./reaction.js"; import { getZalouserRuntime } from "./runtime.js"; import type { From 26e014311f1a408e46c0f835a8643a4cefb46313 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 4 Mar 2026 01:20:37 -0500 Subject: [PATCH 29/74] Extensions: migrate acpx plugin-sdk imports --- extensions/acpx/src/config.ts | 2 +- extensions/acpx/src/ensure.ts | 2 +- extensions/acpx/src/runtime-internals/events.ts | 2 +- extensions/acpx/src/runtime-internals/process.ts | 4 ++-- extensions/acpx/src/runtime.ts | 4 ++-- extensions/acpx/src/service.test.ts | 2 +- extensions/acpx/src/service.ts | 4 ++-- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/extensions/acpx/src/config.ts b/extensions/acpx/src/config.ts index a5441423c5e..91f3eb08b57 100644 --- a/extensions/acpx/src/config.ts +++ b/extensions/acpx/src/config.ts @@ -1,6 +1,6 @@ import path from "node:path"; import { fileURLToPath } from "node:url"; -import type { OpenClawPluginConfigSchema } from "openclaw/plugin-sdk"; +import type { OpenClawPluginConfigSchema } from "openclaw/plugin-sdk/compat"; export const ACPX_PERMISSION_MODES = ["approve-all", "approve-reads", "deny-all"] as const; export type AcpxPermissionMode = (typeof ACPX_PERMISSION_MODES)[number]; diff --git a/extensions/acpx/src/ensure.ts b/extensions/acpx/src/ensure.ts index dbe5807daa4..b86d6b749a8 100644 --- a/extensions/acpx/src/ensure.ts +++ b/extensions/acpx/src/ensure.ts @@ -1,6 +1,6 @@ import fs from "node:fs"; import path from "node:path"; -import type { PluginLogger } from "openclaw/plugin-sdk"; +import type { PluginLogger } from "openclaw/plugin-sdk/compat"; import { ACPX_PINNED_VERSION, ACPX_PLUGIN_ROOT, buildAcpxLocalInstallCommand } from "./config.js"; import { resolveSpawnFailure, diff --git a/extensions/acpx/src/runtime-internals/events.ts b/extensions/acpx/src/runtime-internals/events.ts index 4556cd0d9ca..c2eb8aaef91 100644 --- a/extensions/acpx/src/runtime-internals/events.ts +++ b/extensions/acpx/src/runtime-internals/events.ts @@ -1,4 +1,4 @@ -import type { AcpRuntimeEvent, AcpSessionUpdateTag } from "openclaw/plugin-sdk"; +import type { AcpRuntimeEvent, AcpSessionUpdateTag } from "openclaw/plugin-sdk/compat"; import { asOptionalBoolean, asOptionalString, diff --git a/extensions/acpx/src/runtime-internals/process.ts b/extensions/acpx/src/runtime-internals/process.ts index f215aec8b51..842b2b27fc4 100644 --- a/extensions/acpx/src/runtime-internals/process.ts +++ b/extensions/acpx/src/runtime-internals/process.ts @@ -4,12 +4,12 @@ import type { WindowsSpawnProgram, WindowsSpawnProgramCandidate, WindowsSpawnResolution, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; import { applyWindowsSpawnProgramPolicy, materializeWindowsSpawnProgram, resolveWindowsSpawnProgramCandidate, -} from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; export type SpawnExit = { code: number | null; diff --git a/extensions/acpx/src/runtime.ts b/extensions/acpx/src/runtime.ts index c4a00f008a8..e1b67ab87f6 100644 --- a/extensions/acpx/src/runtime.ts +++ b/extensions/acpx/src/runtime.ts @@ -10,8 +10,8 @@ import type { AcpRuntimeStatus, AcpRuntimeTurnInput, PluginLogger, -} from "openclaw/plugin-sdk"; -import { AcpRuntimeError } from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; +import { AcpRuntimeError } from "openclaw/plugin-sdk/compat"; import { type ResolvedAcpxPluginConfig } from "./config.js"; import { checkAcpxVersion } from "./ensure.js"; import { diff --git a/extensions/acpx/src/service.test.ts b/extensions/acpx/src/service.test.ts index 19cf95f6bee..26205bedde1 100644 --- a/extensions/acpx/src/service.test.ts +++ b/extensions/acpx/src/service.test.ts @@ -1,4 +1,4 @@ -import type { AcpRuntime, OpenClawPluginServiceContext } from "openclaw/plugin-sdk"; +import type { AcpRuntime, OpenClawPluginServiceContext } from "openclaw/plugin-sdk/compat"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { AcpRuntimeError } from "../../../src/acp/runtime/errors.js"; import { diff --git a/extensions/acpx/src/service.ts b/extensions/acpx/src/service.ts index d89b9e281c7..01b4bf2ecc6 100644 --- a/extensions/acpx/src/service.ts +++ b/extensions/acpx/src/service.ts @@ -3,8 +3,8 @@ import type { OpenClawPluginService, OpenClawPluginServiceContext, PluginLogger, -} from "openclaw/plugin-sdk"; -import { registerAcpRuntimeBackend, unregisterAcpRuntimeBackend } from "openclaw/plugin-sdk"; +} from "openclaw/plugin-sdk/compat"; +import { registerAcpRuntimeBackend, unregisterAcpRuntimeBackend } from "openclaw/plugin-sdk/compat"; import { resolveAcpxPluginConfig, type ResolvedAcpxPluginConfig } from "./config.js"; import { ensureAcpx } from "./ensure.js"; import { ACPX_BACKEND_ID, AcpxRuntime } from "./runtime.js"; From 9d941949c9ea47f3e6277aab744860d83f6c0dcd Mon Sep 17 00:00:00 2001 From: Lynn Date: Wed, 4 Mar 2026 14:53:38 +0800 Subject: [PATCH 30/74] fix(tui): normalize session key to lowercase to match gateway canonicalization (#34013) Merged via squash. Prepared head SHA: cfe06ca131661d1fd669270345c549ee8141cc46 Co-authored-by: lynnzc <6257996+lynnzc@users.noreply.github.com> Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com> Reviewed-by: @altaywtf --- CHANGELOG.md | 1 + src/tui/tui.test.ts | 21 +++++++++++++++++++++ src/tui/tui.ts | 4 ++-- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 57307903afd..d853f53faac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- TUI/session-key canonicalization: normalize `openclaw tui --session` values to lowercase so uppercase session names no longer drop real-time streaming updates due to gateway/TUI key mismatches. (#33866, #34013) thanks @lynnzc. - Outbound/send config threading: pass resolved SecretRef config through outbound adapters and helper send paths so send flows do not reload unresolved runtime config. (#33987) Thanks @joshavant. - Sessions/subagent attachments: remove `attachments[].content.maxLength` from `sessions_spawn` schema to avoid llama.cpp GBNF repetition overflow, and preflight UTF-8 byte size before buffer allocation while keeping runtime file-size enforcement unchanged. (#33648) Thanks @anisoptera. - Runtime/tool-state stability: recover from dangling Anthropic `tool_use` after compaction, serialize long-running Discord handler runs without blocking new inbound events, and prevent stale busy snapshots from suppressing stuck-channel recovery. (from #33630, #33583) Thanks @kevinWangSheng and @theotarr. diff --git a/src/tui/tui.test.ts b/src/tui/tui.test.ts index 9b46da66a99..2882cebcd64 100644 --- a/src/tui/tui.test.ts +++ b/src/tui/tui.test.ts @@ -74,6 +74,27 @@ describe("resolveTuiSessionKey", () => { }), ).toBe("agent:ops:incident"); }); + + it("lowercases session keys with uppercase characters", () => { + // Uppercase in agent-prefixed form + expect( + resolveTuiSessionKey({ + raw: "agent:main:Test1", + sessionScope: "global", + currentAgentId: "main", + sessionMainKey: "agent:main:main", + }), + ).toBe("agent:main:test1"); + // Uppercase in bare form (prefixed by currentAgentId) + expect( + resolveTuiSessionKey({ + raw: "Test1", + sessionScope: "global", + currentAgentId: "main", + sessionMainKey: "agent:main:main", + }), + ).toBe("agent:main:test1"); + }); }); describe("resolveGatewayDisconnectState", () => { diff --git a/src/tui/tui.ts b/src/tui/tui.ts index 847245b3b67..fe365477d91 100644 --- a/src/tui/tui.ts +++ b/src/tui/tui.ts @@ -203,9 +203,9 @@ export function resolveTuiSessionKey(params: { return trimmed; } if (trimmed.startsWith("agent:")) { - return trimmed; + return trimmed.toLowerCase(); } - return `agent:${params.currentAgentId}:${trimmed}`; + return `agent:${params.currentAgentId}:${trimmed.toLowerCase()}`; } export function resolveGatewayDisconnectState(reason?: string): { From 7a2f5a0098d192944825f17d3be457af91d2bed1 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 4 Mar 2026 02:31:44 -0500 Subject: [PATCH 31/74] Plugin SDK: add full bundled subpath wiring --- docs/tools/plugin.md | 40 ++++- package.json | 128 ++++++++++++++ ...-no-monolithic-plugin-sdk-entry-imports.ts | 11 +- scripts/check-plugin-sdk-exports.mjs | 33 ++++ scripts/release-check.ts | 68 ++++++++ scripts/write-plugin-sdk-entry-dts.ts | 33 ++++ src/plugin-sdk/acpx.ts | 34 ++++ src/plugin-sdk/bluebubbles.ts | 100 +++++++++++ src/plugin-sdk/copilot-proxy.ts | 9 + src/plugin-sdk/device-pair.ts | 8 + src/plugin-sdk/diagnostics-otel.ts | 13 ++ src/plugin-sdk/diffs.ts | 11 ++ src/plugin-sdk/feishu.ts | 71 ++++++++ src/plugin-sdk/google-gemini-cli-auth.ts | 8 + src/plugin-sdk/googlechat.ts | 78 +++++++++ src/plugin-sdk/irc.ts | 70 ++++++++ src/plugin-sdk/llm-task.ts | 5 + src/plugin-sdk/lobster.ts | 14 ++ src/plugin-sdk/matrix.ts | 96 +++++++++++ src/plugin-sdk/mattermost.ts | 85 +++++++++ src/plugin-sdk/memory-core.ts | 5 + src/plugin-sdk/memory-lancedb.ts | 4 + src/plugin-sdk/minimax-portal-auth.ts | 10 ++ src/plugin-sdk/msteams.ts | 108 ++++++++++++ src/plugin-sdk/nextcloud-talk.ts | 92 ++++++++++ src/plugin-sdk/nostr.ts | 19 +++ src/plugin-sdk/open-prose.ts | 4 + src/plugin-sdk/phone-control.ts | 9 + src/plugin-sdk/qwen-portal-auth.ts | 6 + src/plugin-sdk/subpaths.test.ts | 54 ++++++ src/plugin-sdk/synology-chat.ts | 17 ++ src/plugin-sdk/talk-voice.ts | 4 + src/plugin-sdk/test-utils.ts | 8 + src/plugin-sdk/thread-ownership.ts | 5 + src/plugin-sdk/tlon.ts | 28 +++ src/plugin-sdk/twitch.ts | 19 +++ src/plugin-sdk/voice-call.ts | 18 ++ src/plugin-sdk/whatsapp.ts | 1 + src/plugin-sdk/zalo.ts | 94 ++++++++++ src/plugin-sdk/zalouser.ts | 63 +++++++ src/plugins/loader.ts | 161 +++++++++++------- tsconfig.plugin-sdk.dts.json | 32 ++++ tsdown.config.ts | 125 ++++++-------- vitest.config.ts | 93 +++++----- 44 files changed, 1704 insertions(+), 190 deletions(-) create mode 100644 src/plugin-sdk/acpx.ts create mode 100644 src/plugin-sdk/bluebubbles.ts create mode 100644 src/plugin-sdk/copilot-proxy.ts create mode 100644 src/plugin-sdk/device-pair.ts create mode 100644 src/plugin-sdk/diagnostics-otel.ts create mode 100644 src/plugin-sdk/diffs.ts create mode 100644 src/plugin-sdk/feishu.ts create mode 100644 src/plugin-sdk/google-gemini-cli-auth.ts create mode 100644 src/plugin-sdk/googlechat.ts create mode 100644 src/plugin-sdk/irc.ts create mode 100644 src/plugin-sdk/llm-task.ts create mode 100644 src/plugin-sdk/lobster.ts create mode 100644 src/plugin-sdk/matrix.ts create mode 100644 src/plugin-sdk/mattermost.ts create mode 100644 src/plugin-sdk/memory-core.ts create mode 100644 src/plugin-sdk/memory-lancedb.ts create mode 100644 src/plugin-sdk/minimax-portal-auth.ts create mode 100644 src/plugin-sdk/msteams.ts create mode 100644 src/plugin-sdk/nextcloud-talk.ts create mode 100644 src/plugin-sdk/nostr.ts create mode 100644 src/plugin-sdk/open-prose.ts create mode 100644 src/plugin-sdk/phone-control.ts create mode 100644 src/plugin-sdk/qwen-portal-auth.ts create mode 100644 src/plugin-sdk/synology-chat.ts create mode 100644 src/plugin-sdk/talk-voice.ts create mode 100644 src/plugin-sdk/test-utils.ts create mode 100644 src/plugin-sdk/thread-ownership.ts create mode 100644 src/plugin-sdk/tlon.ts create mode 100644 src/plugin-sdk/twitch.ts create mode 100644 src/plugin-sdk/voice-call.ts create mode 100644 src/plugin-sdk/zalo.ts create mode 100644 src/plugin-sdk/zalouser.ts diff --git a/docs/tools/plugin.md b/docs/tools/plugin.md index 60d5aa61c37..f0335da0e7a 100644 --- a/docs/tools/plugin.md +++ b/docs/tools/plugin.md @@ -120,12 +120,32 @@ authoring plugins: - `openclaw/plugin-sdk/imessage` for iMessage channel plugins. - `openclaw/plugin-sdk/whatsapp` for WhatsApp channel plugins. - `openclaw/plugin-sdk/line` for LINE channel plugins. +- `openclaw/plugin-sdk/msteams` for the bundled Microsoft Teams plugin surface. +- Bundled extension-specific subpaths are also available: + `openclaw/plugin-sdk/acpx`, `openclaw/plugin-sdk/bluebubbles`, + `openclaw/plugin-sdk/copilot-proxy`, `openclaw/plugin-sdk/device-pair`, + `openclaw/plugin-sdk/diagnostics-otel`, `openclaw/plugin-sdk/diffs`, + `openclaw/plugin-sdk/feishu`, + `openclaw/plugin-sdk/google-gemini-cli-auth`, `openclaw/plugin-sdk/googlechat`, + `openclaw/plugin-sdk/irc`, `openclaw/plugin-sdk/llm-task`, + `openclaw/plugin-sdk/lobster`, `openclaw/plugin-sdk/matrix`, + `openclaw/plugin-sdk/mattermost`, `openclaw/plugin-sdk/memory-core`, + `openclaw/plugin-sdk/memory-lancedb`, + `openclaw/plugin-sdk/minimax-portal-auth`, + `openclaw/plugin-sdk/nextcloud-talk`, `openclaw/plugin-sdk/nostr`, + `openclaw/plugin-sdk/open-prose`, `openclaw/plugin-sdk/phone-control`, + `openclaw/plugin-sdk/qwen-portal-auth`, `openclaw/plugin-sdk/synology-chat`, + `openclaw/plugin-sdk/talk-voice`, `openclaw/plugin-sdk/test-utils`, + `openclaw/plugin-sdk/thread-ownership`, `openclaw/plugin-sdk/tlon`, + `openclaw/plugin-sdk/twitch`, `openclaw/plugin-sdk/voice-call`, + `openclaw/plugin-sdk/zalo`, and `openclaw/plugin-sdk/zalouser`. Compatibility note: - `openclaw/plugin-sdk` remains supported for existing external plugins. -- New and migrated bundled plugins should use channel subpaths and `core`; use - `compat` only when broader shared helpers are required. +- New and migrated bundled plugins should use channel or extension-specific + subpaths; use `core` for generic surfaces and `compat` only when broader + shared helpers are required. Performance note: @@ -154,13 +174,21 @@ OpenClaw scans, in order: - `~/.openclaw/extensions/*.ts` - `~/.openclaw/extensions/*/index.ts` -4. Bundled extensions (shipped with OpenClaw, **disabled by default**) +4. Bundled extensions (shipped with OpenClaw, mostly disabled by default) - `/extensions/*` -Bundled plugins must be enabled explicitly via `plugins.entries..enabled` -or `openclaw plugins enable `. Installed plugins are enabled by default, -but can be disabled the same way. +Most bundled plugins must be enabled explicitly via +`plugins.entries..enabled` or `openclaw plugins enable `. + +Default-on bundled plugin exceptions: + +- `device-pair` +- `phone-control` +- `talk-voice` +- active memory slot plugin (default slot: `memory-core`) + +Installed plugins are enabled by default, but can be disabled the same way. Hardening notes: diff --git a/package.json b/package.json index 590f2b4e9a4..6c85410074d 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,134 @@ "types": "./dist/plugin-sdk/line.d.ts", "default": "./dist/plugin-sdk/line.js" }, + "./plugin-sdk/msteams": { + "types": "./dist/plugin-sdk/msteams.d.ts", + "default": "./dist/plugin-sdk/msteams.js" + }, + "./plugin-sdk/acpx": { + "types": "./dist/plugin-sdk/acpx.d.ts", + "default": "./dist/plugin-sdk/acpx.js" + }, + "./plugin-sdk/bluebubbles": { + "types": "./dist/plugin-sdk/bluebubbles.d.ts", + "default": "./dist/plugin-sdk/bluebubbles.js" + }, + "./plugin-sdk/copilot-proxy": { + "types": "./dist/plugin-sdk/copilot-proxy.d.ts", + "default": "./dist/plugin-sdk/copilot-proxy.js" + }, + "./plugin-sdk/device-pair": { + "types": "./dist/plugin-sdk/device-pair.d.ts", + "default": "./dist/plugin-sdk/device-pair.js" + }, + "./plugin-sdk/diagnostics-otel": { + "types": "./dist/plugin-sdk/diagnostics-otel.d.ts", + "default": "./dist/plugin-sdk/diagnostics-otel.js" + }, + "./plugin-sdk/diffs": { + "types": "./dist/plugin-sdk/diffs.d.ts", + "default": "./dist/plugin-sdk/diffs.js" + }, + "./plugin-sdk/feishu": { + "types": "./dist/plugin-sdk/feishu.d.ts", + "default": "./dist/plugin-sdk/feishu.js" + }, + "./plugin-sdk/google-gemini-cli-auth": { + "types": "./dist/plugin-sdk/google-gemini-cli-auth.d.ts", + "default": "./dist/plugin-sdk/google-gemini-cli-auth.js" + }, + "./plugin-sdk/googlechat": { + "types": "./dist/plugin-sdk/googlechat.d.ts", + "default": "./dist/plugin-sdk/googlechat.js" + }, + "./plugin-sdk/irc": { + "types": "./dist/plugin-sdk/irc.d.ts", + "default": "./dist/plugin-sdk/irc.js" + }, + "./plugin-sdk/llm-task": { + "types": "./dist/plugin-sdk/llm-task.d.ts", + "default": "./dist/plugin-sdk/llm-task.js" + }, + "./plugin-sdk/lobster": { + "types": "./dist/plugin-sdk/lobster.d.ts", + "default": "./dist/plugin-sdk/lobster.js" + }, + "./plugin-sdk/matrix": { + "types": "./dist/plugin-sdk/matrix.d.ts", + "default": "./dist/plugin-sdk/matrix.js" + }, + "./plugin-sdk/mattermost": { + "types": "./dist/plugin-sdk/mattermost.d.ts", + "default": "./dist/plugin-sdk/mattermost.js" + }, + "./plugin-sdk/memory-core": { + "types": "./dist/plugin-sdk/memory-core.d.ts", + "default": "./dist/plugin-sdk/memory-core.js" + }, + "./plugin-sdk/memory-lancedb": { + "types": "./dist/plugin-sdk/memory-lancedb.d.ts", + "default": "./dist/plugin-sdk/memory-lancedb.js" + }, + "./plugin-sdk/minimax-portal-auth": { + "types": "./dist/plugin-sdk/minimax-portal-auth.d.ts", + "default": "./dist/plugin-sdk/minimax-portal-auth.js" + }, + "./plugin-sdk/nextcloud-talk": { + "types": "./dist/plugin-sdk/nextcloud-talk.d.ts", + "default": "./dist/plugin-sdk/nextcloud-talk.js" + }, + "./plugin-sdk/nostr": { + "types": "./dist/plugin-sdk/nostr.d.ts", + "default": "./dist/plugin-sdk/nostr.js" + }, + "./plugin-sdk/open-prose": { + "types": "./dist/plugin-sdk/open-prose.d.ts", + "default": "./dist/plugin-sdk/open-prose.js" + }, + "./plugin-sdk/phone-control": { + "types": "./dist/plugin-sdk/phone-control.d.ts", + "default": "./dist/plugin-sdk/phone-control.js" + }, + "./plugin-sdk/qwen-portal-auth": { + "types": "./dist/plugin-sdk/qwen-portal-auth.d.ts", + "default": "./dist/plugin-sdk/qwen-portal-auth.js" + }, + "./plugin-sdk/synology-chat": { + "types": "./dist/plugin-sdk/synology-chat.d.ts", + "default": "./dist/plugin-sdk/synology-chat.js" + }, + "./plugin-sdk/talk-voice": { + "types": "./dist/plugin-sdk/talk-voice.d.ts", + "default": "./dist/plugin-sdk/talk-voice.js" + }, + "./plugin-sdk/test-utils": { + "types": "./dist/plugin-sdk/test-utils.d.ts", + "default": "./dist/plugin-sdk/test-utils.js" + }, + "./plugin-sdk/thread-ownership": { + "types": "./dist/plugin-sdk/thread-ownership.d.ts", + "default": "./dist/plugin-sdk/thread-ownership.js" + }, + "./plugin-sdk/tlon": { + "types": "./dist/plugin-sdk/tlon.d.ts", + "default": "./dist/plugin-sdk/tlon.js" + }, + "./plugin-sdk/twitch": { + "types": "./dist/plugin-sdk/twitch.d.ts", + "default": "./dist/plugin-sdk/twitch.js" + }, + "./plugin-sdk/voice-call": { + "types": "./dist/plugin-sdk/voice-call.d.ts", + "default": "./dist/plugin-sdk/voice-call.js" + }, + "./plugin-sdk/zalo": { + "types": "./dist/plugin-sdk/zalo.d.ts", + "default": "./dist/plugin-sdk/zalo.js" + }, + "./plugin-sdk/zalouser": { + "types": "./dist/plugin-sdk/zalouser.d.ts", + "default": "./dist/plugin-sdk/zalouser.js" + }, "./plugin-sdk/account-id": { "types": "./dist/plugin-sdk/account-id.d.ts", "default": "./dist/plugin-sdk/account-id.js" diff --git a/scripts/check-no-monolithic-plugin-sdk-entry-imports.ts b/scripts/check-no-monolithic-plugin-sdk-entry-imports.ts index bde974d5154..9b77ae9cf61 100644 --- a/scripts/check-no-monolithic-plugin-sdk-entry-imports.ts +++ b/scripts/check-no-monolithic-plugin-sdk-entry-imports.ts @@ -2,15 +2,12 @@ import fs from "node:fs"; import path from "node:path"; import { discoverOpenClawPlugins } from "../src/plugins/discovery.js"; -const ROOT_IMPORT_PATTERNS = [ - /\b(?:import|export)\b[\s\S]*?\bfrom\s+["']openclaw\/plugin-sdk["']/, - /\bimport\s+["']openclaw\/plugin-sdk["']/, - /\bimport\s*\(\s*["']openclaw\/plugin-sdk["']\s*\)/, - /\brequire\s*\(\s*["']openclaw\/plugin-sdk["']\s*\)/, -]; +// Match exact monolithic-root specifier in any code path: +// imports/exports, require/dynamic import, and test mocks (vi.mock/jest.mock). +const ROOT_IMPORT_PATTERN = /["']openclaw\/plugin-sdk["']/; function hasMonolithicRootImport(content: string): boolean { - return ROOT_IMPORT_PATTERNS.some((pattern) => pattern.test(content)); + return ROOT_IMPORT_PATTERN.test(content); } function isSourceFile(filePath: string): boolean { diff --git a/scripts/check-plugin-sdk-exports.mjs b/scripts/check-plugin-sdk-exports.mjs index 87d7826945f..03ff9dfde8f 100755 --- a/scripts/check-plugin-sdk-exports.mjs +++ b/scripts/check-plugin-sdk-exports.mjs @@ -51,7 +51,40 @@ const requiredSubpathEntries = [ "imessage", "whatsapp", "line", + "msteams", + "acpx", + "bluebubbles", + "copilot-proxy", + "device-pair", + "diagnostics-otel", + "diffs", + "feishu", + "google-gemini-cli-auth", + "googlechat", + "irc", + "llm-task", + "lobster", + "matrix", + "mattermost", + "memory-core", + "memory-lancedb", + "minimax-portal-auth", + "nextcloud-talk", + "nostr", + "open-prose", + "phone-control", + "qwen-portal-auth", + "synology-chat", + "talk-voice", + "test-utils", + "thread-ownership", + "tlon", + "twitch", + "voice-call", + "zalo", + "zalouser", "account-id", + "keyed-async-queue", ]; const requiredRuntimeShimEntries = ["root-alias.cjs"]; diff --git a/scripts/release-check.ts b/scripts/release-check.ts index 9b2848e8ead..5eb72113cc5 100755 --- a/scripts/release-check.ts +++ b/scripts/release-check.ts @@ -33,6 +33,74 @@ const requiredPathGroups = [ "dist/plugin-sdk/whatsapp.d.ts", "dist/plugin-sdk/line.js", "dist/plugin-sdk/line.d.ts", + "dist/plugin-sdk/msteams.js", + "dist/plugin-sdk/msteams.d.ts", + "dist/plugin-sdk/acpx.js", + "dist/plugin-sdk/acpx.d.ts", + "dist/plugin-sdk/bluebubbles.js", + "dist/plugin-sdk/bluebubbles.d.ts", + "dist/plugin-sdk/copilot-proxy.js", + "dist/plugin-sdk/copilot-proxy.d.ts", + "dist/plugin-sdk/device-pair.js", + "dist/plugin-sdk/device-pair.d.ts", + "dist/plugin-sdk/diagnostics-otel.js", + "dist/plugin-sdk/diagnostics-otel.d.ts", + "dist/plugin-sdk/diffs.js", + "dist/plugin-sdk/diffs.d.ts", + "dist/plugin-sdk/feishu.js", + "dist/plugin-sdk/feishu.d.ts", + "dist/plugin-sdk/google-gemini-cli-auth.js", + "dist/plugin-sdk/google-gemini-cli-auth.d.ts", + "dist/plugin-sdk/googlechat.js", + "dist/plugin-sdk/googlechat.d.ts", + "dist/plugin-sdk/irc.js", + "dist/plugin-sdk/irc.d.ts", + "dist/plugin-sdk/llm-task.js", + "dist/plugin-sdk/llm-task.d.ts", + "dist/plugin-sdk/lobster.js", + "dist/plugin-sdk/lobster.d.ts", + "dist/plugin-sdk/matrix.js", + "dist/plugin-sdk/matrix.d.ts", + "dist/plugin-sdk/mattermost.js", + "dist/plugin-sdk/mattermost.d.ts", + "dist/plugin-sdk/memory-core.js", + "dist/plugin-sdk/memory-core.d.ts", + "dist/plugin-sdk/memory-lancedb.js", + "dist/plugin-sdk/memory-lancedb.d.ts", + "dist/plugin-sdk/minimax-portal-auth.js", + "dist/plugin-sdk/minimax-portal-auth.d.ts", + "dist/plugin-sdk/nextcloud-talk.js", + "dist/plugin-sdk/nextcloud-talk.d.ts", + "dist/plugin-sdk/nostr.js", + "dist/plugin-sdk/nostr.d.ts", + "dist/plugin-sdk/open-prose.js", + "dist/plugin-sdk/open-prose.d.ts", + "dist/plugin-sdk/phone-control.js", + "dist/plugin-sdk/phone-control.d.ts", + "dist/plugin-sdk/qwen-portal-auth.js", + "dist/plugin-sdk/qwen-portal-auth.d.ts", + "dist/plugin-sdk/synology-chat.js", + "dist/plugin-sdk/synology-chat.d.ts", + "dist/plugin-sdk/talk-voice.js", + "dist/plugin-sdk/talk-voice.d.ts", + "dist/plugin-sdk/test-utils.js", + "dist/plugin-sdk/test-utils.d.ts", + "dist/plugin-sdk/thread-ownership.js", + "dist/plugin-sdk/thread-ownership.d.ts", + "dist/plugin-sdk/tlon.js", + "dist/plugin-sdk/tlon.d.ts", + "dist/plugin-sdk/twitch.js", + "dist/plugin-sdk/twitch.d.ts", + "dist/plugin-sdk/voice-call.js", + "dist/plugin-sdk/voice-call.d.ts", + "dist/plugin-sdk/zalo.js", + "dist/plugin-sdk/zalo.d.ts", + "dist/plugin-sdk/zalouser.js", + "dist/plugin-sdk/zalouser.d.ts", + "dist/plugin-sdk/account-id.js", + "dist/plugin-sdk/account-id.d.ts", + "dist/plugin-sdk/keyed-async-queue.js", + "dist/plugin-sdk/keyed-async-queue.d.ts", "dist/build-info.json", ]; const forbiddenPrefixes = ["dist/OpenClaw.app/"]; diff --git a/scripts/write-plugin-sdk-entry-dts.ts b/scripts/write-plugin-sdk-entry-dts.ts index 197b36004a8..7053feb19a8 100644 --- a/scripts/write-plugin-sdk-entry-dts.ts +++ b/scripts/write-plugin-sdk-entry-dts.ts @@ -17,7 +17,40 @@ const entrypoints = [ "imessage", "whatsapp", "line", + "msteams", + "acpx", + "bluebubbles", + "copilot-proxy", + "device-pair", + "diagnostics-otel", + "diffs", + "feishu", + "google-gemini-cli-auth", + "googlechat", + "irc", + "llm-task", + "lobster", + "matrix", + "mattermost", + "memory-core", + "memory-lancedb", + "minimax-portal-auth", + "nextcloud-talk", + "nostr", + "open-prose", + "phone-control", + "qwen-portal-auth", + "synology-chat", + "talk-voice", + "test-utils", + "thread-ownership", + "tlon", + "twitch", + "voice-call", + "zalo", + "zalouser", "account-id", + "keyed-async-queue", ] as const; for (const entry of entrypoints) { const out = path.join(process.cwd(), `dist/plugin-sdk/${entry}.d.ts`); diff --git a/src/plugin-sdk/acpx.ts b/src/plugin-sdk/acpx.ts new file mode 100644 index 00000000000..7a719800227 --- /dev/null +++ b/src/plugin-sdk/acpx.ts @@ -0,0 +1,34 @@ +// Narrow plugin-sdk surface for the bundled acpx plugin. +// Keep this list additive and scoped to symbols used under extensions/acpx. + +export type { AcpRuntimeErrorCode } from "../acp/runtime/errors.js"; +export { AcpRuntimeError } from "../acp/runtime/errors.js"; +export { registerAcpRuntimeBackend, unregisterAcpRuntimeBackend } from "../acp/runtime/registry.js"; +export type { + AcpRuntime, + AcpRuntimeCapabilities, + AcpRuntimeDoctorReport, + AcpRuntimeEnsureInput, + AcpRuntimeEvent, + AcpRuntimeHandle, + AcpRuntimeStatus, + AcpRuntimeTurnInput, + AcpSessionUpdateTag, +} from "../acp/runtime/types.js"; +export type { + OpenClawPluginApi, + OpenClawPluginConfigSchema, + OpenClawPluginService, + OpenClawPluginServiceContext, + PluginLogger, +} from "../plugins/types.js"; +export type { + WindowsSpawnProgram, + WindowsSpawnProgramCandidate, + WindowsSpawnResolution, +} from "./windows-spawn.js"; +export { + applyWindowsSpawnProgramPolicy, + materializeWindowsSpawnProgram, + resolveWindowsSpawnProgramCandidate, +} from "./windows-spawn.js"; diff --git a/src/plugin-sdk/bluebubbles.ts b/src/plugin-sdk/bluebubbles.ts new file mode 100644 index 00000000000..0d9d8f4e4eb --- /dev/null +++ b/src/plugin-sdk/bluebubbles.ts @@ -0,0 +1,100 @@ +// Narrow plugin-sdk surface for the bundled bluebubbles plugin. +// Keep this list additive and scoped to symbols used under extensions/bluebubbles. + +export { resolveAckReaction } from "../agents/identity.js"; +export { + createActionGate, + jsonResult, + readNumberParam, + readReactionParams, + readStringParam, +} from "../agents/tools/common.js"; +export type { HistoryEntry } from "../auto-reply/reply/history.js"; +export { + evictOldHistoryKeys, + recordPendingHistoryEntryIfEnabled, +} from "../auto-reply/reply/history.js"; +export { resolveControlCommandGate } from "../channels/command-gating.js"; +export { logAckFailure, logInboundDrop, logTypingFailure } from "../channels/logging.js"; +export { + BLUEBUBBLES_ACTION_NAMES, + BLUEBUBBLES_ACTIONS, +} from "../channels/plugins/bluebubbles-actions.js"; +export { + deleteAccountFromConfigSection, + setAccountEnabledInConfigSection, +} from "../channels/plugins/config-helpers.js"; +export { buildChannelConfigSchema } from "../channels/plugins/config-schema.js"; +export { + resolveBlueBubblesGroupRequireMention, + resolveBlueBubblesGroupToolPolicy, +} from "../channels/plugins/group-mentions.js"; +export { formatPairingApproveHint } from "../channels/plugins/helpers.js"; +export { resolveChannelMediaMaxBytes } from "../channels/plugins/media-limits.js"; +export type { + ChannelOnboardingAdapter, + ChannelOnboardingDmPolicy, +} from "../channels/plugins/onboarding-types.js"; +export { + addWildcardAllowFrom, + mergeAllowFromEntries, + promptAccountId, +} from "../channels/plugins/onboarding/helpers.js"; +export { PAIRING_APPROVED_MESSAGE } from "../channels/plugins/pairing-message.js"; +export { + applyAccountNameToChannelSection, + migrateBaseNameToDefaultAccount, +} from "../channels/plugins/setup-helpers.js"; +export { collectBlueBubblesStatusIssues } from "../channels/plugins/status-issues/bluebubbles.js"; +export type { + BaseProbeResult, + ChannelAccountSnapshot, + ChannelMessageActionAdapter, + ChannelMessageActionName, +} from "../channels/plugins/types.js"; +export type { ChannelPlugin } from "../channels/plugins/types.plugin.js"; +export { createReplyPrefixOptions } from "../channels/reply-prefix.js"; +export type { OpenClawConfig } from "../config/config.js"; +export type { DmPolicy, GroupPolicy } from "../config/types.js"; +export { + hasConfiguredSecretInput, + normalizeResolvedSecretInputString, + normalizeSecretInputString, +} from "../config/types.secrets.js"; +export { ToolPolicySchema } from "../config/zod-schema.agent-runtime.js"; +export { MarkdownConfigSchema } from "../config/zod-schema.core.js"; +export type { ParsedChatTarget } from "../imessage/target-parsing-helpers.js"; +export { + parseChatAllowTargetPrefixes, + parseChatTargetPrefixesOrThrow, + resolveServicePrefixedAllowTarget, + resolveServicePrefixedTarget, +} from "../imessage/target-parsing-helpers.js"; +export { stripMarkdown } from "../line/markdown-to-line.js"; +export { emptyPluginConfigSchema } from "../plugins/config-schema.js"; +export type { PluginRuntime } from "../plugins/runtime/types.js"; +export type { OpenClawPluginApi } from "../plugins/types.js"; +export { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js"; +export { + DM_GROUP_ACCESS_REASON, + readStoreAllowFromForDmPolicy, + resolveDmGroupAccessWithLists, +} from "../security/dm-policy-shared.js"; +export { formatDocsLink } from "../terminal/links.js"; +export type { WizardPrompter } from "../wizard/prompts.js"; +export { isAllowedParsedChatSender } from "./allow-from.js"; +export { readBooleanParam } from "./boolean-param.js"; +export { createScopedPairingAccess } from "./pairing-access.js"; +export { buildProbeChannelStatusSummary } from "./status-helpers.js"; +export { extractToolSend } from "./tool-send.js"; +export { normalizeWebhookPath } from "./webhook-path.js"; +export { + beginWebhookRequestPipelineOrReject, + createWebhookInFlightLimiter, + readWebhookBodyOrReject, +} from "./webhook-request-guards.js"; +export { + registerWebhookTargetWithPluginRoute, + resolveWebhookTargets, + resolveWebhookTargetWithAuthOrRejectSync, +} from "./webhook-targets.js"; diff --git a/src/plugin-sdk/copilot-proxy.ts b/src/plugin-sdk/copilot-proxy.ts new file mode 100644 index 00000000000..80a83010c1d --- /dev/null +++ b/src/plugin-sdk/copilot-proxy.ts @@ -0,0 +1,9 @@ +// Narrow plugin-sdk surface for the bundled copilot-proxy plugin. +// Keep this list additive and scoped to symbols used under extensions/copilot-proxy. + +export { emptyPluginConfigSchema } from "../plugins/config-schema.js"; +export type { + OpenClawPluginApi, + ProviderAuthContext, + ProviderAuthResult, +} from "../plugins/types.js"; diff --git a/src/plugin-sdk/device-pair.ts b/src/plugin-sdk/device-pair.ts new file mode 100644 index 00000000000..a2df85772c4 --- /dev/null +++ b/src/plugin-sdk/device-pair.ts @@ -0,0 +1,8 @@ +// Narrow plugin-sdk surface for the bundled device-pair plugin. +// Keep this list additive and scoped to symbols used under extensions/device-pair. + +export { approveDevicePairing, listDevicePairing } from "../infra/device-pairing.js"; +export type { OpenClawPluginApi } from "../plugins/types.js"; +export { resolveGatewayBindUrl } from "../shared/gateway-bind-url.js"; +export { resolveTailnetHostWithRunner } from "../shared/tailscale-status.js"; +export { runPluginCommandWithTimeout } from "./run-command.js"; diff --git a/src/plugin-sdk/diagnostics-otel.ts b/src/plugin-sdk/diagnostics-otel.ts new file mode 100644 index 00000000000..cb5038f4c42 --- /dev/null +++ b/src/plugin-sdk/diagnostics-otel.ts @@ -0,0 +1,13 @@ +// Narrow plugin-sdk surface for the bundled diagnostics-otel plugin. +// Keep this list additive and scoped to symbols used under extensions/diagnostics-otel. + +export type { DiagnosticEventPayload } from "../infra/diagnostic-events.js"; +export { emitDiagnosticEvent, onDiagnosticEvent } from "../infra/diagnostic-events.js"; +export { registerLogTransport } from "../logging/logger.js"; +export { redactSensitiveText } from "../logging/redact.js"; +export { emptyPluginConfigSchema } from "../plugins/config-schema.js"; +export type { + OpenClawPluginApi, + OpenClawPluginService, + OpenClawPluginServiceContext, +} from "../plugins/types.js"; diff --git a/src/plugin-sdk/diffs.ts b/src/plugin-sdk/diffs.ts new file mode 100644 index 00000000000..918536230d7 --- /dev/null +++ b/src/plugin-sdk/diffs.ts @@ -0,0 +1,11 @@ +// Narrow plugin-sdk surface for the bundled diffs plugin. +// Keep this list additive and scoped to symbols used under extensions/diffs. + +export type { OpenClawConfig } from "../config/config.js"; +export { resolvePreferredOpenClawTmpDir } from "../infra/tmp-openclaw-dir.js"; +export type { + AnyAgentTool, + OpenClawPluginApi, + OpenClawPluginConfigSchema, + PluginLogger, +} from "../plugins/types.js"; diff --git a/src/plugin-sdk/feishu.ts b/src/plugin-sdk/feishu.ts new file mode 100644 index 00000000000..959f8af124a --- /dev/null +++ b/src/plugin-sdk/feishu.ts @@ -0,0 +1,71 @@ +// Narrow plugin-sdk surface for the bundled feishu plugin. +// Keep this list additive and scoped to symbols used under extensions/feishu. + +export type { HistoryEntry } from "../auto-reply/reply/history.js"; +export { + buildPendingHistoryContextFromMap, + clearHistoryEntriesIfEnabled, + DEFAULT_GROUP_HISTORY_LIMIT, + recordPendingHistoryEntryIfEnabled, +} from "../auto-reply/reply/history.js"; +export type { ReplyPayload } from "../auto-reply/types.js"; +export { logTypingFailure } from "../channels/logging.js"; +export type { AllowlistMatch } from "../channels/plugins/allowlist-match.js"; +export type { + ChannelOnboardingAdapter, + ChannelOnboardingDmPolicy, +} from "../channels/plugins/onboarding-types.js"; +export { + addWildcardAllowFrom, + promptSingleChannelSecretInput, +} from "../channels/plugins/onboarding/helpers.js"; +export { PAIRING_APPROVED_MESSAGE } from "../channels/plugins/pairing-message.js"; +export type { + BaseProbeResult, + ChannelGroupContext, + ChannelMeta, + ChannelOutboundAdapter, +} from "../channels/plugins/types.js"; +export type { ChannelPlugin } from "../channels/plugins/types.plugin.js"; +export { createReplyPrefixContext } from "../channels/reply-prefix.js"; +export { createTypingCallbacks } from "../channels/typing.js"; +export type { OpenClawConfig as ClawdbotConfig, OpenClawConfig } from "../config/config.js"; +export { + resolveAllowlistProviderRuntimeGroupPolicy, + resolveDefaultGroupPolicy, + resolveOpenProviderRuntimeGroupPolicy, + warnMissingProviderGroupPolicyFallbackOnce, +} from "../config/runtime-group-policy.js"; +export type { DmPolicy, GroupToolPolicyConfig } from "../config/types.js"; +export type { SecretInput } from "../config/types.secrets.js"; +export { + hasConfiguredSecretInput, + normalizeResolvedSecretInputString, + normalizeSecretInputString, +} from "../config/types.secrets.js"; +export { createDedupeCache } from "../infra/dedupe.js"; +export { installRequestBodyLimitGuard } from "../infra/http-body.js"; +export { fetchWithSsrFGuard } from "../infra/net/fetch-guard.js"; +export { emptyPluginConfigSchema } from "../plugins/config-schema.js"; +export type { PluginRuntime } from "../plugins/runtime/types.js"; +export type { AnyAgentTool, OpenClawPluginApi } from "../plugins/types.js"; +export { DEFAULT_ACCOUNT_ID, normalizeAgentId } from "../routing/session-key.js"; +export type { RuntimeEnv } from "../runtime.js"; +export { formatDocsLink } from "../terminal/links.js"; +export type { WizardPrompter } from "../wizard/prompts.js"; +export { buildAgentMediaPayload } from "./agent-media-payload.js"; +export { readJsonFileWithFallback } from "./json-store.js"; +export { createScopedPairingAccess } from "./pairing-access.js"; +export { createPersistentDedupe } from "./persistent-dedupe.js"; +export { + buildBaseChannelStatusSummary, + createDefaultChannelRuntimeState, +} from "./status-helpers.js"; +export { withTempDownloadPath } from "./temp-path.js"; +export { + createFixedWindowRateLimiter, + createWebhookAnomalyTracker, + WEBHOOK_ANOMALY_COUNTER_DEFAULTS, + WEBHOOK_RATE_LIMIT_DEFAULTS, +} from "./webhook-memory-guards.js"; +export { applyBasicWebhookRequestGuards } from "./webhook-request-guards.js"; diff --git a/src/plugin-sdk/google-gemini-cli-auth.ts b/src/plugin-sdk/google-gemini-cli-auth.ts new file mode 100644 index 00000000000..213f78cfc96 --- /dev/null +++ b/src/plugin-sdk/google-gemini-cli-auth.ts @@ -0,0 +1,8 @@ +// Narrow plugin-sdk surface for the bundled google-gemini-cli-auth plugin. +// Keep this list additive and scoped to symbols used under extensions/google-gemini-cli-auth. + +export { fetchWithSsrFGuard } from "../infra/net/fetch-guard.js"; +export { isWSL2Sync } from "../infra/wsl.js"; +export { emptyPluginConfigSchema } from "../plugins/config-schema.js"; +export type { OpenClawPluginApi, ProviderAuthContext } from "../plugins/types.js"; +export { buildOauthProviderAuthResult } from "./provider-auth-result.js"; diff --git a/src/plugin-sdk/googlechat.ts b/src/plugin-sdk/googlechat.ts new file mode 100644 index 00000000000..e7b96355608 --- /dev/null +++ b/src/plugin-sdk/googlechat.ts @@ -0,0 +1,78 @@ +// Narrow plugin-sdk surface for the bundled googlechat plugin. +// Keep this list additive and scoped to symbols used under extensions/googlechat. + +export { + createActionGate, + jsonResult, + readNumberParam, + readReactionParams, + readStringParam, +} from "../agents/tools/common.js"; +export type { ChannelDock } from "../channels/dock.js"; +export { resolveMentionGatingWithBypass } from "../channels/mention-gating.js"; +export { + deleteAccountFromConfigSection, + setAccountEnabledInConfigSection, +} from "../channels/plugins/config-helpers.js"; +export { buildChannelConfigSchema } from "../channels/plugins/config-schema.js"; +export { resolveGoogleChatGroupRequireMention } from "../channels/plugins/group-mentions.js"; +export { formatPairingApproveHint } from "../channels/plugins/helpers.js"; +export { resolveChannelMediaMaxBytes } from "../channels/plugins/media-limits.js"; +export type { + ChannelOnboardingAdapter, + ChannelOnboardingDmPolicy, +} from "../channels/plugins/onboarding-types.js"; +export { + addWildcardAllowFrom, + mergeAllowFromEntries, + promptAccountId, +} from "../channels/plugins/onboarding/helpers.js"; +export { PAIRING_APPROVED_MESSAGE } from "../channels/plugins/pairing-message.js"; +export { + applyAccountNameToChannelSection, + migrateBaseNameToDefaultAccount, +} from "../channels/plugins/setup-helpers.js"; +export type { + ChannelAccountSnapshot, + ChannelMessageActionAdapter, + ChannelMessageActionName, + ChannelStatusIssue, +} from "../channels/plugins/types.js"; +export type { ChannelPlugin } from "../channels/plugins/types.plugin.js"; +export { getChatChannelMeta } from "../channels/registry.js"; +export { createReplyPrefixOptions } from "../channels/reply-prefix.js"; +export type { OpenClawConfig } from "../config/config.js"; +export { isDangerousNameMatchingEnabled } from "../config/dangerous-name-matching.js"; +export { + GROUP_POLICY_BLOCKED_LABEL, + resolveAllowlistProviderRuntimeGroupPolicy, + resolveDefaultGroupPolicy, + warnMissingProviderGroupPolicyFallbackOnce, +} from "../config/runtime-group-policy.js"; +export type { DmPolicy, GoogleChatAccountConfig, GoogleChatConfig } from "../config/types.js"; +export { isSecretRef } from "../config/types.secrets.js"; +export { GoogleChatConfigSchema } from "../config/zod-schema.providers-core.js"; +export { fetchWithSsrFGuard } from "../infra/net/fetch-guard.js"; +export { missingTargetError } from "../infra/outbound/target-errors.js"; +export { emptyPluginConfigSchema } from "../plugins/config-schema.js"; +export type { PluginRuntime } from "../plugins/runtime/types.js"; +export type { OpenClawPluginApi } from "../plugins/types.js"; +export { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js"; +export { resolveDmGroupAccessWithLists } from "../security/dm-policy-shared.js"; +export { formatDocsLink } from "../terminal/links.js"; +export type { WizardPrompter } from "../wizard/prompts.js"; +export { resolveInboundRouteEnvelopeBuilderWithRuntime } from "./inbound-envelope.js"; +export { createScopedPairingAccess } from "./pairing-access.js"; +export { extractToolSend } from "./tool-send.js"; +export { resolveWebhookPath } from "./webhook-path.js"; +export type { WebhookInFlightLimiter } from "./webhook-request-guards.js"; +export { + beginWebhookRequestPipelineOrReject, + createWebhookInFlightLimiter, + readJsonWebhookBodyOrReject, +} from "./webhook-request-guards.js"; +export { + registerWebhookTargetWithPluginRoute, + resolveWebhookTargets, + resolveWebhookTargetWithAuthOrReject, +} from "./webhook-targets.js"; diff --git a/src/plugin-sdk/irc.ts b/src/plugin-sdk/irc.ts new file mode 100644 index 00000000000..9706c552450 --- /dev/null +++ b/src/plugin-sdk/irc.ts @@ -0,0 +1,70 @@ +// Narrow plugin-sdk surface for the bundled irc plugin. +// Keep this list additive and scoped to symbols used under extensions/irc. + +export { resolveControlCommandGate } from "../channels/command-gating.js"; +export { logInboundDrop } from "../channels/logging.js"; +export { + deleteAccountFromConfigSection, + setAccountEnabledInConfigSection, +} from "../channels/plugins/config-helpers.js"; +export { buildChannelConfigSchema } from "../channels/plugins/config-schema.js"; +export { formatPairingApproveHint } from "../channels/plugins/helpers.js"; +export type { + ChannelOnboardingAdapter, + ChannelOnboardingDmPolicy, +} from "../channels/plugins/onboarding-types.js"; +export { promptChannelAccessConfig } from "../channels/plugins/onboarding/channel-access.js"; +export { addWildcardAllowFrom, promptAccountId } from "../channels/plugins/onboarding/helpers.js"; +export { PAIRING_APPROVED_MESSAGE } from "../channels/plugins/pairing-message.js"; +export type { BaseProbeResult } from "../channels/plugins/types.js"; +export type { ChannelPlugin } from "../channels/plugins/types.plugin.js"; +export { getChatChannelMeta } from "../channels/registry.js"; +export { createReplyPrefixOptions } from "../channels/reply-prefix.js"; +export type { OpenClawConfig } from "../config/config.js"; +export { isDangerousNameMatchingEnabled } from "../config/dangerous-name-matching.js"; +export { + GROUP_POLICY_BLOCKED_LABEL, + resolveAllowlistProviderRuntimeGroupPolicy, + resolveDefaultGroupPolicy, + warnMissingProviderGroupPolicyFallbackOnce, +} from "../config/runtime-group-policy.js"; +export type { + BlockStreamingCoalesceConfig, + DmConfig, + DmPolicy, + GroupPolicy, + GroupToolPolicyBySenderConfig, + GroupToolPolicyConfig, + MarkdownConfig, +} from "../config/types.js"; +export { normalizeResolvedSecretInputString } from "../config/types.secrets.js"; +export { ToolPolicySchema } from "../config/zod-schema.agent-runtime.js"; +export { + BlockStreamingCoalesceSchema, + DmConfigSchema, + DmPolicySchema, + GroupPolicySchema, + MarkdownConfigSchema, + ReplyRuntimeConfigSchemaShape, + requireOpenAllowFrom, +} from "../config/zod-schema.core.js"; +export { emptyPluginConfigSchema } from "../plugins/config-schema.js"; +export type { PluginRuntime } from "../plugins/runtime/types.js"; +export type { OpenClawPluginApi } from "../plugins/types.js"; +export { DEFAULT_ACCOUNT_ID } from "../routing/session-key.js"; +export type { RuntimeEnv } from "../runtime.js"; +export { + readStoreAllowFromForDmPolicy, + resolveEffectiveAllowFromLists, +} from "../security/dm-policy-shared.js"; +export { formatDocsLink } from "../terminal/links.js"; +export type { WizardPrompter } from "../wizard/prompts.js"; +export { createScopedPairingAccess } from "./pairing-access.js"; +export type { OutboundReplyPayload } from "./reply-payload.js"; +export { + createNormalizedOutboundDeliverer, + formatTextWithAttachmentLinks, + resolveOutboundMediaUrls, +} from "./reply-payload.js"; +export { createLoggerBackedRuntime } from "./runtime.js"; +export { buildBaseAccountStatusSnapshot, buildBaseChannelStatusSummary } from "./status-helpers.js"; diff --git a/src/plugin-sdk/llm-task.ts b/src/plugin-sdk/llm-task.ts new file mode 100644 index 00000000000..164a28f0440 --- /dev/null +++ b/src/plugin-sdk/llm-task.ts @@ -0,0 +1,5 @@ +// Narrow plugin-sdk surface for the bundled llm-task plugin. +// Keep this list additive and scoped to symbols used under extensions/llm-task. + +export { resolvePreferredOpenClawTmpDir } from "../infra/tmp-openclaw-dir.js"; +export type { AnyAgentTool, OpenClawPluginApi } from "../plugins/types.js"; diff --git a/src/plugin-sdk/lobster.ts b/src/plugin-sdk/lobster.ts new file mode 100644 index 00000000000..436acdf4d45 --- /dev/null +++ b/src/plugin-sdk/lobster.ts @@ -0,0 +1,14 @@ +// Narrow plugin-sdk surface for the bundled lobster plugin. +// Keep this list additive and scoped to symbols used under extensions/lobster. + +export { + applyWindowsSpawnProgramPolicy, + materializeWindowsSpawnProgram, + resolveWindowsSpawnProgramCandidate, +} from "./windows-spawn.js"; +export type { + AnyAgentTool, + OpenClawPluginApi, + OpenClawPluginToolContext, + OpenClawPluginToolFactory, +} from "../plugins/types.js"; diff --git a/src/plugin-sdk/matrix.ts b/src/plugin-sdk/matrix.ts new file mode 100644 index 00000000000..fca8773e9b3 --- /dev/null +++ b/src/plugin-sdk/matrix.ts @@ -0,0 +1,96 @@ +// Narrow plugin-sdk surface for the bundled matrix plugin. +// Keep this list additive and scoped to symbols used under extensions/matrix. + +export { + createActionGate, + jsonResult, + readNumberParam, + readReactionParams, + readStringParam, +} from "../agents/tools/common.js"; +export type { ReplyPayload } from "../auto-reply/types.js"; +export { resolveAllowlistMatchByCandidates } from "../channels/allowlist-match.js"; +export { mergeAllowlist, summarizeMapping } from "../channels/allowlists/resolve-utils.js"; +export { resolveControlCommandGate } from "../channels/command-gating.js"; +export type { NormalizedLocation } from "../channels/location.js"; +export { formatLocationText, toLocationContext } from "../channels/location.js"; +export { logInboundDrop, logTypingFailure } from "../channels/logging.js"; +export type { AllowlistMatch } from "../channels/plugins/allowlist-match.js"; +export { formatAllowlistMatchMeta } from "../channels/plugins/allowlist-match.js"; +export { + buildChannelKeyCandidates, + resolveChannelEntryMatch, +} from "../channels/plugins/channel-config.js"; +export { + deleteAccountFromConfigSection, + setAccountEnabledInConfigSection, +} from "../channels/plugins/config-helpers.js"; +export { buildChannelConfigSchema } from "../channels/plugins/config-schema.js"; +export { formatPairingApproveHint } from "../channels/plugins/helpers.js"; +export type { + ChannelOnboardingAdapter, + ChannelOnboardingDmPolicy, +} from "../channels/plugins/onboarding-types.js"; +export { promptChannelAccessConfig } from "../channels/plugins/onboarding/channel-access.js"; +export { + addWildcardAllowFrom, + mergeAllowFromEntries, + promptSingleChannelSecretInput, +} from "../channels/plugins/onboarding/helpers.js"; +export { PAIRING_APPROVED_MESSAGE } from "../channels/plugins/pairing-message.js"; +export { applyAccountNameToChannelSection } from "../channels/plugins/setup-helpers.js"; +export type { + BaseProbeResult, + ChannelDirectoryEntry, + ChannelGroupContext, + ChannelMessageActionAdapter, + ChannelMessageActionContext, + ChannelMessageActionName, + ChannelOutboundAdapter, + ChannelResolveKind, + ChannelResolveResult, + ChannelToolSend, +} from "../channels/plugins/types.js"; +export type { ChannelPlugin } from "../channels/plugins/types.plugin.js"; +export { createReplyPrefixOptions } from "../channels/reply-prefix.js"; +export { createTypingCallbacks } from "../channels/typing.js"; +export type { OpenClawConfig } from "../config/config.js"; +export { + GROUP_POLICY_BLOCKED_LABEL, + resolveAllowlistProviderRuntimeGroupPolicy, + resolveDefaultGroupPolicy, + warnMissingProviderGroupPolicyFallbackOnce, +} from "../config/runtime-group-policy.js"; +export type { + DmPolicy, + GroupPolicy, + GroupToolPolicyConfig, + MarkdownTableMode, +} from "../config/types.js"; +export type { SecretInput } from "../config/types.secrets.js"; +export { + hasConfiguredSecretInput, + normalizeResolvedSecretInputString, + normalizeSecretInputString, +} from "../config/types.secrets.js"; +export { ToolPolicySchema } from "../config/zod-schema.agent-runtime.js"; +export { MarkdownConfigSchema } from "../config/zod-schema.core.js"; +export { fetchWithSsrFGuard } from "../infra/net/fetch-guard.js"; +export { issuePairingChallenge } from "../pairing/pairing-challenge.js"; +export { emptyPluginConfigSchema } from "../plugins/config-schema.js"; +export type { PluginRuntime, RuntimeLogger } from "../plugins/runtime/types.js"; +export type { OpenClawPluginApi } from "../plugins/types.js"; +export type { PollInput } from "../polls.js"; +export { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js"; +export type { RuntimeEnv } from "../runtime.js"; +export { + readStoreAllowFromForDmPolicy, + resolveDmGroupAccessWithLists, +} from "../security/dm-policy-shared.js"; +export { formatDocsLink } from "../terminal/links.js"; +export type { WizardPrompter } from "../wizard/prompts.js"; +export { createScopedPairingAccess } from "./pairing-access.js"; +export { formatResolvedUnresolvedNote } from "./resolution-notes.js"; +export { runPluginCommandWithTimeout } from "./run-command.js"; +export { createLoggerBackedRuntime } from "./runtime.js"; +export { buildProbeChannelStatusSummary } from "./status-helpers.js"; diff --git a/src/plugin-sdk/mattermost.ts b/src/plugin-sdk/mattermost.ts new file mode 100644 index 00000000000..7afe2890d7b --- /dev/null +++ b/src/plugin-sdk/mattermost.ts @@ -0,0 +1,85 @@ +// Narrow plugin-sdk surface for the bundled mattermost plugin. +// Keep this list additive and scoped to symbols used under extensions/mattermost. + +export { formatInboundFromLabel } from "../auto-reply/envelope.js"; +export type { HistoryEntry } from "../auto-reply/reply/history.js"; +export { + buildPendingHistoryContextFromMap, + clearHistoryEntriesIfEnabled, + DEFAULT_GROUP_HISTORY_LIMIT, + recordPendingHistoryEntryIfEnabled, +} from "../auto-reply/reply/history.js"; +export { listSkillCommandsForAgents } from "../auto-reply/skill-commands.js"; +export type { ReplyPayload } from "../auto-reply/types.js"; +export type { ChatType } from "../channels/chat-type.js"; +export { resolveControlCommandGate } from "../channels/command-gating.js"; +export { logInboundDrop, logTypingFailure } from "../channels/logging.js"; +export { resolveAllowlistMatchSimple } from "../channels/plugins/allowlist-match.js"; +export { + deleteAccountFromConfigSection, + setAccountEnabledInConfigSection, +} from "../channels/plugins/config-helpers.js"; +export { buildChannelConfigSchema } from "../channels/plugins/config-schema.js"; +export { formatPairingApproveHint } from "../channels/plugins/helpers.js"; +export { resolveChannelMediaMaxBytes } from "../channels/plugins/media-limits.js"; +export type { ChannelOnboardingAdapter } from "../channels/plugins/onboarding-types.js"; +export { + promptAccountId, + promptSingleChannelSecretInput, +} from "../channels/plugins/onboarding/helpers.js"; +export { + applyAccountNameToChannelSection, + migrateBaseNameToDefaultAccount, +} from "../channels/plugins/setup-helpers.js"; +export type { + BaseProbeResult, + ChannelAccountSnapshot, + ChannelGroupContext, + ChannelMessageActionAdapter, + ChannelMessageActionName, +} from "../channels/plugins/types.js"; +export type { ChannelPlugin } from "../channels/plugins/types.plugin.js"; +export { createReplyPrefixOptions } from "../channels/reply-prefix.js"; +export { createTypingCallbacks } from "../channels/typing.js"; +export type { OpenClawConfig } from "../config/config.js"; +export { isDangerousNameMatchingEnabled } from "../config/dangerous-name-matching.js"; +export { + resolveAllowlistProviderRuntimeGroupPolicy, + resolveDefaultGroupPolicy, + warnMissingProviderGroupPolicyFallbackOnce, +} from "../config/runtime-group-policy.js"; +export type { BlockStreamingCoalesceConfig, DmPolicy, GroupPolicy } from "../config/types.js"; +export type { SecretInput } from "../config/types.secrets.js"; +export { + hasConfiguredSecretInput, + normalizeResolvedSecretInputString, + normalizeSecretInputString, +} from "../config/types.secrets.js"; +export { + BlockStreamingCoalesceSchema, + DmPolicySchema, + GroupPolicySchema, + MarkdownConfigSchema, + requireOpenAllowFrom, +} from "../config/zod-schema.core.js"; +export { createDedupeCache } from "../infra/dedupe.js"; +export { rawDataToString } from "../infra/ws.js"; +export { emptyPluginConfigSchema } from "../plugins/config-schema.js"; +export type { PluginRuntime } from "../plugins/runtime/types.js"; +export type { OpenClawPluginApi } from "../plugins/types.js"; +export { + DEFAULT_ACCOUNT_ID, + normalizeAccountId, + resolveThreadSessionKeys, +} from "../routing/session-key.js"; +export type { RuntimeEnv } from "../runtime.js"; +export { + DM_GROUP_ACCESS_REASON, + readStoreAllowFromForDmPolicy, + resolveDmGroupAccessWithLists, + resolveEffectiveAllowFromLists, +} from "../security/dm-policy-shared.js"; +export type { WizardPrompter } from "../wizard/prompts.js"; +export { buildAgentMediaPayload } from "./agent-media-payload.js"; +export { loadOutboundMediaFromUrl } from "./outbound-media.js"; +export { createScopedPairingAccess } from "./pairing-access.js"; diff --git a/src/plugin-sdk/memory-core.ts b/src/plugin-sdk/memory-core.ts new file mode 100644 index 00000000000..b715c1f50ca --- /dev/null +++ b/src/plugin-sdk/memory-core.ts @@ -0,0 +1,5 @@ +// Narrow plugin-sdk surface for the bundled memory-core plugin. +// Keep this list additive and scoped to symbols used under extensions/memory-core. + +export { emptyPluginConfigSchema } from "../plugins/config-schema.js"; +export type { OpenClawPluginApi } from "../plugins/types.js"; diff --git a/src/plugin-sdk/memory-lancedb.ts b/src/plugin-sdk/memory-lancedb.ts new file mode 100644 index 00000000000..840ed95982c --- /dev/null +++ b/src/plugin-sdk/memory-lancedb.ts @@ -0,0 +1,4 @@ +// Narrow plugin-sdk surface for the bundled memory-lancedb plugin. +// Keep this list additive and scoped to symbols used under extensions/memory-lancedb. + +export type { OpenClawPluginApi } from "../plugins/types.js"; diff --git a/src/plugin-sdk/minimax-portal-auth.ts b/src/plugin-sdk/minimax-portal-auth.ts new file mode 100644 index 00000000000..2f6ab59e124 --- /dev/null +++ b/src/plugin-sdk/minimax-portal-auth.ts @@ -0,0 +1,10 @@ +// Narrow plugin-sdk surface for the bundled minimax-portal-auth plugin. +// Keep this list additive and scoped to symbols used under extensions/minimax-portal-auth. + +export { emptyPluginConfigSchema } from "../plugins/config-schema.js"; +export type { + OpenClawPluginApi, + ProviderAuthContext, + ProviderAuthResult, +} from "../plugins/types.js"; +export { generatePkceVerifierChallenge, toFormUrlEncoded } from "./oauth-utils.js"; diff --git a/src/plugin-sdk/msteams.ts b/src/plugin-sdk/msteams.ts new file mode 100644 index 00000000000..28f5e10a4c0 --- /dev/null +++ b/src/plugin-sdk/msteams.ts @@ -0,0 +1,108 @@ +// Narrow plugin-sdk surface for the bundled msteams plugin. +// Keep this list additive and scoped to symbols used under extensions/msteams. + +export type { ChunkMode } from "../auto-reply/chunk.js"; +export type { HistoryEntry } from "../auto-reply/reply/history.js"; +export { + buildPendingHistoryContextFromMap, + clearHistoryEntriesIfEnabled, + DEFAULT_GROUP_HISTORY_LIMIT, + recordPendingHistoryEntryIfEnabled, +} from "../auto-reply/reply/history.js"; +export { isSilentReplyText, SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js"; +export type { ReplyPayload } from "../auto-reply/types.js"; +export { mergeAllowlist, summarizeMapping } from "../channels/allowlists/resolve-utils.js"; +export { resolveControlCommandGate } from "../channels/command-gating.js"; +export { logInboundDrop, logTypingFailure } from "../channels/logging.js"; +export { resolveMentionGating } from "../channels/mention-gating.js"; +export type { AllowlistMatch } from "../channels/plugins/allowlist-match.js"; +export { + formatAllowlistMatchMeta, + resolveAllowlistMatchSimple, +} from "../channels/plugins/allowlist-match.js"; +export { + buildChannelKeyCandidates, + normalizeChannelSlug, + resolveChannelEntryMatchWithFallback, + resolveNestedAllowlistDecision, +} from "../channels/plugins/channel-config.js"; +export { buildChannelConfigSchema } from "../channels/plugins/config-schema.js"; +export { resolveChannelMediaMaxBytes } from "../channels/plugins/media-limits.js"; +export { buildMediaPayload } from "../channels/plugins/media-payload.js"; +export type { + ChannelOnboardingAdapter, + ChannelOnboardingDmPolicy, +} from "../channels/plugins/onboarding-types.js"; +export { promptChannelAccessConfig } from "../channels/plugins/onboarding/channel-access.js"; +export { + addWildcardAllowFrom, + mergeAllowFromEntries, +} from "../channels/plugins/onboarding/helpers.js"; +export { PAIRING_APPROVED_MESSAGE } from "../channels/plugins/pairing-message.js"; +export type { + BaseProbeResult, + ChannelDirectoryEntry, + ChannelGroupContext, + ChannelMessageActionName, + ChannelOutboundAdapter, +} from "../channels/plugins/types.js"; +export type { ChannelPlugin } from "../channels/plugins/types.plugin.js"; +export { createReplyPrefixOptions } from "../channels/reply-prefix.js"; +export { createTypingCallbacks } from "../channels/typing.js"; +export type { OpenClawConfig } from "../config/config.js"; +export { isDangerousNameMatchingEnabled } from "../config/dangerous-name-matching.js"; +export { resolveToolsBySender } from "../config/group-policy.js"; +export { + resolveAllowlistProviderRuntimeGroupPolicy, + resolveDefaultGroupPolicy, +} from "../config/runtime-group-policy.js"; +export type { + DmPolicy, + GroupPolicy, + GroupToolPolicyConfig, + MarkdownTableMode, + MSTeamsChannelConfig, + MSTeamsConfig, + MSTeamsReplyStyle, + MSTeamsTeamConfig, +} from "../config/types.js"; +export { + hasConfiguredSecretInput, + normalizeResolvedSecretInputString, + normalizeSecretInputString, +} from "../config/types.secrets.js"; +export { MSTeamsConfigSchema } from "../config/zod-schema.providers-core.js"; +export { DEFAULT_WEBHOOK_MAX_BODY_BYTES } from "../infra/http-body.js"; +export { fetchWithSsrFGuard } from "../infra/net/fetch-guard.js"; +export type { SsrFPolicy } from "../infra/net/ssrf.js"; +export { isPrivateIpAddress } from "../infra/net/ssrf.js"; +export { detectMime, extensionForMime, getFileExtension } from "../media/mime.js"; +export { extractOriginalFilename } from "../media/store.js"; +export { emptyPluginConfigSchema } from "../plugins/config-schema.js"; +export type { PluginRuntime } from "../plugins/runtime/types.js"; +export type { OpenClawPluginApi } from "../plugins/types.js"; +export { DEFAULT_ACCOUNT_ID } from "../routing/session-key.js"; +export type { RuntimeEnv } from "../runtime.js"; +export { + readStoreAllowFromForDmPolicy, + resolveDmGroupAccessWithLists, + resolveEffectiveAllowFromLists, +} from "../security/dm-policy-shared.js"; +export { formatDocsLink } from "../terminal/links.js"; +export { sleep } from "../utils.js"; +export { loadWebMedia } from "../web/media.js"; +export type { WizardPrompter } from "../wizard/prompts.js"; +export { keepHttpServerTaskAlive } from "./channel-lifecycle.js"; +export { withFileLock } from "./file-lock.js"; +export { readJsonFileWithFallback, writeJsonFileAtomically } from "./json-store.js"; +export { loadOutboundMediaFromUrl } from "./outbound-media.js"; +export { createScopedPairingAccess } from "./pairing-access.js"; +export { + buildHostnameAllowlistPolicyFromSuffixAllowlist, + isHttpsUrlAllowedByHostnameSuffixAllowlist, + normalizeHostnameSuffixAllowlist, +} from "./ssrf-policy.js"; +export { + buildBaseChannelStatusSummary, + createDefaultChannelRuntimeState, +} from "./status-helpers.js"; diff --git a/src/plugin-sdk/nextcloud-talk.ts b/src/plugin-sdk/nextcloud-talk.ts new file mode 100644 index 00000000000..7d66c5e66be --- /dev/null +++ b/src/plugin-sdk/nextcloud-talk.ts @@ -0,0 +1,92 @@ +// Narrow plugin-sdk surface for the bundled nextcloud-talk plugin. +// Keep this list additive and scoped to symbols used under extensions/nextcloud-talk. + +export { logInboundDrop } from "../channels/logging.js"; +export { resolveMentionGatingWithBypass } from "../channels/mention-gating.js"; +export type { AllowlistMatch } from "../channels/plugins/allowlist-match.js"; +export { + buildChannelKeyCandidates, + normalizeChannelSlug, + resolveChannelEntryMatchWithFallback, + resolveNestedAllowlistDecision, +} from "../channels/plugins/channel-config.js"; +export { + deleteAccountFromConfigSection, + setAccountEnabledInConfigSection, +} from "../channels/plugins/config-helpers.js"; +export { buildChannelConfigSchema } from "../channels/plugins/config-schema.js"; +export { formatPairingApproveHint } from "../channels/plugins/helpers.js"; +export type { + ChannelOnboardingAdapter, + ChannelOnboardingDmPolicy, +} from "../channels/plugins/onboarding-types.js"; +export { + addWildcardAllowFrom, + mergeAllowFromEntries, + promptAccountId, + promptSingleChannelSecretInput, +} from "../channels/plugins/onboarding/helpers.js"; +export { applyAccountNameToChannelSection } from "../channels/plugins/setup-helpers.js"; +export type { ChannelGroupContext, ChannelSetupInput } from "../channels/plugins/types.js"; +export type { ChannelPlugin } from "../channels/plugins/types.plugin.js"; +export { createReplyPrefixOptions } from "../channels/reply-prefix.js"; +export type { OpenClawConfig } from "../config/config.js"; +export { + GROUP_POLICY_BLOCKED_LABEL, + resolveAllowlistProviderRuntimeGroupPolicy, + resolveDefaultGroupPolicy, + warnMissingProviderGroupPolicyFallbackOnce, +} from "../config/runtime-group-policy.js"; +export type { + BlockStreamingCoalesceConfig, + DmConfig, + DmPolicy, + GroupPolicy, + GroupToolPolicyConfig, +} from "../config/types.js"; +export type { SecretInput } from "../config/types.secrets.js"; +export { + hasConfiguredSecretInput, + normalizeResolvedSecretInputString, + normalizeSecretInputString, +} from "../config/types.secrets.js"; +export { ToolPolicySchema } from "../config/zod-schema.agent-runtime.js"; +export { + BlockStreamingCoalesceSchema, + DmConfigSchema, + DmPolicySchema, + GroupPolicySchema, + MarkdownConfigSchema, + ReplyRuntimeConfigSchemaShape, + requireOpenAllowFrom, +} from "../config/zod-schema.core.js"; +export { + isRequestBodyLimitError, + readRequestBodyWithLimit, + requestBodyErrorToText, +} from "../infra/http-body.js"; +export { fetchWithSsrFGuard } from "../infra/net/fetch-guard.js"; +export { emptyPluginConfigSchema } from "../plugins/config-schema.js"; +export type { PluginRuntime } from "../plugins/runtime/types.js"; +export type { OpenClawPluginApi } from "../plugins/types.js"; +export { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js"; +export type { RuntimeEnv } from "../runtime.js"; +export { + readStoreAllowFromForDmPolicy, + resolveDmGroupAccessWithCommandGate, +} from "../security/dm-policy-shared.js"; +export { formatDocsLink } from "../terminal/links.js"; +export type { WizardPrompter } from "../wizard/prompts.js"; +export { + listConfiguredAccountIds, + resolveAccountWithDefaultFallback, +} from "./account-resolution.js"; +export { createScopedPairingAccess } from "./pairing-access.js"; +export { createPersistentDedupe } from "./persistent-dedupe.js"; +export type { OutboundReplyPayload } from "./reply-payload.js"; +export { + createNormalizedOutboundDeliverer, + formatTextWithAttachmentLinks, + resolveOutboundMediaUrls, +} from "./reply-payload.js"; +export { createLoggerBackedRuntime } from "./runtime.js"; diff --git a/src/plugin-sdk/nostr.ts b/src/plugin-sdk/nostr.ts new file mode 100644 index 00000000000..1eee82f518a --- /dev/null +++ b/src/plugin-sdk/nostr.ts @@ -0,0 +1,19 @@ +// Narrow plugin-sdk surface for the bundled nostr plugin. +// Keep this list additive and scoped to symbols used under extensions/nostr. + +export { buildChannelConfigSchema } from "../channels/plugins/config-schema.js"; +export { formatPairingApproveHint } from "../channels/plugins/helpers.js"; +export type { ChannelPlugin } from "../channels/plugins/types.plugin.js"; +export type { OpenClawConfig } from "../config/config.js"; +export { MarkdownConfigSchema } from "../config/zod-schema.core.js"; +export { readJsonBodyWithLimit, requestBodyErrorToText } from "../infra/http-body.js"; +export { isBlockedHostnameOrIp } from "../infra/net/ssrf.js"; +export { emptyPluginConfigSchema } from "../plugins/config-schema.js"; +export type { PluginRuntime } from "../plugins/runtime/types.js"; +export type { OpenClawPluginApi } from "../plugins/types.js"; +export { DEFAULT_ACCOUNT_ID } from "../routing/session-key.js"; +export { + collectStatusIssuesFromLastError, + createDefaultChannelRuntimeState, +} from "./status-helpers.js"; +export { createFixedWindowRateLimiter } from "./webhook-memory-guards.js"; diff --git a/src/plugin-sdk/open-prose.ts b/src/plugin-sdk/open-prose.ts new file mode 100644 index 00000000000..1973404f2a8 --- /dev/null +++ b/src/plugin-sdk/open-prose.ts @@ -0,0 +1,4 @@ +// Narrow plugin-sdk surface for the bundled open-prose plugin. +// Keep this list additive and scoped to symbols used under extensions/open-prose. + +export type { OpenClawPluginApi } from "../plugins/types.js"; diff --git a/src/plugin-sdk/phone-control.ts b/src/plugin-sdk/phone-control.ts new file mode 100644 index 00000000000..394ff9c88ee --- /dev/null +++ b/src/plugin-sdk/phone-control.ts @@ -0,0 +1,9 @@ +// Narrow plugin-sdk surface for the bundled phone-control plugin. +// Keep this list additive and scoped to symbols used under extensions/phone-control. + +export type { + OpenClawPluginApi, + OpenClawPluginCommandDefinition, + OpenClawPluginService, + PluginCommandContext, +} from "../plugins/types.js"; diff --git a/src/plugin-sdk/qwen-portal-auth.ts b/src/plugin-sdk/qwen-portal-auth.ts new file mode 100644 index 00000000000..33d03ae394b --- /dev/null +++ b/src/plugin-sdk/qwen-portal-auth.ts @@ -0,0 +1,6 @@ +// Narrow plugin-sdk surface for the bundled qwen-portal-auth plugin. +// Keep this list additive and scoped to symbols used under extensions/qwen-portal-auth. + +export { emptyPluginConfigSchema } from "../plugins/config-schema.js"; +export type { OpenClawPluginApi, ProviderAuthContext } from "../plugins/types.js"; +export { generatePkceVerifierChallenge, toFormUrlEncoded } from "./oauth-utils.js"; diff --git a/src/plugin-sdk/subpaths.test.ts b/src/plugin-sdk/subpaths.test.ts index 9065712d235..061230bb7ca 100644 --- a/src/plugin-sdk/subpaths.test.ts +++ b/src/plugin-sdk/subpaths.test.ts @@ -2,11 +2,52 @@ import * as compatSdk from "openclaw/plugin-sdk/compat"; import * as discordSdk from "openclaw/plugin-sdk/discord"; import * as imessageSdk from "openclaw/plugin-sdk/imessage"; import * as lineSdk from "openclaw/plugin-sdk/line"; +import * as msteamsSdk from "openclaw/plugin-sdk/msteams"; import * as signalSdk from "openclaw/plugin-sdk/signal"; import * as slackSdk from "openclaw/plugin-sdk/slack"; import * as whatsappSdk from "openclaw/plugin-sdk/whatsapp"; import { describe, expect, it } from "vitest"; +const bundledExtensionSubpathLoaders = [ + { id: "acpx", load: () => import("openclaw/plugin-sdk/acpx") }, + { id: "bluebubbles", load: () => import("openclaw/plugin-sdk/bluebubbles") }, + { id: "copilot-proxy", load: () => import("openclaw/plugin-sdk/copilot-proxy") }, + { id: "device-pair", load: () => import("openclaw/plugin-sdk/device-pair") }, + { id: "diagnostics-otel", load: () => import("openclaw/plugin-sdk/diagnostics-otel") }, + { id: "diffs", load: () => import("openclaw/plugin-sdk/diffs") }, + { id: "feishu", load: () => import("openclaw/plugin-sdk/feishu") }, + { + id: "google-gemini-cli-auth", + load: () => import("openclaw/plugin-sdk/google-gemini-cli-auth"), + }, + { id: "googlechat", load: () => import("openclaw/plugin-sdk/googlechat") }, + { id: "irc", load: () => import("openclaw/plugin-sdk/irc") }, + { id: "llm-task", load: () => import("openclaw/plugin-sdk/llm-task") }, + { id: "lobster", load: () => import("openclaw/plugin-sdk/lobster") }, + { id: "matrix", load: () => import("openclaw/plugin-sdk/matrix") }, + { id: "mattermost", load: () => import("openclaw/plugin-sdk/mattermost") }, + { id: "memory-core", load: () => import("openclaw/plugin-sdk/memory-core") }, + { id: "memory-lancedb", load: () => import("openclaw/plugin-sdk/memory-lancedb") }, + { + id: "minimax-portal-auth", + load: () => import("openclaw/plugin-sdk/minimax-portal-auth"), + }, + { id: "nextcloud-talk", load: () => import("openclaw/plugin-sdk/nextcloud-talk") }, + { id: "nostr", load: () => import("openclaw/plugin-sdk/nostr") }, + { id: "open-prose", load: () => import("openclaw/plugin-sdk/open-prose") }, + { id: "phone-control", load: () => import("openclaw/plugin-sdk/phone-control") }, + { id: "qwen-portal-auth", load: () => import("openclaw/plugin-sdk/qwen-portal-auth") }, + { id: "synology-chat", load: () => import("openclaw/plugin-sdk/synology-chat") }, + { id: "talk-voice", load: () => import("openclaw/plugin-sdk/talk-voice") }, + { id: "test-utils", load: () => import("openclaw/plugin-sdk/test-utils") }, + { id: "thread-ownership", load: () => import("openclaw/plugin-sdk/thread-ownership") }, + { id: "tlon", load: () => import("openclaw/plugin-sdk/tlon") }, + { id: "twitch", load: () => import("openclaw/plugin-sdk/twitch") }, + { id: "voice-call", load: () => import("openclaw/plugin-sdk/voice-call") }, + { id: "zalo", load: () => import("openclaw/plugin-sdk/zalo") }, + { id: "zalouser", load: () => import("openclaw/plugin-sdk/zalouser") }, +] as const; + describe("plugin-sdk subpath exports", () => { it("exports compat helpers", () => { expect(typeof compatSdk.emptyPluginConfigSchema).toBe("function"); @@ -42,4 +83,17 @@ describe("plugin-sdk subpath exports", () => { expect(typeof lineSdk.processLineMessage).toBe("function"); expect(typeof lineSdk.createInfoCard).toBe("function"); }); + + it("exports Microsoft Teams helpers", () => { + expect(typeof msteamsSdk.resolveControlCommandGate).toBe("function"); + expect(typeof msteamsSdk.loadOutboundMediaFromUrl).toBe("function"); + }); + + it("resolves bundled extension subpaths", async () => { + for (const { id, load } of bundledExtensionSubpathLoaders) { + const mod = await load(); + expect(typeof mod).toBe("object"); + expect(mod, `subpath ${id} should resolve`).toBeTruthy(); + } + }); }); diff --git a/src/plugin-sdk/synology-chat.ts b/src/plugin-sdk/synology-chat.ts new file mode 100644 index 00000000000..dcce2ea760b --- /dev/null +++ b/src/plugin-sdk/synology-chat.ts @@ -0,0 +1,17 @@ +// Narrow plugin-sdk surface for the bundled synology-chat plugin. +// Keep this list additive and scoped to symbols used under extensions/synology-chat. + +export { setAccountEnabledInConfigSection } from "../channels/plugins/config-helpers.js"; +export { buildChannelConfigSchema } from "../channels/plugins/config-schema.js"; +export { + isRequestBodyLimitError, + readRequestBodyWithLimit, + requestBodyErrorToText, +} from "../infra/http-body.js"; +export { emptyPluginConfigSchema } from "../plugins/config-schema.js"; +export { registerPluginHttpRoute } from "../plugins/http-registry.js"; +export type { PluginRuntime } from "../plugins/runtime/types.js"; +export type { OpenClawPluginApi } from "../plugins/types.js"; +export { DEFAULT_ACCOUNT_ID } from "../routing/session-key.js"; +export type { FixedWindowRateLimiter } from "./webhook-memory-guards.js"; +export { createFixedWindowRateLimiter } from "./webhook-memory-guards.js"; diff --git a/src/plugin-sdk/talk-voice.ts b/src/plugin-sdk/talk-voice.ts new file mode 100644 index 00000000000..3ee313ec42f --- /dev/null +++ b/src/plugin-sdk/talk-voice.ts @@ -0,0 +1,4 @@ +// Narrow plugin-sdk surface for the bundled talk-voice plugin. +// Keep this list additive and scoped to symbols used under extensions/talk-voice. + +export type { OpenClawPluginApi } from "../plugins/types.js"; diff --git a/src/plugin-sdk/test-utils.ts b/src/plugin-sdk/test-utils.ts new file mode 100644 index 00000000000..78307f694a6 --- /dev/null +++ b/src/plugin-sdk/test-utils.ts @@ -0,0 +1,8 @@ +// Narrow plugin-sdk surface for the bundled test-utils plugin. +// Keep this list additive and scoped to symbols used under extensions/test-utils. + +export { removeAckReactionAfterReply, shouldAckReaction } from "../channels/ack-reactions.js"; +export type { ChannelAccountSnapshot, ChannelGatewayContext } from "../channels/plugins/types.js"; +export type { OpenClawConfig } from "../config/config.js"; +export type { PluginRuntime } from "../plugins/runtime/types.js"; +export type { RuntimeEnv } from "../runtime.js"; diff --git a/src/plugin-sdk/thread-ownership.ts b/src/plugin-sdk/thread-ownership.ts new file mode 100644 index 00000000000..48d72fa5d35 --- /dev/null +++ b/src/plugin-sdk/thread-ownership.ts @@ -0,0 +1,5 @@ +// Narrow plugin-sdk surface for the bundled thread-ownership plugin. +// Keep this list additive and scoped to symbols used under extensions/thread-ownership. + +export type { OpenClawConfig } from "../config/config.js"; +export type { OpenClawPluginApi } from "../plugins/types.js"; diff --git a/src/plugin-sdk/tlon.ts b/src/plugin-sdk/tlon.ts new file mode 100644 index 00000000000..fe41eba5687 --- /dev/null +++ b/src/plugin-sdk/tlon.ts @@ -0,0 +1,28 @@ +// Narrow plugin-sdk surface for the bundled tlon plugin. +// Keep this list additive and scoped to symbols used under extensions/tlon. + +export type { ReplyPayload } from "../auto-reply/types.js"; +export { buildChannelConfigSchema } from "../channels/plugins/config-schema.js"; +export type { ChannelOnboardingAdapter } from "../channels/plugins/onboarding-types.js"; +export { promptAccountId } from "../channels/plugins/onboarding/helpers.js"; +export { applyAccountNameToChannelSection } from "../channels/plugins/setup-helpers.js"; +export type { + ChannelAccountSnapshot, + ChannelOutboundAdapter, + ChannelSetupInput, +} from "../channels/plugins/types.js"; +export type { ChannelPlugin } from "../channels/plugins/types.plugin.js"; +export { createReplyPrefixOptions } from "../channels/reply-prefix.js"; +export type { OpenClawConfig } from "../config/config.js"; +export { createDedupeCache } from "../infra/dedupe.js"; +export { fetchWithSsrFGuard } from "../infra/net/fetch-guard.js"; +export type { LookupFn, SsrFPolicy } from "../infra/net/ssrf.js"; +export { isBlockedHostnameOrIp, SsrFBlockedError } from "../infra/net/ssrf.js"; +export { emptyPluginConfigSchema } from "../plugins/config-schema.js"; +export type { PluginRuntime } from "../plugins/runtime/types.js"; +export type { OpenClawPluginApi } from "../plugins/types.js"; +export { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js"; +export type { RuntimeEnv } from "../runtime.js"; +export { formatDocsLink } from "../terminal/links.js"; +export type { WizardPrompter } from "../wizard/prompts.js"; +export { createLoggerBackedRuntime } from "./runtime.js"; diff --git a/src/plugin-sdk/twitch.ts b/src/plugin-sdk/twitch.ts new file mode 100644 index 00000000000..bd315b02c9a --- /dev/null +++ b/src/plugin-sdk/twitch.ts @@ -0,0 +1,19 @@ +// Narrow plugin-sdk surface for the bundled twitch plugin. +// Keep this list additive and scoped to symbols used under extensions/twitch. + +export type { ReplyPayload } from "../auto-reply/types.js"; +export { buildChannelConfigSchema } from "../channels/plugins/config-schema.js"; +export type { + ChannelOnboardingAdapter, + ChannelOnboardingDmPolicy, +} from "../channels/plugins/onboarding-types.js"; +export { promptChannelAccessConfig } from "../channels/plugins/onboarding/channel-access.js"; +export type { BaseProbeResult, ChannelStatusIssue } from "../channels/plugins/types.js"; +export { createReplyPrefixOptions } from "../channels/reply-prefix.js"; +export type { OpenClawConfig } from "../config/config.js"; +export { MarkdownConfigSchema } from "../config/zod-schema.core.js"; +export { emptyPluginConfigSchema } from "../plugins/config-schema.js"; +export type { PluginRuntime } from "../plugins/runtime/types.js"; +export type { OpenClawPluginApi } from "../plugins/types.js"; +export { formatDocsLink } from "../terminal/links.js"; +export type { WizardPrompter } from "../wizard/prompts.js"; diff --git a/src/plugin-sdk/voice-call.ts b/src/plugin-sdk/voice-call.ts new file mode 100644 index 00000000000..da8a1f12613 --- /dev/null +++ b/src/plugin-sdk/voice-call.ts @@ -0,0 +1,18 @@ +// Narrow plugin-sdk surface for the bundled voice-call plugin. +// Keep this list additive and scoped to symbols used under extensions/voice-call. + +export { + TtsAutoSchema, + TtsConfigSchema, + TtsModeSchema, + TtsProviderSchema, +} from "../config/zod-schema.core.js"; +export type { GatewayRequestHandlerOptions } from "../gateway/server-methods/types.js"; +export { + isRequestBodyLimitError, + readRequestBodyWithLimit, + requestBodyErrorToText, +} from "../infra/http-body.js"; +export { fetchWithSsrFGuard } from "../infra/net/fetch-guard.js"; +export type { OpenClawPluginApi } from "../plugins/types.js"; +export { sleep } from "../utils.js"; diff --git a/src/plugin-sdk/whatsapp.ts b/src/plugin-sdk/whatsapp.ts index eaa9a890e8b..20869bbdce8 100644 --- a/src/plugin-sdk/whatsapp.ts +++ b/src/plugin-sdk/whatsapp.ts @@ -1,5 +1,6 @@ export type { ChannelMessageActionName } from "../channels/plugins/types.js"; export type { ChannelPlugin } from "../channels/plugins/types.plugin.js"; +export type { OpenClawConfig } from "../config/config.js"; export type { ResolvedWhatsAppAccount } from "../web/accounts.js"; export type { PluginRuntime } from "../plugins/runtime/types.js"; export type { OpenClawPluginApi } from "../plugins/types.js"; diff --git a/src/plugin-sdk/zalo.ts b/src/plugin-sdk/zalo.ts new file mode 100644 index 00000000000..07237369d2e --- /dev/null +++ b/src/plugin-sdk/zalo.ts @@ -0,0 +1,94 @@ +// Narrow plugin-sdk surface for the bundled zalo plugin. +// Keep this list additive and scoped to symbols used under extensions/zalo. + +export { jsonResult, readStringParam } from "../agents/tools/common.js"; +export type { ReplyPayload } from "../auto-reply/types.js"; +export type { ChannelDock } from "../channels/dock.js"; +export { + deleteAccountFromConfigSection, + setAccountEnabledInConfigSection, +} from "../channels/plugins/config-helpers.js"; +export { buildChannelConfigSchema } from "../channels/plugins/config-schema.js"; +export { formatPairingApproveHint } from "../channels/plugins/helpers.js"; +export type { + ChannelOnboardingAdapter, + ChannelOnboardingDmPolicy, +} from "../channels/plugins/onboarding-types.js"; +export { + addWildcardAllowFrom, + mergeAllowFromEntries, + promptAccountId, + promptSingleChannelSecretInput, +} from "../channels/plugins/onboarding/helpers.js"; +export { PAIRING_APPROVED_MESSAGE } from "../channels/plugins/pairing-message.js"; +export { + applyAccountNameToChannelSection, + migrateBaseNameToDefaultAccount, +} from "../channels/plugins/setup-helpers.js"; +export type { + BaseProbeResult, + BaseTokenResolution, + ChannelAccountSnapshot, + ChannelMessageActionAdapter, + ChannelMessageActionName, + ChannelStatusIssue, +} from "../channels/plugins/types.js"; +export type { ChannelPlugin } from "../channels/plugins/types.plugin.js"; +export { createReplyPrefixOptions } from "../channels/reply-prefix.js"; +export type { OpenClawConfig } from "../config/config.js"; +export { + resolveDefaultGroupPolicy, + resolveOpenProviderRuntimeGroupPolicy, + warnMissingProviderGroupPolicyFallbackOnce, +} from "../config/runtime-group-policy.js"; +export type { GroupPolicy, MarkdownTableMode } from "../config/types.js"; +export type { SecretInput } from "../config/types.secrets.js"; +export { + hasConfiguredSecretInput, + normalizeResolvedSecretInputString, + normalizeSecretInputString, +} from "../config/types.secrets.js"; +export { MarkdownConfigSchema } from "../config/zod-schema.core.js"; +export { createDedupeCache } from "../infra/dedupe.js"; +export { emptyPluginConfigSchema } from "../plugins/config-schema.js"; +export type { PluginRuntime } from "../plugins/runtime/types.js"; +export type { OpenClawPluginApi } from "../plugins/types.js"; +export { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js"; +export type { RuntimeEnv } from "../runtime.js"; +export type { WizardPrompter } from "../wizard/prompts.js"; +export { formatAllowFromLowercase, isNormalizedSenderAllowed } from "./allow-from.js"; +export { + resolveDirectDmAuthorizationOutcome, + resolveSenderCommandAuthorizationWithRuntime, +} from "./command-auth.js"; +export { resolveChannelAccountConfigBasePath } from "./config-paths.js"; +export { evaluateSenderGroupAccess } from "./group-access.js"; +export type { SenderGroupAccessDecision } from "./group-access.js"; +export { resolveInboundRouteEnvelopeBuilderWithRuntime } from "./inbound-envelope.js"; +export { createScopedPairingAccess } from "./pairing-access.js"; +export type { OutboundReplyPayload } from "./reply-payload.js"; +export { resolveOutboundMediaUrls, sendMediaWithLeadingCaption } from "./reply-payload.js"; +export { buildTokenChannelStatusSummary } from "./status-helpers.js"; +export { chunkTextForOutbound } from "./text-chunking.js"; +export { extractToolSend } from "./tool-send.js"; +export { + createFixedWindowRateLimiter, + createWebhookAnomalyTracker, + WEBHOOK_ANOMALY_COUNTER_DEFAULTS, + WEBHOOK_RATE_LIMIT_DEFAULTS, +} from "./webhook-memory-guards.js"; +export { resolveWebhookPath } from "./webhook-path.js"; +export { + applyBasicWebhookRequestGuards, + readJsonWebhookBodyOrReject, +} from "./webhook-request-guards.js"; +export type { + RegisterWebhookPluginRouteOptions, + RegisterWebhookTargetOptions, +} from "./webhook-targets.js"; +export { + registerWebhookTarget, + registerWebhookTargetWithPluginRoute, + resolveSingleWebhookTarget, + resolveWebhookTargets, +} from "./webhook-targets.js"; diff --git a/src/plugin-sdk/zalouser.ts b/src/plugin-sdk/zalouser.ts new file mode 100644 index 00000000000..3109802fbb3 --- /dev/null +++ b/src/plugin-sdk/zalouser.ts @@ -0,0 +1,63 @@ +// Narrow plugin-sdk surface for the bundled zalouser plugin. +// Keep this list additive and scoped to symbols used under extensions/zalouser. + +export type { ReplyPayload } from "../auto-reply/types.js"; +export { mergeAllowlist, summarizeMapping } from "../channels/allowlists/resolve-utils.js"; +export type { ChannelDock } from "../channels/dock.js"; +export { resolveMentionGatingWithBypass } from "../channels/mention-gating.js"; +export { + deleteAccountFromConfigSection, + setAccountEnabledInConfigSection, +} from "../channels/plugins/config-helpers.js"; +export { buildChannelConfigSchema } from "../channels/plugins/config-schema.js"; +export { formatPairingApproveHint } from "../channels/plugins/helpers.js"; +export type { + ChannelOnboardingAdapter, + ChannelOnboardingDmPolicy, +} from "../channels/plugins/onboarding-types.js"; +export { promptChannelAccessConfig } from "../channels/plugins/onboarding/channel-access.js"; +export { + addWildcardAllowFrom, + mergeAllowFromEntries, + promptAccountId, +} from "../channels/plugins/onboarding/helpers.js"; +export { + applyAccountNameToChannelSection, + migrateBaseNameToDefaultAccount, +} from "../channels/plugins/setup-helpers.js"; +export type { + BaseProbeResult, + ChannelAccountSnapshot, + ChannelDirectoryEntry, + ChannelGroupContext, + ChannelMessageActionAdapter, + ChannelStatusIssue, +} from "../channels/plugins/types.js"; +export type { ChannelPlugin } from "../channels/plugins/types.plugin.js"; +export { createReplyPrefixOptions } from "../channels/reply-prefix.js"; +export { createTypingCallbacks } from "../channels/typing.js"; +export type { OpenClawConfig } from "../config/config.js"; +export { + resolveDefaultGroupPolicy, + resolveOpenProviderRuntimeGroupPolicy, + warnMissingProviderGroupPolicyFallbackOnce, +} from "../config/runtime-group-policy.js"; +export type { GroupToolPolicyConfig, MarkdownTableMode } from "../config/types.js"; +export { ToolPolicySchema } from "../config/zod-schema.agent-runtime.js"; +export { MarkdownConfigSchema } from "../config/zod-schema.core.js"; +export { resolvePreferredOpenClawTmpDir } from "../infra/tmp-openclaw-dir.js"; +export { emptyPluginConfigSchema } from "../plugins/config-schema.js"; +export type { PluginRuntime } from "../plugins/runtime/types.js"; +export type { AnyAgentTool, OpenClawPluginApi } from "../plugins/types.js"; +export { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js"; +export type { RuntimeEnv } from "../runtime.js"; +export type { WizardPrompter } from "../wizard/prompts.js"; +export { formatAllowFromLowercase } from "./allow-from.js"; +export { resolveSenderCommandAuthorization } from "./command-auth.js"; +export { resolveChannelAccountConfigBasePath } from "./config-paths.js"; +export { loadOutboundMediaFromUrl } from "./outbound-media.js"; +export { createScopedPairingAccess } from "./pairing-access.js"; +export type { OutboundReplyPayload } from "./reply-payload.js"; +export { resolveOutboundMediaUrls, sendMediaWithLeadingCaption } from "./reply-payload.js"; +export { formatResolvedUnresolvedNote } from "./resolution-notes.js"; +export { chunkTextForOutbound } from "./text-chunking.js"; diff --git a/src/plugins/loader.ts b/src/plugins/loader.ts index 953592d1b59..c735249c7ad 100644 --- a/src/plugins/loader.ts +++ b/src/plugins/loader.ts @@ -88,44 +88,108 @@ const resolvePluginSdkAliasFile = (params: { const resolvePluginSdkAlias = (): string | null => resolvePluginSdkAliasFile({ srcFile: "root-alias.cjs", distFile: "root-alias.cjs" }); -const resolvePluginSdkAccountIdAlias = (): string | null => { - return resolvePluginSdkAliasFile({ srcFile: "account-id.ts", distFile: "account-id.js" }); -}; +const pluginSdkScopedAliasEntries = [ + { subpath: "core", srcFile: "core.ts", distFile: "core.js" }, + { subpath: "compat", srcFile: "compat.ts", distFile: "compat.js" }, + { subpath: "telegram", srcFile: "telegram.ts", distFile: "telegram.js" }, + { subpath: "discord", srcFile: "discord.ts", distFile: "discord.js" }, + { subpath: "slack", srcFile: "slack.ts", distFile: "slack.js" }, + { subpath: "signal", srcFile: "signal.ts", distFile: "signal.js" }, + { subpath: "imessage", srcFile: "imessage.ts", distFile: "imessage.js" }, + { subpath: "whatsapp", srcFile: "whatsapp.ts", distFile: "whatsapp.js" }, + { subpath: "line", srcFile: "line.ts", distFile: "line.js" }, + { subpath: "msteams", srcFile: "msteams.ts", distFile: "msteams.js" }, + { subpath: "acpx", srcFile: "acpx.ts", distFile: "acpx.js" }, + { subpath: "bluebubbles", srcFile: "bluebubbles.ts", distFile: "bluebubbles.js" }, + { + subpath: "copilot-proxy", + srcFile: "copilot-proxy.ts", + distFile: "copilot-proxy.js", + }, + { subpath: "device-pair", srcFile: "device-pair.ts", distFile: "device-pair.js" }, + { + subpath: "diagnostics-otel", + srcFile: "diagnostics-otel.ts", + distFile: "diagnostics-otel.js", + }, + { subpath: "diffs", srcFile: "diffs.ts", distFile: "diffs.js" }, + { subpath: "feishu", srcFile: "feishu.ts", distFile: "feishu.js" }, + { + subpath: "google-gemini-cli-auth", + srcFile: "google-gemini-cli-auth.ts", + distFile: "google-gemini-cli-auth.js", + }, + { subpath: "googlechat", srcFile: "googlechat.ts", distFile: "googlechat.js" }, + { subpath: "irc", srcFile: "irc.ts", distFile: "irc.js" }, + { subpath: "llm-task", srcFile: "llm-task.ts", distFile: "llm-task.js" }, + { subpath: "lobster", srcFile: "lobster.ts", distFile: "lobster.js" }, + { subpath: "matrix", srcFile: "matrix.ts", distFile: "matrix.js" }, + { subpath: "mattermost", srcFile: "mattermost.ts", distFile: "mattermost.js" }, + { subpath: "memory-core", srcFile: "memory-core.ts", distFile: "memory-core.js" }, + { + subpath: "memory-lancedb", + srcFile: "memory-lancedb.ts", + distFile: "memory-lancedb.js", + }, + { + subpath: "minimax-portal-auth", + srcFile: "minimax-portal-auth.ts", + distFile: "minimax-portal-auth.js", + }, + { + subpath: "nextcloud-talk", + srcFile: "nextcloud-talk.ts", + distFile: "nextcloud-talk.js", + }, + { subpath: "nostr", srcFile: "nostr.ts", distFile: "nostr.js" }, + { subpath: "open-prose", srcFile: "open-prose.ts", distFile: "open-prose.js" }, + { + subpath: "phone-control", + srcFile: "phone-control.ts", + distFile: "phone-control.js", + }, + { + subpath: "qwen-portal-auth", + srcFile: "qwen-portal-auth.ts", + distFile: "qwen-portal-auth.js", + }, + { + subpath: "synology-chat", + srcFile: "synology-chat.ts", + distFile: "synology-chat.js", + }, + { subpath: "talk-voice", srcFile: "talk-voice.ts", distFile: "talk-voice.js" }, + { subpath: "test-utils", srcFile: "test-utils.ts", distFile: "test-utils.js" }, + { + subpath: "thread-ownership", + srcFile: "thread-ownership.ts", + distFile: "thread-ownership.js", + }, + { subpath: "tlon", srcFile: "tlon.ts", distFile: "tlon.js" }, + { subpath: "twitch", srcFile: "twitch.ts", distFile: "twitch.js" }, + { subpath: "voice-call", srcFile: "voice-call.ts", distFile: "voice-call.js" }, + { subpath: "zalo", srcFile: "zalo.ts", distFile: "zalo.js" }, + { subpath: "zalouser", srcFile: "zalouser.ts", distFile: "zalouser.js" }, + { subpath: "account-id", srcFile: "account-id.ts", distFile: "account-id.js" }, + { + subpath: "keyed-async-queue", + srcFile: "keyed-async-queue.ts", + distFile: "keyed-async-queue.js", + }, +] as const; -const resolvePluginSdkCoreAlias = (): string | null => { - return resolvePluginSdkAliasFile({ srcFile: "core.ts", distFile: "core.js" }); -}; - -const resolvePluginSdkCompatAlias = (): string | null => { - return resolvePluginSdkAliasFile({ srcFile: "compat.ts", distFile: "compat.js" }); -}; - -const resolvePluginSdkTelegramAlias = (): string | null => { - return resolvePluginSdkAliasFile({ srcFile: "telegram.ts", distFile: "telegram.js" }); -}; - -const resolvePluginSdkDiscordAlias = (): string | null => { - return resolvePluginSdkAliasFile({ srcFile: "discord.ts", distFile: "discord.js" }); -}; - -const resolvePluginSdkSlackAlias = (): string | null => { - return resolvePluginSdkAliasFile({ srcFile: "slack.ts", distFile: "slack.js" }); -}; - -const resolvePluginSdkSignalAlias = (): string | null => { - return resolvePluginSdkAliasFile({ srcFile: "signal.ts", distFile: "signal.js" }); -}; - -const resolvePluginSdkIMessageAlias = (): string | null => { - return resolvePluginSdkAliasFile({ srcFile: "imessage.ts", distFile: "imessage.js" }); -}; - -const resolvePluginSdkWhatsAppAlias = (): string | null => { - return resolvePluginSdkAliasFile({ srcFile: "whatsapp.ts", distFile: "whatsapp.js" }); -}; - -const resolvePluginSdkLineAlias = (): string | null => { - return resolvePluginSdkAliasFile({ srcFile: "line.ts", distFile: "line.js" }); +const resolvePluginSdkScopedAliasMap = (): Record => { + const aliasMap: Record = {}; + for (const entry of pluginSdkScopedAliasEntries) { + const resolved = resolvePluginSdkAliasFile({ + srcFile: entry.srcFile, + distFile: entry.distFile, + }); + if (resolved) { + aliasMap[`openclaw/plugin-sdk/${entry.subpath}`] = resolved; + } + } + return aliasMap; }; export const __testing = { @@ -504,30 +568,9 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi return jitiLoader; } const pluginSdkAlias = resolvePluginSdkAlias(); - const pluginSdkAccountIdAlias = resolvePluginSdkAccountIdAlias(); - const pluginSdkCoreAlias = resolvePluginSdkCoreAlias(); - const pluginSdkCompatAlias = resolvePluginSdkCompatAlias(); - const pluginSdkTelegramAlias = resolvePluginSdkTelegramAlias(); - const pluginSdkDiscordAlias = resolvePluginSdkDiscordAlias(); - const pluginSdkSlackAlias = resolvePluginSdkSlackAlias(); - const pluginSdkSignalAlias = resolvePluginSdkSignalAlias(); - const pluginSdkIMessageAlias = resolvePluginSdkIMessageAlias(); - const pluginSdkWhatsAppAlias = resolvePluginSdkWhatsAppAlias(); - const pluginSdkLineAlias = resolvePluginSdkLineAlias(); const aliasMap = { ...(pluginSdkAlias ? { "openclaw/plugin-sdk": pluginSdkAlias } : {}), - ...(pluginSdkCoreAlias ? { "openclaw/plugin-sdk/core": pluginSdkCoreAlias } : {}), - ...(pluginSdkCompatAlias ? { "openclaw/plugin-sdk/compat": pluginSdkCompatAlias } : {}), - ...(pluginSdkTelegramAlias ? { "openclaw/plugin-sdk/telegram": pluginSdkTelegramAlias } : {}), - ...(pluginSdkDiscordAlias ? { "openclaw/plugin-sdk/discord": pluginSdkDiscordAlias } : {}), - ...(pluginSdkSlackAlias ? { "openclaw/plugin-sdk/slack": pluginSdkSlackAlias } : {}), - ...(pluginSdkSignalAlias ? { "openclaw/plugin-sdk/signal": pluginSdkSignalAlias } : {}), - ...(pluginSdkIMessageAlias ? { "openclaw/plugin-sdk/imessage": pluginSdkIMessageAlias } : {}), - ...(pluginSdkWhatsAppAlias ? { "openclaw/plugin-sdk/whatsapp": pluginSdkWhatsAppAlias } : {}), - ...(pluginSdkLineAlias ? { "openclaw/plugin-sdk/line": pluginSdkLineAlias } : {}), - ...(pluginSdkAccountIdAlias - ? { "openclaw/plugin-sdk/account-id": pluginSdkAccountIdAlias } - : {}), + ...resolvePluginSdkScopedAliasMap(), }; jitiLoader = createJiti(import.meta.url, { interopDefault: true, diff --git a/tsconfig.plugin-sdk.dts.json b/tsconfig.plugin-sdk.dts.json index c3efae99617..7e2b76d745e 100644 --- a/tsconfig.plugin-sdk.dts.json +++ b/tsconfig.plugin-sdk.dts.json @@ -21,8 +21,40 @@ "src/plugin-sdk/imessage.ts", "src/plugin-sdk/whatsapp.ts", "src/plugin-sdk/line.ts", + "src/plugin-sdk/msteams.ts", "src/plugin-sdk/account-id.ts", "src/plugin-sdk/keyed-async-queue.ts", + "src/plugin-sdk/acpx.ts", + "src/plugin-sdk/bluebubbles.ts", + "src/plugin-sdk/copilot-proxy.ts", + "src/plugin-sdk/device-pair.ts", + "src/plugin-sdk/diagnostics-otel.ts", + "src/plugin-sdk/diffs.ts", + "src/plugin-sdk/feishu.ts", + "src/plugin-sdk/google-gemini-cli-auth.ts", + "src/plugin-sdk/googlechat.ts", + "src/plugin-sdk/irc.ts", + "src/plugin-sdk/llm-task.ts", + "src/plugin-sdk/lobster.ts", + "src/plugin-sdk/matrix.ts", + "src/plugin-sdk/mattermost.ts", + "src/plugin-sdk/memory-core.ts", + "src/plugin-sdk/memory-lancedb.ts", + "src/plugin-sdk/minimax-portal-auth.ts", + "src/plugin-sdk/nextcloud-talk.ts", + "src/plugin-sdk/nostr.ts", + "src/plugin-sdk/open-prose.ts", + "src/plugin-sdk/phone-control.ts", + "src/plugin-sdk/qwen-portal-auth.ts", + "src/plugin-sdk/synology-chat.ts", + "src/plugin-sdk/talk-voice.ts", + "src/plugin-sdk/test-utils.ts", + "src/plugin-sdk/thread-ownership.ts", + "src/plugin-sdk/tlon.ts", + "src/plugin-sdk/twitch.ts", + "src/plugin-sdk/voice-call.ts", + "src/plugin-sdk/zalo.ts", + "src/plugin-sdk/zalouser.ts", "src/types/**/*.d.ts" ], "exclude": ["node_modules", "dist", "src/**/*.test.ts"] diff --git a/tsdown.config.ts b/tsdown.config.ts index a69be542d08..b0c2d49c676 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -4,6 +4,53 @@ const env = { NODE_ENV: "production", }; +const pluginSdkEntrypoints = [ + "index", + "core", + "compat", + "telegram", + "discord", + "slack", + "signal", + "imessage", + "whatsapp", + "line", + "msteams", + "acpx", + "bluebubbles", + "copilot-proxy", + "device-pair", + "diagnostics-otel", + "diffs", + "feishu", + "google-gemini-cli-auth", + "googlechat", + "irc", + "llm-task", + "lobster", + "matrix", + "mattermost", + "memory-core", + "memory-lancedb", + "minimax-portal-auth", + "nextcloud-talk", + "nostr", + "open-prose", + "phone-control", + "qwen-portal-auth", + "synology-chat", + "talk-voice", + "test-utils", + "thread-ownership", + "tlon", + "twitch", + "voice-call", + "zalo", + "zalouser", + "account-id", + "keyed-async-queue", +] as const; + export default defineConfig([ { entry: "src/index.ts", @@ -48,83 +95,13 @@ export default defineConfig([ fixedExtension: false, platform: "node", }, - { - entry: "src/plugin-sdk/index.ts", + ...pluginSdkEntrypoints.map((entry) => ({ + entry: `src/plugin-sdk/${entry}.ts`, outDir: "dist/plugin-sdk", env, fixedExtension: false, - platform: "node", - }, - { - entry: "src/plugin-sdk/core.ts", - outDir: "dist/plugin-sdk", - env, - fixedExtension: false, - platform: "node", - }, - { - entry: "src/plugin-sdk/compat.ts", - outDir: "dist/plugin-sdk", - env, - fixedExtension: false, - platform: "node", - }, - { - entry: "src/plugin-sdk/telegram.ts", - outDir: "dist/plugin-sdk", - env, - fixedExtension: false, - platform: "node", - }, - { - entry: "src/plugin-sdk/discord.ts", - outDir: "dist/plugin-sdk", - env, - fixedExtension: false, - platform: "node", - }, - { - entry: "src/plugin-sdk/slack.ts", - outDir: "dist/plugin-sdk", - env, - fixedExtension: false, - platform: "node", - }, - { - entry: "src/plugin-sdk/signal.ts", - outDir: "dist/plugin-sdk", - env, - fixedExtension: false, - platform: "node", - }, - { - entry: "src/plugin-sdk/imessage.ts", - outDir: "dist/plugin-sdk", - env, - fixedExtension: false, - platform: "node", - }, - { - entry: "src/plugin-sdk/whatsapp.ts", - outDir: "dist/plugin-sdk", - env, - fixedExtension: false, - platform: "node", - }, - { - entry: "src/plugin-sdk/line.ts", - outDir: "dist/plugin-sdk", - env, - fixedExtension: false, - platform: "node", - }, - { - entry: "src/plugin-sdk/account-id.ts", - outDir: "dist/plugin-sdk", - env, - fixedExtension: false, - platform: "node", - }, + platform: "node" as const, + })), { entry: "src/extensionAPI.ts", env, diff --git a/vitest.config.ts b/vitest.config.ts index 2094476eff1..658437187f5 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -8,55 +8,60 @@ const isCI = process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true"; const isWindows = process.platform === "win32"; const localWorkers = Math.max(4, Math.min(16, os.cpus().length)); const ciWorkers = isWindows ? 2 : 3; +const pluginSdkSubpaths = [ + "account-id", + "core", + "compat", + "telegram", + "discord", + "slack", + "signal", + "imessage", + "whatsapp", + "line", + "msteams", + "acpx", + "bluebubbles", + "copilot-proxy", + "device-pair", + "diagnostics-otel", + "diffs", + "feishu", + "google-gemini-cli-auth", + "googlechat", + "irc", + "llm-task", + "lobster", + "matrix", + "mattermost", + "memory-core", + "memory-lancedb", + "minimax-portal-auth", + "nextcloud-talk", + "nostr", + "open-prose", + "phone-control", + "qwen-portal-auth", + "synology-chat", + "talk-voice", + "test-utils", + "thread-ownership", + "tlon", + "twitch", + "voice-call", + "zalo", + "zalouser", + "keyed-async-queue", +] as const; export default defineConfig({ resolve: { // Keep this ordered: the base `openclaw/plugin-sdk` alias is a prefix match. alias: [ - { - find: "openclaw/plugin-sdk/account-id", - replacement: path.join(repoRoot, "src", "plugin-sdk", "account-id.ts"), - }, - { - find: "openclaw/plugin-sdk/core", - replacement: path.join(repoRoot, "src", "plugin-sdk", "core.ts"), - }, - { - find: "openclaw/plugin-sdk/compat", - replacement: path.join(repoRoot, "src", "plugin-sdk", "compat.ts"), - }, - { - find: "openclaw/plugin-sdk/telegram", - replacement: path.join(repoRoot, "src", "plugin-sdk", "telegram.ts"), - }, - { - find: "openclaw/plugin-sdk/discord", - replacement: path.join(repoRoot, "src", "plugin-sdk", "discord.ts"), - }, - { - find: "openclaw/plugin-sdk/slack", - replacement: path.join(repoRoot, "src", "plugin-sdk", "slack.ts"), - }, - { - find: "openclaw/plugin-sdk/signal", - replacement: path.join(repoRoot, "src", "plugin-sdk", "signal.ts"), - }, - { - find: "openclaw/plugin-sdk/imessage", - replacement: path.join(repoRoot, "src", "plugin-sdk", "imessage.ts"), - }, - { - find: "openclaw/plugin-sdk/whatsapp", - replacement: path.join(repoRoot, "src", "plugin-sdk", "whatsapp.ts"), - }, - { - find: "openclaw/plugin-sdk/line", - replacement: path.join(repoRoot, "src", "plugin-sdk", "line.ts"), - }, - { - find: "openclaw/plugin-sdk/keyed-async-queue", - replacement: path.join(repoRoot, "src", "plugin-sdk", "keyed-async-queue.ts"), - }, + ...pluginSdkSubpaths.map((subpath) => ({ + find: `openclaw/plugin-sdk/${subpath}`, + replacement: path.join(repoRoot, "src", "plugin-sdk", `${subpath}.ts`), + })), { find: "openclaw/plugin-sdk", replacement: path.join(repoRoot, "src", "plugin-sdk", "index.ts"), From c7c25c89027376240587ca8190fa23df964e83c5 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 4 Mar 2026 02:32:53 -0500 Subject: [PATCH 32/74] Plugins/acpx: migrate to scoped plugin-sdk imports --- extensions/acpx/.DS_Store | Bin 0 -> 6148 bytes extensions/acpx/index.ts | 2 +- extensions/acpx/src/config.ts | 2 +- extensions/acpx/src/ensure.ts | 2 +- extensions/acpx/src/runtime-internals/events.ts | 2 +- extensions/acpx/src/runtime-internals/process.ts | 4 ++-- extensions/acpx/src/runtime.ts | 4 ++-- extensions/acpx/src/service.test.ts | 2 +- extensions/acpx/src/service.ts | 4 ++-- 9 files changed, 11 insertions(+), 11 deletions(-) create mode 100644 extensions/acpx/.DS_Store diff --git a/extensions/acpx/.DS_Store b/extensions/acpx/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..ec6208cc9cc2e3f28974712ca2e388c4bf94b311 GIT binary patch literal 6148 zcmeHKy-EW?5T3n+5KW2{777x!va&FOoyaO@X_W_1@U-t5fo+_&K-LquwC*l7^eh^P!@3>GkaBD~HzBZ0S6fy(Z2 zNHHbUp&>;x-eUNR4Dj7m>CE0*m$LWQr9sqdG}}qscZsjQ&hw3vFlrTozlu;NE2J#FP++&UF|Nhtg ze?CZ_gaKjTUooJHVKdyrEBV?w^Kx8kHS`F|!hWg4aR?^16vLNG@iNp3?3yP)<1uvz Q3q<}1SQ=yy27Z-+5Anur8~^|S literal 0 HcmV?d00001 diff --git a/extensions/acpx/index.ts b/extensions/acpx/index.ts index 187dbacd765..20a1cbbefe2 100644 --- a/extensions/acpx/index.ts +++ b/extensions/acpx/index.ts @@ -1,4 +1,4 @@ -import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/acpx"; import { createAcpxPluginConfigSchema } from "./src/config.js"; import { createAcpxRuntimeService } from "./src/service.js"; diff --git a/extensions/acpx/src/config.ts b/extensions/acpx/src/config.ts index 91f3eb08b57..f62e71ae20c 100644 --- a/extensions/acpx/src/config.ts +++ b/extensions/acpx/src/config.ts @@ -1,6 +1,6 @@ import path from "node:path"; import { fileURLToPath } from "node:url"; -import type { OpenClawPluginConfigSchema } from "openclaw/plugin-sdk/compat"; +import type { OpenClawPluginConfigSchema } from "openclaw/plugin-sdk/acpx"; export const ACPX_PERMISSION_MODES = ["approve-all", "approve-reads", "deny-all"] as const; export type AcpxPermissionMode = (typeof ACPX_PERMISSION_MODES)[number]; diff --git a/extensions/acpx/src/ensure.ts b/extensions/acpx/src/ensure.ts index b86d6b749a8..39307db1f4f 100644 --- a/extensions/acpx/src/ensure.ts +++ b/extensions/acpx/src/ensure.ts @@ -1,6 +1,6 @@ import fs from "node:fs"; import path from "node:path"; -import type { PluginLogger } from "openclaw/plugin-sdk/compat"; +import type { PluginLogger } from "openclaw/plugin-sdk/acpx"; import { ACPX_PINNED_VERSION, ACPX_PLUGIN_ROOT, buildAcpxLocalInstallCommand } from "./config.js"; import { resolveSpawnFailure, diff --git a/extensions/acpx/src/runtime-internals/events.ts b/extensions/acpx/src/runtime-internals/events.ts index c2eb8aaef91..f83f4ddabb9 100644 --- a/extensions/acpx/src/runtime-internals/events.ts +++ b/extensions/acpx/src/runtime-internals/events.ts @@ -1,4 +1,4 @@ -import type { AcpRuntimeEvent, AcpSessionUpdateTag } from "openclaw/plugin-sdk/compat"; +import type { AcpRuntimeEvent, AcpSessionUpdateTag } from "openclaw/plugin-sdk/acpx"; import { asOptionalBoolean, asOptionalString, diff --git a/extensions/acpx/src/runtime-internals/process.ts b/extensions/acpx/src/runtime-internals/process.ts index 842b2b27fc4..953f088586e 100644 --- a/extensions/acpx/src/runtime-internals/process.ts +++ b/extensions/acpx/src/runtime-internals/process.ts @@ -4,12 +4,12 @@ import type { WindowsSpawnProgram, WindowsSpawnProgramCandidate, WindowsSpawnResolution, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/acpx"; import { applyWindowsSpawnProgramPolicy, materializeWindowsSpawnProgram, resolveWindowsSpawnProgramCandidate, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/acpx"; export type SpawnExit = { code: number | null; diff --git a/extensions/acpx/src/runtime.ts b/extensions/acpx/src/runtime.ts index e1b67ab87f6..fc66b394b3c 100644 --- a/extensions/acpx/src/runtime.ts +++ b/extensions/acpx/src/runtime.ts @@ -10,8 +10,8 @@ import type { AcpRuntimeStatus, AcpRuntimeTurnInput, PluginLogger, -} from "openclaw/plugin-sdk/compat"; -import { AcpRuntimeError } from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/acpx"; +import { AcpRuntimeError } from "openclaw/plugin-sdk/acpx"; import { type ResolvedAcpxPluginConfig } from "./config.js"; import { checkAcpxVersion } from "./ensure.js"; import { diff --git a/extensions/acpx/src/service.test.ts b/extensions/acpx/src/service.test.ts index 26205bedde1..402fd9ae67b 100644 --- a/extensions/acpx/src/service.test.ts +++ b/extensions/acpx/src/service.test.ts @@ -1,4 +1,4 @@ -import type { AcpRuntime, OpenClawPluginServiceContext } from "openclaw/plugin-sdk/compat"; +import type { AcpRuntime, OpenClawPluginServiceContext } from "openclaw/plugin-sdk/acpx"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { AcpRuntimeError } from "../../../src/acp/runtime/errors.js"; import { diff --git a/extensions/acpx/src/service.ts b/extensions/acpx/src/service.ts index 01b4bf2ecc6..47731652a07 100644 --- a/extensions/acpx/src/service.ts +++ b/extensions/acpx/src/service.ts @@ -3,8 +3,8 @@ import type { OpenClawPluginService, OpenClawPluginServiceContext, PluginLogger, -} from "openclaw/plugin-sdk/compat"; -import { registerAcpRuntimeBackend, unregisterAcpRuntimeBackend } from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/acpx"; +import { registerAcpRuntimeBackend, unregisterAcpRuntimeBackend } from "openclaw/plugin-sdk/acpx"; import { resolveAcpxPluginConfig, type ResolvedAcpxPluginConfig } from "./config.js"; import { ensureAcpx } from "./ensure.js"; import { ACPX_BACKEND_ID, AcpxRuntime } from "./runtime.js"; From 9cfec9c05ec33afec502a0665c08543975925d10 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 4 Mar 2026 02:32:54 -0500 Subject: [PATCH 33/74] Plugins/bluebubbles: migrate to scoped plugin-sdk imports --- extensions/bluebubbles/index.ts | 4 ++-- extensions/bluebubbles/src/account-resolve.ts | 2 +- extensions/bluebubbles/src/accounts.ts | 2 +- extensions/bluebubbles/src/actions.test.ts | 2 +- extensions/bluebubbles/src/actions.ts | 2 +- extensions/bluebubbles/src/attachments.test.ts | 2 +- extensions/bluebubbles/src/attachments.ts | 2 +- extensions/bluebubbles/src/channel.ts | 4 ++-- extensions/bluebubbles/src/chat.ts | 2 +- extensions/bluebubbles/src/config-schema.ts | 2 +- extensions/bluebubbles/src/history.ts | 2 +- extensions/bluebubbles/src/media-send.test.ts | 2 +- extensions/bluebubbles/src/media-send.ts | 2 +- extensions/bluebubbles/src/monitor-debounce.ts | 2 +- extensions/bluebubbles/src/monitor-processing.ts | 4 ++-- extensions/bluebubbles/src/monitor-shared.ts | 2 +- extensions/bluebubbles/src/monitor.test.ts | 2 +- extensions/bluebubbles/src/monitor.ts | 2 +- extensions/bluebubbles/src/monitor.webhook-auth.test.ts | 2 +- extensions/bluebubbles/src/monitor.webhook-route.test.ts | 2 +- extensions/bluebubbles/src/onboarding.secret-input.test.ts | 4 ++-- extensions/bluebubbles/src/onboarding.ts | 4 ++-- extensions/bluebubbles/src/probe.ts | 2 +- extensions/bluebubbles/src/reactions.ts | 2 +- extensions/bluebubbles/src/runtime.ts | 2 +- extensions/bluebubbles/src/secret-input.ts | 2 +- extensions/bluebubbles/src/send.test.ts | 2 +- extensions/bluebubbles/src/send.ts | 4 ++-- extensions/bluebubbles/src/targets.ts | 2 +- extensions/bluebubbles/src/types.ts | 4 ++-- 30 files changed, 37 insertions(+), 37 deletions(-) diff --git a/extensions/bluebubbles/index.ts b/extensions/bluebubbles/index.ts index 15d583bd342..f04afb40959 100644 --- a/extensions/bluebubbles/index.ts +++ b/extensions/bluebubbles/index.ts @@ -1,5 +1,5 @@ -import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core"; -import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/core"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/bluebubbles"; +import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/bluebubbles"; import { bluebubblesPlugin } from "./src/channel.js"; import { setBlueBubblesRuntime } from "./src/runtime.js"; diff --git a/extensions/bluebubbles/src/account-resolve.ts b/extensions/bluebubbles/src/account-resolve.ts index b69c613e548..7d28d0dd3c8 100644 --- a/extensions/bluebubbles/src/account-resolve.ts +++ b/extensions/bluebubbles/src/account-resolve.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/bluebubbles"; import { resolveBlueBubblesAccount } from "./accounts.js"; import { normalizeResolvedSecretInputString } from "./secret-input.js"; diff --git a/extensions/bluebubbles/src/accounts.ts b/extensions/bluebubbles/src/accounts.ts index bbde1cf4656..4b86c6d0364 100644 --- a/extensions/bluebubbles/src/accounts.ts +++ b/extensions/bluebubbles/src/accounts.ts @@ -3,7 +3,7 @@ import { normalizeAccountId, normalizeOptionalAccountId, } from "openclaw/plugin-sdk/account-id"; -import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/bluebubbles"; import { hasConfiguredSecretInput, normalizeSecretInputString } from "./secret-input.js"; import { normalizeBlueBubblesServerUrl, type BlueBubblesAccountConfig } from "./types.js"; diff --git a/extensions/bluebubbles/src/actions.test.ts b/extensions/bluebubbles/src/actions.test.ts index e958035bd4b..0560567c5fb 100644 --- a/extensions/bluebubbles/src/actions.test.ts +++ b/extensions/bluebubbles/src/actions.test.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/bluebubbles"; import { describe, expect, it, vi, beforeEach } from "vitest"; import { bluebubblesMessageActions } from "./actions.js"; import { getCachedBlueBubblesPrivateApiStatus } from "./probe.js"; diff --git a/extensions/bluebubbles/src/actions.ts b/extensions/bluebubbles/src/actions.ts index daf9dd0c872..a8ce9f62c5f 100644 --- a/extensions/bluebubbles/src/actions.ts +++ b/extensions/bluebubbles/src/actions.ts @@ -10,7 +10,7 @@ import { readStringParam, type ChannelMessageActionAdapter, type ChannelMessageActionName, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/bluebubbles"; import { resolveBlueBubblesAccount } from "./accounts.js"; import { sendBlueBubblesAttachment } from "./attachments.js"; import { diff --git a/extensions/bluebubbles/src/attachments.test.ts b/extensions/bluebubbles/src/attachments.test.ts index 169f79f6b43..8ef94cf08ae 100644 --- a/extensions/bluebubbles/src/attachments.test.ts +++ b/extensions/bluebubbles/src/attachments.test.ts @@ -1,4 +1,4 @@ -import type { PluginRuntime } from "openclaw/plugin-sdk/compat"; +import type { PluginRuntime } from "openclaw/plugin-sdk/bluebubbles"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import "./test-mocks.js"; import { downloadBlueBubblesAttachment, sendBlueBubblesAttachment } from "./attachments.js"; diff --git a/extensions/bluebubbles/src/attachments.ts b/extensions/bluebubbles/src/attachments.ts index 582c5f92abd..cbd8a74d807 100644 --- a/extensions/bluebubbles/src/attachments.ts +++ b/extensions/bluebubbles/src/attachments.ts @@ -1,6 +1,6 @@ import crypto from "node:crypto"; import path from "node:path"; -import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/bluebubbles"; import { resolveBlueBubblesServerAccount } from "./account-resolve.js"; import { postMultipartFormData } from "./multipart.js"; import { diff --git a/extensions/bluebubbles/src/channel.ts b/extensions/bluebubbles/src/channel.ts index 7fc3b97de19..e00364cf115 100644 --- a/extensions/bluebubbles/src/channel.ts +++ b/extensions/bluebubbles/src/channel.ts @@ -2,7 +2,7 @@ import type { ChannelAccountSnapshot, ChannelPlugin, OpenClawConfig, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/bluebubbles"; import { applyAccountNameToChannelSection, buildChannelConfigSchema, @@ -17,7 +17,7 @@ import { resolveBlueBubblesGroupRequireMention, resolveBlueBubblesGroupToolPolicy, setAccountEnabledInConfigSection, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/bluebubbles"; import { listBlueBubblesAccountIds, type ResolvedBlueBubblesAccount, diff --git a/extensions/bluebubbles/src/chat.ts b/extensions/bluebubbles/src/chat.ts index 17a782c9312..5489077eaca 100644 --- a/extensions/bluebubbles/src/chat.ts +++ b/extensions/bluebubbles/src/chat.ts @@ -1,6 +1,6 @@ import crypto from "node:crypto"; import path from "node:path"; -import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/bluebubbles"; import { resolveBlueBubblesServerAccount } from "./account-resolve.js"; import { postMultipartFormData } from "./multipart.js"; import { getCachedBlueBubblesPrivateApiStatus } from "./probe.js"; diff --git a/extensions/bluebubbles/src/config-schema.ts b/extensions/bluebubbles/src/config-schema.ts index 54f9b175ce5..bc4ec0e3f67 100644 --- a/extensions/bluebubbles/src/config-schema.ts +++ b/extensions/bluebubbles/src/config-schema.ts @@ -1,4 +1,4 @@ -import { MarkdownConfigSchema, ToolPolicySchema } from "openclaw/plugin-sdk/compat"; +import { MarkdownConfigSchema, ToolPolicySchema } from "openclaw/plugin-sdk/bluebubbles"; import { z } from "zod"; import { buildSecretInputSchema, hasConfiguredSecretInput } from "./secret-input.js"; diff --git a/extensions/bluebubbles/src/history.ts b/extensions/bluebubbles/src/history.ts index 82c6de6b635..388af325d1a 100644 --- a/extensions/bluebubbles/src/history.ts +++ b/extensions/bluebubbles/src/history.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/bluebubbles"; import { resolveBlueBubblesServerAccount } from "./account-resolve.js"; import { blueBubblesFetchWithTimeout, buildBlueBubblesApiUrl } from "./types.js"; diff --git a/extensions/bluebubbles/src/media-send.test.ts b/extensions/bluebubbles/src/media-send.test.ts index 2129fd2c252..9f065599bfb 100644 --- a/extensions/bluebubbles/src/media-send.test.ts +++ b/extensions/bluebubbles/src/media-send.test.ts @@ -2,7 +2,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { pathToFileURL } from "node:url"; -import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk/compat"; +import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk/bluebubbles"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { sendBlueBubblesMedia } from "./media-send.js"; import { setBlueBubblesRuntime } from "./runtime.js"; diff --git a/extensions/bluebubbles/src/media-send.ts b/extensions/bluebubbles/src/media-send.ts index 4b52aa6aa72..8bd505efcf7 100644 --- a/extensions/bluebubbles/src/media-send.ts +++ b/extensions/bluebubbles/src/media-send.ts @@ -3,7 +3,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; -import { resolveChannelMediaMaxBytes, type OpenClawConfig } from "openclaw/plugin-sdk/compat"; +import { resolveChannelMediaMaxBytes, type OpenClawConfig } from "openclaw/plugin-sdk/bluebubbles"; import { resolveBlueBubblesAccount } from "./accounts.js"; import { sendBlueBubblesAttachment } from "./attachments.js"; import { resolveBlueBubblesMessageId } from "./monitor.js"; diff --git a/extensions/bluebubbles/src/monitor-debounce.ts b/extensions/bluebubbles/src/monitor-debounce.ts index 2c4daa55028..3a3189cc7ea 100644 --- a/extensions/bluebubbles/src/monitor-debounce.ts +++ b/extensions/bluebubbles/src/monitor-debounce.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/bluebubbles"; import type { NormalizedWebhookMessage } from "./monitor-normalize.js"; import type { BlueBubblesCoreRuntime, WebhookTarget } from "./monitor-shared.js"; diff --git a/extensions/bluebubbles/src/monitor-processing.ts b/extensions/bluebubbles/src/monitor-processing.ts index 62a8564ae0c..a1c316429e4 100644 --- a/extensions/bluebubbles/src/monitor-processing.ts +++ b/extensions/bluebubbles/src/monitor-processing.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/bluebubbles"; import { DM_GROUP_ACCESS_REASON, createScopedPairingAccess, @@ -14,7 +14,7 @@ import { resolveControlCommandGate, stripMarkdown, type HistoryEntry, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/bluebubbles"; import { downloadBlueBubblesAttachment } from "./attachments.js"; import { markBlueBubblesChatRead, sendBlueBubblesTyping } from "./chat.js"; import { fetchBlueBubblesHistory } from "./history.js"; diff --git a/extensions/bluebubbles/src/monitor-shared.ts b/extensions/bluebubbles/src/monitor-shared.ts index 00477a020c5..2d40ac7b8d8 100644 --- a/extensions/bluebubbles/src/monitor-shared.ts +++ b/extensions/bluebubbles/src/monitor-shared.ts @@ -1,4 +1,4 @@ -import { normalizeWebhookPath, type OpenClawConfig } from "openclaw/plugin-sdk/compat"; +import { normalizeWebhookPath, type OpenClawConfig } from "openclaw/plugin-sdk/bluebubbles"; import type { ResolvedBlueBubblesAccount } from "./accounts.js"; import { getBlueBubblesRuntime } from "./runtime.js"; import type { BlueBubblesAccountConfig } from "./types.js"; diff --git a/extensions/bluebubbles/src/monitor.test.ts b/extensions/bluebubbles/src/monitor.test.ts index 9dc41fa26b9..b64cabe63e9 100644 --- a/extensions/bluebubbles/src/monitor.test.ts +++ b/extensions/bluebubbles/src/monitor.test.ts @@ -1,6 +1,6 @@ import { EventEmitter } from "node:events"; import type { IncomingMessage, ServerResponse } from "node:http"; -import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk/compat"; +import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk/bluebubbles"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { createPluginRuntimeMock } from "../../test-utils/plugin-runtime-mock.js"; import type { ResolvedBlueBubblesAccount } from "./accounts.js"; diff --git a/extensions/bluebubbles/src/monitor.ts b/extensions/bluebubbles/src/monitor.ts index 97c47dc0118..8c7aa9e17c0 100644 --- a/extensions/bluebubbles/src/monitor.ts +++ b/extensions/bluebubbles/src/monitor.ts @@ -7,7 +7,7 @@ import { readWebhookBodyOrReject, resolveWebhookTargetWithAuthOrRejectSync, resolveWebhookTargets, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/bluebubbles"; import { createBlueBubblesDebounceRegistry } from "./monitor-debounce.js"; import { normalizeWebhookMessage, normalizeWebhookReaction } from "./monitor-normalize.js"; import { logVerbose, processMessage, processReaction } from "./monitor-processing.js"; diff --git a/extensions/bluebubbles/src/monitor.webhook-auth.test.ts b/extensions/bluebubbles/src/monitor.webhook-auth.test.ts index 271b9521987..9dd8e6f470b 100644 --- a/extensions/bluebubbles/src/monitor.webhook-auth.test.ts +++ b/extensions/bluebubbles/src/monitor.webhook-auth.test.ts @@ -1,6 +1,6 @@ import { EventEmitter } from "node:events"; import type { IncomingMessage, ServerResponse } from "node:http"; -import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk/compat"; +import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk/bluebubbles"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { createPluginRuntimeMock } from "../../test-utils/plugin-runtime-mock.js"; import type { ResolvedBlueBubblesAccount } from "./accounts.js"; diff --git a/extensions/bluebubbles/src/monitor.webhook-route.test.ts b/extensions/bluebubbles/src/monitor.webhook-route.test.ts index 322e8a76377..fc48606b8ed 100644 --- a/extensions/bluebubbles/src/monitor.webhook-route.test.ts +++ b/extensions/bluebubbles/src/monitor.webhook-route.test.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/bluebubbles"; import { afterEach, describe, expect, it } from "vitest"; import { createEmptyPluginRegistry } from "../../../src/plugins/registry.js"; import { setActivePluginRegistry } from "../../../src/plugins/runtime.js"; diff --git a/extensions/bluebubbles/src/onboarding.secret-input.test.ts b/extensions/bluebubbles/src/onboarding.secret-input.test.ts index a7ca9e1d3eb..a96e30ab20a 100644 --- a/extensions/bluebubbles/src/onboarding.secret-input.test.ts +++ b/extensions/bluebubbles/src/onboarding.secret-input.test.ts @@ -1,7 +1,7 @@ -import type { WizardPrompter } from "openclaw/plugin-sdk/compat"; +import type { WizardPrompter } from "openclaw/plugin-sdk/bluebubbles"; import { describe, expect, it, vi } from "vitest"; -vi.mock("openclaw/plugin-sdk", () => ({ +vi.mock("openclaw/plugin-sdk/bluebubbles", () => ({ DEFAULT_ACCOUNT_ID: "default", addWildcardAllowFrom: vi.fn(), formatDocsLink: (_url: string, fallback: string) => fallback, diff --git a/extensions/bluebubbles/src/onboarding.ts b/extensions/bluebubbles/src/onboarding.ts index 1e716c73b75..8936d3d5c52 100644 --- a/extensions/bluebubbles/src/onboarding.ts +++ b/extensions/bluebubbles/src/onboarding.ts @@ -4,7 +4,7 @@ import type { OpenClawConfig, DmPolicy, WizardPrompter, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/bluebubbles"; import { DEFAULT_ACCOUNT_ID, addWildcardAllowFrom, @@ -12,7 +12,7 @@ import { mergeAllowFromEntries, normalizeAccountId, promptAccountId, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/bluebubbles"; import { listBlueBubblesAccountIds, resolveBlueBubblesAccount, diff --git a/extensions/bluebubbles/src/probe.ts b/extensions/bluebubbles/src/probe.ts index b0af1252d06..135423bc0fc 100644 --- a/extensions/bluebubbles/src/probe.ts +++ b/extensions/bluebubbles/src/probe.ts @@ -1,4 +1,4 @@ -import type { BaseProbeResult } from "openclaw/plugin-sdk/compat"; +import type { BaseProbeResult } from "openclaw/plugin-sdk/bluebubbles"; import { normalizeSecretInputString } from "./secret-input.js"; import { buildBlueBubblesApiUrl, blueBubblesFetchWithTimeout } from "./types.js"; diff --git a/extensions/bluebubbles/src/reactions.ts b/extensions/bluebubbles/src/reactions.ts index 411fcc759ec..8a3837c12e4 100644 --- a/extensions/bluebubbles/src/reactions.ts +++ b/extensions/bluebubbles/src/reactions.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/bluebubbles"; import { resolveBlueBubblesServerAccount } from "./account-resolve.js"; import { getCachedBlueBubblesPrivateApiStatus } from "./probe.js"; import { blueBubblesFetchWithTimeout, buildBlueBubblesApiUrl } from "./types.js"; diff --git a/extensions/bluebubbles/src/runtime.ts b/extensions/bluebubbles/src/runtime.ts index 6f82c5916e3..89ee04cf8a4 100644 --- a/extensions/bluebubbles/src/runtime.ts +++ b/extensions/bluebubbles/src/runtime.ts @@ -1,4 +1,4 @@ -import type { PluginRuntime } from "openclaw/plugin-sdk/compat"; +import type { PluginRuntime } from "openclaw/plugin-sdk/bluebubbles"; let runtime: PluginRuntime | null = null; type LegacyRuntimeLogShape = { log?: (message: string) => void }; diff --git a/extensions/bluebubbles/src/secret-input.ts b/extensions/bluebubbles/src/secret-input.ts index 3fc82b3ac91..8a5530f4607 100644 --- a/extensions/bluebubbles/src/secret-input.ts +++ b/extensions/bluebubbles/src/secret-input.ts @@ -2,7 +2,7 @@ import { hasConfiguredSecretInput, normalizeResolvedSecretInputString, normalizeSecretInputString, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/bluebubbles"; import { z } from "zod"; export { hasConfiguredSecretInput, normalizeResolvedSecretInputString, normalizeSecretInputString }; diff --git a/extensions/bluebubbles/src/send.test.ts b/extensions/bluebubbles/src/send.test.ts index 0895da0e4bb..f820ebd9b8b 100644 --- a/extensions/bluebubbles/src/send.test.ts +++ b/extensions/bluebubbles/src/send.test.ts @@ -1,4 +1,4 @@ -import type { PluginRuntime } from "openclaw/plugin-sdk/compat"; +import type { PluginRuntime } from "openclaw/plugin-sdk/bluebubbles"; import { beforeEach, describe, expect, it, vi } from "vitest"; import "./test-mocks.js"; import { getCachedBlueBubblesPrivateApiStatus } from "./probe.js"; diff --git a/extensions/bluebubbles/src/send.ts b/extensions/bluebubbles/src/send.ts index 307e5e4d839..a32fd92d470 100644 --- a/extensions/bluebubbles/src/send.ts +++ b/extensions/bluebubbles/src/send.ts @@ -1,6 +1,6 @@ import crypto from "node:crypto"; -import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; -import { stripMarkdown } from "openclaw/plugin-sdk/compat"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/bluebubbles"; +import { stripMarkdown } from "openclaw/plugin-sdk/bluebubbles"; import { resolveBlueBubblesAccount } from "./accounts.js"; import { getCachedBlueBubblesPrivateApiStatus, diff --git a/extensions/bluebubbles/src/targets.ts b/extensions/bluebubbles/src/targets.ts index da62f1f8445..ab297471fc3 100644 --- a/extensions/bluebubbles/src/targets.ts +++ b/extensions/bluebubbles/src/targets.ts @@ -5,7 +5,7 @@ import { type ParsedChatTarget, resolveServicePrefixedAllowTarget, resolveServicePrefixedTarget, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/bluebubbles"; export type BlueBubblesService = "imessage" | "sms" | "auto"; diff --git a/extensions/bluebubbles/src/types.ts b/extensions/bluebubbles/src/types.ts index a310f02f86a..43e8c739775 100644 --- a/extensions/bluebubbles/src/types.ts +++ b/extensions/bluebubbles/src/types.ts @@ -1,6 +1,6 @@ -import type { DmPolicy, GroupPolicy } from "openclaw/plugin-sdk/compat"; +import type { DmPolicy, GroupPolicy } from "openclaw/plugin-sdk/bluebubbles"; -export type { DmPolicy, GroupPolicy } from "openclaw/plugin-sdk/compat"; +export type { DmPolicy, GroupPolicy } from "openclaw/plugin-sdk/bluebubbles"; export type BlueBubblesGroupConfig = { /** If true, only respond in this group when mentioned. */ From 04ff4a0c267f39a1a8bed83d8ea74947a78865a0 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 4 Mar 2026 02:32:55 -0500 Subject: [PATCH 34/74] Plugins/copilot-proxy: migrate to scoped plugin-sdk imports --- extensions/copilot-proxy/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/copilot-proxy/index.ts b/extensions/copilot-proxy/index.ts index 990752782e7..6fad48228cd 100644 --- a/extensions/copilot-proxy/index.ts +++ b/extensions/copilot-proxy/index.ts @@ -3,7 +3,7 @@ import { type OpenClawPluginApi, type ProviderAuthContext, type ProviderAuthResult, -} from "openclaw/plugin-sdk/core"; +} from "openclaw/plugin-sdk/copilot-proxy"; const DEFAULT_BASE_URL = "http://localhost:3000/v1"; const DEFAULT_API_KEY = "n/a"; From 04385a61b7760a91a1d384f5e2181b6ee35f9ef2 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 4 Mar 2026 02:32:56 -0500 Subject: [PATCH 35/74] Plugins/device-pair: migrate to scoped plugin-sdk imports --- extensions/device-pair/index.ts | 4 ++-- extensions/device-pair/notify.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/extensions/device-pair/index.ts b/extensions/device-pair/index.ts index c9772a422f2..7590703a32b 100644 --- a/extensions/device-pair/index.ts +++ b/extensions/device-pair/index.ts @@ -1,12 +1,12 @@ import os from "node:os"; -import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/device-pair"; import { approveDevicePairing, listDevicePairing, resolveGatewayBindUrl, runPluginCommandWithTimeout, resolveTailnetHostWithRunner, -} from "openclaw/plugin-sdk/core"; +} from "openclaw/plugin-sdk/device-pair"; import qrcode from "qrcode-terminal"; import { armPairNotifyOnce, diff --git a/extensions/device-pair/notify.ts b/extensions/device-pair/notify.ts index dbdf483ed73..3ef3005cf73 100644 --- a/extensions/device-pair/notify.ts +++ b/extensions/device-pair/notify.ts @@ -1,7 +1,7 @@ import { promises as fs } from "node:fs"; import path from "node:path"; -import type { OpenClawPluginApi } from "openclaw/plugin-sdk/compat"; -import { listDevicePairing } from "openclaw/plugin-sdk/compat"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/device-pair"; +import { listDevicePairing } from "openclaw/plugin-sdk/device-pair"; const NOTIFY_STATE_FILE = "device-pair-notify.json"; const NOTIFY_POLL_INTERVAL_MS = 10_000; From 54d78bb423d9f760cc66e8d13daed8da2ed68d44 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 4 Mar 2026 02:32:56 -0500 Subject: [PATCH 36/74] Plugins/diagnostics-otel: migrate to scoped plugin-sdk imports --- extensions/diagnostics-otel/index.ts | 4 ++-- extensions/diagnostics-otel/src/service.test.ts | 10 +++++----- extensions/diagnostics-otel/src/service.ts | 7 +++++-- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/extensions/diagnostics-otel/index.ts b/extensions/diagnostics-otel/index.ts index 4c460a125d8..a6ab6c133b6 100644 --- a/extensions/diagnostics-otel/index.ts +++ b/extensions/diagnostics-otel/index.ts @@ -1,5 +1,5 @@ -import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core"; -import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/core"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/diagnostics-otel"; +import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/diagnostics-otel"; import { createDiagnosticsOtelService } from "./src/service.js"; const plugin = { diff --git a/extensions/diagnostics-otel/src/service.test.ts b/extensions/diagnostics-otel/src/service.test.ts index 031922f2c5d..e77d1f3cabe 100644 --- a/extensions/diagnostics-otel/src/service.test.ts +++ b/extensions/diagnostics-otel/src/service.test.ts @@ -98,9 +98,9 @@ vi.mock("@opentelemetry/semantic-conventions", () => ({ ATTR_SERVICE_NAME: "service.name", })); -vi.mock("openclaw/plugin-sdk/compat", async () => { - const actual = await vi.importActual( - "openclaw/plugin-sdk/compat", +vi.mock("openclaw/plugin-sdk/diagnostics-otel", async () => { + const actual = await vi.importActual( + "openclaw/plugin-sdk/diagnostics-otel", ); return { ...actual, @@ -108,8 +108,8 @@ vi.mock("openclaw/plugin-sdk/compat", async () => { }; }); -import type { OpenClawPluginServiceContext } from "openclaw/plugin-sdk/compat"; -import { emitDiagnosticEvent } from "openclaw/plugin-sdk/compat"; +import type { OpenClawPluginServiceContext } from "openclaw/plugin-sdk/diagnostics-otel"; +import { emitDiagnosticEvent } from "openclaw/plugin-sdk/diagnostics-otel"; import { createDiagnosticsOtelService } from "./service.js"; const OTEL_TEST_STATE_DIR = "/tmp/openclaw-diagnostics-otel-test"; diff --git a/extensions/diagnostics-otel/src/service.ts b/extensions/diagnostics-otel/src/service.ts index a63620a3e9a..b7224d034dd 100644 --- a/extensions/diagnostics-otel/src/service.ts +++ b/extensions/diagnostics-otel/src/service.ts @@ -9,12 +9,15 @@ import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics"; import { NodeSDK } from "@opentelemetry/sdk-node"; import { ParentBasedSampler, TraceIdRatioBasedSampler } from "@opentelemetry/sdk-trace-base"; import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions"; -import type { DiagnosticEventPayload, OpenClawPluginService } from "openclaw/plugin-sdk/compat"; +import type { + DiagnosticEventPayload, + OpenClawPluginService, +} from "openclaw/plugin-sdk/diagnostics-otel"; import { onDiagnosticEvent, redactSensitiveText, registerLogTransport, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/diagnostics-otel"; const DEFAULT_SERVICE_NAME = "openclaw"; From ed857547223a730e6d2e63696d2c846bba62c7c0 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 4 Mar 2026 02:32:57 -0500 Subject: [PATCH 37/74] Plugins/diffs: migrate to scoped plugin-sdk imports --- extensions/diffs/index.ts | 4 ++-- extensions/diffs/src/browser.test.ts | 2 +- extensions/diffs/src/browser.ts | 2 +- extensions/diffs/src/config.ts | 2 +- extensions/diffs/src/http.ts | 2 +- extensions/diffs/src/store.ts | 2 +- extensions/diffs/src/tool.test.ts | 2 +- extensions/diffs/src/tool.ts | 2 +- extensions/diffs/src/url.ts | 2 +- 9 files changed, 10 insertions(+), 10 deletions(-) diff --git a/extensions/diffs/index.ts b/extensions/diffs/index.ts index ccd3ef77b5a..8b038b42fcc 100644 --- a/extensions/diffs/index.ts +++ b/extensions/diffs/index.ts @@ -1,6 +1,6 @@ import path from "node:path"; -import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core"; -import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/core"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/diffs"; +import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/diffs"; import { diffsPluginConfigSchema, resolveDiffsPluginDefaults, diff --git a/extensions/diffs/src/browser.test.ts b/extensions/diffs/src/browser.test.ts index 11f4befe122..9c3cf1365ea 100644 --- a/extensions/diffs/src/browser.test.ts +++ b/extensions/diffs/src/browser.test.ts @@ -1,7 +1,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/diffs"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; const { launchMock } = vi.hoisted(() => ({ diff --git a/extensions/diffs/src/browser.ts b/extensions/diffs/src/browser.ts index caa8319a237..904996946b6 100644 --- a/extensions/diffs/src/browser.ts +++ b/extensions/diffs/src/browser.ts @@ -1,7 +1,7 @@ import { constants as fsConstants } from "node:fs"; import fs from "node:fs/promises"; import path from "node:path"; -import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/diffs"; import { chromium } from "playwright-core"; import type { DiffRenderOptions, DiffTheme } from "./types.js"; import { VIEWER_ASSET_PREFIX, getServedViewerAsset } from "./viewer-assets.js"; diff --git a/extensions/diffs/src/config.ts b/extensions/diffs/src/config.ts index 066c9ed0948..fbc9a108060 100644 --- a/extensions/diffs/src/config.ts +++ b/extensions/diffs/src/config.ts @@ -1,4 +1,4 @@ -import type { OpenClawPluginConfigSchema } from "openclaw/plugin-sdk/compat"; +import type { OpenClawPluginConfigSchema } from "openclaw/plugin-sdk/diffs"; import { DIFF_IMAGE_QUALITY_PRESETS, DIFF_INDICATORS, diff --git a/extensions/diffs/src/http.ts b/extensions/diffs/src/http.ts index f53b6e0c386..0f17e77fd9e 100644 --- a/extensions/diffs/src/http.ts +++ b/extensions/diffs/src/http.ts @@ -1,5 +1,5 @@ import type { IncomingMessage, ServerResponse } from "node:http"; -import type { PluginLogger } from "openclaw/plugin-sdk/compat"; +import type { PluginLogger } from "openclaw/plugin-sdk/diffs"; import type { DiffArtifactStore } from "./store.js"; import { DIFF_ARTIFACT_ID_PATTERN, DIFF_ARTIFACT_TOKEN_PATTERN } from "./types.js"; import { VIEWER_ASSET_PREFIX, getServedViewerAsset } from "./viewer-assets.js"; diff --git a/extensions/diffs/src/store.ts b/extensions/diffs/src/store.ts index 36d3d9f45a0..e53a555356c 100644 --- a/extensions/diffs/src/store.ts +++ b/extensions/diffs/src/store.ts @@ -1,7 +1,7 @@ import crypto from "node:crypto"; import fs from "node:fs/promises"; import path from "node:path"; -import type { PluginLogger } from "openclaw/plugin-sdk/compat"; +import type { PluginLogger } from "openclaw/plugin-sdk/diffs"; import type { DiffArtifactMeta, DiffOutputFormat } from "./types.js"; const DEFAULT_TTL_MS = 30 * 60 * 1000; diff --git a/extensions/diffs/src/tool.test.ts b/extensions/diffs/src/tool.test.ts index a7a3b29261f..db66255cba6 100644 --- a/extensions/diffs/src/tool.test.ts +++ b/extensions/diffs/src/tool.test.ts @@ -1,7 +1,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import type { OpenClawPluginApi } from "openclaw/plugin-sdk/compat"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/diffs"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { DiffScreenshotter } from "./browser.js"; import { DEFAULT_DIFFS_TOOL_DEFAULTS } from "./config.js"; diff --git a/extensions/diffs/src/tool.ts b/extensions/diffs/src/tool.ts index 2b4d5885033..c6eb4b528c4 100644 --- a/extensions/diffs/src/tool.ts +++ b/extensions/diffs/src/tool.ts @@ -1,6 +1,6 @@ import fs from "node:fs/promises"; import { Static, Type } from "@sinclair/typebox"; -import type { AnyAgentTool, OpenClawPluginApi } from "openclaw/plugin-sdk/compat"; +import type { AnyAgentTool, OpenClawPluginApi } from "openclaw/plugin-sdk/diffs"; import { PlaywrightDiffScreenshotter, type DiffScreenshotter } from "./browser.js"; import { resolveDiffImageRenderOptions } from "./config.js"; import { renderDiffDocument } from "./render.js"; diff --git a/extensions/diffs/src/url.ts b/extensions/diffs/src/url.ts index 516bcf2e1aa..feee5c7af05 100644 --- a/extensions/diffs/src/url.ts +++ b/extensions/diffs/src/url.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/diffs"; const DEFAULT_GATEWAY_PORT = 18789; From 3e1ca111afcdeb7b310122a962817e2c496c19df Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 4 Mar 2026 02:32:58 -0500 Subject: [PATCH 38/74] Plugins/feishu: migrate to scoped plugin-sdk imports --- extensions/feishu/index.ts | 4 ++-- extensions/feishu/src/accounts.ts | 2 +- extensions/feishu/src/bitable.ts | 2 +- extensions/feishu/src/bot.test.ts | 2 +- extensions/feishu/src/bot.ts | 4 ++-- extensions/feishu/src/card-action.ts | 2 +- extensions/feishu/src/channel.test.ts | 2 +- extensions/feishu/src/channel.ts | 4 ++-- extensions/feishu/src/chat.ts | 2 +- extensions/feishu/src/dedup.ts | 2 +- extensions/feishu/src/directory.ts | 2 +- extensions/feishu/src/docx.account-selection.test.ts | 2 +- extensions/feishu/src/docx.ts | 2 +- extensions/feishu/src/drive.ts | 2 +- extensions/feishu/src/dynamic-agent.ts | 2 +- extensions/feishu/src/media.ts | 2 +- extensions/feishu/src/monitor.account.ts | 2 +- extensions/feishu/src/monitor.reaction.test.ts | 2 +- extensions/feishu/src/monitor.startup.test.ts | 2 +- extensions/feishu/src/monitor.startup.ts | 2 +- extensions/feishu/src/monitor.state.ts | 2 +- extensions/feishu/src/monitor.transport.ts | 2 +- extensions/feishu/src/monitor.ts | 2 +- extensions/feishu/src/monitor.webhook-security.test.ts | 2 +- extensions/feishu/src/onboarding.status.test.ts | 2 +- extensions/feishu/src/onboarding.ts | 4 ++-- extensions/feishu/src/outbound.ts | 2 +- extensions/feishu/src/perm.ts | 2 +- extensions/feishu/src/policy.ts | 2 +- extensions/feishu/src/reactions.ts | 2 +- extensions/feishu/src/reply-dispatcher.ts | 2 +- extensions/feishu/src/runtime.ts | 2 +- extensions/feishu/src/secret-input.ts | 2 +- extensions/feishu/src/send-target.test.ts | 2 +- extensions/feishu/src/send-target.ts | 2 +- extensions/feishu/src/send.test.ts | 2 +- extensions/feishu/src/send.ts | 2 +- extensions/feishu/src/streaming-card.ts | 2 +- extensions/feishu/src/tool-account-routing.test.ts | 2 +- extensions/feishu/src/tool-account.ts | 2 +- extensions/feishu/src/tool-factory-test-harness.ts | 2 +- extensions/feishu/src/types.ts | 2 +- extensions/feishu/src/typing.ts | 2 +- extensions/feishu/src/wiki.ts | 2 +- 44 files changed, 48 insertions(+), 48 deletions(-) diff --git a/extensions/feishu/index.ts b/extensions/feishu/index.ts index 62f311262d7..bd26346c8ec 100644 --- a/extensions/feishu/index.ts +++ b/extensions/feishu/index.ts @@ -1,5 +1,5 @@ -import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core"; -import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/core"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/feishu"; +import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/feishu"; import { registerFeishuBitableTools } from "./src/bitable.js"; import { feishuPlugin } from "./src/channel.js"; import { registerFeishuChatTools } from "./src/chat.js"; diff --git a/extensions/feishu/src/accounts.ts b/extensions/feishu/src/accounts.ts index 4f84a477b91..016bc997458 100644 --- a/extensions/feishu/src/accounts.ts +++ b/extensions/feishu/src/accounts.ts @@ -1,5 +1,5 @@ import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id"; -import type { ClawdbotConfig } from "openclaw/plugin-sdk/compat"; +import type { ClawdbotConfig } from "openclaw/plugin-sdk/feishu"; import { normalizeResolvedSecretInputString, normalizeSecretInputString } from "./secret-input.js"; import type { FeishuConfig, diff --git a/extensions/feishu/src/bitable.ts b/extensions/feishu/src/bitable.ts index 891cbb8dd19..e7d027694d1 100644 --- a/extensions/feishu/src/bitable.ts +++ b/extensions/feishu/src/bitable.ts @@ -1,6 +1,6 @@ import type * as Lark from "@larksuiteoapi/node-sdk"; import { Type } from "@sinclair/typebox"; -import type { OpenClawPluginApi } from "openclaw/plugin-sdk/compat"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/feishu"; import { listEnabledFeishuAccounts } from "./accounts.js"; import { createFeishuToolClient } from "./tool-account.js"; diff --git a/extensions/feishu/src/bot.test.ts b/extensions/feishu/src/bot.test.ts index 271bf2a618b..9b36e922526 100644 --- a/extensions/feishu/src/bot.test.ts +++ b/extensions/feishu/src/bot.test.ts @@ -1,4 +1,4 @@ -import type { ClawdbotConfig, PluginRuntime, RuntimeEnv } from "openclaw/plugin-sdk/compat"; +import type { ClawdbotConfig, PluginRuntime, RuntimeEnv } from "openclaw/plugin-sdk/feishu"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { createPluginRuntimeMock } from "../../test-utils/plugin-runtime-mock.js"; import type { FeishuMessageEvent } from "./bot.js"; diff --git a/extensions/feishu/src/bot.ts b/extensions/feishu/src/bot.ts index 0aa9cbb15a2..d97fcd4cf6b 100644 --- a/extensions/feishu/src/bot.ts +++ b/extensions/feishu/src/bot.ts @@ -1,4 +1,4 @@ -import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk/compat"; +import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk/feishu"; import { buildAgentMediaPayload, buildPendingHistoryContextFromMap, @@ -11,7 +11,7 @@ import { resolveOpenProviderRuntimeGroupPolicy, resolveDefaultGroupPolicy, warnMissingProviderGroupPolicyFallbackOnce, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/feishu"; import { resolveFeishuAccount } from "./accounts.js"; import { createFeishuClient } from "./client.js"; import { tryRecordMessage, tryRecordMessagePersistent } from "./dedup.js"; diff --git a/extensions/feishu/src/card-action.ts b/extensions/feishu/src/card-action.ts index d0c2dbdde5e..b3030c39a1a 100644 --- a/extensions/feishu/src/card-action.ts +++ b/extensions/feishu/src/card-action.ts @@ -1,4 +1,4 @@ -import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk/compat"; +import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk/feishu"; import { resolveFeishuAccount } from "./accounts.js"; import { handleFeishuMessage, type FeishuMessageEvent } from "./bot.js"; diff --git a/extensions/feishu/src/channel.test.ts b/extensions/feishu/src/channel.test.ts index 8a59efed263..936ba4c0054 100644 --- a/extensions/feishu/src/channel.test.ts +++ b/extensions/feishu/src/channel.test.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/feishu"; import { describe, expect, it, vi } from "vitest"; const probeFeishuMock = vi.hoisted(() => vi.fn()); diff --git a/extensions/feishu/src/channel.ts b/extensions/feishu/src/channel.ts index 64e64f5e6b3..1e631c407e0 100644 --- a/extensions/feishu/src/channel.ts +++ b/extensions/feishu/src/channel.ts @@ -1,4 +1,4 @@ -import type { ChannelMeta, ChannelPlugin, ClawdbotConfig } from "openclaw/plugin-sdk/compat"; +import type { ChannelMeta, ChannelPlugin, ClawdbotConfig } from "openclaw/plugin-sdk/feishu"; import { buildBaseChannelStatusSummary, createDefaultChannelRuntimeState, @@ -6,7 +6,7 @@ import { PAIRING_APPROVED_MESSAGE, resolveAllowlistProviderRuntimeGroupPolicy, resolveDefaultGroupPolicy, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/feishu"; import { resolveFeishuAccount, resolveFeishuCredentials, diff --git a/extensions/feishu/src/chat.ts b/extensions/feishu/src/chat.ts index 7aae3683978..df168d579ee 100644 --- a/extensions/feishu/src/chat.ts +++ b/extensions/feishu/src/chat.ts @@ -1,5 +1,5 @@ import type * as Lark from "@larksuiteoapi/node-sdk"; -import type { OpenClawPluginApi } from "openclaw/plugin-sdk/compat"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/feishu"; import { listEnabledFeishuAccounts } from "./accounts.js"; import { FeishuChatSchema, type FeishuChatParams } from "./chat-schema.js"; import { createFeishuClient } from "./client.js"; diff --git a/extensions/feishu/src/dedup.ts b/extensions/feishu/src/dedup.ts index 55f983ebd90..35f95d5c76b 100644 --- a/extensions/feishu/src/dedup.ts +++ b/extensions/feishu/src/dedup.ts @@ -4,7 +4,7 @@ import { createDedupeCache, createPersistentDedupe, readJsonFileWithFallback, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/feishu"; // Persistent TTL: 24 hours — survives restarts & WebSocket reconnects. const DEDUP_TTL_MS = 24 * 60 * 60 * 1000; diff --git a/extensions/feishu/src/directory.ts b/extensions/feishu/src/directory.ts index 464c11fa6e1..e88b94b229c 100644 --- a/extensions/feishu/src/directory.ts +++ b/extensions/feishu/src/directory.ts @@ -1,4 +1,4 @@ -import type { ClawdbotConfig } from "openclaw/plugin-sdk/compat"; +import type { ClawdbotConfig } from "openclaw/plugin-sdk/feishu"; import { resolveFeishuAccount } from "./accounts.js"; import { createFeishuClient } from "./client.js"; import { normalizeFeishuTarget } from "./targets.js"; diff --git a/extensions/feishu/src/docx.account-selection.test.ts b/extensions/feishu/src/docx.account-selection.test.ts index 324bccacbcf..18b4083e324 100644 --- a/extensions/feishu/src/docx.account-selection.test.ts +++ b/extensions/feishu/src/docx.account-selection.test.ts @@ -1,4 +1,4 @@ -import type { OpenClawPluginApi } from "openclaw/plugin-sdk/compat"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/feishu"; import { describe, expect, test, vi } from "vitest"; import { registerFeishuDocTools } from "./docx.js"; import { createToolFactoryHarness } from "./tool-factory-test-harness.js"; diff --git a/extensions/feishu/src/docx.ts b/extensions/feishu/src/docx.ts index b1ad52ca131..8c6a4b6cd02 100644 --- a/extensions/feishu/src/docx.ts +++ b/extensions/feishu/src/docx.ts @@ -4,7 +4,7 @@ import { isAbsolute } from "node:path"; import { basename } from "node:path"; import type * as Lark from "@larksuiteoapi/node-sdk"; import { Type } from "@sinclair/typebox"; -import type { OpenClawPluginApi } from "openclaw/plugin-sdk/compat"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/feishu"; import { listEnabledFeishuAccounts } from "./accounts.js"; import { FeishuDocSchema, type FeishuDocParams } from "./doc-schema.js"; import { BATCH_SIZE, insertBlocksInBatches } from "./docx-batch-insert.js"; diff --git a/extensions/feishu/src/drive.ts b/extensions/feishu/src/drive.ts index 35f3c798054..f9eacc9287d 100644 --- a/extensions/feishu/src/drive.ts +++ b/extensions/feishu/src/drive.ts @@ -1,5 +1,5 @@ import type * as Lark from "@larksuiteoapi/node-sdk"; -import type { OpenClawPluginApi } from "openclaw/plugin-sdk/compat"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/feishu"; import { listEnabledFeishuAccounts } from "./accounts.js"; import { FeishuDriveSchema, type FeishuDriveParams } from "./drive-schema.js"; import { createFeishuToolClient, resolveAnyEnabledFeishuToolsConfig } from "./tool-account.js"; diff --git a/extensions/feishu/src/dynamic-agent.ts b/extensions/feishu/src/dynamic-agent.ts index f7341dd94db..6f22683294c 100644 --- a/extensions/feishu/src/dynamic-agent.ts +++ b/extensions/feishu/src/dynamic-agent.ts @@ -1,7 +1,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk/compat"; +import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk/feishu"; import type { DynamicAgentCreationConfig } from "./types.js"; export type MaybeCreateDynamicAgentResult = { diff --git a/extensions/feishu/src/media.ts b/extensions/feishu/src/media.ts index 1945d672367..42f98ab7305 100644 --- a/extensions/feishu/src/media.ts +++ b/extensions/feishu/src/media.ts @@ -1,7 +1,7 @@ import fs from "fs"; import path from "path"; import { Readable } from "stream"; -import { withTempDownloadPath, type ClawdbotConfig } from "openclaw/plugin-sdk/compat"; +import { withTempDownloadPath, type ClawdbotConfig } from "openclaw/plugin-sdk/feishu"; import { resolveFeishuAccount } from "./accounts.js"; import { createFeishuClient } from "./client.js"; import { normalizeFeishuExternalKey } from "./external-keys.js"; diff --git a/extensions/feishu/src/monitor.account.ts b/extensions/feishu/src/monitor.account.ts index c8c0d908cb9..9fe5eb86a91 100644 --- a/extensions/feishu/src/monitor.account.ts +++ b/extensions/feishu/src/monitor.account.ts @@ -1,6 +1,6 @@ import * as crypto from "crypto"; import * as Lark from "@larksuiteoapi/node-sdk"; -import type { ClawdbotConfig, RuntimeEnv, HistoryEntry } from "openclaw/plugin-sdk/compat"; +import type { ClawdbotConfig, RuntimeEnv, HistoryEntry } from "openclaw/plugin-sdk/feishu"; import { resolveFeishuAccount } from "./accounts.js"; import { raceWithTimeoutAndAbort } from "./async.js"; import { diff --git a/extensions/feishu/src/monitor.reaction.test.ts b/extensions/feishu/src/monitor.reaction.test.ts index 7cef897e559..8bf06b57bab 100644 --- a/extensions/feishu/src/monitor.reaction.test.ts +++ b/extensions/feishu/src/monitor.reaction.test.ts @@ -1,4 +1,4 @@ -import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk/compat"; +import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk/feishu"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { hasControlCommand } from "../../../src/auto-reply/command-detection.js"; import { diff --git a/extensions/feishu/src/monitor.startup.test.ts b/extensions/feishu/src/monitor.startup.test.ts index 62f5aea41be..29b00fab200 100644 --- a/extensions/feishu/src/monitor.startup.test.ts +++ b/extensions/feishu/src/monitor.startup.test.ts @@ -1,4 +1,4 @@ -import type { ClawdbotConfig } from "openclaw/plugin-sdk/compat"; +import type { ClawdbotConfig } from "openclaw/plugin-sdk/feishu"; import { afterEach, describe, expect, it, vi } from "vitest"; import { monitorFeishuProvider, stopFeishuMonitor } from "./monitor.js"; diff --git a/extensions/feishu/src/monitor.startup.ts b/extensions/feishu/src/monitor.startup.ts index f3f59a7c5f6..a2d284c879e 100644 --- a/extensions/feishu/src/monitor.startup.ts +++ b/extensions/feishu/src/monitor.startup.ts @@ -1,4 +1,4 @@ -import type { RuntimeEnv } from "openclaw/plugin-sdk/compat"; +import type { RuntimeEnv } from "openclaw/plugin-sdk/feishu"; import { probeFeishu } from "./probe.js"; import type { ResolvedFeishuAccount } from "./types.js"; diff --git a/extensions/feishu/src/monitor.state.ts b/extensions/feishu/src/monitor.state.ts index a41b5ab2034..6326dcf9444 100644 --- a/extensions/feishu/src/monitor.state.ts +++ b/extensions/feishu/src/monitor.state.ts @@ -6,7 +6,7 @@ import { type RuntimeEnv, WEBHOOK_ANOMALY_COUNTER_DEFAULTS as WEBHOOK_ANOMALY_COUNTER_DEFAULTS_FROM_SDK, WEBHOOK_RATE_LIMIT_DEFAULTS as WEBHOOK_RATE_LIMIT_DEFAULTS_FROM_SDK, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/feishu"; export const wsClients = new Map(); export const httpServers = new Map(); diff --git a/extensions/feishu/src/monitor.transport.ts b/extensions/feishu/src/monitor.transport.ts index f7c21cc1e23..e067e0e9f99 100644 --- a/extensions/feishu/src/monitor.transport.ts +++ b/extensions/feishu/src/monitor.transport.ts @@ -4,7 +4,7 @@ import { applyBasicWebhookRequestGuards, type RuntimeEnv, installRequestBodyLimitGuard, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/feishu"; import { createFeishuWSClient } from "./client.js"; import { botOpenIds, diff --git a/extensions/feishu/src/monitor.ts b/extensions/feishu/src/monitor.ts index 87ae8db7884..8617a928ac7 100644 --- a/extensions/feishu/src/monitor.ts +++ b/extensions/feishu/src/monitor.ts @@ -1,4 +1,4 @@ -import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk/compat"; +import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk/feishu"; import { listEnabledFeishuAccounts, resolveFeishuAccount } from "./accounts.js"; import { monitorSingleAccount, diff --git a/extensions/feishu/src/monitor.webhook-security.test.ts b/extensions/feishu/src/monitor.webhook-security.test.ts index eddbae01db5..d52b417009f 100644 --- a/extensions/feishu/src/monitor.webhook-security.test.ts +++ b/extensions/feishu/src/monitor.webhook-security.test.ts @@ -1,6 +1,6 @@ import { createServer } from "node:http"; import type { AddressInfo } from "node:net"; -import type { ClawdbotConfig } from "openclaw/plugin-sdk/compat"; +import type { ClawdbotConfig } from "openclaw/plugin-sdk/feishu"; import { afterEach, describe, expect, it, vi } from "vitest"; const probeFeishuMock = vi.hoisted(() => vi.fn()); diff --git a/extensions/feishu/src/onboarding.status.test.ts b/extensions/feishu/src/onboarding.status.test.ts index 1a5bae00080..eda2bafa242 100644 --- a/extensions/feishu/src/onboarding.status.test.ts +++ b/extensions/feishu/src/onboarding.status.test.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/feishu"; import { describe, expect, it } from "vitest"; import { feishuOnboardingAdapter } from "./onboarding.js"; diff --git a/extensions/feishu/src/onboarding.ts b/extensions/feishu/src/onboarding.ts index 255f4ff7134..b29b544dd08 100644 --- a/extensions/feishu/src/onboarding.ts +++ b/extensions/feishu/src/onboarding.ts @@ -5,14 +5,14 @@ import type { DmPolicy, SecretInput, WizardPrompter, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/feishu"; import { addWildcardAllowFrom, DEFAULT_ACCOUNT_ID, formatDocsLink, hasConfiguredSecretInput, promptSingleChannelSecretInput, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/feishu"; import { resolveFeishuCredentials } from "./accounts.js"; import { probeFeishu } from "./probe.js"; import type { FeishuConfig } from "./types.js"; diff --git a/extensions/feishu/src/outbound.ts b/extensions/feishu/src/outbound.ts index 5aeb71514a1..ab4037fcae0 100644 --- a/extensions/feishu/src/outbound.ts +++ b/extensions/feishu/src/outbound.ts @@ -1,6 +1,6 @@ import fs from "fs"; import path from "path"; -import type { ChannelOutboundAdapter } from "openclaw/plugin-sdk/compat"; +import type { ChannelOutboundAdapter } from "openclaw/plugin-sdk/feishu"; import { resolveFeishuAccount } from "./accounts.js"; import { sendMediaFeishu } from "./media.js"; import { getFeishuRuntime } from "./runtime.js"; diff --git a/extensions/feishu/src/perm.ts b/extensions/feishu/src/perm.ts index 332b3992640..8ff1a794e29 100644 --- a/extensions/feishu/src/perm.ts +++ b/extensions/feishu/src/perm.ts @@ -1,5 +1,5 @@ import type * as Lark from "@larksuiteoapi/node-sdk"; -import type { OpenClawPluginApi } from "openclaw/plugin-sdk/compat"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/feishu"; import { listEnabledFeishuAccounts } from "./accounts.js"; import { FeishuPermSchema, type FeishuPermParams } from "./perm-schema.js"; import { createFeishuToolClient, resolveAnyEnabledFeishuToolsConfig } from "./tool-account.js"; diff --git a/extensions/feishu/src/policy.ts b/extensions/feishu/src/policy.ts index c0f480e449f..9c6164fc9e0 100644 --- a/extensions/feishu/src/policy.ts +++ b/extensions/feishu/src/policy.ts @@ -2,7 +2,7 @@ import type { AllowlistMatch, ChannelGroupContext, GroupToolPolicyConfig, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/feishu"; import { normalizeFeishuTarget } from "./targets.js"; import type { FeishuConfig, FeishuGroupConfig } from "./types.js"; diff --git a/extensions/feishu/src/reactions.ts b/extensions/feishu/src/reactions.ts index 73a515cd037..d446a674b88 100644 --- a/extensions/feishu/src/reactions.ts +++ b/extensions/feishu/src/reactions.ts @@ -1,4 +1,4 @@ -import type { ClawdbotConfig } from "openclaw/plugin-sdk/compat"; +import type { ClawdbotConfig } from "openclaw/plugin-sdk/feishu"; import { resolveFeishuAccount } from "./accounts.js"; import { createFeishuClient } from "./client.js"; diff --git a/extensions/feishu/src/reply-dispatcher.ts b/extensions/feishu/src/reply-dispatcher.ts index 23aa835347e..857e4cec023 100644 --- a/extensions/feishu/src/reply-dispatcher.ts +++ b/extensions/feishu/src/reply-dispatcher.ts @@ -5,7 +5,7 @@ import { type ClawdbotConfig, type ReplyPayload, type RuntimeEnv, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/feishu"; import { resolveFeishuAccount } from "./accounts.js"; import { createFeishuClient } from "./client.js"; import { sendMediaFeishu } from "./media.js"; diff --git a/extensions/feishu/src/runtime.ts b/extensions/feishu/src/runtime.ts index 9f3b8ed9d58..b66579e8775 100644 --- a/extensions/feishu/src/runtime.ts +++ b/extensions/feishu/src/runtime.ts @@ -1,4 +1,4 @@ -import type { PluginRuntime } from "openclaw/plugin-sdk/compat"; +import type { PluginRuntime } from "openclaw/plugin-sdk/feishu"; let runtime: PluginRuntime | null = null; diff --git a/extensions/feishu/src/secret-input.ts b/extensions/feishu/src/secret-input.ts index 3fc82b3ac91..a2c2f517f3a 100644 --- a/extensions/feishu/src/secret-input.ts +++ b/extensions/feishu/src/secret-input.ts @@ -2,7 +2,7 @@ import { hasConfiguredSecretInput, normalizeResolvedSecretInputString, normalizeSecretInputString, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/feishu"; import { z } from "zod"; export { hasConfiguredSecretInput, normalizeResolvedSecretInputString, normalizeSecretInputString }; diff --git a/extensions/feishu/src/send-target.test.ts b/extensions/feishu/src/send-target.test.ts index 123e0d020ca..b4f5f81ae09 100644 --- a/extensions/feishu/src/send-target.test.ts +++ b/extensions/feishu/src/send-target.test.ts @@ -1,4 +1,4 @@ -import type { ClawdbotConfig } from "openclaw/plugin-sdk/compat"; +import type { ClawdbotConfig } from "openclaw/plugin-sdk/feishu"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { resolveFeishuSendTarget } from "./send-target.js"; diff --git a/extensions/feishu/src/send-target.ts b/extensions/feishu/src/send-target.ts index 15d0e7ae6e4..cc1780e9223 100644 --- a/extensions/feishu/src/send-target.ts +++ b/extensions/feishu/src/send-target.ts @@ -1,4 +1,4 @@ -import type { ClawdbotConfig } from "openclaw/plugin-sdk/compat"; +import type { ClawdbotConfig } from "openclaw/plugin-sdk/feishu"; import { resolveFeishuAccount } from "./accounts.js"; import { createFeishuClient } from "./client.js"; import { resolveReceiveIdType, normalizeFeishuTarget } from "./targets.js"; diff --git a/extensions/feishu/src/send.test.ts b/extensions/feishu/src/send.test.ts index fab23a64829..18e14b20d79 100644 --- a/extensions/feishu/src/send.test.ts +++ b/extensions/feishu/src/send.test.ts @@ -1,4 +1,4 @@ -import type { ClawdbotConfig } from "openclaw/plugin-sdk/compat"; +import type { ClawdbotConfig } from "openclaw/plugin-sdk/feishu"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { getMessageFeishu } from "./send.js"; diff --git a/extensions/feishu/src/send.ts b/extensions/feishu/src/send.ts index 7703fde77cb..e637cf13810 100644 --- a/extensions/feishu/src/send.ts +++ b/extensions/feishu/src/send.ts @@ -1,4 +1,4 @@ -import type { ClawdbotConfig } from "openclaw/plugin-sdk/compat"; +import type { ClawdbotConfig } from "openclaw/plugin-sdk/feishu"; import { resolveFeishuAccount } from "./accounts.js"; import { createFeishuClient } from "./client.js"; import type { MentionTarget } from "./mention.js"; diff --git a/extensions/feishu/src/streaming-card.ts b/extensions/feishu/src/streaming-card.ts index d93fc9937dd..bb92faebf70 100644 --- a/extensions/feishu/src/streaming-card.ts +++ b/extensions/feishu/src/streaming-card.ts @@ -3,7 +3,7 @@ */ import type { Client } from "@larksuiteoapi/node-sdk"; -import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/compat"; +import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/feishu"; import type { FeishuDomain } from "./types.js"; type Credentials = { appId: string; appSecret: string; domain?: FeishuDomain }; diff --git a/extensions/feishu/src/tool-account-routing.test.ts b/extensions/feishu/src/tool-account-routing.test.ts index aebc9fe71ad..0631067a07b 100644 --- a/extensions/feishu/src/tool-account-routing.test.ts +++ b/extensions/feishu/src/tool-account-routing.test.ts @@ -1,4 +1,4 @@ -import type { OpenClawPluginApi } from "openclaw/plugin-sdk/compat"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/feishu"; import { beforeEach, describe, expect, test, vi } from "vitest"; import { registerFeishuBitableTools } from "./bitable.js"; import { registerFeishuDriveTools } from "./drive.js"; diff --git a/extensions/feishu/src/tool-account.ts b/extensions/feishu/src/tool-account.ts index 37b069d8c57..cf8a7e62286 100644 --- a/extensions/feishu/src/tool-account.ts +++ b/extensions/feishu/src/tool-account.ts @@ -1,5 +1,5 @@ import type * as Lark from "@larksuiteoapi/node-sdk"; -import type { OpenClawPluginApi } from "openclaw/plugin-sdk/compat"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/feishu"; import { resolveFeishuAccount } from "./accounts.js"; import { createFeishuClient } from "./client.js"; import { resolveToolsConfig } from "./tools-config.js"; diff --git a/extensions/feishu/src/tool-factory-test-harness.ts b/extensions/feishu/src/tool-factory-test-harness.ts index 9f3e801b070..f5bd19672dd 100644 --- a/extensions/feishu/src/tool-factory-test-harness.ts +++ b/extensions/feishu/src/tool-factory-test-harness.ts @@ -1,4 +1,4 @@ -import type { AnyAgentTool, OpenClawPluginApi } from "openclaw/plugin-sdk/compat"; +import type { AnyAgentTool, OpenClawPluginApi } from "openclaw/plugin-sdk/feishu"; type ToolContextLike = { agentAccountId?: string; diff --git a/extensions/feishu/src/types.ts b/extensions/feishu/src/types.ts index 3bfba9fe168..2160ae05c25 100644 --- a/extensions/feishu/src/types.ts +++ b/extensions/feishu/src/types.ts @@ -1,4 +1,4 @@ -import type { BaseProbeResult } from "openclaw/plugin-sdk/compat"; +import type { BaseProbeResult } from "openclaw/plugin-sdk/feishu"; import type { FeishuConfigSchema, FeishuGroupSchema, diff --git a/extensions/feishu/src/typing.ts b/extensions/feishu/src/typing.ts index 65abb9ae832..f32996003bb 100644 --- a/extensions/feishu/src/typing.ts +++ b/extensions/feishu/src/typing.ts @@ -1,4 +1,4 @@ -import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk/compat"; +import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk/feishu"; import { resolveFeishuAccount } from "./accounts.js"; import { createFeishuClient } from "./client.js"; import { getFeishuRuntime } from "./runtime.js"; diff --git a/extensions/feishu/src/wiki.ts b/extensions/feishu/src/wiki.ts index 66e2c291af0..ef74b5dc0a7 100644 --- a/extensions/feishu/src/wiki.ts +++ b/extensions/feishu/src/wiki.ts @@ -1,5 +1,5 @@ import type * as Lark from "@larksuiteoapi/node-sdk"; -import type { OpenClawPluginApi } from "openclaw/plugin-sdk/compat"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/feishu"; import { listEnabledFeishuAccounts } from "./accounts.js"; import { createFeishuToolClient, resolveAnyEnabledFeishuToolsConfig } from "./tool-account.js"; import { FeishuWikiSchema, type FeishuWikiParams } from "./wiki-schema.js"; From 5174b386268185f91f1489a1a57347934bf28a2a Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 4 Mar 2026 02:32:59 -0500 Subject: [PATCH 39/74] Plugins/google-gemini-cli-auth: migrate to scoped plugin-sdk imports --- extensions/google-gemini-cli-auth/index.ts | 2 +- extensions/google-gemini-cli-auth/oauth.test.ts | 2 +- extensions/google-gemini-cli-auth/oauth.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/extensions/google-gemini-cli-auth/index.ts b/extensions/google-gemini-cli-auth/index.ts index 254b3994bd5..9a7b770502f 100644 --- a/extensions/google-gemini-cli-auth/index.ts +++ b/extensions/google-gemini-cli-auth/index.ts @@ -3,7 +3,7 @@ import { emptyPluginConfigSchema, type OpenClawPluginApi, type ProviderAuthContext, -} from "openclaw/plugin-sdk/core"; +} from "openclaw/plugin-sdk/google-gemini-cli-auth"; import { loginGeminiCliOAuth } from "./oauth.js"; const PROVIDER_ID = "google-gemini-cli"; diff --git a/extensions/google-gemini-cli-auth/oauth.test.ts b/extensions/google-gemini-cli-auth/oauth.test.ts index 86b1fe7c712..0ec4b6185e9 100644 --- a/extensions/google-gemini-cli-auth/oauth.test.ts +++ b/extensions/google-gemini-cli-auth/oauth.test.ts @@ -1,7 +1,7 @@ import { join, parse } from "node:path"; import { describe, expect, it, vi, beforeEach, afterEach } from "vitest"; -vi.mock("openclaw/plugin-sdk", () => ({ +vi.mock("openclaw/plugin-sdk/google-gemini-cli-auth", () => ({ isWSL2Sync: () => false, fetchWithSsrFGuard: async (params: { url: string; diff --git a/extensions/google-gemini-cli-auth/oauth.ts b/extensions/google-gemini-cli-auth/oauth.ts index 0730127bf2e..62881ec3a73 100644 --- a/extensions/google-gemini-cli-auth/oauth.ts +++ b/extensions/google-gemini-cli-auth/oauth.ts @@ -2,7 +2,7 @@ import { createHash, randomBytes } from "node:crypto"; import { existsSync, readFileSync, readdirSync, realpathSync } from "node:fs"; import { createServer } from "node:http"; import { delimiter, dirname, join } from "node:path"; -import { fetchWithSsrFGuard, isWSL2Sync } from "openclaw/plugin-sdk/compat"; +import { fetchWithSsrFGuard, isWSL2Sync } from "openclaw/plugin-sdk/google-gemini-cli-auth"; const CLIENT_ID_KEYS = ["OPENCLAW_GEMINI_OAUTH_CLIENT_ID", "GEMINI_CLI_OAUTH_CLIENT_ID"]; const CLIENT_SECRET_KEYS = [ From a1e21bc02de28af52aa564cb32bd48fa96422e41 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 4 Mar 2026 02:33:00 -0500 Subject: [PATCH 40/74] Plugins/googlechat: migrate to scoped plugin-sdk imports --- extensions/googlechat/index.ts | 4 ++-- extensions/googlechat/src/accounts.ts | 4 ++-- extensions/googlechat/src/actions.ts | 4 ++-- extensions/googlechat/src/api.ts | 2 +- extensions/googlechat/src/channel.outbound.test.ts | 2 +- extensions/googlechat/src/channel.startup.test.ts | 2 +- extensions/googlechat/src/channel.ts | 4 ++-- extensions/googlechat/src/monitor-access.ts | 4 ++-- extensions/googlechat/src/monitor-types.ts | 2 +- extensions/googlechat/src/monitor-webhook.ts | 2 +- extensions/googlechat/src/monitor.ts | 4 ++-- extensions/googlechat/src/monitor.webhook-routing.test.ts | 2 +- extensions/googlechat/src/onboarding.ts | 4 ++-- extensions/googlechat/src/resolve-target.test.ts | 4 ++-- extensions/googlechat/src/runtime.ts | 2 +- extensions/googlechat/src/types.config.ts | 2 +- 16 files changed, 24 insertions(+), 24 deletions(-) diff --git a/extensions/googlechat/index.ts b/extensions/googlechat/index.ts index 8bcb1f76e3a..e218a15c8de 100644 --- a/extensions/googlechat/index.ts +++ b/extensions/googlechat/index.ts @@ -1,5 +1,5 @@ -import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core"; -import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/core"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/googlechat"; +import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/googlechat"; import { googlechatDock, googlechatPlugin } from "./src/channel.js"; import { setGoogleChatRuntime } from "./src/runtime.js"; diff --git a/extensions/googlechat/src/accounts.ts b/extensions/googlechat/src/accounts.ts index 494e1481b6d..537c898d77e 100644 --- a/extensions/googlechat/src/accounts.ts +++ b/extensions/googlechat/src/accounts.ts @@ -3,8 +3,8 @@ import { normalizeAccountId, normalizeOptionalAccountId, } from "openclaw/plugin-sdk/account-id"; -import { isSecretRef } from "openclaw/plugin-sdk/compat"; -import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; +import { isSecretRef } from "openclaw/plugin-sdk/googlechat"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/googlechat"; import type { GoogleChatAccountConfig } from "./types.config.js"; export type GoogleChatCredentialSource = "file" | "inline" | "env" | "none"; diff --git a/extensions/googlechat/src/actions.ts b/extensions/googlechat/src/actions.ts index 41f94593c67..4685ac0bd26 100644 --- a/extensions/googlechat/src/actions.ts +++ b/extensions/googlechat/src/actions.ts @@ -2,7 +2,7 @@ import type { ChannelMessageActionAdapter, ChannelMessageActionName, OpenClawConfig, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/googlechat"; import { createActionGate, extractToolSend, @@ -10,7 +10,7 @@ import { readNumberParam, readReactionParams, readStringParam, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/googlechat"; import { listEnabledGoogleChatAccounts, resolveGoogleChatAccount } from "./accounts.js"; import { createGoogleChatReaction, diff --git a/extensions/googlechat/src/api.ts b/extensions/googlechat/src/api.ts index e7db6b76022..7c4f26b8db9 100644 --- a/extensions/googlechat/src/api.ts +++ b/extensions/googlechat/src/api.ts @@ -1,5 +1,5 @@ import crypto from "node:crypto"; -import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/compat"; +import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/googlechat"; import type { ResolvedGoogleChatAccount } from "./accounts.js"; import { getGoogleChatAccessToken } from "./auth.js"; import type { GoogleChatReaction } from "./types.js"; diff --git a/extensions/googlechat/src/channel.outbound.test.ts b/extensions/googlechat/src/channel.outbound.test.ts index b50dbc7c6ae..a530d3afe4d 100644 --- a/extensions/googlechat/src/channel.outbound.test.ts +++ b/extensions/googlechat/src/channel.outbound.test.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk"; +import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk/googlechat"; import { describe, expect, it, vi } from "vitest"; const uploadGoogleChatAttachmentMock = vi.hoisted(() => vi.fn()); diff --git a/extensions/googlechat/src/channel.startup.test.ts b/extensions/googlechat/src/channel.startup.test.ts index 421aa474328..521cbb94c5f 100644 --- a/extensions/googlechat/src/channel.startup.test.ts +++ b/extensions/googlechat/src/channel.startup.test.ts @@ -1,4 +1,4 @@ -import type { ChannelAccountSnapshot } from "openclaw/plugin-sdk/compat"; +import type { ChannelAccountSnapshot } from "openclaw/plugin-sdk/googlechat"; import { afterEach, describe, expect, it, vi } from "vitest"; import { createStartAccountContext } from "../../test-utils/start-account-context.js"; import type { ResolvedGoogleChatAccount } from "./accounts.js"; diff --git a/extensions/googlechat/src/channel.ts b/extensions/googlechat/src/channel.ts index bfb2aabe356..6dd896e9f00 100644 --- a/extensions/googlechat/src/channel.ts +++ b/extensions/googlechat/src/channel.ts @@ -19,8 +19,8 @@ import { type ChannelPlugin, type ChannelStatusIssue, type OpenClawConfig, -} from "openclaw/plugin-sdk/compat"; -import { GoogleChatConfigSchema } from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/googlechat"; +import { GoogleChatConfigSchema } from "openclaw/plugin-sdk/googlechat"; import { listGoogleChatAccountIds, resolveDefaultGoogleChatAccountId, diff --git a/extensions/googlechat/src/monitor-access.ts b/extensions/googlechat/src/monitor-access.ts index 58c13ab9b9c..daecea59f8a 100644 --- a/extensions/googlechat/src/monitor-access.ts +++ b/extensions/googlechat/src/monitor-access.ts @@ -7,8 +7,8 @@ import { resolveDmGroupAccessWithLists, resolveMentionGatingWithBypass, warnMissingProviderGroupPolicyFallbackOnce, -} from "openclaw/plugin-sdk/compat"; -import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/googlechat"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/googlechat"; import type { ResolvedGoogleChatAccount } from "./accounts.js"; import { sendGoogleChatMessage } from "./api.js"; import type { GoogleChatCoreRuntime } from "./monitor-types.js"; diff --git a/extensions/googlechat/src/monitor-types.ts b/extensions/googlechat/src/monitor-types.ts index 5cc43d2eedf..792eb66bccb 100644 --- a/extensions/googlechat/src/monitor-types.ts +++ b/extensions/googlechat/src/monitor-types.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/googlechat"; import type { ResolvedGoogleChatAccount } from "./accounts.js"; import type { GoogleChatAudienceType } from "./auth.js"; import { getGoogleChatRuntime } from "./runtime.js"; diff --git a/extensions/googlechat/src/monitor-webhook.ts b/extensions/googlechat/src/monitor-webhook.ts index e7ba9107e0f..4272b2bfa87 100644 --- a/extensions/googlechat/src/monitor-webhook.ts +++ b/extensions/googlechat/src/monitor-webhook.ts @@ -5,7 +5,7 @@ import { resolveWebhookTargetWithAuthOrReject, resolveWebhookTargets, type WebhookInFlightLimiter, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/googlechat"; import { verifyGoogleChatRequest } from "./auth.js"; import type { WebhookTarget } from "./monitor-types.js"; import type { diff --git a/extensions/googlechat/src/monitor.ts b/extensions/googlechat/src/monitor.ts index 138de4b1882..ad89a9c74eb 100644 --- a/extensions/googlechat/src/monitor.ts +++ b/extensions/googlechat/src/monitor.ts @@ -1,12 +1,12 @@ import type { IncomingMessage, ServerResponse } from "node:http"; -import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/googlechat"; import { createWebhookInFlightLimiter, createReplyPrefixOptions, registerWebhookTargetWithPluginRoute, resolveInboundRouteEnvelopeBuilderWithRuntime, resolveWebhookPath, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/googlechat"; import { type ResolvedGoogleChatAccount } from "./accounts.js"; import { downloadGoogleChatMedia, diff --git a/extensions/googlechat/src/monitor.webhook-routing.test.ts b/extensions/googlechat/src/monitor.webhook-routing.test.ts index d35077d2167..812883f1b4c 100644 --- a/extensions/googlechat/src/monitor.webhook-routing.test.ts +++ b/extensions/googlechat/src/monitor.webhook-routing.test.ts @@ -1,6 +1,6 @@ import { EventEmitter } from "node:events"; import type { IncomingMessage } from "node:http"; -import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk/compat"; +import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk/googlechat"; import { afterEach, describe, expect, it, vi } from "vitest"; import { createEmptyPluginRegistry } from "../../../src/plugins/registry.js"; import { setActivePluginRegistry } from "../../../src/plugins/runtime.js"; diff --git a/extensions/googlechat/src/onboarding.ts b/extensions/googlechat/src/onboarding.ts index da0c647698b..9c0aac823b9 100644 --- a/extensions/googlechat/src/onboarding.ts +++ b/extensions/googlechat/src/onboarding.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig, DmPolicy } from "openclaw/plugin-sdk/compat"; +import type { OpenClawConfig, DmPolicy } from "openclaw/plugin-sdk/googlechat"; import { addWildcardAllowFrom, formatDocsLink, @@ -10,7 +10,7 @@ import { DEFAULT_ACCOUNT_ID, normalizeAccountId, migrateBaseNameToDefaultAccount, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/googlechat"; import { listGoogleChatAccountIds, resolveDefaultGoogleChatAccountId, diff --git a/extensions/googlechat/src/resolve-target.test.ts b/extensions/googlechat/src/resolve-target.test.ts index 82e340874df..2f898c48b8c 100644 --- a/extensions/googlechat/src/resolve-target.test.ts +++ b/extensions/googlechat/src/resolve-target.test.ts @@ -6,7 +6,7 @@ const runtimeMocks = vi.hoisted(() => ({ fetchRemoteMedia: vi.fn(), })); -vi.mock("openclaw/plugin-sdk", () => ({ +vi.mock("openclaw/plugin-sdk/googlechat", () => ({ getChatChannelMeta: () => ({ id: "googlechat", label: "Google Chat" }), missingTargetError: (provider: string, hint: string) => new Error(`Delivering to ${provider} requires target ${hint}`), @@ -72,7 +72,7 @@ vi.mock("./targets.js", () => ({ resolveGoogleChatOutboundSpace: vi.fn(), })); -import { resolveChannelMediaMaxBytes } from "openclaw/plugin-sdk"; +import { resolveChannelMediaMaxBytes } from "openclaw/plugin-sdk/googlechat"; import { resolveGoogleChatAccount } from "./accounts.js"; import { sendGoogleChatMessage, uploadGoogleChatAttachment } from "./api.js"; import { googlechatPlugin } from "./channel.js"; diff --git a/extensions/googlechat/src/runtime.ts b/extensions/googlechat/src/runtime.ts index 67a21a21e9c..55af03db04d 100644 --- a/extensions/googlechat/src/runtime.ts +++ b/extensions/googlechat/src/runtime.ts @@ -1,4 +1,4 @@ -import type { PluginRuntime } from "openclaw/plugin-sdk/compat"; +import type { PluginRuntime } from "openclaw/plugin-sdk/googlechat"; let runtime: PluginRuntime | null = null; diff --git a/extensions/googlechat/src/types.config.ts b/extensions/googlechat/src/types.config.ts index 09062a2661d..cbc1034ae3e 100644 --- a/extensions/googlechat/src/types.config.ts +++ b/extensions/googlechat/src/types.config.ts @@ -1,3 +1,3 @@ -import type { GoogleChatAccountConfig, GoogleChatConfig } from "openclaw/plugin-sdk/compat"; +import type { GoogleChatAccountConfig, GoogleChatConfig } from "openclaw/plugin-sdk/googlechat"; export type { GoogleChatAccountConfig, GoogleChatConfig }; From 7b8e36583fc7ffc54da90d398887d7392ea6b8c7 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 4 Mar 2026 02:33:00 -0500 Subject: [PATCH 41/74] Plugins/irc: migrate to scoped plugin-sdk imports --- extensions/irc/index.ts | 4 ++-- extensions/irc/src/accounts.ts | 2 +- extensions/irc/src/channel.ts | 2 +- extensions/irc/src/config-schema.ts | 2 +- extensions/irc/src/inbound.ts | 2 +- extensions/irc/src/monitor.ts | 2 +- extensions/irc/src/onboarding.test.ts | 2 +- extensions/irc/src/onboarding.ts | 2 +- extensions/irc/src/runtime.ts | 2 +- extensions/irc/src/types.ts | 4 ++-- 10 files changed, 12 insertions(+), 12 deletions(-) diff --git a/extensions/irc/index.ts b/extensions/irc/index.ts index 6c5e19f16e3..40182558dcb 100644 --- a/extensions/irc/index.ts +++ b/extensions/irc/index.ts @@ -1,5 +1,5 @@ -import type { ChannelPlugin, OpenClawPluginApi } from "openclaw/plugin-sdk/core"; -import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/core"; +import type { ChannelPlugin, OpenClawPluginApi } from "openclaw/plugin-sdk/irc"; +import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/irc"; import { ircPlugin } from "./src/channel.js"; import { setIrcRuntime } from "./src/runtime.js"; diff --git a/extensions/irc/src/accounts.ts b/extensions/irc/src/accounts.ts index 78f15bbc6ec..3f9640925c8 100644 --- a/extensions/irc/src/accounts.ts +++ b/extensions/irc/src/accounts.ts @@ -4,7 +4,7 @@ import { normalizeAccountId, normalizeOptionalAccountId, } from "openclaw/plugin-sdk/account-id"; -import { normalizeResolvedSecretInputString } from "openclaw/plugin-sdk/compat"; +import { normalizeResolvedSecretInputString } from "openclaw/plugin-sdk/irc"; import type { CoreConfig, IrcAccountConfig, IrcNickServConfig } from "./types.js"; const TRUTHY_ENV = new Set(["true", "1", "yes", "on"]); diff --git a/extensions/irc/src/channel.ts b/extensions/irc/src/channel.ts index c29186cb700..a41a46f3db0 100644 --- a/extensions/irc/src/channel.ts +++ b/extensions/irc/src/channel.ts @@ -11,7 +11,7 @@ import { resolveDefaultGroupPolicy, setAccountEnabledInConfigSection, type ChannelPlugin, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/irc"; import { listIrcAccountIds, resolveDefaultIrcAccountId, diff --git a/extensions/irc/src/config-schema.ts b/extensions/irc/src/config-schema.ts index 373e3c79ba4..aa37b596cd1 100644 --- a/extensions/irc/src/config-schema.ts +++ b/extensions/irc/src/config-schema.ts @@ -7,7 +7,7 @@ import { ReplyRuntimeConfigSchemaShape, ToolPolicySchema, requireOpenAllowFrom, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/irc"; import { z } from "zod"; const IrcGroupSchema = z diff --git a/extensions/irc/src/inbound.ts b/extensions/irc/src/inbound.ts index b0139c853c7..2c3378de1c1 100644 --- a/extensions/irc/src/inbound.ts +++ b/extensions/irc/src/inbound.ts @@ -16,7 +16,7 @@ import { type OutboundReplyPayload, type OpenClawConfig, type RuntimeEnv, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/irc"; import type { ResolvedIrcAccount } from "./accounts.js"; import { normalizeIrcAllowlist, resolveIrcAllowlistMatch } from "./normalize.js"; import { diff --git a/extensions/irc/src/monitor.ts b/extensions/irc/src/monitor.ts index 8ebd6766ed3..e416d95f8eb 100644 --- a/extensions/irc/src/monitor.ts +++ b/extensions/irc/src/monitor.ts @@ -1,4 +1,4 @@ -import { createLoggerBackedRuntime, type RuntimeEnv } from "openclaw/plugin-sdk/compat"; +import { createLoggerBackedRuntime, type RuntimeEnv } from "openclaw/plugin-sdk/irc"; import { resolveIrcAccount } from "./accounts.js"; import { connectIrcClient, type IrcClient } from "./client.js"; import { buildIrcConnectOptions } from "./connect-options.js"; diff --git a/extensions/irc/src/onboarding.test.ts b/extensions/irc/src/onboarding.test.ts index d597ccdb9e9..21f3e978c1a 100644 --- a/extensions/irc/src/onboarding.test.ts +++ b/extensions/irc/src/onboarding.test.ts @@ -1,4 +1,4 @@ -import type { RuntimeEnv, WizardPrompter } from "openclaw/plugin-sdk/compat"; +import type { RuntimeEnv, WizardPrompter } from "openclaw/plugin-sdk/irc"; import { describe, expect, it, vi } from "vitest"; import { ircOnboardingAdapter } from "./onboarding.js"; import type { CoreConfig } from "./types.js"; diff --git a/extensions/irc/src/onboarding.ts b/extensions/irc/src/onboarding.ts index 6e6cf707fc0..4a3ea982bd5 100644 --- a/extensions/irc/src/onboarding.ts +++ b/extensions/irc/src/onboarding.ts @@ -8,7 +8,7 @@ import { type ChannelOnboardingDmPolicy, type DmPolicy, type WizardPrompter, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/irc"; import { listIrcAccountIds, resolveDefaultIrcAccountId, resolveIrcAccount } from "./accounts.js"; import { isChannelTarget, diff --git a/extensions/irc/src/runtime.ts b/extensions/irc/src/runtime.ts index 305a0b8bdf4..51fcdd7c454 100644 --- a/extensions/irc/src/runtime.ts +++ b/extensions/irc/src/runtime.ts @@ -1,4 +1,4 @@ -import type { PluginRuntime } from "openclaw/plugin-sdk/compat"; +import type { PluginRuntime } from "openclaw/plugin-sdk/irc"; let runtime: PluginRuntime | null = null; diff --git a/extensions/irc/src/types.ts b/extensions/irc/src/types.ts index 4add1357bce..42a3cafc237 100644 --- a/extensions/irc/src/types.ts +++ b/extensions/irc/src/types.ts @@ -1,4 +1,4 @@ -import type { BaseProbeResult } from "openclaw/plugin-sdk/compat"; +import type { BaseProbeResult } from "openclaw/plugin-sdk/irc"; import type { BlockStreamingCoalesceConfig, DmConfig, @@ -8,7 +8,7 @@ import type { GroupToolPolicyConfig, MarkdownConfig, OpenClawConfig, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/irc"; export type IrcChannelConfig = { requireMention?: boolean; From ccd2d7dc2795a86952ef6696496f62cd66091e2c Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 4 Mar 2026 02:33:01 -0500 Subject: [PATCH 42/74] Plugins/llm-task: migrate to scoped plugin-sdk imports --- extensions/llm-task/index.ts | 2 +- extensions/llm-task/src/llm-task-tool.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/extensions/llm-task/index.ts b/extensions/llm-task/index.ts index 27bc98dcb7b..7d258ab6a39 100644 --- a/extensions/llm-task/index.ts +++ b/extensions/llm-task/index.ts @@ -1,4 +1,4 @@ -import type { AnyAgentTool, OpenClawPluginApi } from "../../src/plugins/types.js"; +import type { AnyAgentTool, OpenClawPluginApi } from "openclaw/plugin-sdk/llm-task"; import { createLlmTaskTool } from "./src/llm-task-tool.js"; export default function register(api: OpenClawPluginApi) { diff --git a/extensions/llm-task/src/llm-task-tool.ts b/extensions/llm-task/src/llm-task-tool.ts index 34e7607c378..cf0c0250d0a 100644 --- a/extensions/llm-task/src/llm-task-tool.ts +++ b/extensions/llm-task/src/llm-task-tool.ts @@ -2,12 +2,12 @@ import fs from "node:fs/promises"; import path from "node:path"; import { Type } from "@sinclair/typebox"; import Ajv from "ajv"; -import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/compat"; +import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/llm-task"; // NOTE: This extension is intended to be bundled with OpenClaw. // When running from source (tests/dev), OpenClaw internals live under src/. // When running from a built install, internals live under dist/ (no src/ tree). // So we resolve internal imports dynamically with src-first, dist-fallback. -import type { OpenClawPluginApi } from "../../../src/plugins/types.js"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/llm-task"; type RunEmbeddedPiAgentFn = (params: Record) => Promise; From a5f56e8b4e7844c4c0ceee1c845d08881ec8ad7c Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 4 Mar 2026 02:33:02 -0500 Subject: [PATCH 43/74] Plugins/lobster: migrate to scoped plugin-sdk imports --- extensions/lobster/index.ts | 2 +- extensions/lobster/src/lobster-tool.test.ts | 2 +- extensions/lobster/src/lobster-tool.ts | 2 +- extensions/lobster/src/windows-spawn.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/extensions/lobster/index.ts b/extensions/lobster/index.ts index b0e8f3a00d8..1d5775c4d74 100644 --- a/extensions/lobster/index.ts +++ b/extensions/lobster/index.ts @@ -2,7 +2,7 @@ import type { AnyAgentTool, OpenClawPluginApi, OpenClawPluginToolFactory, -} from "../../src/plugins/types.js"; +} from "openclaw/plugin-sdk/lobster"; import { createLobsterTool } from "./src/lobster-tool.js"; export default function register(api: OpenClawPluginApi) { diff --git a/extensions/lobster/src/lobster-tool.test.ts b/extensions/lobster/src/lobster-tool.test.ts index d318e2dda8e..970c2ad4fd1 100644 --- a/extensions/lobster/src/lobster-tool.test.ts +++ b/extensions/lobster/src/lobster-tool.test.ts @@ -3,8 +3,8 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { PassThrough } from "node:stream"; +import type { OpenClawPluginApi, OpenClawPluginToolContext } from "openclaw/plugin-sdk/lobster"; import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import type { OpenClawPluginApi, OpenClawPluginToolContext } from "../../../src/plugins/types.js"; import { createWindowsCmdShimFixture, restorePlatformPathEnv, diff --git a/extensions/lobster/src/lobster-tool.ts b/extensions/lobster/src/lobster-tool.ts index e4402861ef5..96276bb9d69 100644 --- a/extensions/lobster/src/lobster-tool.ts +++ b/extensions/lobster/src/lobster-tool.ts @@ -1,7 +1,7 @@ import { spawn } from "node:child_process"; import path from "node:path"; import { Type } from "@sinclair/typebox"; -import type { OpenClawPluginApi } from "../../../src/plugins/types.js"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/lobster"; import { resolveWindowsLobsterSpawn } from "./windows-spawn.js"; type LobsterEnvelope = diff --git a/extensions/lobster/src/windows-spawn.ts b/extensions/lobster/src/windows-spawn.ts index a28252a6536..7c35deab2a7 100644 --- a/extensions/lobster/src/windows-spawn.ts +++ b/extensions/lobster/src/windows-spawn.ts @@ -2,7 +2,7 @@ import { applyWindowsSpawnProgramPolicy, materializeWindowsSpawnProgram, resolveWindowsSpawnProgramCandidate, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/lobster"; type SpawnTarget = { command: string; From b69b2a7ae0c40157ba6a31c77317e42f98a6d8ac Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 4 Mar 2026 02:33:03 -0500 Subject: [PATCH 44/74] Plugins/matrix: migrate to scoped plugin-sdk imports --- extensions/matrix/index.ts | 4 ++-- extensions/matrix/src/actions.ts | 2 +- extensions/matrix/src/channel.directory.test.ts | 2 +- extensions/matrix/src/channel.ts | 2 +- extensions/matrix/src/config-schema.ts | 2 +- extensions/matrix/src/directory-live.ts | 2 +- extensions/matrix/src/group-mentions.ts | 2 +- extensions/matrix/src/matrix/client/config.ts | 2 +- extensions/matrix/src/matrix/deps.ts | 2 +- extensions/matrix/src/matrix/monitor/access-policy.ts | 2 +- extensions/matrix/src/matrix/monitor/allowlist.ts | 2 +- extensions/matrix/src/matrix/monitor/auto-join.ts | 2 +- extensions/matrix/src/matrix/monitor/events.test.ts | 2 +- extensions/matrix/src/matrix/monitor/events.ts | 2 +- .../matrix/src/matrix/monitor/handler.body-for-agent.test.ts | 2 +- extensions/matrix/src/matrix/monitor/handler.ts | 2 +- extensions/matrix/src/matrix/monitor/index.ts | 2 +- extensions/matrix/src/matrix/monitor/location.ts | 2 +- extensions/matrix/src/matrix/monitor/media.test.ts | 2 +- extensions/matrix/src/matrix/monitor/replies.test.ts | 2 +- extensions/matrix/src/matrix/monitor/replies.ts | 2 +- extensions/matrix/src/matrix/monitor/rooms.ts | 2 +- extensions/matrix/src/matrix/poll-types.ts | 2 +- extensions/matrix/src/matrix/probe.ts | 2 +- extensions/matrix/src/matrix/send.test.ts | 2 +- extensions/matrix/src/matrix/send.ts | 2 +- extensions/matrix/src/onboarding.ts | 4 ++-- extensions/matrix/src/outbound.test.ts | 2 +- extensions/matrix/src/outbound.ts | 2 +- extensions/matrix/src/resolve-targets.test.ts | 2 +- extensions/matrix/src/resolve-targets.ts | 2 +- extensions/matrix/src/runtime.ts | 2 +- extensions/matrix/src/secret-input.ts | 2 +- extensions/matrix/src/tool-actions.ts | 2 +- extensions/matrix/src/types.ts | 2 +- 35 files changed, 37 insertions(+), 37 deletions(-) diff --git a/extensions/matrix/index.ts b/extensions/matrix/index.ts index 320b256d3a2..9e4863a1ed8 100644 --- a/extensions/matrix/index.ts +++ b/extensions/matrix/index.ts @@ -1,5 +1,5 @@ -import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core"; -import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/core"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/matrix"; +import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/matrix"; import { matrixPlugin } from "./src/channel.js"; import { ensureMatrixCryptoRuntime } from "./src/matrix/deps.js"; import { setMatrixRuntime } from "./src/runtime.js"; diff --git a/extensions/matrix/src/actions.ts b/extensions/matrix/src/actions.ts index a2b9e9560fe..9e7e0a0653e 100644 --- a/extensions/matrix/src/actions.ts +++ b/extensions/matrix/src/actions.ts @@ -6,7 +6,7 @@ import { type ChannelMessageActionContext, type ChannelMessageActionName, type ChannelToolSend, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/matrix"; import { resolveMatrixAccount } from "./matrix/accounts.js"; import { handleMatrixAction } from "./tool-actions.js"; import type { CoreConfig } from "./types.js"; diff --git a/extensions/matrix/src/channel.directory.test.ts b/extensions/matrix/src/channel.directory.test.ts index f790152d761..51c781c0b75 100644 --- a/extensions/matrix/src/channel.directory.test.ts +++ b/extensions/matrix/src/channel.directory.test.ts @@ -1,4 +1,4 @@ -import type { PluginRuntime, RuntimeEnv } from "openclaw/plugin-sdk/compat"; +import type { PluginRuntime, RuntimeEnv } from "openclaw/plugin-sdk/matrix"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { matrixPlugin } from "./channel.js"; import { setMatrixRuntime } from "./runtime.js"; diff --git a/extensions/matrix/src/channel.ts b/extensions/matrix/src/channel.ts index 73569223eac..3ccfd2a8ae4 100644 --- a/extensions/matrix/src/channel.ts +++ b/extensions/matrix/src/channel.ts @@ -11,7 +11,7 @@ import { resolveDefaultGroupPolicy, setAccountEnabledInConfigSection, type ChannelPlugin, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/matrix"; import { matrixMessageActions } from "./actions.js"; import { MatrixConfigSchema } from "./config-schema.js"; import { listMatrixDirectoryGroupsLive, listMatrixDirectoryPeersLive } from "./directory-live.js"; diff --git a/extensions/matrix/src/config-schema.ts b/extensions/matrix/src/config-schema.ts index 88f18d4e0fb..cd1c89fbdb6 100644 --- a/extensions/matrix/src/config-schema.ts +++ b/extensions/matrix/src/config-schema.ts @@ -1,4 +1,4 @@ -import { MarkdownConfigSchema, ToolPolicySchema } from "openclaw/plugin-sdk/compat"; +import { MarkdownConfigSchema, ToolPolicySchema } from "openclaw/plugin-sdk/matrix"; import { z } from "zod"; import { buildSecretInputSchema } from "./secret-input.js"; diff --git a/extensions/matrix/src/directory-live.ts b/extensions/matrix/src/directory-live.ts index 59afbfb0a7e..b915915fdcd 100644 --- a/extensions/matrix/src/directory-live.ts +++ b/extensions/matrix/src/directory-live.ts @@ -1,4 +1,4 @@ -import type { ChannelDirectoryEntry } from "openclaw/plugin-sdk/compat"; +import type { ChannelDirectoryEntry } from "openclaw/plugin-sdk/matrix"; import { resolveMatrixAuth } from "./matrix/client.js"; type MatrixUserResult = { diff --git a/extensions/matrix/src/group-mentions.ts b/extensions/matrix/src/group-mentions.ts index 18b3da2fd1b..71b49f59b20 100644 --- a/extensions/matrix/src/group-mentions.ts +++ b/extensions/matrix/src/group-mentions.ts @@ -1,4 +1,4 @@ -import type { ChannelGroupContext, GroupToolPolicyConfig } from "openclaw/plugin-sdk/compat"; +import type { ChannelGroupContext, GroupToolPolicyConfig } from "openclaw/plugin-sdk/matrix"; import { resolveMatrixAccountConfig } from "./matrix/accounts.js"; import { resolveMatrixRoomConfig } from "./matrix/monitor/rooms.js"; import type { CoreConfig } from "./types.js"; diff --git a/extensions/matrix/src/matrix/client/config.ts b/extensions/matrix/src/matrix/client/config.ts index 7d3a0812d04..2867af33f03 100644 --- a/extensions/matrix/src/matrix/client/config.ts +++ b/extensions/matrix/src/matrix/client/config.ts @@ -1,5 +1,5 @@ import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id"; -import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/compat"; +import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/matrix"; import { getMatrixRuntime } from "../../runtime.js"; import { normalizeResolvedSecretInputString, diff --git a/extensions/matrix/src/matrix/deps.ts b/extensions/matrix/src/matrix/deps.ts index 27bdd3e03af..25c0ead4c48 100644 --- a/extensions/matrix/src/matrix/deps.ts +++ b/extensions/matrix/src/matrix/deps.ts @@ -2,7 +2,7 @@ import fs from "node:fs"; import { createRequire } from "node:module"; import path from "node:path"; import { fileURLToPath } from "node:url"; -import { runPluginCommandWithTimeout, type RuntimeEnv } from "openclaw/plugin-sdk/compat"; +import { runPluginCommandWithTimeout, type RuntimeEnv } from "openclaw/plugin-sdk/matrix"; const MATRIX_SDK_PACKAGE = "@vector-im/matrix-bot-sdk"; const MATRIX_CRYPTO_DOWNLOAD_HELPER = "@matrix-org/matrix-sdk-crypto-nodejs/download-lib.js"; diff --git a/extensions/matrix/src/matrix/monitor/access-policy.ts b/extensions/matrix/src/matrix/monitor/access-policy.ts index 77cc1c86d05..272bc15f0a4 100644 --- a/extensions/matrix/src/matrix/monitor/access-policy.ts +++ b/extensions/matrix/src/matrix/monitor/access-policy.ts @@ -3,7 +3,7 @@ import { issuePairingChallenge, readStoreAllowFromForDmPolicy, resolveDmGroupAccessWithLists, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/matrix"; import { normalizeMatrixAllowList, resolveMatrixAllowListMatch, diff --git a/extensions/matrix/src/matrix/monitor/allowlist.ts b/extensions/matrix/src/matrix/monitor/allowlist.ts index 76e193c1b6a..1a38866b059 100644 --- a/extensions/matrix/src/matrix/monitor/allowlist.ts +++ b/extensions/matrix/src/matrix/monitor/allowlist.ts @@ -1,4 +1,4 @@ -import { resolveAllowlistMatchByCandidates, type AllowlistMatch } from "openclaw/plugin-sdk/compat"; +import { resolveAllowlistMatchByCandidates, type AllowlistMatch } from "openclaw/plugin-sdk/matrix"; function normalizeAllowList(list?: Array) { return (list ?? []).map((entry) => String(entry).trim()).filter(Boolean); diff --git a/extensions/matrix/src/matrix/monitor/auto-join.ts b/extensions/matrix/src/matrix/monitor/auto-join.ts index ee5f3a5b95d..221e1df504a 100644 --- a/extensions/matrix/src/matrix/monitor/auto-join.ts +++ b/extensions/matrix/src/matrix/monitor/auto-join.ts @@ -1,5 +1,5 @@ import type { MatrixClient } from "@vector-im/matrix-bot-sdk"; -import type { RuntimeEnv } from "openclaw/plugin-sdk/compat"; +import type { RuntimeEnv } from "openclaw/plugin-sdk/matrix"; import { getMatrixRuntime } from "../../runtime.js"; import type { CoreConfig } from "../../types.js"; import { loadMatrixSdk } from "../sdk-runtime.js"; diff --git a/extensions/matrix/src/matrix/monitor/events.test.ts b/extensions/matrix/src/matrix/monitor/events.test.ts index 4f28a8a42e9..9179cf69ee3 100644 --- a/extensions/matrix/src/matrix/monitor/events.test.ts +++ b/extensions/matrix/src/matrix/monitor/events.test.ts @@ -1,5 +1,5 @@ import type { MatrixClient } from "@vector-im/matrix-bot-sdk"; -import type { PluginRuntime, RuntimeLogger } from "openclaw/plugin-sdk/compat"; +import type { PluginRuntime, RuntimeLogger } from "openclaw/plugin-sdk/matrix"; import { beforeEach, describe, expect, it, vi } from "vitest"; import type { MatrixAuth } from "../client.js"; import { registerMatrixMonitorEvents } from "./events.js"; diff --git a/extensions/matrix/src/matrix/monitor/events.ts b/extensions/matrix/src/matrix/monitor/events.ts index 3e4e40e8637..edc9e2f5edd 100644 --- a/extensions/matrix/src/matrix/monitor/events.ts +++ b/extensions/matrix/src/matrix/monitor/events.ts @@ -1,5 +1,5 @@ import type { MatrixClient } from "@vector-im/matrix-bot-sdk"; -import type { PluginRuntime, RuntimeLogger } from "openclaw/plugin-sdk/compat"; +import type { PluginRuntime, RuntimeLogger } from "openclaw/plugin-sdk/matrix"; import type { MatrixAuth } from "../client.js"; import { sendReadReceiptMatrix } from "../send.js"; import type { MatrixRawEvent } from "./types.js"; diff --git a/extensions/matrix/src/matrix/monitor/handler.body-for-agent.test.ts b/extensions/matrix/src/matrix/monitor/handler.body-for-agent.test.ts index cfd2c314b91..83cab3b4780 100644 --- a/extensions/matrix/src/matrix/monitor/handler.body-for-agent.test.ts +++ b/extensions/matrix/src/matrix/monitor/handler.body-for-agent.test.ts @@ -1,5 +1,5 @@ import type { MatrixClient } from "@vector-im/matrix-bot-sdk"; -import type { PluginRuntime, RuntimeEnv, RuntimeLogger } from "openclaw/plugin-sdk/compat"; +import type { PluginRuntime, RuntimeEnv, RuntimeLogger } from "openclaw/plugin-sdk/matrix"; import { describe, expect, it, vi } from "vitest"; import { createMatrixRoomMessageHandler } from "./handler.js"; import { EventType, type MatrixRawEvent } from "./types.js"; diff --git a/extensions/matrix/src/matrix/monitor/handler.ts b/extensions/matrix/src/matrix/monitor/handler.ts index 5fe935821e3..53651ce4b16 100644 --- a/extensions/matrix/src/matrix/monitor/handler.ts +++ b/extensions/matrix/src/matrix/monitor/handler.ts @@ -11,7 +11,7 @@ import { type PluginRuntime, type RuntimeEnv, type RuntimeLogger, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/matrix"; import type { CoreConfig, MatrixRoomConfig, ReplyToMode } from "../../types.js"; import { fetchEventSummary } from "../actions/summary.js"; import { diff --git a/extensions/matrix/src/matrix/monitor/index.ts b/extensions/matrix/src/matrix/monitor/index.ts index c9e5f835907..2449b215715 100644 --- a/extensions/matrix/src/matrix/monitor/index.ts +++ b/extensions/matrix/src/matrix/monitor/index.ts @@ -7,7 +7,7 @@ import { summarizeMapping, warnMissingProviderGroupPolicyFallbackOnce, type RuntimeEnv, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/matrix"; import { resolveMatrixTargets } from "../../resolve-targets.js"; import { getMatrixRuntime } from "../../runtime.js"; import type { CoreConfig, MatrixConfig, MatrixRoomConfig, ReplyToMode } from "../../types.js"; diff --git a/extensions/matrix/src/matrix/monitor/location.ts b/extensions/matrix/src/matrix/monitor/location.ts index 5f999ce121d..ff80ea82b5a 100644 --- a/extensions/matrix/src/matrix/monitor/location.ts +++ b/extensions/matrix/src/matrix/monitor/location.ts @@ -3,7 +3,7 @@ import { formatLocationText, toLocationContext, type NormalizedLocation, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/matrix"; import { EventType } from "./types.js"; export type MatrixLocationPayload = { diff --git a/extensions/matrix/src/matrix/monitor/media.test.ts b/extensions/matrix/src/matrix/monitor/media.test.ts index 3f8fbc1d2d3..a3803108af2 100644 --- a/extensions/matrix/src/matrix/monitor/media.test.ts +++ b/extensions/matrix/src/matrix/monitor/media.test.ts @@ -1,4 +1,4 @@ -import type { PluginRuntime } from "openclaw/plugin-sdk/compat"; +import type { PluginRuntime } from "openclaw/plugin-sdk/matrix"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { setMatrixRuntime } from "../../runtime.js"; import { downloadMatrixMedia } from "./media.js"; diff --git a/extensions/matrix/src/matrix/monitor/replies.test.ts b/extensions/matrix/src/matrix/monitor/replies.test.ts index 56c35623db4..838f955abdf 100644 --- a/extensions/matrix/src/matrix/monitor/replies.test.ts +++ b/extensions/matrix/src/matrix/monitor/replies.test.ts @@ -1,5 +1,5 @@ import type { MatrixClient } from "@vector-im/matrix-bot-sdk"; -import type { PluginRuntime, RuntimeEnv } from "openclaw/plugin-sdk/compat"; +import type { PluginRuntime, RuntimeEnv } from "openclaw/plugin-sdk/matrix"; import { beforeEach, describe, expect, it, vi } from "vitest"; const sendMessageMatrixMock = vi.hoisted(() => vi.fn().mockResolvedValue({ messageId: "mx-1" })); diff --git a/extensions/matrix/src/matrix/monitor/replies.ts b/extensions/matrix/src/matrix/monitor/replies.ts index bef1757cf04..5f501139dfa 100644 --- a/extensions/matrix/src/matrix/monitor/replies.ts +++ b/extensions/matrix/src/matrix/monitor/replies.ts @@ -1,5 +1,5 @@ import type { MatrixClient } from "@vector-im/matrix-bot-sdk"; -import type { MarkdownTableMode, ReplyPayload, RuntimeEnv } from "openclaw/plugin-sdk/compat"; +import type { MarkdownTableMode, ReplyPayload, RuntimeEnv } from "openclaw/plugin-sdk/matrix"; import { getMatrixRuntime } from "../../runtime.js"; import { sendMessageMatrix } from "../send.js"; diff --git a/extensions/matrix/src/matrix/monitor/rooms.ts b/extensions/matrix/src/matrix/monitor/rooms.ts index 30e813c6f49..215a3f3811e 100644 --- a/extensions/matrix/src/matrix/monitor/rooms.ts +++ b/extensions/matrix/src/matrix/monitor/rooms.ts @@ -1,4 +1,4 @@ -import { buildChannelKeyCandidates, resolveChannelEntryMatch } from "openclaw/plugin-sdk/compat"; +import { buildChannelKeyCandidates, resolveChannelEntryMatch } from "openclaw/plugin-sdk/matrix"; import type { MatrixRoomConfig } from "../../types.js"; export type MatrixRoomConfigResolved = { diff --git a/extensions/matrix/src/matrix/poll-types.ts b/extensions/matrix/src/matrix/poll-types.ts index 2bf1fb87f7b..068b5fafd99 100644 --- a/extensions/matrix/src/matrix/poll-types.ts +++ b/extensions/matrix/src/matrix/poll-types.ts @@ -7,7 +7,7 @@ * - m.poll.end - Closes a poll */ -import type { PollInput } from "openclaw/plugin-sdk/compat"; +import type { PollInput } from "openclaw/plugin-sdk/matrix"; export const M_POLL_START = "m.poll.start" as const; export const M_POLL_RESPONSE = "m.poll.response" as const; diff --git a/extensions/matrix/src/matrix/probe.ts b/extensions/matrix/src/matrix/probe.ts index 42d2273b8fe..2919d9d9c2f 100644 --- a/extensions/matrix/src/matrix/probe.ts +++ b/extensions/matrix/src/matrix/probe.ts @@ -1,4 +1,4 @@ -import type { BaseProbeResult } from "openclaw/plugin-sdk/compat"; +import type { BaseProbeResult } from "openclaw/plugin-sdk/matrix"; import { createMatrixClient, isBunRuntime } from "./client.js"; export type MatrixProbe = BaseProbeResult & { diff --git a/extensions/matrix/src/matrix/send.test.ts b/extensions/matrix/src/matrix/send.test.ts index 74af672c9d2..dabe915b388 100644 --- a/extensions/matrix/src/matrix/send.test.ts +++ b/extensions/matrix/src/matrix/send.test.ts @@ -1,4 +1,4 @@ -import type { PluginRuntime } from "openclaw/plugin-sdk/compat"; +import type { PluginRuntime } from "openclaw/plugin-sdk/matrix"; import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { setMatrixRuntime } from "../runtime.js"; diff --git a/extensions/matrix/src/matrix/send.ts b/extensions/matrix/src/matrix/send.ts index 31cc2bdab52..86c703b93de 100644 --- a/extensions/matrix/src/matrix/send.ts +++ b/extensions/matrix/src/matrix/send.ts @@ -1,5 +1,5 @@ import type { MatrixClient } from "@vector-im/matrix-bot-sdk"; -import type { PollInput } from "openclaw/plugin-sdk/compat"; +import type { PollInput } from "openclaw/plugin-sdk/matrix"; import { getMatrixRuntime } from "../runtime.js"; import { buildPollStartContent, M_POLL_START } from "./poll-types.js"; import { enqueueSend } from "./send-queue.js"; diff --git a/extensions/matrix/src/onboarding.ts b/extensions/matrix/src/onboarding.ts index a55cc5676f5..44d2ca00604 100644 --- a/extensions/matrix/src/onboarding.ts +++ b/extensions/matrix/src/onboarding.ts @@ -1,4 +1,4 @@ -import type { DmPolicy } from "openclaw/plugin-sdk/compat"; +import type { DmPolicy } from "openclaw/plugin-sdk/matrix"; import { addWildcardAllowFrom, formatResolvedUnresolvedNote, @@ -11,7 +11,7 @@ import { type ChannelOnboardingAdapter, type ChannelOnboardingDmPolicy, type WizardPrompter, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/matrix"; import { listMatrixDirectoryGroupsLive } from "./directory-live.js"; import { resolveMatrixAccount } from "./matrix/accounts.js"; import { ensureMatrixSdkInstalled, isMatrixSdkAvailable } from "./matrix/deps.js"; diff --git a/extensions/matrix/src/outbound.test.ts b/extensions/matrix/src/outbound.test.ts index cc70d5cd75b..e0b62c1c00b 100644 --- a/extensions/matrix/src/outbound.test.ts +++ b/extensions/matrix/src/outbound.test.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig } from "openclaw/plugin-sdk"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/matrix"; import { beforeEach, describe, expect, it, vi } from "vitest"; const mocks = vi.hoisted(() => ({ diff --git a/extensions/matrix/src/outbound.ts b/extensions/matrix/src/outbound.ts index 547f053c1d3..be4f8d3426d 100644 --- a/extensions/matrix/src/outbound.ts +++ b/extensions/matrix/src/outbound.ts @@ -1,4 +1,4 @@ -import type { ChannelOutboundAdapter } from "openclaw/plugin-sdk/compat"; +import type { ChannelOutboundAdapter } from "openclaw/plugin-sdk/matrix"; import { sendMessageMatrix, sendPollMatrix } from "./matrix/send.js"; import { getMatrixRuntime } from "./runtime.js"; diff --git a/extensions/matrix/src/resolve-targets.test.ts b/extensions/matrix/src/resolve-targets.test.ts index 750ee73a7b1..10dff313a2e 100644 --- a/extensions/matrix/src/resolve-targets.test.ts +++ b/extensions/matrix/src/resolve-targets.test.ts @@ -1,4 +1,4 @@ -import type { ChannelDirectoryEntry } from "openclaw/plugin-sdk/compat"; +import type { ChannelDirectoryEntry } from "openclaw/plugin-sdk/matrix"; import { describe, expect, it, vi, beforeEach } from "vitest"; import { listMatrixDirectoryGroupsLive, listMatrixDirectoryPeersLive } from "./directory-live.js"; import { resolveMatrixTargets } from "./resolve-targets.js"; diff --git a/extensions/matrix/src/resolve-targets.ts b/extensions/matrix/src/resolve-targets.ts index 2da1eb12c1c..23f0e33727e 100644 --- a/extensions/matrix/src/resolve-targets.ts +++ b/extensions/matrix/src/resolve-targets.ts @@ -3,7 +3,7 @@ import type { ChannelResolveKind, ChannelResolveResult, RuntimeEnv, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/matrix"; import { listMatrixDirectoryGroupsLive, listMatrixDirectoryPeersLive } from "./directory-live.js"; function findExactDirectoryMatches( diff --git a/extensions/matrix/src/runtime.ts b/extensions/matrix/src/runtime.ts index 504566a0868..4d94aacf99d 100644 --- a/extensions/matrix/src/runtime.ts +++ b/extensions/matrix/src/runtime.ts @@ -1,4 +1,4 @@ -import type { PluginRuntime } from "openclaw/plugin-sdk/compat"; +import type { PluginRuntime } from "openclaw/plugin-sdk/matrix"; let runtime: PluginRuntime | null = null; diff --git a/extensions/matrix/src/secret-input.ts b/extensions/matrix/src/secret-input.ts index 3fc82b3ac91..a5de1214773 100644 --- a/extensions/matrix/src/secret-input.ts +++ b/extensions/matrix/src/secret-input.ts @@ -2,7 +2,7 @@ import { hasConfiguredSecretInput, normalizeResolvedSecretInputString, normalizeSecretInputString, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/matrix"; import { z } from "zod"; export { hasConfiguredSecretInput, normalizeResolvedSecretInputString, normalizeSecretInputString }; diff --git a/extensions/matrix/src/tool-actions.ts b/extensions/matrix/src/tool-actions.ts index 5f4f2830312..28c8d5676d1 100644 --- a/extensions/matrix/src/tool-actions.ts +++ b/extensions/matrix/src/tool-actions.ts @@ -5,7 +5,7 @@ import { readNumberParam, readReactionParams, readStringParam, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/matrix"; import { deleteMatrixMessage, editMatrixMessage, diff --git a/extensions/matrix/src/types.ts b/extensions/matrix/src/types.ts index 28e4711319f..e6feaf9f619 100644 --- a/extensions/matrix/src/types.ts +++ b/extensions/matrix/src/types.ts @@ -1,4 +1,4 @@ -import type { DmPolicy, GroupPolicy, SecretInput } from "openclaw/plugin-sdk/compat"; +import type { DmPolicy, GroupPolicy, SecretInput } from "openclaw/plugin-sdk/matrix"; export type { DmPolicy, GroupPolicy }; export type ReplyToMode = "off" | "first" | "all"; From b1922762837af91b1cbdc74f6c80392dbee28794 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 4 Mar 2026 02:33:03 -0500 Subject: [PATCH 45/74] Plugins/mattermost: migrate to scoped plugin-sdk imports --- extensions/mattermost/index.ts | 4 ++-- extensions/mattermost/src/channel.test.ts | 4 ++-- extensions/mattermost/src/channel.ts | 2 +- extensions/mattermost/src/config-schema.ts | 2 +- extensions/mattermost/src/group-mentions.ts | 2 +- extensions/mattermost/src/mattermost/accounts.test.ts | 2 +- extensions/mattermost/src/mattermost/accounts.ts | 2 +- extensions/mattermost/src/mattermost/monitor-auth.ts | 2 +- extensions/mattermost/src/mattermost/monitor-helpers.ts | 4 ++-- .../mattermost/src/mattermost/monitor-websocket.test.ts | 2 +- extensions/mattermost/src/mattermost/monitor-websocket.ts | 2 +- extensions/mattermost/src/mattermost/monitor.authz.test.ts | 2 +- extensions/mattermost/src/mattermost/monitor.ts | 4 ++-- extensions/mattermost/src/mattermost/probe.ts | 2 +- .../mattermost/src/mattermost/reactions.test-helpers.ts | 2 +- extensions/mattermost/src/mattermost/reactions.ts | 2 +- extensions/mattermost/src/mattermost/send.test.ts | 2 +- extensions/mattermost/src/mattermost/send.ts | 2 +- extensions/mattermost/src/mattermost/slash-http.test.ts | 2 +- extensions/mattermost/src/mattermost/slash-http.ts | 4 ++-- extensions/mattermost/src/mattermost/slash-state.ts | 6 +++--- extensions/mattermost/src/onboarding-helpers.ts | 2 +- extensions/mattermost/src/onboarding.status.test.ts | 2 +- extensions/mattermost/src/onboarding.ts | 2 +- extensions/mattermost/src/runtime.ts | 2 +- extensions/mattermost/src/secret-input.ts | 2 +- extensions/mattermost/src/types.ts | 2 +- 27 files changed, 34 insertions(+), 34 deletions(-) diff --git a/extensions/mattermost/index.ts b/extensions/mattermost/index.ts index 75b28cc1559..1dbf616c061 100644 --- a/extensions/mattermost/index.ts +++ b/extensions/mattermost/index.ts @@ -1,5 +1,5 @@ -import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core"; -import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/core"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/mattermost"; +import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/mattermost"; import { mattermostPlugin } from "./src/channel.js"; import { getSlashCommandState, registerSlashCommandRoute } from "./src/mattermost/slash-state.js"; import { setMattermostRuntime } from "./src/runtime.js"; diff --git a/extensions/mattermost/src/channel.test.ts b/extensions/mattermost/src/channel.test.ts index b0a24137c4d..e8f1480565c 100644 --- a/extensions/mattermost/src/channel.test.ts +++ b/extensions/mattermost/src/channel.test.ts @@ -1,5 +1,5 @@ -import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; -import { createReplyPrefixOptions } from "openclaw/plugin-sdk/compat"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/mattermost"; +import { createReplyPrefixOptions } from "openclaw/plugin-sdk/mattermost"; import { beforeEach, describe, expect, it, vi } from "vitest"; const { sendMessageMattermostMock } = vi.hoisted(() => ({ sendMessageMattermostMock: vi.fn(), diff --git a/extensions/mattermost/src/channel.ts b/extensions/mattermost/src/channel.ts index a45e7b57e5b..9134af26704 100644 --- a/extensions/mattermost/src/channel.ts +++ b/extensions/mattermost/src/channel.ts @@ -12,7 +12,7 @@ import { type ChannelMessageActionAdapter, type ChannelMessageActionName, type ChannelPlugin, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/mattermost"; import { MattermostConfigSchema } from "./config-schema.js"; import { resolveMattermostGroupRequireMention } from "./group-mentions.js"; import { diff --git a/extensions/mattermost/src/config-schema.ts b/extensions/mattermost/src/config-schema.ts index a3dd07900e2..0bc43f22164 100644 --- a/extensions/mattermost/src/config-schema.ts +++ b/extensions/mattermost/src/config-schema.ts @@ -4,7 +4,7 @@ import { GroupPolicySchema, MarkdownConfigSchema, requireOpenAllowFrom, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/mattermost"; import { z } from "zod"; import { buildSecretInputSchema } from "./secret-input.js"; diff --git a/extensions/mattermost/src/group-mentions.ts b/extensions/mattermost/src/group-mentions.ts index 6f57c8f7970..22e5d53dc78 100644 --- a/extensions/mattermost/src/group-mentions.ts +++ b/extensions/mattermost/src/group-mentions.ts @@ -1,4 +1,4 @@ -import type { ChannelGroupContext } from "openclaw/plugin-sdk/compat"; +import type { ChannelGroupContext } from "openclaw/plugin-sdk/mattermost"; import { resolveMattermostAccount } from "./mattermost/accounts.js"; export function resolveMattermostGroupRequireMention( diff --git a/extensions/mattermost/src/mattermost/accounts.test.ts b/extensions/mattermost/src/mattermost/accounts.test.ts index 29011495398..b3ad8d49e04 100644 --- a/extensions/mattermost/src/mattermost/accounts.test.ts +++ b/extensions/mattermost/src/mattermost/accounts.test.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/mattermost"; import { describe, expect, it } from "vitest"; import { resolveDefaultMattermostAccountId } from "./accounts.js"; diff --git a/extensions/mattermost/src/mattermost/accounts.ts b/extensions/mattermost/src/mattermost/accounts.ts index 521bab481df..e8a3f5d9572 100644 --- a/extensions/mattermost/src/mattermost/accounts.ts +++ b/extensions/mattermost/src/mattermost/accounts.ts @@ -3,7 +3,7 @@ import { normalizeAccountId, normalizeOptionalAccountId, } from "openclaw/plugin-sdk/account-id"; -import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/mattermost"; import { normalizeResolvedSecretInputString, normalizeSecretInputString } from "../secret-input.js"; import type { MattermostAccountConfig, MattermostChatMode } from "../types.js"; import { normalizeMattermostBaseUrl } from "./client.js"; diff --git a/extensions/mattermost/src/mattermost/monitor-auth.ts b/extensions/mattermost/src/mattermost/monitor-auth.ts index edf945dbd16..1685d4b560a 100644 --- a/extensions/mattermost/src/mattermost/monitor-auth.ts +++ b/extensions/mattermost/src/mattermost/monitor-auth.ts @@ -1,7 +1,7 @@ import { resolveAllowlistMatchSimple, resolveEffectiveAllowFromLists, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/mattermost"; export function normalizeMattermostAllowEntry(entry: string): string { const trimmed = entry.trim(); diff --git a/extensions/mattermost/src/mattermost/monitor-helpers.ts b/extensions/mattermost/src/mattermost/monitor-helpers.ts index 74d6f7689df..1724f577485 100644 --- a/extensions/mattermost/src/mattermost/monitor-helpers.ts +++ b/extensions/mattermost/src/mattermost/monitor-helpers.ts @@ -2,8 +2,8 @@ import { formatInboundFromLabel as formatInboundFromLabelShared, resolveThreadSessionKeys as resolveThreadSessionKeysShared, type OpenClawConfig, -} from "openclaw/plugin-sdk/compat"; -export { createDedupeCache, rawDataToString } from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/mattermost"; +export { createDedupeCache, rawDataToString } from "openclaw/plugin-sdk/mattermost"; export type ResponsePrefixContext = { model?: string; diff --git a/extensions/mattermost/src/mattermost/monitor-websocket.test.ts b/extensions/mattermost/src/mattermost/monitor-websocket.test.ts index 02b9f6a2742..171052637ce 100644 --- a/extensions/mattermost/src/mattermost/monitor-websocket.test.ts +++ b/extensions/mattermost/src/mattermost/monitor-websocket.test.ts @@ -1,4 +1,4 @@ -import type { RuntimeEnv } from "openclaw/plugin-sdk/compat"; +import type { RuntimeEnv } from "openclaw/plugin-sdk/mattermost"; import { describe, expect, it, vi } from "vitest"; import { createMattermostConnectOnce, diff --git a/extensions/mattermost/src/mattermost/monitor-websocket.ts b/extensions/mattermost/src/mattermost/monitor-websocket.ts index cfe5ab96fdc..7f04a18f09b 100644 --- a/extensions/mattermost/src/mattermost/monitor-websocket.ts +++ b/extensions/mattermost/src/mattermost/monitor-websocket.ts @@ -1,4 +1,4 @@ -import type { ChannelAccountSnapshot, RuntimeEnv } from "openclaw/plugin-sdk/compat"; +import type { ChannelAccountSnapshot, RuntimeEnv } from "openclaw/plugin-sdk/mattermost"; import WebSocket from "ws"; import type { MattermostPost } from "./client.js"; import { rawDataToString } from "./monitor-helpers.js"; diff --git a/extensions/mattermost/src/mattermost/monitor.authz.test.ts b/extensions/mattermost/src/mattermost/monitor.authz.test.ts index 9ee7071dac5..065904f373c 100644 --- a/extensions/mattermost/src/mattermost/monitor.authz.test.ts +++ b/extensions/mattermost/src/mattermost/monitor.authz.test.ts @@ -1,4 +1,4 @@ -import { resolveControlCommandGate } from "openclaw/plugin-sdk/compat"; +import { resolveControlCommandGate } from "openclaw/plugin-sdk/mattermost"; import { describe, expect, it } from "vitest"; import { resolveMattermostEffectiveAllowFromLists } from "./monitor-auth.js"; diff --git a/extensions/mattermost/src/mattermost/monitor.ts b/extensions/mattermost/src/mattermost/monitor.ts index d62ddd80896..0b7111fb941 100644 --- a/extensions/mattermost/src/mattermost/monitor.ts +++ b/extensions/mattermost/src/mattermost/monitor.ts @@ -4,7 +4,7 @@ import type { OpenClawConfig, ReplyPayload, RuntimeEnv, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/mattermost"; import { buildAgentMediaPayload, DM_GROUP_ACCESS_REASON, @@ -27,7 +27,7 @@ import { warnMissingProviderGroupPolicyFallbackOnce, listSkillCommandsForAgents, type HistoryEntry, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/mattermost"; import { getMattermostRuntime } from "../runtime.js"; import { resolveMattermostAccount } from "./accounts.js"; import { diff --git a/extensions/mattermost/src/mattermost/probe.ts b/extensions/mattermost/src/mattermost/probe.ts index f9ec2005af6..2966e20f209 100644 --- a/extensions/mattermost/src/mattermost/probe.ts +++ b/extensions/mattermost/src/mattermost/probe.ts @@ -1,4 +1,4 @@ -import type { BaseProbeResult } from "openclaw/plugin-sdk/compat"; +import type { BaseProbeResult } from "openclaw/plugin-sdk/mattermost"; import { normalizeMattermostBaseUrl, readMattermostError, type MattermostUser } from "./client.js"; export type MattermostProbe = BaseProbeResult & { diff --git a/extensions/mattermost/src/mattermost/reactions.test-helpers.ts b/extensions/mattermost/src/mattermost/reactions.test-helpers.ts index a19f9b00222..248b9355918 100644 --- a/extensions/mattermost/src/mattermost/reactions.test-helpers.ts +++ b/extensions/mattermost/src/mattermost/reactions.test-helpers.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/mattermost"; import { expect, vi } from "vitest"; export function createMattermostTestConfig(): OpenClawConfig { diff --git a/extensions/mattermost/src/mattermost/reactions.ts b/extensions/mattermost/src/mattermost/reactions.ts index 7bb3d5eca08..3515153edd2 100644 --- a/extensions/mattermost/src/mattermost/reactions.ts +++ b/extensions/mattermost/src/mattermost/reactions.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/mattermost"; import { resolveMattermostAccount } from "./accounts.js"; import { createMattermostClient, fetchMattermostMe, type MattermostClient } from "./client.js"; diff --git a/extensions/mattermost/src/mattermost/send.test.ts b/extensions/mattermost/src/mattermost/send.test.ts index d924529517c..a4a710a41b4 100644 --- a/extensions/mattermost/src/mattermost/send.test.ts +++ b/extensions/mattermost/src/mattermost/send.test.ts @@ -18,7 +18,7 @@ const mockState = vi.hoisted(() => ({ uploadMattermostFile: vi.fn(), })); -vi.mock("openclaw/plugin-sdk", () => ({ +vi.mock("openclaw/plugin-sdk/mattermost", () => ({ loadOutboundMediaFromUrl: mockState.loadOutboundMediaFromUrl, })); diff --git a/extensions/mattermost/src/mattermost/send.ts b/extensions/mattermost/src/mattermost/send.ts index e7805f39308..6beb18539bd 100644 --- a/extensions/mattermost/src/mattermost/send.ts +++ b/extensions/mattermost/src/mattermost/send.ts @@ -1,4 +1,4 @@ -import { loadOutboundMediaFromUrl, type OpenClawConfig } from "openclaw/plugin-sdk/compat"; +import { loadOutboundMediaFromUrl, type OpenClawConfig } from "openclaw/plugin-sdk/mattermost"; import { getMattermostRuntime } from "../runtime.js"; import { resolveMattermostAccount } from "./accounts.js"; import { diff --git a/extensions/mattermost/src/mattermost/slash-http.test.ts b/extensions/mattermost/src/mattermost/slash-http.test.ts index 7592de3c4dd..92a6babe35c 100644 --- a/extensions/mattermost/src/mattermost/slash-http.test.ts +++ b/extensions/mattermost/src/mattermost/slash-http.test.ts @@ -1,6 +1,6 @@ import type { IncomingMessage, ServerResponse } from "node:http"; import { PassThrough } from "node:stream"; -import type { OpenClawConfig, RuntimeEnv } from "openclaw/plugin-sdk/compat"; +import type { OpenClawConfig, RuntimeEnv } from "openclaw/plugin-sdk/mattermost"; import { describe, expect, it } from "vitest"; import type { ResolvedMattermostAccount } from "./accounts.js"; import { createSlashCommandHttpHandler } from "./slash-http.js"; diff --git a/extensions/mattermost/src/mattermost/slash-http.ts b/extensions/mattermost/src/mattermost/slash-http.ts index 49f0e89c1ea..004d8af80d7 100644 --- a/extensions/mattermost/src/mattermost/slash-http.ts +++ b/extensions/mattermost/src/mattermost/slash-http.ts @@ -6,14 +6,14 @@ */ import type { IncomingMessage, ServerResponse } from "node:http"; -import type { OpenClawConfig, ReplyPayload, RuntimeEnv } from "openclaw/plugin-sdk/compat"; +import type { OpenClawConfig, ReplyPayload, RuntimeEnv } from "openclaw/plugin-sdk/mattermost"; import { createReplyPrefixOptions, createTypingCallbacks, isDangerousNameMatchingEnabled, logTypingFailure, resolveControlCommandGate, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/mattermost"; import type { ResolvedMattermostAccount } from "../mattermost/accounts.js"; import { getMattermostRuntime } from "../runtime.js"; import { diff --git a/extensions/mattermost/src/mattermost/slash-state.ts b/extensions/mattermost/src/mattermost/slash-state.ts index 2ecf9eba2bf..f79f670df8d 100644 --- a/extensions/mattermost/src/mattermost/slash-state.ts +++ b/extensions/mattermost/src/mattermost/slash-state.ts @@ -10,7 +10,7 @@ */ import type { IncomingMessage, ServerResponse } from "node:http"; -import type { OpenClawPluginApi } from "openclaw/plugin-sdk/compat"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/mattermost"; import type { ResolvedMattermostAccount } from "./accounts.js"; import { resolveSlashCommandConfig, type MattermostRegisteredCommand } from "./slash-commands.js"; import { createSlashCommandHttpHandler } from "./slash-http.js"; @@ -86,8 +86,8 @@ export function activateSlashCommands(params: { registeredCommands: MattermostRegisteredCommand[]; triggerMap?: Map; api: { - cfg: import("openclaw/plugin-sdk/compat").OpenClawConfig; - runtime: import("openclaw/plugin-sdk/compat").RuntimeEnv; + cfg: import("openclaw/plugin-sdk/mattermost").OpenClawConfig; + runtime: import("openclaw/plugin-sdk/mattermost").RuntimeEnv; }; log?: (msg: string) => void; }) { diff --git a/extensions/mattermost/src/onboarding-helpers.ts b/extensions/mattermost/src/onboarding-helpers.ts index f3797c608ad..b125b0371e5 100644 --- a/extensions/mattermost/src/onboarding-helpers.ts +++ b/extensions/mattermost/src/onboarding-helpers.ts @@ -1 +1 @@ -export { promptAccountId } from "openclaw/plugin-sdk/compat"; +export { promptAccountId } from "openclaw/plugin-sdk/mattermost"; diff --git a/extensions/mattermost/src/onboarding.status.test.ts b/extensions/mattermost/src/onboarding.status.test.ts index fa1cc13c382..af0e9be5b00 100644 --- a/extensions/mattermost/src/onboarding.status.test.ts +++ b/extensions/mattermost/src/onboarding.status.test.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/mattermost"; import { describe, expect, it } from "vitest"; import { mattermostOnboardingAdapter } from "./onboarding.js"; diff --git a/extensions/mattermost/src/onboarding.ts b/extensions/mattermost/src/onboarding.ts index c1f01b4622c..5204f512d23 100644 --- a/extensions/mattermost/src/onboarding.ts +++ b/extensions/mattermost/src/onboarding.ts @@ -6,7 +6,7 @@ import { type OpenClawConfig, type SecretInput, type WizardPrompter, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/mattermost"; import { listMattermostAccountIds, resolveDefaultMattermostAccountId, diff --git a/extensions/mattermost/src/runtime.ts b/extensions/mattermost/src/runtime.ts index e2cb807f30f..f6e5e83f270 100644 --- a/extensions/mattermost/src/runtime.ts +++ b/extensions/mattermost/src/runtime.ts @@ -1,4 +1,4 @@ -import type { PluginRuntime } from "openclaw/plugin-sdk/compat"; +import type { PluginRuntime } from "openclaw/plugin-sdk/mattermost"; let runtime: PluginRuntime | null = null; diff --git a/extensions/mattermost/src/secret-input.ts b/extensions/mattermost/src/secret-input.ts index 3fc82b3ac91..017109424bc 100644 --- a/extensions/mattermost/src/secret-input.ts +++ b/extensions/mattermost/src/secret-input.ts @@ -2,7 +2,7 @@ import { hasConfiguredSecretInput, normalizeResolvedSecretInputString, normalizeSecretInputString, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/mattermost"; import { z } from "zod"; export { hasConfiguredSecretInput, normalizeResolvedSecretInputString, normalizeSecretInputString }; diff --git a/extensions/mattermost/src/types.ts b/extensions/mattermost/src/types.ts index d2b50e3a510..5de38e7833c 100644 --- a/extensions/mattermost/src/types.ts +++ b/extensions/mattermost/src/types.ts @@ -3,7 +3,7 @@ import type { DmPolicy, GroupPolicy, SecretInput, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/mattermost"; export type MattermostChatMode = "oncall" | "onmessage" | "onchar"; From 61a2a3417f11cd6ac927978c8b4860622c8189f7 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 4 Mar 2026 02:33:04 -0500 Subject: [PATCH 46/74] Plugins/memory-core: migrate to scoped plugin-sdk imports --- extensions/memory-core/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/memory-core/index.ts b/extensions/memory-core/index.ts index 05f6aa069fe..6559485e46a 100644 --- a/extensions/memory-core/index.ts +++ b/extensions/memory-core/index.ts @@ -1,5 +1,5 @@ -import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core"; -import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/core"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/memory-core"; +import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/memory-core"; const memoryCorePlugin = { id: "memory-core", From 6b19b7f37af39fff72c4786d5a676ce4f06086ac Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 4 Mar 2026 02:33:05 -0500 Subject: [PATCH 47/74] Plugins/memory-lancedb: migrate to scoped plugin-sdk imports --- extensions/memory-lancedb/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/memory-lancedb/index.ts b/extensions/memory-lancedb/index.ts index cbed48dd9ef..6ae7574aaa8 100644 --- a/extensions/memory-lancedb/index.ts +++ b/extensions/memory-lancedb/index.ts @@ -10,7 +10,7 @@ import { randomUUID } from "node:crypto"; import type * as LanceDB from "@lancedb/lancedb"; import { Type } from "@sinclair/typebox"; import OpenAI from "openai"; -import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/memory-lancedb"; import { DEFAULT_CAPTURE_MAX_CHARS, MEMORY_CATEGORIES, From e42d345aee1400455826b4bcdd7c549db2f7efd1 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 4 Mar 2026 02:33:06 -0500 Subject: [PATCH 48/74] Plugins/minimax-portal-auth: migrate to scoped plugin-sdk imports --- extensions/minimax-portal-auth/index.ts | 2 +- extensions/minimax-portal-auth/oauth.ts | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/extensions/minimax-portal-auth/index.ts b/extensions/minimax-portal-auth/index.ts index 731404eb867..6eee6bdabe1 100644 --- a/extensions/minimax-portal-auth/index.ts +++ b/extensions/minimax-portal-auth/index.ts @@ -3,7 +3,7 @@ import { type OpenClawPluginApi, type ProviderAuthContext, type ProviderAuthResult, -} from "openclaw/plugin-sdk/core"; +} from "openclaw/plugin-sdk/minimax-portal-auth"; import { loginMiniMaxPortalOAuth, type MiniMaxRegion } from "./oauth.js"; const PROVIDER_ID = "minimax-portal"; diff --git a/extensions/minimax-portal-auth/oauth.ts b/extensions/minimax-portal-auth/oauth.ts index 016af72dbd5..5b18c13d3a4 100644 --- a/extensions/minimax-portal-auth/oauth.ts +++ b/extensions/minimax-portal-auth/oauth.ts @@ -1,5 +1,8 @@ import { randomBytes, randomUUID } from "node:crypto"; -import { generatePkceVerifierChallenge, toFormUrlEncoded } from "openclaw/plugin-sdk/compat"; +import { + generatePkceVerifierChallenge, + toFormUrlEncoded, +} from "openclaw/plugin-sdk/minimax-portal-auth"; export type MiniMaxRegion = "cn" | "global"; From adb400f9b1f8a6fd0b1c1f8b813ddd44f72c39f8 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 4 Mar 2026 02:33:07 -0500 Subject: [PATCH 49/74] Plugins/msteams: migrate to scoped plugin-sdk imports --- extensions/msteams/index.ts | 4 ++-- extensions/msteams/src/attachments.test.ts | 2 +- extensions/msteams/src/attachments/graph.ts | 2 +- extensions/msteams/src/attachments/payload.ts | 2 +- extensions/msteams/src/attachments/remote-media.ts | 2 +- extensions/msteams/src/attachments/shared.ts | 4 ++-- extensions/msteams/src/channel.directory.test.ts | 2 +- extensions/msteams/src/channel.ts | 4 ++-- extensions/msteams/src/directory-live.ts | 2 +- extensions/msteams/src/file-lock.ts | 2 +- extensions/msteams/src/graph.ts | 2 +- extensions/msteams/src/media-helpers.ts | 2 +- extensions/msteams/src/messenger.test.ts | 2 +- extensions/msteams/src/messenger.ts | 2 +- extensions/msteams/src/monitor-handler.file-consent.test.ts | 2 +- extensions/msteams/src/monitor-handler.ts | 2 +- .../msteams/src/monitor-handler/message-handler.authz.test.ts | 2 +- extensions/msteams/src/monitor-handler/message-handler.ts | 2 +- extensions/msteams/src/monitor.lifecycle.test.ts | 4 ++-- extensions/msteams/src/monitor.ts | 2 +- extensions/msteams/src/onboarding.ts | 4 ++-- extensions/msteams/src/outbound.test.ts | 2 +- extensions/msteams/src/outbound.ts | 2 +- extensions/msteams/src/policy.test.ts | 2 +- extensions/msteams/src/policy.ts | 4 ++-- extensions/msteams/src/probe.test.ts | 2 +- extensions/msteams/src/probe.ts | 2 +- extensions/msteams/src/reply-dispatcher.ts | 2 +- extensions/msteams/src/runtime.ts | 2 +- extensions/msteams/src/secret-input.ts | 2 +- extensions/msteams/src/send-context.ts | 2 +- extensions/msteams/src/send.test.ts | 4 ++-- extensions/msteams/src/send.ts | 4 ++-- extensions/msteams/src/store-fs.ts | 2 +- extensions/msteams/src/test-runtime.ts | 2 +- extensions/msteams/src/token.ts | 2 +- 36 files changed, 44 insertions(+), 44 deletions(-) diff --git a/extensions/msteams/index.ts b/extensions/msteams/index.ts index 9d5fde61d4d..725ad40dfdf 100644 --- a/extensions/msteams/index.ts +++ b/extensions/msteams/index.ts @@ -1,5 +1,5 @@ -import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core"; -import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/core"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/msteams"; +import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/msteams"; import { msteamsPlugin } from "./src/channel.js"; import { setMSTeamsRuntime } from "./src/runtime.js"; diff --git a/extensions/msteams/src/attachments.test.ts b/extensions/msteams/src/attachments.test.ts index 9f0de10992f..6887fad7fcb 100644 --- a/extensions/msteams/src/attachments.test.ts +++ b/extensions/msteams/src/attachments.test.ts @@ -1,4 +1,4 @@ -import type { PluginRuntime, SsrFPolicy } from "openclaw/plugin-sdk/compat"; +import type { PluginRuntime, SsrFPolicy } from "openclaw/plugin-sdk/msteams"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { createPluginRuntimeMock } from "../../test-utils/plugin-runtime-mock.js"; import { diff --git a/extensions/msteams/src/attachments/graph.ts b/extensions/msteams/src/attachments/graph.ts index c9f632c7f38..1798d438d1e 100644 --- a/extensions/msteams/src/attachments/graph.ts +++ b/extensions/msteams/src/attachments/graph.ts @@ -1,4 +1,4 @@ -import { fetchWithSsrFGuard, type SsrFPolicy } from "openclaw/plugin-sdk/compat"; +import { fetchWithSsrFGuard, type SsrFPolicy } from "openclaw/plugin-sdk/msteams"; import { getMSTeamsRuntime } from "../runtime.js"; import { downloadMSTeamsAttachments } from "./download.js"; import { downloadAndStoreMSTeamsRemoteMedia } from "./remote-media.js"; diff --git a/extensions/msteams/src/attachments/payload.ts b/extensions/msteams/src/attachments/payload.ts index 2134a382f0c..8cfd79b29ce 100644 --- a/extensions/msteams/src/attachments/payload.ts +++ b/extensions/msteams/src/attachments/payload.ts @@ -1,4 +1,4 @@ -import { buildMediaPayload } from "openclaw/plugin-sdk/compat"; +import { buildMediaPayload } from "openclaw/plugin-sdk/msteams"; export function buildMSTeamsMediaPayload( mediaList: Array<{ path: string; contentType?: string }>, diff --git a/extensions/msteams/src/attachments/remote-media.ts b/extensions/msteams/src/attachments/remote-media.ts index b31b47723b9..87c018b0290 100644 --- a/extensions/msteams/src/attachments/remote-media.ts +++ b/extensions/msteams/src/attachments/remote-media.ts @@ -1,4 +1,4 @@ -import type { SsrFPolicy } from "openclaw/plugin-sdk/compat"; +import type { SsrFPolicy } from "openclaw/plugin-sdk/msteams"; import { getMSTeamsRuntime } from "../runtime.js"; import { inferPlaceholder } from "./shared.js"; import type { MSTeamsInboundMedia } from "./types.js"; diff --git a/extensions/msteams/src/attachments/shared.ts b/extensions/msteams/src/attachments/shared.ts index 3222be248bc..cde483b0283 100644 --- a/extensions/msteams/src/attachments/shared.ts +++ b/extensions/msteams/src/attachments/shared.ts @@ -4,8 +4,8 @@ import { isHttpsUrlAllowedByHostnameSuffixAllowlist, isPrivateIpAddress, normalizeHostnameSuffixAllowlist, -} from "openclaw/plugin-sdk/compat"; -import type { SsrFPolicy } from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/msteams"; +import type { SsrFPolicy } from "openclaw/plugin-sdk/msteams"; import type { MSTeamsAttachmentLike } from "./types.js"; type InlineImageCandidate = diff --git a/extensions/msteams/src/channel.directory.test.ts b/extensions/msteams/src/channel.directory.test.ts index 97bfd227f5f..0746f78aabb 100644 --- a/extensions/msteams/src/channel.directory.test.ts +++ b/extensions/msteams/src/channel.directory.test.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig, RuntimeEnv } from "openclaw/plugin-sdk/compat"; +import type { OpenClawConfig, RuntimeEnv } from "openclaw/plugin-sdk/msteams"; import { describe, expect, it } from "vitest"; import { msteamsPlugin } from "./channel.js"; diff --git a/extensions/msteams/src/channel.ts b/extensions/msteams/src/channel.ts index 057f37c83a4..90223956988 100644 --- a/extensions/msteams/src/channel.ts +++ b/extensions/msteams/src/channel.ts @@ -2,7 +2,7 @@ import type { ChannelMessageActionName, ChannelPlugin, OpenClawConfig, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/msteams"; import { buildBaseChannelStatusSummary, buildChannelConfigSchema, @@ -12,7 +12,7 @@ import { PAIRING_APPROVED_MESSAGE, resolveAllowlistProviderRuntimeGroupPolicy, resolveDefaultGroupPolicy, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/msteams"; import { listMSTeamsDirectoryGroupsLive, listMSTeamsDirectoryPeersLive } from "./directory-live.js"; import { msteamsOnboardingAdapter } from "./onboarding.js"; import { msteamsOutbound } from "./outbound.js"; diff --git a/extensions/msteams/src/directory-live.ts b/extensions/msteams/src/directory-live.ts index 0e2464aa0ce..66fbe16e876 100644 --- a/extensions/msteams/src/directory-live.ts +++ b/extensions/msteams/src/directory-live.ts @@ -1,4 +1,4 @@ -import type { ChannelDirectoryEntry } from "openclaw/plugin-sdk/compat"; +import type { ChannelDirectoryEntry } from "openclaw/plugin-sdk/msteams"; import { searchGraphUsers } from "./graph-users.js"; import { type GraphChannel, diff --git a/extensions/msteams/src/file-lock.ts b/extensions/msteams/src/file-lock.ts index 9c5782b9ff5..ef61d1b6214 100644 --- a/extensions/msteams/src/file-lock.ts +++ b/extensions/msteams/src/file-lock.ts @@ -1 +1 @@ -export { withFileLock } from "openclaw/plugin-sdk/compat"; +export { withFileLock } from "openclaw/plugin-sdk/msteams"; diff --git a/extensions/msteams/src/graph.ts b/extensions/msteams/src/graph.ts index 983bfe9ed64..269216c7cd2 100644 --- a/extensions/msteams/src/graph.ts +++ b/extensions/msteams/src/graph.ts @@ -1,4 +1,4 @@ -import type { MSTeamsConfig } from "openclaw/plugin-sdk/compat"; +import type { MSTeamsConfig } from "openclaw/plugin-sdk/msteams"; import { GRAPH_ROOT } from "./attachments/shared.js"; import { loadMSTeamsSdkWithAuth } from "./sdk.js"; import { readAccessToken } from "./token-response.js"; diff --git a/extensions/msteams/src/media-helpers.ts b/extensions/msteams/src/media-helpers.ts index f8a36e55f81..8de456b8c39 100644 --- a/extensions/msteams/src/media-helpers.ts +++ b/extensions/msteams/src/media-helpers.ts @@ -8,7 +8,7 @@ import { extensionForMime, extractOriginalFilename, getFileExtension, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/msteams"; /** * Detect MIME type from URL extension or data URL. diff --git a/extensions/msteams/src/messenger.test.ts b/extensions/msteams/src/messenger.test.ts index 973bbb67973..627bad15d94 100644 --- a/extensions/msteams/src/messenger.test.ts +++ b/extensions/msteams/src/messenger.test.ts @@ -1,7 +1,7 @@ import { mkdtemp, rm, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import { SILENT_REPLY_TOKEN, type PluginRuntime } from "openclaw/plugin-sdk/compat"; +import { SILENT_REPLY_TOKEN, type PluginRuntime } from "openclaw/plugin-sdk/msteams"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { createPluginRuntimeMock } from "../../test-utils/plugin-runtime-mock.js"; import type { StoredConversationReference } from "./conversation-store.js"; diff --git a/extensions/msteams/src/messenger.ts b/extensions/msteams/src/messenger.ts index c412c47d048..b45c39ac3fb 100644 --- a/extensions/msteams/src/messenger.ts +++ b/extensions/msteams/src/messenger.ts @@ -7,7 +7,7 @@ import { type ReplyPayload, SILENT_REPLY_TOKEN, sleep, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/msteams"; import type { MSTeamsAccessTokenProvider } from "./attachments/types.js"; import type { StoredConversationReference } from "./conversation-store.js"; import { classifyMSTeamsSendError } from "./errors.js"; diff --git a/extensions/msteams/src/monitor-handler.file-consent.test.ts b/extensions/msteams/src/monitor-handler.file-consent.test.ts index 8288668ba67..88a6a67a838 100644 --- a/extensions/msteams/src/monitor-handler.file-consent.test.ts +++ b/extensions/msteams/src/monitor-handler.file-consent.test.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig, PluginRuntime, RuntimeEnv } from "openclaw/plugin-sdk/compat"; +import type { OpenClawConfig, PluginRuntime, RuntimeEnv } from "openclaw/plugin-sdk/msteams"; import { beforeEach, describe, expect, it, vi } from "vitest"; import type { MSTeamsConversationStore } from "./conversation-store.js"; import type { MSTeamsAdapter } from "./messenger.js"; diff --git a/extensions/msteams/src/monitor-handler.ts b/extensions/msteams/src/monitor-handler.ts index b64fdee6d67..bad810322a9 100644 --- a/extensions/msteams/src/monitor-handler.ts +++ b/extensions/msteams/src/monitor-handler.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig, RuntimeEnv } from "openclaw/plugin-sdk/compat"; +import type { OpenClawConfig, RuntimeEnv } from "openclaw/plugin-sdk/msteams"; import type { MSTeamsConversationStore } from "./conversation-store.js"; import { buildFileInfoCard, parseFileConsentInvoke, uploadToConsentUrl } from "./file-consent.js"; import { normalizeMSTeamsConversationId } from "./inbound.js"; diff --git a/extensions/msteams/src/monitor-handler/message-handler.authz.test.ts b/extensions/msteams/src/monitor-handler/message-handler.authz.test.ts index 7e8118b5629..f019287e151 100644 --- a/extensions/msteams/src/monitor-handler/message-handler.authz.test.ts +++ b/extensions/msteams/src/monitor-handler/message-handler.authz.test.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig, PluginRuntime, RuntimeEnv } from "openclaw/plugin-sdk/compat"; +import type { OpenClawConfig, PluginRuntime, RuntimeEnv } from "openclaw/plugin-sdk/msteams"; import { describe, expect, it, vi } from "vitest"; import type { MSTeamsMessageHandlerDeps } from "../monitor-handler.js"; import { setMSTeamsRuntime } from "../runtime.js"; diff --git a/extensions/msteams/src/monitor-handler/message-handler.ts b/extensions/msteams/src/monitor-handler/message-handler.ts index 0bdb9142641..b4a305fd7d4 100644 --- a/extensions/msteams/src/monitor-handler/message-handler.ts +++ b/extensions/msteams/src/monitor-handler/message-handler.ts @@ -15,7 +15,7 @@ import { resolveEffectiveAllowFromLists, resolveDmGroupAccessWithLists, type HistoryEntry, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/msteams"; import { buildMSTeamsAttachmentPlaceholder, buildMSTeamsMediaPayload, diff --git a/extensions/msteams/src/monitor.lifecycle.test.ts b/extensions/msteams/src/monitor.lifecycle.test.ts index 560b2839efe..eb323d9a353 100644 --- a/extensions/msteams/src/monitor.lifecycle.test.ts +++ b/extensions/msteams/src/monitor.lifecycle.test.ts @@ -1,5 +1,5 @@ import { EventEmitter } from "node:events"; -import type { OpenClawConfig, RuntimeEnv } from "openclaw/plugin-sdk/compat"; +import type { OpenClawConfig, RuntimeEnv } from "openclaw/plugin-sdk/msteams"; import { afterEach, describe, expect, it, vi } from "vitest"; import type { MSTeamsConversationStore } from "./conversation-store.js"; import type { MSTeamsPollStore } from "./polls.js"; @@ -15,7 +15,7 @@ const expressControl = vi.hoisted(() => ({ mode: { value: "listening" as "listening" | "error" }, })); -vi.mock("openclaw/plugin-sdk", () => ({ +vi.mock("openclaw/plugin-sdk/msteams", () => ({ DEFAULT_WEBHOOK_MAX_BODY_BYTES: 1024 * 1024, normalizeSecretInputString: (value: unknown) => typeof value === "string" && value.trim() ? value.trim() : undefined, diff --git a/extensions/msteams/src/monitor.ts b/extensions/msteams/src/monitor.ts index 432af67ad8d..5393a28e0f3 100644 --- a/extensions/msteams/src/monitor.ts +++ b/extensions/msteams/src/monitor.ts @@ -7,7 +7,7 @@ import { summarizeMapping, type OpenClawConfig, type RuntimeEnv, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/msteams"; import { createMSTeamsConversationStoreFs } from "./conversation-store-fs.js"; import type { MSTeamsConversationStore } from "./conversation-store.js"; import { formatUnknownError } from "./errors.js"; diff --git a/extensions/msteams/src/onboarding.ts b/extensions/msteams/src/onboarding.ts index e5836cc73e6..9c95cc2b3cd 100644 --- a/extensions/msteams/src/onboarding.ts +++ b/extensions/msteams/src/onboarding.ts @@ -5,14 +5,14 @@ import type { DmPolicy, WizardPrompter, MSTeamsTeamConfig, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/msteams"; import { addWildcardAllowFrom, DEFAULT_ACCOUNT_ID, formatDocsLink, mergeAllowFromEntries, promptChannelAccessConfig, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/msteams"; import { parseMSTeamsTeamEntry, resolveMSTeamsChannelAllowlist, diff --git a/extensions/msteams/src/outbound.test.ts b/extensions/msteams/src/outbound.test.ts index 950ccd4ece2..a4fc6cc5373 100644 --- a/extensions/msteams/src/outbound.test.ts +++ b/extensions/msteams/src/outbound.test.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig } from "openclaw/plugin-sdk"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/msteams"; import { beforeEach, describe, expect, it, vi } from "vitest"; const mocks = vi.hoisted(() => ({ diff --git a/extensions/msteams/src/outbound.ts b/extensions/msteams/src/outbound.ts index ca2edc3985f..9f3f55c6414 100644 --- a/extensions/msteams/src/outbound.ts +++ b/extensions/msteams/src/outbound.ts @@ -1,4 +1,4 @@ -import type { ChannelOutboundAdapter } from "openclaw/plugin-sdk/compat"; +import type { ChannelOutboundAdapter } from "openclaw/plugin-sdk/msteams"; import { createMSTeamsPollStoreFs } from "./polls.js"; import { getMSTeamsRuntime } from "./runtime.js"; import { sendMessageMSTeams, sendPollMSTeams } from "./send.js"; diff --git a/extensions/msteams/src/policy.test.ts b/extensions/msteams/src/policy.test.ts index 81582cb857a..02d59a99723 100644 --- a/extensions/msteams/src/policy.test.ts +++ b/extensions/msteams/src/policy.test.ts @@ -1,4 +1,4 @@ -import type { MSTeamsConfig } from "openclaw/plugin-sdk/compat"; +import type { MSTeamsConfig } from "openclaw/plugin-sdk/msteams"; import { describe, expect, it } from "vitest"; import { isMSTeamsGroupAllowed, diff --git a/extensions/msteams/src/policy.ts b/extensions/msteams/src/policy.ts index c55a8433b17..b0fe163362b 100644 --- a/extensions/msteams/src/policy.ts +++ b/extensions/msteams/src/policy.ts @@ -7,7 +7,7 @@ import type { MSTeamsConfig, MSTeamsReplyStyle, MSTeamsTeamConfig, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/msteams"; import { buildChannelKeyCandidates, normalizeChannelSlug, @@ -15,7 +15,7 @@ import { resolveToolsBySender, resolveChannelEntryMatchWithFallback, resolveNestedAllowlistDecision, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/msteams"; export type MSTeamsResolvedRouteConfig = { teamConfig?: MSTeamsTeamConfig; diff --git a/extensions/msteams/src/probe.test.ts b/extensions/msteams/src/probe.test.ts index 9ab758b2709..3c6ac3b5d04 100644 --- a/extensions/msteams/src/probe.test.ts +++ b/extensions/msteams/src/probe.test.ts @@ -1,4 +1,4 @@ -import type { MSTeamsConfig } from "openclaw/plugin-sdk/compat"; +import type { MSTeamsConfig } from "openclaw/plugin-sdk/msteams"; import { describe, expect, it, vi } from "vitest"; const hostMockState = vi.hoisted(() => ({ diff --git a/extensions/msteams/src/probe.ts b/extensions/msteams/src/probe.ts index 46dd2747785..11027033cf0 100644 --- a/extensions/msteams/src/probe.ts +++ b/extensions/msteams/src/probe.ts @@ -1,4 +1,4 @@ -import type { BaseProbeResult, MSTeamsConfig } from "openclaw/plugin-sdk/compat"; +import type { BaseProbeResult, MSTeamsConfig } from "openclaw/plugin-sdk/msteams"; import { formatUnknownError } from "./errors.js"; import { loadMSTeamsSdkWithAuth } from "./sdk.js"; import { readAccessToken } from "./token-response.js"; diff --git a/extensions/msteams/src/reply-dispatcher.ts b/extensions/msteams/src/reply-dispatcher.ts index e940fd23738..bf1e21f5e78 100644 --- a/extensions/msteams/src/reply-dispatcher.ts +++ b/extensions/msteams/src/reply-dispatcher.ts @@ -6,7 +6,7 @@ import { type OpenClawConfig, type MSTeamsReplyStyle, type RuntimeEnv, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/msteams"; import type { MSTeamsAccessTokenProvider } from "./attachments/types.js"; import type { StoredConversationReference } from "./conversation-store.js"; import { diff --git a/extensions/msteams/src/runtime.ts b/extensions/msteams/src/runtime.ts index 86c8f9a34a3..97d2272c101 100644 --- a/extensions/msteams/src/runtime.ts +++ b/extensions/msteams/src/runtime.ts @@ -1,4 +1,4 @@ -import type { PluginRuntime } from "openclaw/plugin-sdk/compat"; +import type { PluginRuntime } from "openclaw/plugin-sdk/msteams"; let runtime: PluginRuntime | null = null; diff --git a/extensions/msteams/src/secret-input.ts b/extensions/msteams/src/secret-input.ts index fc64ca00fd1..e2087fbc3c2 100644 --- a/extensions/msteams/src/secret-input.ts +++ b/extensions/msteams/src/secret-input.ts @@ -2,6 +2,6 @@ import { hasConfiguredSecretInput, normalizeResolvedSecretInputString, normalizeSecretInputString, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/msteams"; export { hasConfiguredSecretInput, normalizeResolvedSecretInputString, normalizeSecretInputString }; diff --git a/extensions/msteams/src/send-context.ts b/extensions/msteams/src/send-context.ts index 389aed43b91..d42d0c7d149 100644 --- a/extensions/msteams/src/send-context.ts +++ b/extensions/msteams/src/send-context.ts @@ -2,7 +2,7 @@ import { resolveChannelMediaMaxBytes, type OpenClawConfig, type PluginRuntime, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/msteams"; import type { MSTeamsAccessTokenProvider } from "./attachments/types.js"; import { createMSTeamsConversationStoreFs } from "./conversation-store-fs.js"; import type { diff --git a/extensions/msteams/src/send.test.ts b/extensions/msteams/src/send.test.ts index 3c826310c58..ce6acbaf9b6 100644 --- a/extensions/msteams/src/send.test.ts +++ b/extensions/msteams/src/send.test.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/msteams"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { sendMessageMSTeams } from "./send.js"; @@ -11,7 +11,7 @@ const mockState = vi.hoisted(() => ({ sendMSTeamsMessages: vi.fn(), })); -vi.mock("openclaw/plugin-sdk", () => ({ +vi.mock("openclaw/plugin-sdk/msteams", () => ({ loadOutboundMediaFromUrl: mockState.loadOutboundMediaFromUrl, })); diff --git a/extensions/msteams/src/send.ts b/extensions/msteams/src/send.ts index 3adb3a1436c..cfa023d8871 100644 --- a/extensions/msteams/src/send.ts +++ b/extensions/msteams/src/send.ts @@ -1,5 +1,5 @@ -import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; -import { loadOutboundMediaFromUrl } from "openclaw/plugin-sdk/compat"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/msteams"; +import { loadOutboundMediaFromUrl } from "openclaw/plugin-sdk/msteams"; import { createMSTeamsConversationStoreFs } from "./conversation-store-fs.js"; import { classifyMSTeamsSendError, diff --git a/extensions/msteams/src/store-fs.ts b/extensions/msteams/src/store-fs.ts index c96e96d898c..8f109914db1 100644 --- a/extensions/msteams/src/store-fs.ts +++ b/extensions/msteams/src/store-fs.ts @@ -1,5 +1,5 @@ import fs from "node:fs"; -import { readJsonFileWithFallback, writeJsonFileAtomically } from "openclaw/plugin-sdk/compat"; +import { readJsonFileWithFallback, writeJsonFileAtomically } from "openclaw/plugin-sdk/msteams"; import { withFileLock as withPathLock } from "./file-lock.js"; const STORE_LOCK_OPTIONS = { diff --git a/extensions/msteams/src/test-runtime.ts b/extensions/msteams/src/test-runtime.ts index 6cc4800350a..6232e28ba07 100644 --- a/extensions/msteams/src/test-runtime.ts +++ b/extensions/msteams/src/test-runtime.ts @@ -1,6 +1,6 @@ import os from "node:os"; import path from "node:path"; -import type { PluginRuntime } from "openclaw/plugin-sdk/compat"; +import type { PluginRuntime } from "openclaw/plugin-sdk/msteams"; export const msteamsRuntimeStub = { state: { diff --git a/extensions/msteams/src/token.ts b/extensions/msteams/src/token.ts index 862030ba086..5f72ae444c1 100644 --- a/extensions/msteams/src/token.ts +++ b/extensions/msteams/src/token.ts @@ -1,4 +1,4 @@ -import type { MSTeamsConfig } from "openclaw/plugin-sdk/compat"; +import type { MSTeamsConfig } from "openclaw/plugin-sdk/msteams"; import { hasConfiguredSecretInput, normalizeResolvedSecretInputString, From 20ed90f1ba5326336c7273e316c028a123fcdd5b Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 4 Mar 2026 02:33:08 -0500 Subject: [PATCH 50/74] Plugins/nextcloud-talk: migrate to scoped plugin-sdk imports --- extensions/nextcloud-talk/index.ts | 4 ++-- extensions/nextcloud-talk/src/accounts.ts | 2 +- extensions/nextcloud-talk/src/channel.ts | 2 +- extensions/nextcloud-talk/src/config-schema.ts | 2 +- extensions/nextcloud-talk/src/inbound.authz.test.ts | 2 +- extensions/nextcloud-talk/src/inbound.ts | 2 +- extensions/nextcloud-talk/src/monitor.ts | 2 +- extensions/nextcloud-talk/src/onboarding.ts | 2 +- extensions/nextcloud-talk/src/policy.ts | 4 ++-- extensions/nextcloud-talk/src/replay-guard.ts | 2 +- extensions/nextcloud-talk/src/room-info.ts | 4 ++-- extensions/nextcloud-talk/src/runtime.ts | 2 +- extensions/nextcloud-talk/src/secret-input.ts | 2 +- extensions/nextcloud-talk/src/types.ts | 2 +- 14 files changed, 17 insertions(+), 17 deletions(-) diff --git a/extensions/nextcloud-talk/index.ts b/extensions/nextcloud-talk/index.ts index 92e68fdcfb7..697a810009f 100644 --- a/extensions/nextcloud-talk/index.ts +++ b/extensions/nextcloud-talk/index.ts @@ -1,5 +1,5 @@ -import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core"; -import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/core"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/nextcloud-talk"; +import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/nextcloud-talk"; import { nextcloudTalkPlugin } from "./src/channel.js"; import { setNextcloudTalkRuntime } from "./src/runtime.js"; diff --git a/extensions/nextcloud-talk/src/accounts.ts b/extensions/nextcloud-talk/src/accounts.ts index 9ec61f59384..c2d9d8f40f0 100644 --- a/extensions/nextcloud-talk/src/accounts.ts +++ b/extensions/nextcloud-talk/src/accounts.ts @@ -7,7 +7,7 @@ import { import { listConfiguredAccountIds as listConfiguredAccountIdsFromSection, resolveAccountWithDefaultFallback, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/nextcloud-talk"; import { normalizeResolvedSecretInputString } from "./secret-input.js"; import type { CoreConfig, NextcloudTalkAccountConfig } from "./types.js"; diff --git a/extensions/nextcloud-talk/src/channel.ts b/extensions/nextcloud-talk/src/channel.ts index ac3591e3806..003a118e2ef 100644 --- a/extensions/nextcloud-talk/src/channel.ts +++ b/extensions/nextcloud-talk/src/channel.ts @@ -11,7 +11,7 @@ import { type ChannelPlugin, type OpenClawConfig, type ChannelSetupInput, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/nextcloud-talk"; import { waitForAbortSignal } from "../../../src/infra/abort-signal.js"; import { listNextcloudTalkAccountIds, diff --git a/extensions/nextcloud-talk/src/config-schema.ts b/extensions/nextcloud-talk/src/config-schema.ts index c683fb6b562..5ab3e632d22 100644 --- a/extensions/nextcloud-talk/src/config-schema.ts +++ b/extensions/nextcloud-talk/src/config-schema.ts @@ -7,7 +7,7 @@ import { ReplyRuntimeConfigSchemaShape, ToolPolicySchema, requireOpenAllowFrom, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/nextcloud-talk"; import { z } from "zod"; import { buildSecretInputSchema } from "./secret-input.js"; diff --git a/extensions/nextcloud-talk/src/inbound.authz.test.ts b/extensions/nextcloud-talk/src/inbound.authz.test.ts index be64f0968c0..188820eeb6d 100644 --- a/extensions/nextcloud-talk/src/inbound.authz.test.ts +++ b/extensions/nextcloud-talk/src/inbound.authz.test.ts @@ -1,4 +1,4 @@ -import type { PluginRuntime, RuntimeEnv } from "openclaw/plugin-sdk/compat"; +import type { PluginRuntime, RuntimeEnv } from "openclaw/plugin-sdk/nextcloud-talk"; import { describe, expect, it, vi } from "vitest"; import type { ResolvedNextcloudTalkAccount } from "./accounts.js"; import { handleNextcloudTalkInbound } from "./inbound.js"; diff --git a/extensions/nextcloud-talk/src/inbound.ts b/extensions/nextcloud-talk/src/inbound.ts index b9f9c6f98da..3b0addf257d 100644 --- a/extensions/nextcloud-talk/src/inbound.ts +++ b/extensions/nextcloud-talk/src/inbound.ts @@ -14,7 +14,7 @@ import { type OutboundReplyPayload, type OpenClawConfig, type RuntimeEnv, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/nextcloud-talk"; import type { ResolvedNextcloudTalkAccount } from "./accounts.js"; import { normalizeNextcloudTalkAllowlist, diff --git a/extensions/nextcloud-talk/src/monitor.ts b/extensions/nextcloud-talk/src/monitor.ts index fc5c0955f45..f940195a28b 100644 --- a/extensions/nextcloud-talk/src/monitor.ts +++ b/extensions/nextcloud-talk/src/monitor.ts @@ -6,7 +6,7 @@ import { isRequestBodyLimitError, readRequestBodyWithLimit, requestBodyErrorToText, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/nextcloud-talk"; import { resolveNextcloudTalkAccount } from "./accounts.js"; import { handleNextcloudTalkInbound } from "./inbound.js"; import { createNextcloudTalkReplayGuard } from "./replay-guard.js"; diff --git a/extensions/nextcloud-talk/src/onboarding.ts b/extensions/nextcloud-talk/src/onboarding.ts index a5f819d9b6a..1f07ce48162 100644 --- a/extensions/nextcloud-talk/src/onboarding.ts +++ b/extensions/nextcloud-talk/src/onboarding.ts @@ -12,7 +12,7 @@ import { type ChannelOnboardingDmPolicy, type OpenClawConfig, type WizardPrompter, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/nextcloud-talk"; import { listNextcloudTalkAccountIds, resolveDefaultNextcloudTalkAccountId, diff --git a/extensions/nextcloud-talk/src/policy.ts b/extensions/nextcloud-talk/src/policy.ts index ae1c9b1cb73..329aaeb3d40 100644 --- a/extensions/nextcloud-talk/src/policy.ts +++ b/extensions/nextcloud-talk/src/policy.ts @@ -3,14 +3,14 @@ import type { ChannelGroupContext, GroupPolicy, GroupToolPolicyConfig, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/nextcloud-talk"; import { buildChannelKeyCandidates, normalizeChannelSlug, resolveChannelEntryMatchWithFallback, resolveMentionGatingWithBypass, resolveNestedAllowlistDecision, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/nextcloud-talk"; import type { NextcloudTalkRoomConfig } from "./types.js"; function normalizeAllowEntry(raw: string): string { diff --git a/extensions/nextcloud-talk/src/replay-guard.ts b/extensions/nextcloud-talk/src/replay-guard.ts index 3291e80ed6a..8dc8477e13f 100644 --- a/extensions/nextcloud-talk/src/replay-guard.ts +++ b/extensions/nextcloud-talk/src/replay-guard.ts @@ -1,5 +1,5 @@ import path from "node:path"; -import { createPersistentDedupe } from "openclaw/plugin-sdk/compat"; +import { createPersistentDedupe } from "openclaw/plugin-sdk/nextcloud-talk"; const DEFAULT_REPLAY_TTL_MS = 24 * 60 * 60 * 1000; const DEFAULT_MEMORY_MAX_SIZE = 1_000; diff --git a/extensions/nextcloud-talk/src/room-info.ts b/extensions/nextcloud-talk/src/room-info.ts index a59195690e8..eae5a1eeb51 100644 --- a/extensions/nextcloud-talk/src/room-info.ts +++ b/extensions/nextcloud-talk/src/room-info.ts @@ -1,6 +1,6 @@ import { readFileSync } from "node:fs"; -import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/compat"; -import type { RuntimeEnv } from "openclaw/plugin-sdk/compat"; +import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/nextcloud-talk"; +import type { RuntimeEnv } from "openclaw/plugin-sdk/nextcloud-talk"; import type { ResolvedNextcloudTalkAccount } from "./accounts.js"; import { normalizeResolvedSecretInputString } from "./secret-input.js"; diff --git a/extensions/nextcloud-talk/src/runtime.ts b/extensions/nextcloud-talk/src/runtime.ts index 1a56f24de10..2a7718e1661 100644 --- a/extensions/nextcloud-talk/src/runtime.ts +++ b/extensions/nextcloud-talk/src/runtime.ts @@ -1,4 +1,4 @@ -import type { PluginRuntime } from "openclaw/plugin-sdk/compat"; +import type { PluginRuntime } from "openclaw/plugin-sdk/nextcloud-talk"; let runtime: PluginRuntime | null = null; diff --git a/extensions/nextcloud-talk/src/secret-input.ts b/extensions/nextcloud-talk/src/secret-input.ts index 3fc82b3ac91..f51a0ad6872 100644 --- a/extensions/nextcloud-talk/src/secret-input.ts +++ b/extensions/nextcloud-talk/src/secret-input.ts @@ -2,7 +2,7 @@ import { hasConfiguredSecretInput, normalizeResolvedSecretInputString, normalizeSecretInputString, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/nextcloud-talk"; import { z } from "zod"; export { hasConfiguredSecretInput, normalizeResolvedSecretInputString, normalizeSecretInputString }; diff --git a/extensions/nextcloud-talk/src/types.ts b/extensions/nextcloud-talk/src/types.ts index 3f7a9905399..a9cfbef7d06 100644 --- a/extensions/nextcloud-talk/src/types.ts +++ b/extensions/nextcloud-talk/src/types.ts @@ -4,7 +4,7 @@ import type { DmPolicy, GroupPolicy, SecretInput, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/nextcloud-talk"; export type { DmPolicy, GroupPolicy }; From 3dda4aaf08bac5b05b149dc81b9e3c9df5823a2d Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 4 Mar 2026 02:33:08 -0500 Subject: [PATCH 51/74] Plugins/nostr: migrate to scoped plugin-sdk imports --- extensions/nostr/index.ts | 4 ++-- extensions/nostr/src/channel.outbound.test.ts | 2 +- extensions/nostr/src/channel.ts | 2 +- extensions/nostr/src/config-schema.ts | 2 +- extensions/nostr/src/nostr-profile-http.ts | 2 +- extensions/nostr/src/nostr-state-store.test.ts | 2 +- extensions/nostr/src/runtime.ts | 2 +- extensions/nostr/src/types.ts | 2 +- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/extensions/nostr/index.ts b/extensions/nostr/index.ts index bcebb2fc06a..aa8901bd2b9 100644 --- a/extensions/nostr/index.ts +++ b/extensions/nostr/index.ts @@ -1,5 +1,5 @@ -import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core"; -import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/core"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/nostr"; +import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/nostr"; import { nostrPlugin } from "./src/channel.js"; import type { NostrProfile } from "./src/config-schema.js"; import { createNostrProfileHttpHandler } from "./src/nostr-profile-http.js"; diff --git a/extensions/nostr/src/channel.outbound.test.ts b/extensions/nostr/src/channel.outbound.test.ts index 9b4717136b0..96f2f29b46b 100644 --- a/extensions/nostr/src/channel.outbound.test.ts +++ b/extensions/nostr/src/channel.outbound.test.ts @@ -1,4 +1,4 @@ -import type { PluginRuntime } from "openclaw/plugin-sdk"; +import type { PluginRuntime } from "openclaw/plugin-sdk/nostr"; import { afterEach, describe, expect, it, vi } from "vitest"; import { createStartAccountContext } from "../../test-utils/start-account-context.js"; import { nostrPlugin } from "./channel.js"; diff --git a/extensions/nostr/src/channel.ts b/extensions/nostr/src/channel.ts index fef181810f2..1757d14c43d 100644 --- a/extensions/nostr/src/channel.ts +++ b/extensions/nostr/src/channel.ts @@ -5,7 +5,7 @@ import { DEFAULT_ACCOUNT_ID, formatPairingApproveHint, type ChannelPlugin, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/nostr"; import type { NostrProfile } from "./config-schema.js"; import { NostrConfigSchema } from "./config-schema.js"; import type { MetricEvent, MetricsSnapshot } from "./metrics.js"; diff --git a/extensions/nostr/src/config-schema.ts b/extensions/nostr/src/config-schema.ts index 0f94c099dca..a25868da356 100644 --- a/extensions/nostr/src/config-schema.ts +++ b/extensions/nostr/src/config-schema.ts @@ -1,4 +1,4 @@ -import { MarkdownConfigSchema, buildChannelConfigSchema } from "openclaw/plugin-sdk/compat"; +import { MarkdownConfigSchema, buildChannelConfigSchema } from "openclaw/plugin-sdk/nostr"; import { z } from "zod"; const allowFromEntry = z.union([z.string(), z.number()]); diff --git a/extensions/nostr/src/nostr-profile-http.ts b/extensions/nostr/src/nostr-profile-http.ts index d1367b0ddab..b4d53e16a4e 100644 --- a/extensions/nostr/src/nostr-profile-http.ts +++ b/extensions/nostr/src/nostr-profile-http.ts @@ -13,7 +13,7 @@ import { isBlockedHostnameOrIp, readJsonBodyWithLimit, requestBodyErrorToText, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/nostr"; import { z } from "zod"; import { publishNostrProfile, getNostrProfileState } from "./channel.js"; import { NostrProfileSchema, type NostrProfile } from "./config-schema.js"; diff --git a/extensions/nostr/src/nostr-state-store.test.ts b/extensions/nostr/src/nostr-state-store.test.ts index beb5caa0048..5ab5b0c2946 100644 --- a/extensions/nostr/src/nostr-state-store.test.ts +++ b/extensions/nostr/src/nostr-state-store.test.ts @@ -1,7 +1,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import type { PluginRuntime } from "openclaw/plugin-sdk/compat"; +import type { PluginRuntime } from "openclaw/plugin-sdk/nostr"; import { describe, expect, it } from "vitest"; import { readNostrBusState, diff --git a/extensions/nostr/src/runtime.ts b/extensions/nostr/src/runtime.ts index e3e2e7028b0..dbcffde4979 100644 --- a/extensions/nostr/src/runtime.ts +++ b/extensions/nostr/src/runtime.ts @@ -1,4 +1,4 @@ -import type { PluginRuntime } from "openclaw/plugin-sdk/compat"; +import type { PluginRuntime } from "openclaw/plugin-sdk/nostr"; let runtime: PluginRuntime | null = null; diff --git a/extensions/nostr/src/types.ts b/extensions/nostr/src/types.ts index 9e25fb6f392..9baf78a0ca8 100644 --- a/extensions/nostr/src/types.ts +++ b/extensions/nostr/src/types.ts @@ -3,7 +3,7 @@ import { normalizeAccountId, normalizeOptionalAccountId, } from "openclaw/plugin-sdk/account-id"; -import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/nostr"; import type { NostrProfile } from "./config-schema.js"; import { getPublicKeyFromPrivate } from "./nostr-bus.js"; import { DEFAULT_RELAYS } from "./nostr-bus.js"; From c1c1af9d7bd4fbc334ef03e6d9993ffe5148d0e6 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 4 Mar 2026 02:33:09 -0500 Subject: [PATCH 52/74] Plugins/open-prose: migrate to scoped plugin-sdk imports --- extensions/open-prose/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/open-prose/index.ts b/extensions/open-prose/index.ts index 8b02c30fb5b..76fa2b18f9e 100644 --- a/extensions/open-prose/index.ts +++ b/extensions/open-prose/index.ts @@ -1,4 +1,4 @@ -import type { OpenClawPluginApi } from "../../src/plugins/types.js"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/open-prose"; export default function register(_api: OpenClawPluginApi) { // OpenProse is delivered via plugin-shipped skills. From 71e62a77e8d45c675c01cf4096f74e2c696d7f54 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 4 Mar 2026 02:33:10 -0500 Subject: [PATCH 53/74] Plugins/phone-control: migrate to scoped plugin-sdk imports --- extensions/phone-control/index.test.ts | 4 ++-- extensions/phone-control/index.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/extensions/phone-control/index.test.ts b/extensions/phone-control/index.test.ts index 4711400c700..a4d05e3d431 100644 --- a/extensions/phone-control/index.test.ts +++ b/extensions/phone-control/index.test.ts @@ -1,12 +1,12 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import { describe, expect, it, vi } from "vitest"; import type { OpenClawPluginApi, OpenClawPluginCommandDefinition, PluginCommandContext, -} from "../../src/plugins/types.js"; +} from "openclaw/plugin-sdk/phone-control"; +import { describe, expect, it, vi } from "vitest"; import registerPhoneControl from "./index.js"; function createApi(params: { diff --git a/extensions/phone-control/index.ts b/extensions/phone-control/index.ts index f2f9acac892..7b63b67b10c 100644 --- a/extensions/phone-control/index.ts +++ b/extensions/phone-control/index.ts @@ -1,6 +1,6 @@ import fs from "node:fs/promises"; import path from "node:path"; -import type { OpenClawPluginApi, OpenClawPluginService } from "openclaw/plugin-sdk/core"; +import type { OpenClawPluginApi, OpenClawPluginService } from "openclaw/plugin-sdk/phone-control"; type ArmGroup = "camera" | "screen" | "writes" | "all"; From 6521965e40de51c50438dfcc90c262916a880670 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 4 Mar 2026 02:33:11 -0500 Subject: [PATCH 54/74] Plugins/qwen-portal-auth: migrate to scoped plugin-sdk imports --- extensions/qwen-portal-auth/index.ts | 2 +- extensions/qwen-portal-auth/oauth.ts | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/extensions/qwen-portal-auth/index.ts b/extensions/qwen-portal-auth/index.ts index 6cbbe8dd9c8..c592c0e223c 100644 --- a/extensions/qwen-portal-auth/index.ts +++ b/extensions/qwen-portal-auth/index.ts @@ -2,7 +2,7 @@ import { emptyPluginConfigSchema, type OpenClawPluginApi, type ProviderAuthContext, -} from "openclaw/plugin-sdk/core"; +} from "openclaw/plugin-sdk/qwen-portal-auth"; import { loginQwenPortalOAuth } from "./oauth.js"; const PROVIDER_ID = "qwen-portal"; diff --git a/extensions/qwen-portal-auth/oauth.ts b/extensions/qwen-portal-auth/oauth.ts index 9b5129bb45e..cdb8ab1bc36 100644 --- a/extensions/qwen-portal-auth/oauth.ts +++ b/extensions/qwen-portal-auth/oauth.ts @@ -1,5 +1,8 @@ import { randomUUID } from "node:crypto"; -import { generatePkceVerifierChallenge, toFormUrlEncoded } from "openclaw/plugin-sdk/compat"; +import { + generatePkceVerifierChallenge, + toFormUrlEncoded, +} from "openclaw/plugin-sdk/qwen-portal-auth"; const QWEN_OAUTH_BASE_URL = "https://chat.qwen.ai"; const QWEN_OAUTH_DEVICE_CODE_ENDPOINT = `${QWEN_OAUTH_BASE_URL}/api/v1/oauth2/device/code`; From 65ffa676a51f58901eae0c64403ac8dc4e2b3734 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 4 Mar 2026 02:33:12 -0500 Subject: [PATCH 55/74] Plugins/synology-chat: migrate to scoped plugin-sdk imports --- extensions/synology-chat/index.ts | 4 ++-- extensions/synology-chat/src/channel.integration.test.ts | 4 ++-- extensions/synology-chat/src/channel.test.ts | 4 ++-- extensions/synology-chat/src/channel.ts | 2 +- extensions/synology-chat/src/runtime.ts | 2 +- extensions/synology-chat/src/security.ts | 2 +- extensions/synology-chat/src/webhook-handler.ts | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/extensions/synology-chat/index.ts b/extensions/synology-chat/index.ts index 87b752bbb33..69dbfb9edbf 100644 --- a/extensions/synology-chat/index.ts +++ b/extensions/synology-chat/index.ts @@ -1,5 +1,5 @@ -import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core"; -import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/core"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/synology-chat"; +import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/synology-chat"; import { createSynologyChatPlugin } from "./src/channel.js"; import { setSynologyRuntime } from "./src/runtime.js"; diff --git a/extensions/synology-chat/src/channel.integration.test.ts b/extensions/synology-chat/src/channel.integration.test.ts index 338efbc7676..b9cb5484621 100644 --- a/extensions/synology-chat/src/channel.integration.test.ts +++ b/extensions/synology-chat/src/channel.integration.test.ts @@ -11,8 +11,8 @@ type RegisteredRoute = { const registerPluginHttpRouteMock = vi.fn<(params: RegisteredRoute) => () => void>(() => vi.fn()); const dispatchReplyWithBufferedBlockDispatcher = vi.fn().mockResolvedValue({ counts: {} }); -vi.mock("openclaw/plugin-sdk/compat", async (importOriginal) => { - const actual = await importOriginal(); +vi.mock("openclaw/plugin-sdk/synology-chat", async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, DEFAULT_ACCOUNT_ID: "default", diff --git a/extensions/synology-chat/src/channel.test.ts b/extensions/synology-chat/src/channel.test.ts index af5d1ed78b0..713ecf7f8c3 100644 --- a/extensions/synology-chat/src/channel.test.ts +++ b/extensions/synology-chat/src/channel.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; // Mock external dependencies -vi.mock("openclaw/plugin-sdk/compat", () => ({ +vi.mock("openclaw/plugin-sdk/synology-chat", () => ({ DEFAULT_ACCOUNT_ID: "default", setAccountEnabledInConfigSection: vi.fn((_opts: any) => ({})), registerPluginHttpRoute: vi.fn(() => vi.fn()), @@ -44,7 +44,7 @@ vi.mock("zod", () => ({ })); const { createSynologyChatPlugin } = await import("./channel.js"); -const { registerPluginHttpRoute } = await import("openclaw/plugin-sdk/compat"); +const { registerPluginHttpRoute } = await import("openclaw/plugin-sdk/synology-chat"); describe("createSynologyChatPlugin", () => { it("returns a plugin object with all required sections", () => { diff --git a/extensions/synology-chat/src/channel.ts b/extensions/synology-chat/src/channel.ts index ed003d69a9d..81ef191ba77 100644 --- a/extensions/synology-chat/src/channel.ts +++ b/extensions/synology-chat/src/channel.ts @@ -9,7 +9,7 @@ import { setAccountEnabledInConfigSection, registerPluginHttpRoute, buildChannelConfigSchema, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/synology-chat"; import { z } from "zod"; import { listAccountIds, resolveAccount } from "./accounts.js"; import { sendMessage, sendFileUrl } from "./client.js"; diff --git a/extensions/synology-chat/src/runtime.ts b/extensions/synology-chat/src/runtime.ts index a27e67d77ec..f7ef39ff65f 100644 --- a/extensions/synology-chat/src/runtime.ts +++ b/extensions/synology-chat/src/runtime.ts @@ -4,7 +4,7 @@ * Used by channel.ts to access dispatch functions. */ -import type { PluginRuntime } from "openclaw/plugin-sdk/compat"; +import type { PluginRuntime } from "openclaw/plugin-sdk/synology-chat"; let runtime: PluginRuntime | null = null; diff --git a/extensions/synology-chat/src/security.ts b/extensions/synology-chat/src/security.ts index dd4bed35b29..5b661eb6b84 100644 --- a/extensions/synology-chat/src/security.ts +++ b/extensions/synology-chat/src/security.ts @@ -6,7 +6,7 @@ import * as crypto from "node:crypto"; import { createFixedWindowRateLimiter, type FixedWindowRateLimiter, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/synology-chat"; export type DmAuthorizationResult = | { allowed: true } diff --git a/extensions/synology-chat/src/webhook-handler.ts b/extensions/synology-chat/src/webhook-handler.ts index aa0e3187bc1..fab4b9a0238 100644 --- a/extensions/synology-chat/src/webhook-handler.ts +++ b/extensions/synology-chat/src/webhook-handler.ts @@ -9,7 +9,7 @@ import { isRequestBodyLimitError, readRequestBodyWithLimit, requestBodyErrorToText, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/synology-chat"; import { sendMessage, resolveChatUserId } from "./client.js"; import { validateToken, authorizeUserForDm, sanitizeInput, RateLimiter } from "./security.js"; import type { SynologyWebhookPayload, ResolvedSynologyChatAccount } from "./types.js"; From f006c5f5c1125b75b5f193906957e3a792a0ef5b Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 4 Mar 2026 02:33:12 -0500 Subject: [PATCH 56/74] Plugins/talk-voice: migrate to scoped plugin-sdk imports --- extensions/talk-voice/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/talk-voice/index.ts b/extensions/talk-voice/index.ts index 328e69a8f87..4473fa05ea9 100644 --- a/extensions/talk-voice/index.ts +++ b/extensions/talk-voice/index.ts @@ -1,4 +1,4 @@ -import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/talk-voice"; type ElevenLabsVoice = { voice_id: string; From 8377dbba309b204ba6b1f2b3a370c2eae8a3ff55 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 4 Mar 2026 02:33:13 -0500 Subject: [PATCH 57/74] Plugins/test-utils: migrate to scoped plugin-sdk imports --- extensions/test-utils/plugin-runtime-mock.ts | 4 ++-- extensions/test-utils/runtime-env.ts | 2 +- extensions/test-utils/start-account-context.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/extensions/test-utils/plugin-runtime-mock.ts b/extensions/test-utils/plugin-runtime-mock.ts index bc2d97c4bac..f01c87d6c77 100644 --- a/extensions/test-utils/plugin-runtime-mock.ts +++ b/extensions/test-utils/plugin-runtime-mock.ts @@ -1,5 +1,5 @@ -import type { PluginRuntime } from "openclaw/plugin-sdk/compat"; -import { removeAckReactionAfterReply, shouldAckReaction } from "openclaw/plugin-sdk/compat"; +import type { PluginRuntime } from "openclaw/plugin-sdk/test-utils"; +import { removeAckReactionAfterReply, shouldAckReaction } from "openclaw/plugin-sdk/test-utils"; import { vi } from "vitest"; type DeepPartial = { diff --git a/extensions/test-utils/runtime-env.ts b/extensions/test-utils/runtime-env.ts index ef67c61429a..a5e52665b0e 100644 --- a/extensions/test-utils/runtime-env.ts +++ b/extensions/test-utils/runtime-env.ts @@ -1,4 +1,4 @@ -import type { RuntimeEnv } from "openclaw/plugin-sdk/compat"; +import type { RuntimeEnv } from "openclaw/plugin-sdk/test-utils"; import { vi } from "vitest"; export function createRuntimeEnv(): RuntimeEnv { diff --git a/extensions/test-utils/start-account-context.ts b/extensions/test-utils/start-account-context.ts index 179444b445c..a878b3dbfd9 100644 --- a/extensions/test-utils/start-account-context.ts +++ b/extensions/test-utils/start-account-context.ts @@ -2,7 +2,7 @@ import type { ChannelAccountSnapshot, ChannelGatewayContext, OpenClawConfig, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/test-utils"; import { vi } from "vitest"; import { createRuntimeEnv } from "./runtime-env.js"; From 7c96d821129ad46e0a4f61e6e2d97923d95b9693 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 4 Mar 2026 02:33:14 -0500 Subject: [PATCH 58/74] Plugins/thread-ownership: migrate to scoped plugin-sdk imports --- extensions/thread-ownership/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/thread-ownership/index.ts b/extensions/thread-ownership/index.ts index 1960b067f28..f0d2cb6291b 100644 --- a/extensions/thread-ownership/index.ts +++ b/extensions/thread-ownership/index.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig, OpenClawPluginApi } from "openclaw/plugin-sdk/core"; +import type { OpenClawConfig, OpenClawPluginApi } from "openclaw/plugin-sdk/thread-ownership"; type ThreadOwnershipConfig = { forwarderUrl?: string; From 72e774431c1e425e34b64574aa429f9f88a2ceba Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 4 Mar 2026 02:33:15 -0500 Subject: [PATCH 59/74] Plugins/tlon: migrate to scoped plugin-sdk imports --- extensions/tlon/index.ts | 4 ++-- extensions/tlon/src/channel.ts | 8 ++++---- extensions/tlon/src/config-schema.ts | 2 +- extensions/tlon/src/monitor/discovery.ts | 2 +- extensions/tlon/src/monitor/history.ts | 2 +- extensions/tlon/src/monitor/index.ts | 4 ++-- extensions/tlon/src/monitor/media.ts | 2 +- extensions/tlon/src/monitor/processed-messages.ts | 2 +- extensions/tlon/src/onboarding.ts | 4 ++-- extensions/tlon/src/runtime.ts | 2 +- extensions/tlon/src/types.ts | 2 +- extensions/tlon/src/urbit/auth.ssrf.test.ts | 4 ++-- extensions/tlon/src/urbit/auth.ts | 2 +- extensions/tlon/src/urbit/base-url.ts | 2 +- extensions/tlon/src/urbit/channel-ops.ts | 2 +- extensions/tlon/src/urbit/context.ts | 2 +- extensions/tlon/src/urbit/fetch.ts | 4 ++-- extensions/tlon/src/urbit/sse-client.ts | 2 +- extensions/tlon/src/urbit/upload.test.ts | 14 +++++++------- extensions/tlon/src/urbit/upload.ts | 2 +- 20 files changed, 34 insertions(+), 34 deletions(-) diff --git a/extensions/tlon/index.ts b/extensions/tlon/index.ts index 5179c74c61d..4365253a1fc 100644 --- a/extensions/tlon/index.ts +++ b/extensions/tlon/index.ts @@ -2,8 +2,8 @@ import { spawn } from "node:child_process"; import { existsSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; -import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core"; -import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/core"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/tlon"; +import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/tlon"; import { tlonPlugin } from "./src/channel.js"; import { setTlonRuntime } from "./src/runtime.js"; diff --git a/extensions/tlon/src/channel.ts b/extensions/tlon/src/channel.ts index 3432973c7d5..3c5bedbf841 100644 --- a/extensions/tlon/src/channel.ts +++ b/extensions/tlon/src/channel.ts @@ -5,12 +5,12 @@ import type { ChannelPlugin, ChannelSetupInput, OpenClawConfig, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/tlon"; import { applyAccountNameToChannelSection, DEFAULT_ACCOUNT_ID, normalizeAccountId, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/tlon"; import { buildTlonAccountFields } from "./account-fields.js"; import { tlonChannelConfigSchema } from "./config-schema.js"; import { monitorTlonProvider } from "./monitor/index.js"; @@ -497,7 +497,7 @@ export const tlonPlugin: ChannelPlugin = { lastError: runtime?.lastError ?? null, probe, }; - return snapshot as import("openclaw/plugin-sdk/compat").ChannelAccountSnapshot; + return snapshot as import("openclaw/plugin-sdk/tlon").ChannelAccountSnapshot; }, }, gateway: { @@ -507,7 +507,7 @@ export const tlonPlugin: ChannelPlugin = { accountId: account.accountId, ship: account.ship, url: account.url, - } as import("openclaw/plugin-sdk/compat").ChannelAccountSnapshot); + } as import("openclaw/plugin-sdk/tlon").ChannelAccountSnapshot); ctx.log?.info(`[${account.accountId}] starting Tlon provider for ${account.ship ?? "tlon"}`); return monitorTlonProvider({ runtime: ctx.runtime, diff --git a/extensions/tlon/src/config-schema.ts b/extensions/tlon/src/config-schema.ts index 8bcf8300069..666f65e35da 100644 --- a/extensions/tlon/src/config-schema.ts +++ b/extensions/tlon/src/config-schema.ts @@ -1,4 +1,4 @@ -import { buildChannelConfigSchema } from "openclaw/plugin-sdk/compat"; +import { buildChannelConfigSchema } from "openclaw/plugin-sdk/tlon"; import { z } from "zod"; const ShipSchema = z.string().min(1); diff --git a/extensions/tlon/src/monitor/discovery.ts b/extensions/tlon/src/monitor/discovery.ts index ae0ea47d7b9..a7224608bf0 100644 --- a/extensions/tlon/src/monitor/discovery.ts +++ b/extensions/tlon/src/monitor/discovery.ts @@ -1,4 +1,4 @@ -import type { RuntimeEnv } from "openclaw/plugin-sdk/compat"; +import type { RuntimeEnv } from "openclaw/plugin-sdk/tlon"; import type { Foreigns } from "../urbit/foreigns.js"; import { formatChangesDate } from "./utils.js"; diff --git a/extensions/tlon/src/monitor/history.ts b/extensions/tlon/src/monitor/history.ts index 0636c102f7f..a67fae7ada4 100644 --- a/extensions/tlon/src/monitor/history.ts +++ b/extensions/tlon/src/monitor/history.ts @@ -1,4 +1,4 @@ -import type { RuntimeEnv } from "openclaw/plugin-sdk/compat"; +import type { RuntimeEnv } from "openclaw/plugin-sdk/tlon"; import { extractMessageText } from "./utils.js"; /** diff --git a/extensions/tlon/src/monitor/index.ts b/extensions/tlon/src/monitor/index.ts index 3d12393cd90..a9291878101 100644 --- a/extensions/tlon/src/monitor/index.ts +++ b/extensions/tlon/src/monitor/index.ts @@ -1,5 +1,5 @@ -import type { RuntimeEnv, ReplyPayload, OpenClawConfig } from "openclaw/plugin-sdk/compat"; -import { createLoggerBackedRuntime, createReplyPrefixOptions } from "openclaw/plugin-sdk/compat"; +import type { RuntimeEnv, ReplyPayload, OpenClawConfig } from "openclaw/plugin-sdk/tlon"; +import { createLoggerBackedRuntime, createReplyPrefixOptions } from "openclaw/plugin-sdk/tlon"; import { getTlonRuntime } from "../runtime.js"; import { createSettingsManager, type TlonSettingsStore } from "../settings.js"; import { normalizeShip, parseChannelNest } from "../targets.js"; diff --git a/extensions/tlon/src/monitor/media.ts b/extensions/tlon/src/monitor/media.ts index e8301976a85..588598e4d2d 100644 --- a/extensions/tlon/src/monitor/media.ts +++ b/extensions/tlon/src/monitor/media.ts @@ -5,7 +5,7 @@ import { homedir } from "node:os"; import * as path from "node:path"; import { Readable } from "node:stream"; import { pipeline } from "node:stream/promises"; -import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/compat"; +import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/tlon"; import { getDefaultSsrFPolicy } from "../urbit/context.js"; // Default to OpenClaw workspace media directory diff --git a/extensions/tlon/src/monitor/processed-messages.ts b/extensions/tlon/src/monitor/processed-messages.ts index e2a533ee7da..d849724c4a5 100644 --- a/extensions/tlon/src/monitor/processed-messages.ts +++ b/extensions/tlon/src/monitor/processed-messages.ts @@ -1,4 +1,4 @@ -import { createDedupeCache } from "openclaw/plugin-sdk/compat"; +import { createDedupeCache } from "openclaw/plugin-sdk/tlon"; export type ProcessedMessageTracker = { mark: (id?: string | null) => boolean; diff --git a/extensions/tlon/src/onboarding.ts b/extensions/tlon/src/onboarding.ts index f0b84ab5bef..39256e34362 100644 --- a/extensions/tlon/src/onboarding.ts +++ b/extensions/tlon/src/onboarding.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/tlon"; import { formatDocsLink, promptAccountId, @@ -6,7 +6,7 @@ import { normalizeAccountId, type ChannelOnboardingAdapter, type WizardPrompter, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/tlon"; import { buildTlonAccountFields } from "./account-fields.js"; import type { TlonResolvedAccount } from "./types.js"; import { listTlonAccountIds, resolveTlonAccount } from "./types.js"; diff --git a/extensions/tlon/src/runtime.ts b/extensions/tlon/src/runtime.ts index 79ad7a872b9..0400d636b57 100644 --- a/extensions/tlon/src/runtime.ts +++ b/extensions/tlon/src/runtime.ts @@ -1,4 +1,4 @@ -import type { PluginRuntime } from "openclaw/plugin-sdk/compat"; +import type { PluginRuntime } from "openclaw/plugin-sdk/tlon"; let runtime: PluginRuntime | null = null; diff --git a/extensions/tlon/src/types.ts b/extensions/tlon/src/types.ts index 4352e88bb63..e9bc27ac169 100644 --- a/extensions/tlon/src/types.ts +++ b/extensions/tlon/src/types.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/tlon"; export type TlonResolvedAccount = { accountId: string; diff --git a/extensions/tlon/src/urbit/auth.ssrf.test.ts b/extensions/tlon/src/urbit/auth.ssrf.test.ts index f28e7e217e1..18dd6142ad3 100644 --- a/extensions/tlon/src/urbit/auth.ssrf.test.ts +++ b/extensions/tlon/src/urbit/auth.ssrf.test.ts @@ -1,5 +1,5 @@ -import type { LookupFn } from "openclaw/plugin-sdk/compat"; -import { SsrFBlockedError } from "openclaw/plugin-sdk/compat"; +import type { LookupFn } from "openclaw/plugin-sdk/tlon"; +import { SsrFBlockedError } from "openclaw/plugin-sdk/tlon"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { authenticate } from "./auth.js"; diff --git a/extensions/tlon/src/urbit/auth.ts b/extensions/tlon/src/urbit/auth.ts index 7ae150980a1..3b7ccd16593 100644 --- a/extensions/tlon/src/urbit/auth.ts +++ b/extensions/tlon/src/urbit/auth.ts @@ -1,4 +1,4 @@ -import type { LookupFn, SsrFPolicy } from "openclaw/plugin-sdk/compat"; +import type { LookupFn, SsrFPolicy } from "openclaw/plugin-sdk/tlon"; import { UrbitAuthError } from "./errors.js"; import { urbitFetch } from "./fetch.js"; diff --git a/extensions/tlon/src/urbit/base-url.ts b/extensions/tlon/src/urbit/base-url.ts index 46619449315..e90168b47a9 100644 --- a/extensions/tlon/src/urbit/base-url.ts +++ b/extensions/tlon/src/urbit/base-url.ts @@ -1,4 +1,4 @@ -import { isBlockedHostnameOrIp } from "openclaw/plugin-sdk/compat"; +import { isBlockedHostnameOrIp } from "openclaw/plugin-sdk/tlon"; export type UrbitBaseUrlValidation = | { ok: true; baseUrl: string; hostname: string } diff --git a/extensions/tlon/src/urbit/channel-ops.ts b/extensions/tlon/src/urbit/channel-ops.ts index c58652b62eb..f5401d3bb73 100644 --- a/extensions/tlon/src/urbit/channel-ops.ts +++ b/extensions/tlon/src/urbit/channel-ops.ts @@ -1,4 +1,4 @@ -import type { LookupFn, SsrFPolicy } from "openclaw/plugin-sdk/compat"; +import type { LookupFn, SsrFPolicy } from "openclaw/plugin-sdk/tlon"; import { UrbitHttpError } from "./errors.js"; import { urbitFetch } from "./fetch.js"; diff --git a/extensions/tlon/src/urbit/context.ts b/extensions/tlon/src/urbit/context.ts index 25381df8e75..6fbae002f5d 100644 --- a/extensions/tlon/src/urbit/context.ts +++ b/extensions/tlon/src/urbit/context.ts @@ -1,4 +1,4 @@ -import type { SsrFPolicy } from "openclaw/plugin-sdk/compat"; +import type { SsrFPolicy } from "openclaw/plugin-sdk/tlon"; import { validateUrbitBaseUrl } from "./base-url.js"; import { UrbitUrlError } from "./errors.js"; diff --git a/extensions/tlon/src/urbit/fetch.ts b/extensions/tlon/src/urbit/fetch.ts index 1c60a0b6dd2..a1551df547d 100644 --- a/extensions/tlon/src/urbit/fetch.ts +++ b/extensions/tlon/src/urbit/fetch.ts @@ -1,5 +1,5 @@ -import type { LookupFn, SsrFPolicy } from "openclaw/plugin-sdk/compat"; -import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/compat"; +import type { LookupFn, SsrFPolicy } from "openclaw/plugin-sdk/tlon"; +import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/tlon"; import { validateUrbitBaseUrl } from "./base-url.js"; import { UrbitUrlError } from "./errors.js"; diff --git a/extensions/tlon/src/urbit/sse-client.ts b/extensions/tlon/src/urbit/sse-client.ts index 94056e523e3..ab12977d0e8 100644 --- a/extensions/tlon/src/urbit/sse-client.ts +++ b/extensions/tlon/src/urbit/sse-client.ts @@ -1,6 +1,6 @@ import { randomUUID } from "node:crypto"; import { Readable } from "node:stream"; -import type { LookupFn, SsrFPolicy } from "openclaw/plugin-sdk/compat"; +import type { LookupFn, SsrFPolicy } from "openclaw/plugin-sdk/tlon"; import { ensureUrbitChannelOpen, pokeUrbitChannel, scryUrbitPath } from "./channel-ops.js"; import { getUrbitContext, normalizeUrbitCookie } from "./context.js"; import { urbitFetch } from "./fetch.js"; diff --git a/extensions/tlon/src/urbit/upload.test.ts b/extensions/tlon/src/urbit/upload.test.ts index 0f078669859..ca95a0412d4 100644 --- a/extensions/tlon/src/urbit/upload.test.ts +++ b/extensions/tlon/src/urbit/upload.test.ts @@ -1,8 +1,8 @@ import { describe, expect, it, vi, afterEach, beforeEach } from "vitest"; // Mock fetchWithSsrFGuard from plugin-sdk -vi.mock("openclaw/plugin-sdk/compat", async (importOriginal) => { - const actual = await importOriginal(); +vi.mock("openclaw/plugin-sdk/tlon", async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, fetchWithSsrFGuard: vi.fn(), @@ -24,7 +24,7 @@ describe("uploadImageFromUrl", () => { }); it("fetches image and calls uploadFile, returns uploaded URL", async () => { - const { fetchWithSsrFGuard } = await import("openclaw/plugin-sdk/compat"); + const { fetchWithSsrFGuard } = await import("openclaw/plugin-sdk/tlon"); const mockFetch = vi.mocked(fetchWithSsrFGuard); const { uploadFile } = await import("@tloncorp/api"); @@ -59,7 +59,7 @@ describe("uploadImageFromUrl", () => { }); it("returns original URL if fetch fails", async () => { - const { fetchWithSsrFGuard } = await import("openclaw/plugin-sdk/compat"); + const { fetchWithSsrFGuard } = await import("openclaw/plugin-sdk/tlon"); const mockFetch = vi.mocked(fetchWithSsrFGuard); // Mock fetchWithSsrFGuard to return a failed response @@ -79,7 +79,7 @@ describe("uploadImageFromUrl", () => { }); it("returns original URL if upload fails", async () => { - const { fetchWithSsrFGuard } = await import("openclaw/plugin-sdk/compat"); + const { fetchWithSsrFGuard } = await import("openclaw/plugin-sdk/tlon"); const mockFetch = vi.mocked(fetchWithSsrFGuard); const { uploadFile } = await import("@tloncorp/api"); @@ -127,7 +127,7 @@ describe("uploadImageFromUrl", () => { }); it("extracts filename from URL path", async () => { - const { fetchWithSsrFGuard } = await import("openclaw/plugin-sdk/compat"); + const { fetchWithSsrFGuard } = await import("openclaw/plugin-sdk/tlon"); const mockFetch = vi.mocked(fetchWithSsrFGuard); const { uploadFile } = await import("@tloncorp/api"); @@ -157,7 +157,7 @@ describe("uploadImageFromUrl", () => { }); it("uses default filename when URL has no path", async () => { - const { fetchWithSsrFGuard } = await import("openclaw/plugin-sdk/compat"); + const { fetchWithSsrFGuard } = await import("openclaw/plugin-sdk/tlon"); const mockFetch = vi.mocked(fetchWithSsrFGuard); const { uploadFile } = await import("@tloncorp/api"); diff --git a/extensions/tlon/src/urbit/upload.ts b/extensions/tlon/src/urbit/upload.ts index 78c9c706e7c..81aaef84a06 100644 --- a/extensions/tlon/src/urbit/upload.ts +++ b/extensions/tlon/src/urbit/upload.ts @@ -2,7 +2,7 @@ * Upload an image from a URL to Tlon storage. */ import { uploadFile } from "@tloncorp/api"; -import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/compat"; +import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/tlon"; import { getDefaultSsrFPolicy } from "./context.js"; /** From a9af9334868da48869719ed9640abc6142bd1a90 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 4 Mar 2026 02:33:15 -0500 Subject: [PATCH 60/74] Plugins/twitch: migrate to scoped plugin-sdk imports --- extensions/twitch/index.ts | 4 ++-- extensions/twitch/src/config-schema.ts | 2 +- extensions/twitch/src/config.ts | 2 +- extensions/twitch/src/monitor.ts | 4 ++-- extensions/twitch/src/onboarding.test.ts | 4 ++-- extensions/twitch/src/onboarding.ts | 4 ++-- extensions/twitch/src/plugin.test.ts | 2 +- extensions/twitch/src/plugin.ts | 4 ++-- extensions/twitch/src/probe.ts | 2 +- extensions/twitch/src/runtime.ts | 2 +- extensions/twitch/src/send.ts | 2 +- extensions/twitch/src/status.ts | 2 +- extensions/twitch/src/test-fixtures.ts | 2 +- extensions/twitch/src/token.test.ts | 2 +- extensions/twitch/src/twitch-client.ts | 2 +- 15 files changed, 20 insertions(+), 20 deletions(-) diff --git a/extensions/twitch/index.ts b/extensions/twitch/index.ts index 7cf7b7f85e8..cbdb20bff4d 100644 --- a/extensions/twitch/index.ts +++ b/extensions/twitch/index.ts @@ -1,5 +1,5 @@ -import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core"; -import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/core"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/twitch"; +import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/twitch"; import { twitchPlugin } from "./src/plugin.js"; import { setTwitchRuntime } from "./src/runtime.js"; diff --git a/extensions/twitch/src/config-schema.ts b/extensions/twitch/src/config-schema.ts index 2542591d8f9..1b45004ba6b 100644 --- a/extensions/twitch/src/config-schema.ts +++ b/extensions/twitch/src/config-schema.ts @@ -1,4 +1,4 @@ -import { MarkdownConfigSchema } from "openclaw/plugin-sdk/compat"; +import { MarkdownConfigSchema } from "openclaw/plugin-sdk/twitch"; import { z } from "zod"; /** diff --git a/extensions/twitch/src/config.ts b/extensions/twitch/src/config.ts index 7b65cce7e9a..de960f4dc8a 100644 --- a/extensions/twitch/src/config.ts +++ b/extensions/twitch/src/config.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/twitch"; import type { TwitchAccountConfig } from "./types.js"; /** diff --git a/extensions/twitch/src/monitor.ts b/extensions/twitch/src/monitor.ts index d70c04cc2d8..f5c3d690b52 100644 --- a/extensions/twitch/src/monitor.ts +++ b/extensions/twitch/src/monitor.ts @@ -5,8 +5,8 @@ * resolves agent routes, and handles replies. */ -import type { ReplyPayload, OpenClawConfig } from "openclaw/plugin-sdk/compat"; -import { createReplyPrefixOptions } from "openclaw/plugin-sdk/compat"; +import type { ReplyPayload, OpenClawConfig } from "openclaw/plugin-sdk/twitch"; +import { createReplyPrefixOptions } from "openclaw/plugin-sdk/twitch"; import { checkTwitchAccessControl } from "./access-control.js"; import { getOrCreateClientManager } from "./client-manager-registry.js"; import { getTwitchRuntime } from "./runtime.js"; diff --git a/extensions/twitch/src/onboarding.test.ts b/extensions/twitch/src/onboarding.test.ts index 4df95f39fb3..b8946eefc49 100644 --- a/extensions/twitch/src/onboarding.test.ts +++ b/extensions/twitch/src/onboarding.test.ts @@ -11,11 +11,11 @@ * - setTwitchAccount config updates */ -import type { WizardPrompter } from "openclaw/plugin-sdk/compat"; +import type { WizardPrompter } from "openclaw/plugin-sdk/twitch"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { TwitchAccountConfig } from "./types.js"; -vi.mock("openclaw/plugin-sdk", () => ({ +vi.mock("openclaw/plugin-sdk/twitch", () => ({ formatDocsLink: (url: string, fallback: string) => fallback || url, promptChannelAccessConfig: vi.fn(async () => null), })); diff --git a/extensions/twitch/src/onboarding.ts b/extensions/twitch/src/onboarding.ts index 6148f165fb4..060857bf383 100644 --- a/extensions/twitch/src/onboarding.ts +++ b/extensions/twitch/src/onboarding.ts @@ -2,14 +2,14 @@ * Twitch onboarding adapter for CLI setup wizard. */ -import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/twitch"; import { formatDocsLink, promptChannelAccessConfig, type ChannelOnboardingAdapter, type ChannelOnboardingDmPolicy, type WizardPrompter, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/twitch"; import { DEFAULT_ACCOUNT_ID, getAccountConfig } from "./config.js"; import type { TwitchAccountConfig, TwitchRole } from "./types.js"; import { isAccountConfigured } from "./utils/twitch.js"; diff --git a/extensions/twitch/src/plugin.test.ts b/extensions/twitch/src/plugin.test.ts index fa1a9a51d39..cc52a7ca7c2 100644 --- a/extensions/twitch/src/plugin.test.ts +++ b/extensions/twitch/src/plugin.test.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/twitch"; import { describe, expect, it } from "vitest"; import { twitchPlugin } from "./plugin.js"; diff --git a/extensions/twitch/src/plugin.ts b/extensions/twitch/src/plugin.ts index 25776af9c7b..f6cf576b6a0 100644 --- a/extensions/twitch/src/plugin.ts +++ b/extensions/twitch/src/plugin.ts @@ -5,8 +5,8 @@ * This is the primary entry point for the Twitch channel integration. */ -import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; -import { buildChannelConfigSchema } from "openclaw/plugin-sdk/compat"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/twitch"; +import { buildChannelConfigSchema } from "openclaw/plugin-sdk/twitch"; import { twitchMessageActions } from "./actions.js"; import { removeClientManager } from "./client-manager-registry.js"; import { TwitchConfigSchema } from "./config-schema.js"; diff --git a/extensions/twitch/src/probe.ts b/extensions/twitch/src/probe.ts index 8a55f2425c8..7ce02501007 100644 --- a/extensions/twitch/src/probe.ts +++ b/extensions/twitch/src/probe.ts @@ -1,6 +1,6 @@ import { StaticAuthProvider } from "@twurple/auth"; import { ChatClient } from "@twurple/chat"; -import type { BaseProbeResult } from "openclaw/plugin-sdk/compat"; +import type { BaseProbeResult } from "openclaw/plugin-sdk/twitch"; import type { TwitchAccountConfig } from "./types.js"; import { normalizeToken } from "./utils/twitch.js"; diff --git a/extensions/twitch/src/runtime.ts b/extensions/twitch/src/runtime.ts index f20cbbf475d..5dfdd225c4c 100644 --- a/extensions/twitch/src/runtime.ts +++ b/extensions/twitch/src/runtime.ts @@ -1,4 +1,4 @@ -import type { PluginRuntime } from "openclaw/plugin-sdk/compat"; +import type { PluginRuntime } from "openclaw/plugin-sdk/twitch"; let runtime: PluginRuntime | null = null; diff --git a/extensions/twitch/src/send.ts b/extensions/twitch/src/send.ts index 67f8452296b..f62aadc0e10 100644 --- a/extensions/twitch/src/send.ts +++ b/extensions/twitch/src/send.ts @@ -5,7 +5,7 @@ * They support dependency injection via the `deps` parameter for testability. */ -import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/twitch"; import { getClientManager as getRegistryClientManager } from "./client-manager-registry.js"; import { DEFAULT_ACCOUNT_ID, getAccountConfig } from "./config.js"; import { resolveTwitchToken } from "./token.js"; diff --git a/extensions/twitch/src/status.ts b/extensions/twitch/src/status.ts index 12f220c897b..c30e129f9f1 100644 --- a/extensions/twitch/src/status.ts +++ b/extensions/twitch/src/status.ts @@ -4,7 +4,7 @@ * Detects and reports configuration issues for Twitch accounts. */ -import type { ChannelStatusIssue } from "openclaw/plugin-sdk/compat"; +import type { ChannelStatusIssue } from "openclaw/plugin-sdk/twitch"; import { getAccountConfig } from "./config.js"; import { resolveTwitchToken } from "./token.js"; import type { ChannelAccountSnapshot } from "./types.js"; diff --git a/extensions/twitch/src/test-fixtures.ts b/extensions/twitch/src/test-fixtures.ts index f6c59f6f2df..efc5877765a 100644 --- a/extensions/twitch/src/test-fixtures.ts +++ b/extensions/twitch/src/test-fixtures.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/twitch"; import { afterEach, beforeEach, vi } from "vitest"; export const BASE_TWITCH_TEST_ACCOUNT = { diff --git a/extensions/twitch/src/token.test.ts b/extensions/twitch/src/token.test.ts index f5b702ea9a6..132a87ae811 100644 --- a/extensions/twitch/src/token.test.ts +++ b/extensions/twitch/src/token.test.ts @@ -8,7 +8,7 @@ * - Account ID normalization */ -import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/twitch"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { resolveTwitchToken, type TwitchTokenSource } from "./token.js"; diff --git a/extensions/twitch/src/twitch-client.ts b/extensions/twitch/src/twitch-client.ts index 4dd1ea8495c..deafd4e01b9 100644 --- a/extensions/twitch/src/twitch-client.ts +++ b/extensions/twitch/src/twitch-client.ts @@ -1,6 +1,6 @@ import { RefreshingAuthProvider, StaticAuthProvider } from "@twurple/auth"; import { ChatClient, LogLevel } from "@twurple/chat"; -import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/twitch"; import { resolveTwitchToken } from "./token.js"; import type { ChannelLogSink, TwitchAccountConfig, TwitchChatMessage } from "./types.js"; import { normalizeToken } from "./utils/twitch.js"; From bbf29201b8fefc507c7339421810fa61559743a6 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 4 Mar 2026 02:33:16 -0500 Subject: [PATCH 61/74] Plugins/voice-call: migrate to scoped plugin-sdk imports --- extensions/voice-call/index.ts | 5 ++++- extensions/voice-call/src/cli.ts | 2 +- extensions/voice-call/src/config.ts | 2 +- .../voice-call/src/providers/shared/guarded-json-api.ts | 2 +- extensions/voice-call/src/webhook.ts | 2 +- 5 files changed, 8 insertions(+), 5 deletions(-) diff --git a/extensions/voice-call/index.ts b/extensions/voice-call/index.ts index 1e97ec5fac3..c4b543b232a 100644 --- a/extensions/voice-call/index.ts +++ b/extensions/voice-call/index.ts @@ -1,5 +1,8 @@ import { Type } from "@sinclair/typebox"; -import type { GatewayRequestHandlerOptions, OpenClawPluginApi } from "openclaw/plugin-sdk/core"; +import type { + GatewayRequestHandlerOptions, + OpenClawPluginApi, +} from "openclaw/plugin-sdk/voice-call"; import { registerVoiceCallCli } from "./src/cli.js"; import { VoiceCallConfigSchema, diff --git a/extensions/voice-call/src/cli.ts b/extensions/voice-call/src/cli.ts index 82b459c336c..c1abc9a1f0e 100644 --- a/extensions/voice-call/src/cli.ts +++ b/extensions/voice-call/src/cli.ts @@ -2,7 +2,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import type { Command } from "commander"; -import { sleep } from "openclaw/plugin-sdk/compat"; +import { sleep } from "openclaw/plugin-sdk/voice-call"; import type { VoiceCallConfig } from "./config.js"; import type { VoiceCallRuntime } from "./runtime.js"; import { resolveUserPath } from "./utils.js"; diff --git a/extensions/voice-call/src/config.ts b/extensions/voice-call/src/config.ts index fc572a6b426..75012723680 100644 --- a/extensions/voice-call/src/config.ts +++ b/extensions/voice-call/src/config.ts @@ -3,7 +3,7 @@ import { TtsConfigSchema, TtsModeSchema, TtsProviderSchema, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/voice-call"; import { z } from "zod"; // ----------------------------------------------------------------------------- diff --git a/extensions/voice-call/src/providers/shared/guarded-json-api.ts b/extensions/voice-call/src/providers/shared/guarded-json-api.ts index 39ca5b73625..cc8d1f33e03 100644 --- a/extensions/voice-call/src/providers/shared/guarded-json-api.ts +++ b/extensions/voice-call/src/providers/shared/guarded-json-api.ts @@ -1,4 +1,4 @@ -import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/compat"; +import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/voice-call"; type GuardedJsonApiRequestParams = { url: string; diff --git a/extensions/voice-call/src/webhook.ts b/extensions/voice-call/src/webhook.ts index 38fca1db0d2..cb0955b830b 100644 --- a/extensions/voice-call/src/webhook.ts +++ b/extensions/voice-call/src/webhook.ts @@ -4,7 +4,7 @@ import { isRequestBodyLimitError, readRequestBodyWithLimit, requestBodyErrorToText, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/voice-call"; import type { VoiceCallConfig } from "./config.js"; import type { CoreConfig } from "./core-bridge.js"; import type { CallManager } from "./manager.js"; From d25bf0d0ca035ed2735f63208d6aba26e15c9d15 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 4 Mar 2026 02:33:17 -0500 Subject: [PATCH 62/74] Plugins/whatsapp: migrate to scoped plugin-sdk imports --- extensions/whatsapp/src/channel.outbound.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/whatsapp/src/channel.outbound.test.ts b/extensions/whatsapp/src/channel.outbound.test.ts index 3c51e9c1bef..758274619e0 100644 --- a/extensions/whatsapp/src/channel.outbound.test.ts +++ b/extensions/whatsapp/src/channel.outbound.test.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig } from "openclaw/plugin-sdk"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/whatsapp"; import { describe, expect, it, vi } from "vitest"; const hoisted = vi.hoisted(() => ({ From e9c7bb6e15185cdf40f19b66227c26beb362d06c Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 4 Mar 2026 02:33:18 -0500 Subject: [PATCH 63/74] Plugins/zalo: migrate to scoped plugin-sdk imports --- extensions/zalo/index.ts | 4 ++-- extensions/zalo/src/accounts.ts | 2 +- extensions/zalo/src/actions.ts | 4 ++-- extensions/zalo/src/channel.directory.test.ts | 2 +- extensions/zalo/src/channel.sendpayload.test.ts | 2 +- extensions/zalo/src/channel.ts | 4 ++-- extensions/zalo/src/config-schema.ts | 2 +- extensions/zalo/src/group-access.ts | 4 ++-- extensions/zalo/src/monitor.ts | 4 ++-- extensions/zalo/src/monitor.webhook.test.ts | 2 +- extensions/zalo/src/monitor.webhook.ts | 4 ++-- extensions/zalo/src/onboarding.status.test.ts | 2 +- extensions/zalo/src/onboarding.ts | 4 ++-- extensions/zalo/src/probe.ts | 2 +- extensions/zalo/src/runtime.ts | 2 +- extensions/zalo/src/secret-input.ts | 2 +- extensions/zalo/src/send.ts | 2 +- extensions/zalo/src/status-issues.ts | 2 +- extensions/zalo/src/token.ts | 2 +- extensions/zalo/src/types.ts | 2 +- 20 files changed, 27 insertions(+), 27 deletions(-) diff --git a/extensions/zalo/index.ts b/extensions/zalo/index.ts index ccdc4aaacad..3028b8b492f 100644 --- a/extensions/zalo/index.ts +++ b/extensions/zalo/index.ts @@ -1,5 +1,5 @@ -import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core"; -import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/core"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk/zalo"; +import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/zalo"; import { zaloDock, zaloPlugin } from "./src/channel.js"; import { setZaloRuntime } from "./src/runtime.js"; diff --git a/extensions/zalo/src/accounts.ts b/extensions/zalo/src/accounts.ts index d74d906fce6..c4cb8930cca 100644 --- a/extensions/zalo/src/accounts.ts +++ b/extensions/zalo/src/accounts.ts @@ -3,7 +3,7 @@ import { normalizeAccountId, normalizeOptionalAccountId, } from "openclaw/plugin-sdk/account-id"; -import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/zalo"; import { resolveZaloToken } from "./token.js"; import type { ResolvedZaloAccount, ZaloAccountConfig, ZaloConfig } from "./types.js"; diff --git a/extensions/zalo/src/actions.ts b/extensions/zalo/src/actions.ts index e3fe5d22fdf..4604cc77310 100644 --- a/extensions/zalo/src/actions.ts +++ b/extensions/zalo/src/actions.ts @@ -2,8 +2,8 @@ import type { ChannelMessageActionAdapter, ChannelMessageActionName, OpenClawConfig, -} from "openclaw/plugin-sdk/compat"; -import { extractToolSend, jsonResult, readStringParam } from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/zalo"; +import { extractToolSend, jsonResult, readStringParam } from "openclaw/plugin-sdk/zalo"; import { listEnabledZaloAccounts } from "./accounts.js"; import { sendMessageZalo } from "./send.js"; diff --git a/extensions/zalo/src/channel.directory.test.ts b/extensions/zalo/src/channel.directory.test.ts index 5159ae3a6ac..99821c85017 100644 --- a/extensions/zalo/src/channel.directory.test.ts +++ b/extensions/zalo/src/channel.directory.test.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig, RuntimeEnv } from "openclaw/plugin-sdk/compat"; +import type { OpenClawConfig, RuntimeEnv } from "openclaw/plugin-sdk/zalo"; import { describe, expect, it } from "vitest"; import { zaloPlugin } from "./channel.js"; diff --git a/extensions/zalo/src/channel.sendpayload.test.ts b/extensions/zalo/src/channel.sendpayload.test.ts index d51eb4660fb..6cc072ac6dd 100644 --- a/extensions/zalo/src/channel.sendpayload.test.ts +++ b/extensions/zalo/src/channel.sendpayload.test.ts @@ -1,4 +1,4 @@ -import type { ReplyPayload } from "openclaw/plugin-sdk/compat"; +import type { ReplyPayload } from "openclaw/plugin-sdk/zalo"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { zaloPlugin } from "./channel.js"; diff --git a/extensions/zalo/src/channel.ts b/extensions/zalo/src/channel.ts index a5d743c3efd..a3233ce5228 100644 --- a/extensions/zalo/src/channel.ts +++ b/extensions/zalo/src/channel.ts @@ -3,7 +3,7 @@ import type { ChannelDock, ChannelPlugin, OpenClawConfig, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/zalo"; import { applyAccountNameToChannelSection, buildChannelConfigSchema, @@ -20,7 +20,7 @@ import { resolveOpenProviderRuntimeGroupPolicy, resolveChannelAccountConfigBasePath, setAccountEnabledInConfigSection, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/zalo"; import { listZaloAccountIds, resolveDefaultZaloAccountId, diff --git a/extensions/zalo/src/config-schema.ts b/extensions/zalo/src/config-schema.ts index 0786429755b..7f2c0f360ba 100644 --- a/extensions/zalo/src/config-schema.ts +++ b/extensions/zalo/src/config-schema.ts @@ -1,4 +1,4 @@ -import { MarkdownConfigSchema } from "openclaw/plugin-sdk/compat"; +import { MarkdownConfigSchema } from "openclaw/plugin-sdk/zalo"; import { z } from "zod"; import { buildSecretInputSchema } from "./secret-input.js"; diff --git a/extensions/zalo/src/group-access.ts b/extensions/zalo/src/group-access.ts index 48292e5ac80..56a929cc23a 100644 --- a/extensions/zalo/src/group-access.ts +++ b/extensions/zalo/src/group-access.ts @@ -1,9 +1,9 @@ -import type { GroupPolicy, SenderGroupAccessDecision } from "openclaw/plugin-sdk/compat"; +import type { GroupPolicy, SenderGroupAccessDecision } from "openclaw/plugin-sdk/zalo"; import { evaluateSenderGroupAccess, isNormalizedSenderAllowed, resolveOpenProviderRuntimeGroupPolicy, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/zalo"; const ZALO_ALLOW_FROM_PREFIX_RE = /^(zalo|zl):/i; diff --git a/extensions/zalo/src/monitor.ts b/extensions/zalo/src/monitor.ts index aa3b37f463d..b276019879e 100644 --- a/extensions/zalo/src/monitor.ts +++ b/extensions/zalo/src/monitor.ts @@ -3,7 +3,7 @@ import type { MarkdownTableMode, OpenClawConfig, OutboundReplyPayload, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/zalo"; import { createScopedPairingAccess, createReplyPrefixOptions, @@ -15,7 +15,7 @@ import { sendMediaWithLeadingCaption, resolveWebhookPath, warnMissingProviderGroupPolicyFallbackOnce, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/zalo"; import type { ResolvedZaloAccount } from "./accounts.js"; import { ZaloApiError, diff --git a/extensions/zalo/src/monitor.webhook.test.ts b/extensions/zalo/src/monitor.webhook.test.ts index 33fa7530f6b..8cdecd0560c 100644 --- a/extensions/zalo/src/monitor.webhook.test.ts +++ b/extensions/zalo/src/monitor.webhook.test.ts @@ -1,6 +1,6 @@ import { createServer, type RequestListener } from "node:http"; import type { AddressInfo } from "node:net"; -import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk/compat"; +import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk/zalo"; import { afterEach, describe, expect, it, vi } from "vitest"; import { createEmptyPluginRegistry } from "../../../src/plugins/registry.js"; import { setActivePluginRegistry } from "../../../src/plugins/runtime.js"; diff --git a/extensions/zalo/src/monitor.webhook.ts b/extensions/zalo/src/monitor.webhook.ts index 82f09811c9d..3bcc35aa43c 100644 --- a/extensions/zalo/src/monitor.webhook.ts +++ b/extensions/zalo/src/monitor.webhook.ts @@ -1,6 +1,6 @@ import { timingSafeEqual } from "node:crypto"; import type { IncomingMessage, ServerResponse } from "node:http"; -import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/zalo"; import { createDedupeCache, createFixedWindowRateLimiter, @@ -15,7 +15,7 @@ import { resolveWebhookTargets, WEBHOOK_ANOMALY_COUNTER_DEFAULTS, WEBHOOK_RATE_LIMIT_DEFAULTS, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/zalo"; import type { ResolvedZaloAccount } from "./accounts.js"; import type { ZaloFetch, ZaloUpdate } from "./api.js"; import type { ZaloRuntimeEnv } from "./monitor.js"; diff --git a/extensions/zalo/src/onboarding.status.test.ts b/extensions/zalo/src/onboarding.status.test.ts index 6282b7eaf67..fed5ea95f89 100644 --- a/extensions/zalo/src/onboarding.status.test.ts +++ b/extensions/zalo/src/onboarding.status.test.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/zalo"; import { describe, expect, it } from "vitest"; import { zaloOnboardingAdapter } from "./onboarding.js"; diff --git a/extensions/zalo/src/onboarding.ts b/extensions/zalo/src/onboarding.ts index 0f68bd4f36d..b8c3b0ef011 100644 --- a/extensions/zalo/src/onboarding.ts +++ b/extensions/zalo/src/onboarding.ts @@ -4,7 +4,7 @@ import type { OpenClawConfig, SecretInput, WizardPrompter, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/zalo"; import { addWildcardAllowFrom, DEFAULT_ACCOUNT_ID, @@ -13,7 +13,7 @@ import { normalizeAccountId, promptAccountId, promptSingleChannelSecretInput, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/zalo"; import { listZaloAccountIds, resolveDefaultZaloAccountId, resolveZaloAccount } from "./accounts.js"; const channel = "zalo" as const; diff --git a/extensions/zalo/src/probe.ts b/extensions/zalo/src/probe.ts index f8fa6b87943..67015ac5f08 100644 --- a/extensions/zalo/src/probe.ts +++ b/extensions/zalo/src/probe.ts @@ -1,4 +1,4 @@ -import type { BaseProbeResult } from "openclaw/plugin-sdk/compat"; +import type { BaseProbeResult } from "openclaw/plugin-sdk/zalo"; import { getMe, ZaloApiError, type ZaloBotInfo, type ZaloFetch } from "./api.js"; export type ZaloProbeResult = BaseProbeResult & { diff --git a/extensions/zalo/src/runtime.ts b/extensions/zalo/src/runtime.ts index 706fe2587d5..5d96660a7d3 100644 --- a/extensions/zalo/src/runtime.ts +++ b/extensions/zalo/src/runtime.ts @@ -1,4 +1,4 @@ -import type { PluginRuntime } from "openclaw/plugin-sdk/compat"; +import type { PluginRuntime } from "openclaw/plugin-sdk/zalo"; let runtime: PluginRuntime | null = null; diff --git a/extensions/zalo/src/secret-input.ts b/extensions/zalo/src/secret-input.ts index 3fc82b3ac91..702548454c3 100644 --- a/extensions/zalo/src/secret-input.ts +++ b/extensions/zalo/src/secret-input.ts @@ -2,7 +2,7 @@ import { hasConfiguredSecretInput, normalizeResolvedSecretInputString, normalizeSecretInputString, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/zalo"; import { z } from "zod"; export { hasConfiguredSecretInput, normalizeResolvedSecretInputString, normalizeSecretInputString }; diff --git a/extensions/zalo/src/send.ts b/extensions/zalo/src/send.ts index 29120cfce02..c58142f8633 100644 --- a/extensions/zalo/src/send.ts +++ b/extensions/zalo/src/send.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/zalo"; import { resolveZaloAccount } from "./accounts.js"; import type { ZaloFetch } from "./api.js"; import { sendMessage, sendPhoto } from "./api.js"; diff --git a/extensions/zalo/src/status-issues.ts b/extensions/zalo/src/status-issues.ts index ecff1af2b14..cf6b3a3a384 100644 --- a/extensions/zalo/src/status-issues.ts +++ b/extensions/zalo/src/status-issues.ts @@ -1,4 +1,4 @@ -import type { ChannelAccountSnapshot, ChannelStatusIssue } from "openclaw/plugin-sdk/compat"; +import type { ChannelAccountSnapshot, ChannelStatusIssue } from "openclaw/plugin-sdk/zalo"; type ZaloAccountStatus = { accountId?: unknown; diff --git a/extensions/zalo/src/token.ts b/extensions/zalo/src/token.ts index 992d4b1b2ad..2d9496fa5c2 100644 --- a/extensions/zalo/src/token.ts +++ b/extensions/zalo/src/token.ts @@ -1,6 +1,6 @@ import { readFileSync } from "node:fs"; import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id"; -import type { BaseTokenResolution } from "openclaw/plugin-sdk/compat"; +import type { BaseTokenResolution } from "openclaw/plugin-sdk/zalo"; import { normalizeResolvedSecretInputString, normalizeSecretInputString } from "./secret-input.js"; import type { ZaloConfig } from "./types.js"; diff --git a/extensions/zalo/src/types.ts b/extensions/zalo/src/types.ts index 3ad17ef5b88..f112f5f69b9 100644 --- a/extensions/zalo/src/types.ts +++ b/extensions/zalo/src/types.ts @@ -1,4 +1,4 @@ -import type { SecretInput } from "openclaw/plugin-sdk/compat"; +import type { SecretInput } from "openclaw/plugin-sdk/zalo"; export type ZaloAccountConfig = { /** Optional display name for this account (used in CLI/UI lists). */ From 5c4ab999b022b3a420b02276281be8970df17a3c Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 4 Mar 2026 02:33:19 -0500 Subject: [PATCH 64/74] Plugins/zalouser: migrate to scoped plugin-sdk imports --- extensions/zalouser/index.ts | 4 ++-- extensions/zalouser/src/accounts.test.ts | 2 +- extensions/zalouser/src/accounts.ts | 2 +- extensions/zalouser/src/channel.sendpayload.test.ts | 2 +- extensions/zalouser/src/channel.ts | 4 ++-- extensions/zalouser/src/config-schema.ts | 2 +- extensions/zalouser/src/monitor.account-scope.test.ts | 2 +- extensions/zalouser/src/monitor.group-gating.test.ts | 2 +- extensions/zalouser/src/monitor.ts | 4 ++-- extensions/zalouser/src/onboarding.ts | 4 ++-- extensions/zalouser/src/probe.ts | 2 +- extensions/zalouser/src/runtime.ts | 2 +- extensions/zalouser/src/status-issues.ts | 2 +- extensions/zalouser/src/zalo-js.ts | 2 +- 14 files changed, 18 insertions(+), 18 deletions(-) diff --git a/extensions/zalouser/index.ts b/extensions/zalouser/index.ts index 6b5d470b85d..b169292e954 100644 --- a/extensions/zalouser/index.ts +++ b/extensions/zalouser/index.ts @@ -1,5 +1,5 @@ -import type { AnyAgentTool, OpenClawPluginApi } from "openclaw/plugin-sdk/core"; -import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/core"; +import type { AnyAgentTool, OpenClawPluginApi } from "openclaw/plugin-sdk/zalouser"; +import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/zalouser"; import { zalouserDock, zalouserPlugin } from "./src/channel.js"; import { setZalouserRuntime } from "./src/runtime.js"; import { ZalouserToolSchema, executeZalouserTool } from "./src/tool.js"; diff --git a/extensions/zalouser/src/accounts.test.ts b/extensions/zalouser/src/accounts.test.ts index 672a3618431..7b6a63d66a7 100644 --- a/extensions/zalouser/src/accounts.test.ts +++ b/extensions/zalouser/src/accounts.test.ts @@ -1,5 +1,5 @@ import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id"; -import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/zalouser"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { getZcaUserInfo, diff --git a/extensions/zalouser/src/accounts.ts b/extensions/zalouser/src/accounts.ts index 860c1202155..ebf4182f15e 100644 --- a/extensions/zalouser/src/accounts.ts +++ b/extensions/zalouser/src/accounts.ts @@ -3,7 +3,7 @@ import { normalizeAccountId, normalizeOptionalAccountId, } from "openclaw/plugin-sdk/account-id"; -import type { OpenClawConfig } from "openclaw/plugin-sdk/compat"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/zalouser"; import type { ResolvedZalouserAccount, ZalouserAccountConfig, ZalouserConfig } from "./types.js"; import { checkZaloAuthenticated, getZaloUserInfo } from "./zalo-js.js"; diff --git a/extensions/zalouser/src/channel.sendpayload.test.ts b/extensions/zalouser/src/channel.sendpayload.test.ts index 9ca29d8bea3..31eb6136cd5 100644 --- a/extensions/zalouser/src/channel.sendpayload.test.ts +++ b/extensions/zalouser/src/channel.sendpayload.test.ts @@ -1,4 +1,4 @@ -import type { ReplyPayload } from "openclaw/plugin-sdk/compat"; +import type { ReplyPayload } from "openclaw/plugin-sdk/zalouser"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { zalouserPlugin } from "./channel.js"; diff --git a/extensions/zalouser/src/channel.ts b/extensions/zalouser/src/channel.ts index fa5411e2ccc..2c2228b05b9 100644 --- a/extensions/zalouser/src/channel.ts +++ b/extensions/zalouser/src/channel.ts @@ -9,7 +9,7 @@ import type { ChannelPlugin, OpenClawConfig, GroupToolPolicyConfig, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/zalouser"; import { applyAccountNameToChannelSection, buildChannelConfigSchema, @@ -23,7 +23,7 @@ import { resolvePreferredOpenClawTmpDir, resolveChannelAccountConfigBasePath, setAccountEnabledInConfigSection, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/zalouser"; import { listZalouserAccountIds, resolveDefaultZalouserAccountId, diff --git a/extensions/zalouser/src/config-schema.ts b/extensions/zalouser/src/config-schema.ts index 0f4b505d38e..bbc8457da6e 100644 --- a/extensions/zalouser/src/config-schema.ts +++ b/extensions/zalouser/src/config-schema.ts @@ -1,4 +1,4 @@ -import { MarkdownConfigSchema, ToolPolicySchema } from "openclaw/plugin-sdk/compat"; +import { MarkdownConfigSchema, ToolPolicySchema } from "openclaw/plugin-sdk/zalouser"; import { z } from "zod"; const allowFromEntry = z.union([z.string(), z.number()]); diff --git a/extensions/zalouser/src/monitor.account-scope.test.ts b/extensions/zalouser/src/monitor.account-scope.test.ts index eca0cff6c8c..931a6cde6eb 100644 --- a/extensions/zalouser/src/monitor.account-scope.test.ts +++ b/extensions/zalouser/src/monitor.account-scope.test.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig, PluginRuntime, RuntimeEnv } from "openclaw/plugin-sdk/compat"; +import type { OpenClawConfig, PluginRuntime, RuntimeEnv } from "openclaw/plugin-sdk/zalouser"; import { describe, expect, it, vi } from "vitest"; import { __testing } from "./monitor.js"; import { setZalouserRuntime } from "./runtime.js"; diff --git a/extensions/zalouser/src/monitor.group-gating.test.ts b/extensions/zalouser/src/monitor.group-gating.test.ts index 146ae563589..dda0ed0a3de 100644 --- a/extensions/zalouser/src/monitor.group-gating.test.ts +++ b/extensions/zalouser/src/monitor.group-gating.test.ts @@ -1,4 +1,4 @@ -import type { OpenClawConfig, PluginRuntime, RuntimeEnv } from "openclaw/plugin-sdk/compat"; +import type { OpenClawConfig, PluginRuntime, RuntimeEnv } from "openclaw/plugin-sdk/zalouser"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { __testing } from "./monitor.js"; import { setZalouserRuntime } from "./runtime.js"; diff --git a/extensions/zalouser/src/monitor.ts b/extensions/zalouser/src/monitor.ts index 9382bdb9e7f..fc3e07c564e 100644 --- a/extensions/zalouser/src/monitor.ts +++ b/extensions/zalouser/src/monitor.ts @@ -3,7 +3,7 @@ import type { OpenClawConfig, OutboundReplyPayload, RuntimeEnv, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/zalouser"; import { createTypingCallbacks, createScopedPairingAccess, @@ -17,7 +17,7 @@ import { sendMediaWithLeadingCaption, summarizeMapping, warnMissingProviderGroupPolicyFallbackOnce, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/zalouser"; import { buildZalouserGroupCandidates, findZalouserGroupEntry, diff --git a/extensions/zalouser/src/onboarding.ts b/extensions/zalouser/src/onboarding.ts index 22039413085..728edff704a 100644 --- a/extensions/zalouser/src/onboarding.ts +++ b/extensions/zalouser/src/onboarding.ts @@ -5,7 +5,7 @@ import type { ChannelOnboardingDmPolicy, OpenClawConfig, WizardPrompter, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/zalouser"; import { addWildcardAllowFrom, DEFAULT_ACCOUNT_ID, @@ -15,7 +15,7 @@ import { promptAccountId, promptChannelAccessConfig, resolvePreferredOpenClawTmpDir, -} from "openclaw/plugin-sdk/compat"; +} from "openclaw/plugin-sdk/zalouser"; import { listZalouserAccountIds, resolveDefaultZalouserAccountId, diff --git a/extensions/zalouser/src/probe.ts b/extensions/zalouser/src/probe.ts index cfa33b0c645..b3213010f26 100644 --- a/extensions/zalouser/src/probe.ts +++ b/extensions/zalouser/src/probe.ts @@ -1,4 +1,4 @@ -import type { BaseProbeResult } from "openclaw/plugin-sdk/compat"; +import type { BaseProbeResult } from "openclaw/plugin-sdk/zalouser"; import type { ZcaUserInfo } from "./types.js"; import { getZaloUserInfo } from "./zalo-js.js"; diff --git a/extensions/zalouser/src/runtime.ts b/extensions/zalouser/src/runtime.ts index 66287f1280f..42cb9def444 100644 --- a/extensions/zalouser/src/runtime.ts +++ b/extensions/zalouser/src/runtime.ts @@ -1,4 +1,4 @@ -import type { PluginRuntime } from "openclaw/plugin-sdk/compat"; +import type { PluginRuntime } from "openclaw/plugin-sdk/zalouser"; let runtime: PluginRuntime | null = null; diff --git a/extensions/zalouser/src/status-issues.ts b/extensions/zalouser/src/status-issues.ts index d9f47361d30..fca889a5115 100644 --- a/extensions/zalouser/src/status-issues.ts +++ b/extensions/zalouser/src/status-issues.ts @@ -1,4 +1,4 @@ -import type { ChannelAccountSnapshot, ChannelStatusIssue } from "openclaw/plugin-sdk/compat"; +import type { ChannelAccountSnapshot, ChannelStatusIssue } from "openclaw/plugin-sdk/zalouser"; type ZalouserAccountStatus = { accountId?: unknown; diff --git a/extensions/zalouser/src/zalo-js.ts b/extensions/zalouser/src/zalo-js.ts index 2e230c81ec1..206efaed2a5 100644 --- a/extensions/zalouser/src/zalo-js.ts +++ b/extensions/zalouser/src/zalo-js.ts @@ -3,7 +3,7 @@ import fs from "node:fs"; import fsp from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import { loadOutboundMediaFromUrl } from "openclaw/plugin-sdk/compat"; +import { loadOutboundMediaFromUrl } from "openclaw/plugin-sdk/zalouser"; import { normalizeZaloReactionIcon } from "./reaction.js"; import { getZalouserRuntime } from "./runtime.js"; import type { From ad9ceafec2b97bb6d5cec680f6457bc0a3d8d7d9 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 4 Mar 2026 02:34:10 -0500 Subject: [PATCH 65/74] Chore: remove accidental .DS_Store artifact --- extensions/acpx/.DS_Store | Bin 6148 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 extensions/acpx/.DS_Store diff --git a/extensions/acpx/.DS_Store b/extensions/acpx/.DS_Store deleted file mode 100644 index ec6208cc9cc2e3f28974712ca2e388c4bf94b311..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKy-EW?5T3n+5KW2{777x!va&FOoyaO@X_W_1@U-t5fo+_&K-LquwC*l7^eh^P!@3>GkaBD~HzBZ0S6fy(Z2 zNHHbUp&>;x-eUNR4Dj7m>CE0*m$LWQr9sqdG}}qscZsjQ&hw3vFlrTozlu;NE2J#FP++&UF|Nhtg ze?CZ_gaKjTUooJHVKdyrEBV?w^Kx8kHS`F|!hWg4aR?^16vLNG@iNp3?3yP)<1uvz Q3q<}1SQ=yy27Z-+5Anur8~^|S From 6a40f69d4d823a26293ede43aa761b6ca9c29d56 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Wed, 4 Mar 2026 02:39:11 -0500 Subject: [PATCH 66/74] chore(docs): add plugins refactor changelog entry --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d853f53faac..0ad03596f00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,7 +26,8 @@ Docs: https://docs.openclaw.ai - Plugins/startup performance: reduce bursty plugin discovery/manifest overhead with short in-process caches, skip importing bundled memory plugins that are disabled by slot selection, and speed legacy root `openclaw/plugin-sdk` compatibility via runtime root-alias routing while preserving backward compatibility. Thanks @gumadeiras. - Build/lazy runtime boundaries: replace ineffective dynamic import sites with dedicated lazy runtime boundaries across Slack slash handling, Telegram audit, CLI send deps, memory fallback, and outbound delivery paths while preserving behavior. (#33690) thanks @gumadeiras. - Config/heartbeat legacy-path handling: auto-migrate top-level `heartbeat` into `agents.defaults.heartbeat` (with merge semantics that preserve explicit defaults), and keep startup failures on non-migratable legacy entries in the detailed invalid-config path instead of generic migration-failed errors. (#32706) thanks @xiwan. -- Plugins/SDK subpath parity: add channel-specific plugin SDK subpaths for Discord, Slack, Signal, iMessage, WhatsApp, and LINE; migrate bundled plugin entrypoints to scoped subpaths/core with CI guardrails; and keep `openclaw/plugin-sdk` root import compatibility for existing external plugins. (#33737) thanks @gumadeiras. +- Plugins/SDK subpath parity: expand plugin SDK subpaths across bundled channels/extensions (Discord, Slack, Signal, iMessage, WhatsApp, LINE, and bundled companion plugins), with build/export/type/runtime wiring so scoped imports resolve consistently in source and dist while preserving compatibility. (#33737) thanks @gumadeiras. +- Plugins/bundled scoped-import migration: migrate bundled plugins from monolithic `openclaw/plugin-sdk` imports to scoped subpaths (or `openclaw/plugin-sdk/core`) across registration and startup-sensitive runtime files, add CI/release guardrails to prevent regressions, and keep root `openclaw/plugin-sdk` support for external/community plugins. Thanks @gumadeiras. - Routing/session duplicate suppression synthesis: align shared session delivery-context inheritance, channel-paired route-field merges, and reply-surface target matching so dmScope=main turns avoid cross-surface duplicate replies while thread-aware forwarding keeps intended routing semantics. (from #33629, #26889, #17337, #33250) Thanks @Yuandiaodiaodiao, @kevinwildenradt, @Glucksberg, and @bmendonca3. - Routing/legacy session route inheritance: preserve external route metadata inheritance for legacy channel session keys (`agent:::` and `...:thread:`) so `chat.send` does not incorrectly fall back to webchat when valid delivery context exists. Follow-up to #33786. - Routing/legacy route guard tightening: require legacy session-key channel hints to match the saved delivery channel before inheriting external routing metadata, preventing custom namespaced keys like `agent::work:` from inheriting stale non-webchat routes. From bd25182d5a9f04114873c5f5eb3d310bbf48938e Mon Sep 17 00:00:00 2001 From: Mariano <132747814+mbelinky@users.noreply.github.com> Date: Wed, 4 Mar 2026 07:44:42 +0000 Subject: [PATCH 67/74] feat(ios): add Live Activity connection status + stale cleanup (#33591) * feat(ios): add live activity connection status and cleanup Add lock-screen/Dynamic Island connection health states and prune duplicate/stale activities before reuse. This intentionally excludes AI/title generation and heavier UX rewrites from #27488. Co-authored-by: leepokai <1663017+leepokai@users.noreply.github.com> * fix(ios): treat ended live activities as inactive * chore(changelog): add PR reference and author thanks --------- Co-authored-by: leepokai <1663017+leepokai@users.noreply.github.com> --- .../Assets.xcassets/Contents.json | 6 + apps/ios/ActivityWidget/Info.plist | 31 +++++ .../OpenClawActivityWidgetBundle.swift | 9 ++ .../ActivityWidget/OpenClawLiveActivity.swift | 84 ++++++++++++ apps/ios/Config/Signing.xcconfig | 1 + apps/ios/Sources/Info.plist | 2 + .../LiveActivity/LiveActivityManager.swift | 125 ++++++++++++++++++ .../OpenClawActivityAttributes.swift | 45 +++++++ apps/ios/Sources/Model/NodeAppModel.swift | 12 ++ apps/ios/SwiftSources.input.xcfilelist | 4 + apps/ios/project.yml | 35 +++++ .../ios-live-activity-status-cleanup.md | 1 + 12 files changed, 355 insertions(+) create mode 100644 apps/ios/ActivityWidget/Assets.xcassets/Contents.json create mode 100644 apps/ios/ActivityWidget/Info.plist create mode 100644 apps/ios/ActivityWidget/OpenClawActivityWidgetBundle.swift create mode 100644 apps/ios/ActivityWidget/OpenClawLiveActivity.swift create mode 100644 apps/ios/Sources/LiveActivity/LiveActivityManager.swift create mode 100644 apps/ios/Sources/LiveActivity/OpenClawActivityAttributes.swift create mode 100644 changelog/fragments/ios-live-activity-status-cleanup.md diff --git a/apps/ios/ActivityWidget/Assets.xcassets/Contents.json b/apps/ios/ActivityWidget/Assets.xcassets/Contents.json new file mode 100644 index 00000000000..73c00596a7f --- /dev/null +++ b/apps/ios/ActivityWidget/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/ios/ActivityWidget/Info.plist b/apps/ios/ActivityWidget/Info.plist new file mode 100644 index 00000000000..4e12dc4f884 --- /dev/null +++ b/apps/ios/ActivityWidget/Info.plist @@ -0,0 +1,31 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + OpenClaw Activity + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + XPC! + CFBundleShortVersionString + 2026.3.2 + CFBundleVersion + 20260301 + NSExtension + + NSExtensionPointIdentifier + com.apple.widgetkit-extension + + NSSupportsLiveActivities + + + diff --git a/apps/ios/ActivityWidget/OpenClawActivityWidgetBundle.swift b/apps/ios/ActivityWidget/OpenClawActivityWidgetBundle.swift new file mode 100644 index 00000000000..424a97c1982 --- /dev/null +++ b/apps/ios/ActivityWidget/OpenClawActivityWidgetBundle.swift @@ -0,0 +1,9 @@ +import SwiftUI +import WidgetKit + +@main +struct OpenClawActivityWidgetBundle: WidgetBundle { + var body: some Widget { + OpenClawLiveActivity() + } +} diff --git a/apps/ios/ActivityWidget/OpenClawLiveActivity.swift b/apps/ios/ActivityWidget/OpenClawLiveActivity.swift new file mode 100644 index 00000000000..836803f403f --- /dev/null +++ b/apps/ios/ActivityWidget/OpenClawLiveActivity.swift @@ -0,0 +1,84 @@ +import ActivityKit +import SwiftUI +import WidgetKit + +struct OpenClawLiveActivity: Widget { + var body: some WidgetConfiguration { + ActivityConfiguration(for: OpenClawActivityAttributes.self) { context in + lockScreenView(context: context) + } dynamicIsland: { context in + DynamicIsland { + DynamicIslandExpandedRegion(.leading) { + statusDot(state: context.state) + } + DynamicIslandExpandedRegion(.center) { + Text(context.state.statusText) + .font(.subheadline) + .lineLimit(1) + } + DynamicIslandExpandedRegion(.trailing) { + trailingView(state: context.state) + } + } compactLeading: { + statusDot(state: context.state) + } compactTrailing: { + Text(context.state.statusText) + .font(.caption2) + .lineLimit(1) + .frame(maxWidth: 64) + } minimal: { + statusDot(state: context.state) + } + } + } + + @ViewBuilder + private func lockScreenView(context: ActivityViewContext) -> some View { + HStack(spacing: 8) { + statusDot(state: context.state) + .frame(width: 10, height: 10) + VStack(alignment: .leading, spacing: 2) { + Text("OpenClaw") + .font(.subheadline.bold()) + Text(context.state.statusText) + .font(.caption) + .foregroundStyle(.secondary) + } + Spacer() + trailingView(state: context.state) + } + .padding(.vertical, 4) + } + + @ViewBuilder + private func trailingView(state: OpenClawActivityAttributes.ContentState) -> some View { + if state.isConnecting { + ProgressView().controlSize(.small) + } else if state.isDisconnected { + Image(systemName: "wifi.slash") + .foregroundStyle(.red) + } else if state.isIdle { + Image(systemName: "antenna.radiowaves.left.and.right") + .foregroundStyle(.green) + } else { + Text(state.startedAt, style: .timer) + .font(.caption) + .monospacedDigit() + .foregroundStyle(.secondary) + } + } + + @ViewBuilder + private func statusDot(state: OpenClawActivityAttributes.ContentState) -> some View { + Circle() + .fill(dotColor(state: state)) + .frame(width: 6, height: 6) + } + + private func dotColor(state: OpenClawActivityAttributes.ContentState) -> Color { + if state.isDisconnected { return .red } + if state.isConnecting { return .gray } + if state.isIdle { return .green } + return .blue + } +} diff --git a/apps/ios/Config/Signing.xcconfig b/apps/ios/Config/Signing.xcconfig index e0afd46aa7e..1285d2a38a4 100644 --- a/apps/ios/Config/Signing.xcconfig +++ b/apps/ios/Config/Signing.xcconfig @@ -4,6 +4,7 @@ OPENCLAW_IOS_SELECTED_TEAM = $(OPENCLAW_IOS_DEFAULT_TEAM) OPENCLAW_APP_BUNDLE_ID = ai.openclaw.ios OPENCLAW_WATCH_APP_BUNDLE_ID = ai.openclaw.ios.watchkitapp OPENCLAW_WATCH_EXTENSION_BUNDLE_ID = ai.openclaw.ios.watchkitapp.extension +OPENCLAW_ACTIVITY_WIDGET_BUNDLE_ID = ai.openclaw.ios.activitywidget // Local contributors can override this by running scripts/ios-configure-signing.sh. // Keep include after defaults: xcconfig is evaluated top-to-bottom. diff --git a/apps/ios/Sources/Info.plist b/apps/ios/Sources/Info.plist index 86556e094b0..b4d6ed3109a 100644 --- a/apps/ios/Sources/Info.plist +++ b/apps/ios/Sources/Info.plist @@ -54,6 +54,8 @@ OpenClaw needs microphone access for voice wake. NSSpeechRecognitionUsageDescription OpenClaw uses on-device speech recognition for voice wake. + NSSupportsLiveActivities + UIApplicationSceneManifest UIApplicationSupportsMultipleScenes diff --git a/apps/ios/Sources/LiveActivity/LiveActivityManager.swift b/apps/ios/Sources/LiveActivity/LiveActivityManager.swift new file mode 100644 index 00000000000..b7be7597e35 --- /dev/null +++ b/apps/ios/Sources/LiveActivity/LiveActivityManager.swift @@ -0,0 +1,125 @@ +import ActivityKit +import Foundation +import os + +/// Minimal Live Activity lifecycle focused on connection health + stale cleanup. +@MainActor +final class LiveActivityManager { + static let shared = LiveActivityManager() + + private let logger = Logger(subsystem: "ai.openclaw.ios", category: "LiveActivity") + private var currentActivity: Activity? + private var activityStartDate: Date = .now + + private init() { + self.hydrateCurrentAndPruneDuplicates() + } + + var isActive: Bool { + guard let activity = self.currentActivity else { return false } + guard activity.activityState == .active else { + self.currentActivity = nil + return false + } + return true + } + + func startActivity(agentName: String, sessionKey: String) { + self.hydrateCurrentAndPruneDuplicates() + + if self.currentActivity != nil { + self.handleConnecting() + return + } + + let authInfo = ActivityAuthorizationInfo() + guard authInfo.areActivitiesEnabled else { + self.logger.info("Live Activities disabled; skipping start") + return + } + + self.activityStartDate = .now + let attributes = OpenClawActivityAttributes(agentName: agentName, sessionKey: sessionKey) + + do { + let activity = try Activity.request( + attributes: attributes, + content: ActivityContent(state: self.connectingState(), staleDate: nil), + pushType: nil) + self.currentActivity = activity + self.logger.info("started live activity id=\(activity.id, privacy: .public)") + } catch { + self.logger.error("failed to start live activity: \(error.localizedDescription, privacy: .public)") + } + } + + func handleConnecting() { + self.updateCurrent(state: self.connectingState()) + } + + func handleReconnect() { + self.updateCurrent(state: self.idleState()) + } + + func handleDisconnect() { + self.updateCurrent(state: self.disconnectedState()) + } + + private func hydrateCurrentAndPruneDuplicates() { + let active = Activity.activities + guard !active.isEmpty else { + self.currentActivity = nil + return + } + + let keeper = active.max { lhs, rhs in + lhs.content.state.startedAt < rhs.content.state.startedAt + } ?? active[0] + + self.currentActivity = keeper + self.activityStartDate = keeper.content.state.startedAt + + let stale = active.filter { $0.id != keeper.id } + for activity in stale { + Task { + await activity.end( + ActivityContent(state: self.disconnectedState(), staleDate: nil), + dismissalPolicy: .immediate) + } + } + } + + private func updateCurrent(state: OpenClawActivityAttributes.ContentState) { + guard let activity = self.currentActivity else { return } + Task { + await activity.update(ActivityContent(state: state, staleDate: nil)) + } + } + + private func connectingState() -> OpenClawActivityAttributes.ContentState { + OpenClawActivityAttributes.ContentState( + statusText: "Connecting...", + isIdle: false, + isDisconnected: false, + isConnecting: true, + startedAt: self.activityStartDate) + } + + private func idleState() -> OpenClawActivityAttributes.ContentState { + OpenClawActivityAttributes.ContentState( + statusText: "Idle", + isIdle: true, + isDisconnected: false, + isConnecting: false, + startedAt: self.activityStartDate) + } + + private func disconnectedState() -> OpenClawActivityAttributes.ContentState { + OpenClawActivityAttributes.ContentState( + statusText: "Disconnected", + isIdle: false, + isDisconnected: true, + isConnecting: false, + startedAt: self.activityStartDate) + } +} diff --git a/apps/ios/Sources/LiveActivity/OpenClawActivityAttributes.swift b/apps/ios/Sources/LiveActivity/OpenClawActivityAttributes.swift new file mode 100644 index 00000000000..d9d879c84b5 --- /dev/null +++ b/apps/ios/Sources/LiveActivity/OpenClawActivityAttributes.swift @@ -0,0 +1,45 @@ +import ActivityKit +import Foundation + +/// Shared schema used by iOS app + Live Activity widget extension. +struct OpenClawActivityAttributes: ActivityAttributes { + var agentName: String + var sessionKey: String + + struct ContentState: Codable, Hashable { + var statusText: String + var isIdle: Bool + var isDisconnected: Bool + var isConnecting: Bool + var startedAt: Date + } +} + +#if DEBUG +extension OpenClawActivityAttributes { + static let preview = OpenClawActivityAttributes(agentName: "main", sessionKey: "main") +} + +extension OpenClawActivityAttributes.ContentState { + static let connecting = OpenClawActivityAttributes.ContentState( + statusText: "Connecting...", + isIdle: false, + isDisconnected: false, + isConnecting: true, + startedAt: .now) + + static let idle = OpenClawActivityAttributes.ContentState( + statusText: "Idle", + isIdle: true, + isDisconnected: false, + isConnecting: false, + startedAt: .now) + + static let disconnected = OpenClawActivityAttributes.ContentState( + statusText: "Disconnected", + isIdle: false, + isDisconnected: true, + isConnecting: false, + startedAt: .now) +} +#endif diff --git a/apps/ios/Sources/Model/NodeAppModel.swift b/apps/ios/Sources/Model/NodeAppModel.swift index 54548eb8d96..34826aefeaf 100644 --- a/apps/ios/Sources/Model/NodeAppModel.swift +++ b/apps/ios/Sources/Model/NodeAppModel.swift @@ -1695,6 +1695,7 @@ extension NodeAppModel { self.operatorGatewayTask = nil self.voiceWakeSyncTask?.cancel() self.voiceWakeSyncTask = nil + LiveActivityManager.shared.handleDisconnect() self.gatewayHealthMonitor.stop() Task { await self.operatorGateway.disconnect() @@ -1731,6 +1732,7 @@ private extension NodeAppModel { self.operatorConnected = false self.voiceWakeSyncTask?.cancel() self.voiceWakeSyncTask = nil + LiveActivityManager.shared.handleDisconnect() self.gatewayDefaultAgentId = nil self.gatewayAgents = [] self.selectedAgentId = GatewaySettingsStore.loadGatewaySelectedAgentId(stableID: stableID) @@ -1811,6 +1813,7 @@ private extension NodeAppModel { await self.refreshAgentsFromGateway() await self.refreshShareRouteFromGateway() await self.startVoiceWakeSync() + await MainActor.run { LiveActivityManager.shared.handleReconnect() } await MainActor.run { self.startGatewayHealthMonitor() } }, onDisconnected: { [weak self] reason in @@ -1818,6 +1821,7 @@ private extension NodeAppModel { await MainActor.run { self.operatorConnected = false self.talkMode.updateGatewayConnected(false) + LiveActivityManager.shared.handleDisconnect() } GatewayDiagnostics.log("operator gateway disconnected reason=\(reason)") await MainActor.run { self.stopGatewayHealthMonitor() } @@ -1882,6 +1886,14 @@ private extension NodeAppModel { self.gatewayStatusText = (attempt == 0) ? "Connecting…" : "Reconnecting…" self.gatewayServerName = nil self.gatewayRemoteAddress = nil + let liveActivity = LiveActivityManager.shared + if liveActivity.isActive { + liveActivity.handleConnecting() + } else { + liveActivity.startActivity( + agentName: self.selectedAgentId ?? "main", + sessionKey: self.mainSessionKey) + } } do { diff --git a/apps/ios/SwiftSources.input.xcfilelist b/apps/ios/SwiftSources.input.xcfilelist index 514ca732673..c94ef48fa32 100644 --- a/apps/ios/SwiftSources.input.xcfilelist +++ b/apps/ios/SwiftSources.input.xcfilelist @@ -62,3 +62,7 @@ Sources/Voice/VoiceWakePreferences.swift ../../Swabble/Sources/SwabbleKit/WakeWordGate.swift Sources/Voice/TalkModeManager.swift Sources/Voice/TalkOrbOverlay.swift +Sources/LiveActivity/OpenClawActivityAttributes.swift +Sources/LiveActivity/LiveActivityManager.swift +ActivityWidget/OpenClawActivityWidgetBundle.swift +ActivityWidget/OpenClawLiveActivity.swift diff --git a/apps/ios/project.yml b/apps/ios/project.yml index 1f3cad955bf..3cc4444ce09 100644 --- a/apps/ios/project.yml +++ b/apps/ios/project.yml @@ -38,6 +38,8 @@ targets: dependencies: - target: OpenClawShareExtension embed: true + - target: OpenClawActivityWidget + embed: true - target: OpenClawWatchApp - package: OpenClawKit - package: OpenClawKit @@ -84,6 +86,7 @@ targets: TARGETED_DEVICE_FAMILY: "1" SWIFT_VERSION: "6.0" SWIFT_STRICT_CONCURRENCY: complete + SUPPORTS_LIVE_ACTIVITIES: YES ENABLE_APPINTENTS_METADATA: NO ENABLE_APP_INTENTS_METADATA_GENERATION: NO info: @@ -115,6 +118,7 @@ targets: NSLocationAlwaysAndWhenInUseUsageDescription: OpenClaw can share your location in the background when you enable Always. NSMicrophoneUsageDescription: OpenClaw needs microphone access for voice wake. NSSpeechRecognitionUsageDescription: OpenClaw uses on-device speech recognition for voice wake. + NSSupportsLiveActivities: true UISupportedInterfaceOrientations: - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown @@ -164,6 +168,37 @@ targets: NSExtensionActivationSupportsImageWithMaxCount: 10 NSExtensionActivationSupportsMovieWithMaxCount: 1 + OpenClawActivityWidget: + type: app-extension + platform: iOS + configFiles: + Debug: Signing.xcconfig + Release: Signing.xcconfig + sources: + - path: ActivityWidget + - path: Sources/LiveActivity/OpenClawActivityAttributes.swift + dependencies: + - sdk: WidgetKit.framework + - sdk: ActivityKit.framework + settings: + base: + CODE_SIGN_IDENTITY: "Apple Development" + CODE_SIGN_STYLE: "$(OPENCLAW_CODE_SIGN_STYLE)" + DEVELOPMENT_TEAM: "$(OPENCLAW_DEVELOPMENT_TEAM)" + PRODUCT_BUNDLE_IDENTIFIER: "$(OPENCLAW_ACTIVITY_WIDGET_BUNDLE_ID)" + SWIFT_VERSION: "6.0" + SWIFT_STRICT_CONCURRENCY: complete + SUPPORTS_LIVE_ACTIVITIES: YES + info: + path: ActivityWidget/Info.plist + properties: + CFBundleDisplayName: OpenClaw Activity + CFBundleShortVersionString: "2026.3.2" + CFBundleVersion: "20260301" + NSSupportsLiveActivities: true + NSExtension: + NSExtensionPointIdentifier: com.apple.widgetkit-extension + OpenClawWatchApp: type: application.watchapp2 platform: watchOS diff --git a/changelog/fragments/ios-live-activity-status-cleanup.md b/changelog/fragments/ios-live-activity-status-cleanup.md new file mode 100644 index 00000000000..06a6004080f --- /dev/null +++ b/changelog/fragments/ios-live-activity-status-cleanup.md @@ -0,0 +1 @@ +- iOS: add Live Activity connection status (connecting/idle/disconnected) on Lock Screen and Dynamic Island, and clean up duplicate/stale activities before starting a new one (#33591) (thanks @mbelinky, @leepokai) From 61f7cea48bd78190f5c73bedb32cab1411f87ddb Mon Sep 17 00:00:00 2001 From: Bob Date: Wed, 4 Mar 2026 10:52:28 +0100 Subject: [PATCH 68/74] fix: kill stuck ACP child processes on startup and harden sessions in discord threads (#33699) * Gateway: resolve agent.wait for chat.send runs * Discord: harden ACP thread binding + listener timeout * ACPX: handle already-exited child wait * Gateway/Discord: address PR review findings * Discord: keep ACP error-state thread bindings on startup * gateway: make agent.wait dedupe bridge event-driven * discord: harden ACP probe classification and cap startup fan-out * discord: add cooperative timeout cancellation * discord: fix startup probe concurrency helper typing * plugin-sdk: avoid Windows root-alias shard timeout * plugin-sdk: keep root alias reflection path non-blocking * discord+gateway: resolve remaining PR review findings * gateway+discord: fix codex review regressions * Discord/Gateway: address Codex review findings * Gateway: keep agent.wait lifecycle active with shared run IDs * Discord: clean up status reactions on aborted runs * fix: add changelog note for ACP/Discord startup hardening (#33699) (thanks @dutifulbob) --------- Co-authored-by: Onur <2453968+osolmaz@users.noreply.github.com> --- CHANGELOG.md | 1 + .../src/runtime-internals/process.test.ts | 67 +++- .../acpx/src/runtime-internals/process.ts | 68 +++- extensions/acpx/src/runtime.ts | 10 +- src/acp/control-plane/manager.core.ts | 147 ++++---- src/acp/runtime/types.ts | 2 +- src/daemon/systemd-unit.test.ts | 9 + src/daemon/systemd-unit.ts | 7 +- src/discord/monitor.test.ts | 6 +- src/discord/monitor/listeners.test.ts | 106 ++++++ src/discord/monitor/listeners.ts | 129 ++++++- .../monitor/message-handler.preflight.ts | 24 ++ .../message-handler.preflight.types.ts | 2 + .../monitor/message-handler.process.test.ts | 26 ++ .../monitor/message-handler.process.ts | 79 ++++- .../monitor/message-handler.queue.test.ts | 51 +++ src/discord/monitor/message-handler.ts | 172 +++++++++- src/discord/monitor/preflight-audio.ts | 16 + src/discord/monitor/provider.test.ts | 200 +++++++++++ src/discord/monitor/provider.ts | 115 ++++++- .../monitor/thread-bindings.lifecycle.test.ts | 285 +++++++++++++++- .../monitor/thread-bindings.lifecycle.ts | 130 ++++++- src/gateway/server-methods/agent-job.ts | 26 +- .../server-methods/agent-wait-dedupe.test.ts | 323 ++++++++++++++++++ .../server-methods/agent-wait-dedupe.ts | 244 +++++++++++++ src/gateway/server-methods/agent.ts | 98 +++++- src/gateway/server-methods/chat.ts | 47 ++- .../server-methods/server-methods.test.ts | 37 ++ .../server.chat.gateway-server-chat.test.ts | 239 +++++++++++++ src/plugin-sdk/root-alias.cjs | 82 ++++- 30 files changed, 2568 insertions(+), 180 deletions(-) create mode 100644 src/gateway/server-methods/agent-wait-dedupe.test.ts create mode 100644 src/gateway/server-methods/agent-wait-dedupe.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ad03596f00..db6e5f310e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ Docs: https://docs.openclaw.ai - Outbound/send config threading: pass resolved SecretRef config through outbound adapters and helper send paths so send flows do not reload unresolved runtime config. (#33987) Thanks @joshavant. - Sessions/subagent attachments: remove `attachments[].content.maxLength` from `sessions_spawn` schema to avoid llama.cpp GBNF repetition overflow, and preflight UTF-8 byte size before buffer allocation while keeping runtime file-size enforcement unchanged. (#33648) Thanks @anisoptera. - Runtime/tool-state stability: recover from dangling Anthropic `tool_use` after compaction, serialize long-running Discord handler runs without blocking new inbound events, and prevent stale busy snapshots from suppressing stuck-channel recovery. (from #33630, #33583) Thanks @kevinWangSheng and @theotarr. +- ACP/Discord startup hardening: clean up stuck ACP worker children on gateway restart, unbind stale ACP thread bindings during Discord startup reconciliation, and add per-thread listener watchdog timeouts so wedged turns cannot block later messages. (#33699) Thanks @dutifulbob. - Extensions/media local-root propagation: consistently forward `mediaLocalRoots` through extension `sendMedia` adapters (Google Chat, Slack, iMessage, Signal, WhatsApp), preserving non-local media behavior while restoring local attachment resolution from configured roots. Synthesis of #33581, #33545, #33540, #33536, #33528. Thanks @bmendonca3. - Gateway/security default response headers: add `Permissions-Policy: camera=(), microphone=(), geolocation=()` to baseline gateway HTTP security headers for all responses. (#30186) thanks @habakan. - Plugins/startup loading: lazily initialize plugin runtime, split startup-critical plugin SDK imports into `openclaw/plugin-sdk/core` and `openclaw/plugin-sdk/telegram`, and preserve `api.runtime` reflection semantics for plugin compatibility. (#28620) thanks @hmemcpy. diff --git a/extensions/acpx/src/runtime-internals/process.test.ts b/extensions/acpx/src/runtime-internals/process.test.ts index 85a72a13398..0eee162eddf 100644 --- a/extensions/acpx/src/runtime-internals/process.test.ts +++ b/extensions/acpx/src/runtime-internals/process.test.ts @@ -1,9 +1,15 @@ +import { spawn } from "node:child_process"; import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import { afterEach, describe, expect, it } from "vitest"; import { createWindowsCmdShimFixture } from "../../../shared/windows-cmd-shim-test-fixtures.js"; -import { resolveSpawnCommand, type SpawnCommandCache } from "./process.js"; +import { + resolveSpawnCommand, + spawnAndCollect, + type SpawnCommandCache, + waitForExit, +} from "./process.js"; const tempDirs: string[] = []; @@ -225,3 +231,62 @@ describe("resolveSpawnCommand", () => { expect(second.args[0]).toBe(scriptPath); }); }); + +describe("waitForExit", () => { + it("resolves when the child already exited before waiting starts", async () => { + const child = spawn(process.execPath, ["-e", "process.exit(0)"], { + stdio: ["pipe", "pipe", "pipe"], + }); + + await new Promise((resolve, reject) => { + child.once("close", () => { + resolve(); + }); + child.once("error", reject); + }); + + const exit = await waitForExit(child); + expect(exit.code).toBe(0); + expect(exit.signal).toBeNull(); + expect(exit.error).toBeNull(); + }); +}); + +describe("spawnAndCollect", () => { + it("returns abort error immediately when signal is already aborted", async () => { + const controller = new AbortController(); + controller.abort(); + const result = await spawnAndCollect( + { + command: process.execPath, + args: ["-e", "process.exit(0)"], + cwd: process.cwd(), + }, + undefined, + { signal: controller.signal }, + ); + + expect(result.code).toBeNull(); + expect(result.error?.name).toBe("AbortError"); + }); + + it("terminates a running process when signal aborts", async () => { + const controller = new AbortController(); + const resultPromise = spawnAndCollect( + { + command: process.execPath, + args: ["-e", "setTimeout(() => process.stdout.write('done'), 10_000)"], + cwd: process.cwd(), + }, + undefined, + { signal: controller.signal }, + ); + + setTimeout(() => { + controller.abort(); + }, 10); + + const result = await resultPromise; + expect(result.error?.name).toBe("AbortError"); + }); +}); diff --git a/extensions/acpx/src/runtime-internals/process.ts b/extensions/acpx/src/runtime-internals/process.ts index 953f088586e..4df84aece2f 100644 --- a/extensions/acpx/src/runtime-internals/process.ts +++ b/extensions/acpx/src/runtime-internals/process.ts @@ -114,6 +114,12 @@ export function resolveSpawnCommand( }; } +function createAbortError(): Error { + const error = new Error("Operation aborted."); + error.name = "AbortError"; + return error; +} + export function spawnWithResolvedCommand( params: { command: string; @@ -140,6 +146,15 @@ export function spawnWithResolvedCommand( } export async function waitForExit(child: ChildProcessWithoutNullStreams): Promise { + // Handle callers that start waiting after the child has already exited. + if (child.exitCode !== null || child.signalCode !== null) { + return { + code: child.exitCode, + signal: child.signalCode, + error: null, + }; + } + return await new Promise((resolve) => { let settled = false; const finish = (result: SpawnExit) => { @@ -167,12 +182,23 @@ export async function spawnAndCollect( cwd: string; }, options?: SpawnCommandOptions, + runtime?: { + signal?: AbortSignal; + }, ): Promise<{ stdout: string; stderr: string; code: number | null; error: Error | null; }> { + if (runtime?.signal?.aborted) { + return { + stdout: "", + stderr: "", + code: null, + error: createAbortError(), + }; + } const child = spawnWithResolvedCommand(params, options); child.stdin.end(); @@ -185,13 +211,43 @@ export async function spawnAndCollect( stderr += String(chunk); }); - const exit = await waitForExit(child); - return { - stdout, - stderr, - code: exit.code, - error: exit.error, + let abortKillTimer: NodeJS.Timeout | undefined; + let aborted = false; + const onAbort = () => { + aborted = true; + try { + child.kill("SIGTERM"); + } catch { + // Ignore kill races when child already exited. + } + abortKillTimer = setTimeout(() => { + if (child.exitCode !== null || child.signalCode !== null) { + return; + } + try { + child.kill("SIGKILL"); + } catch { + // Ignore kill races when child already exited. + } + }, 250); + abortKillTimer.unref?.(); }; + runtime?.signal?.addEventListener("abort", onAbort, { once: true }); + + try { + const exit = await waitForExit(child); + return { + stdout, + stderr, + code: exit.code, + error: aborted ? createAbortError() : exit.error, + }; + } finally { + runtime?.signal?.removeEventListener("abort", onAbort); + if (abortKillTimer) { + clearTimeout(abortKillTimer); + } + } } export function resolveSpawnFailure( diff --git a/extensions/acpx/src/runtime.ts b/extensions/acpx/src/runtime.ts index fc66b394b3c..8a7783a704c 100644 --- a/extensions/acpx/src/runtime.ts +++ b/extensions/acpx/src/runtime.ts @@ -353,7 +353,10 @@ export class AcpxRuntime implements AcpRuntime { return ACPX_CAPABILITIES; } - async getStatus(input: { handle: AcpRuntimeHandle }): Promise { + async getStatus(input: { + handle: AcpRuntimeHandle; + signal?: AbortSignal; + }): Promise { const state = this.resolveHandleState(input.handle); const events = await this.runControlCommand({ args: this.buildControlArgs({ @@ -363,6 +366,7 @@ export class AcpxRuntime implements AcpRuntime { cwd: state.cwd, fallbackCode: "ACP_TURN_FAILED", ignoreNoSession: true, + signal: input.signal, }); const detail = events.find((event) => !toAcpxErrorEvent(event)) ?? events[0]; if (!detail) { @@ -586,6 +590,7 @@ export class AcpxRuntime implements AcpRuntime { cwd: string; fallbackCode: AcpRuntimeErrorCode; ignoreNoSession?: boolean; + signal?: AbortSignal; }): Promise { const result = await spawnAndCollect( { @@ -594,6 +599,9 @@ export class AcpxRuntime implements AcpRuntime { cwd: params.cwd, }, this.spawnCommandOptions, + { + signal: params.signal, + }, ); if (result.error) { diff --git a/src/acp/control-plane/manager.core.ts b/src/acp/control-plane/manager.core.ts index 99ec096bb7f..4d45a7693a9 100644 --- a/src/acp/control-plane/manager.core.ts +++ b/src/acp/control-plane/manager.core.ts @@ -316,70 +316,85 @@ export class AcpSessionManager { async getSessionStatus(params: { cfg: OpenClawConfig; sessionKey: string; + signal?: AbortSignal; }): Promise { const sessionKey = normalizeSessionKey(params.sessionKey); if (!sessionKey) { throw new AcpRuntimeError("ACP_SESSION_INIT_FAILED", "ACP session key is required."); } + this.throwIfAborted(params.signal); await this.evictIdleRuntimeHandles({ cfg: params.cfg }); - return await this.withSessionActor(sessionKey, async () => { - const resolution = this.resolveSession({ - cfg: params.cfg, - sessionKey, - }); - if (resolution.kind === "none") { - throw new AcpRuntimeError( - "ACP_SESSION_INIT_FAILED", - `Session is not ACP-enabled: ${sessionKey}`, - ); - } - if (resolution.kind === "stale") { - throw resolution.error; - } - const { - runtime, - handle: ensuredHandle, - meta: ensuredMeta, - } = await this.ensureRuntimeHandle({ - cfg: params.cfg, - sessionKey, - meta: resolution.meta, - }); - let handle = ensuredHandle; - let meta = ensuredMeta; - const capabilities = await this.resolveRuntimeCapabilities({ runtime, handle }); - let runtimeStatus: AcpRuntimeStatus | undefined; - if (runtime.getStatus) { - runtimeStatus = await withAcpRuntimeErrorBoundary({ - run: async () => await runtime.getStatus!({ handle }), - fallbackCode: "ACP_TURN_FAILED", - fallbackMessage: "Could not read ACP runtime status.", + return await this.withSessionActor( + sessionKey, + async () => { + this.throwIfAborted(params.signal); + const resolution = this.resolveSession({ + cfg: params.cfg, + sessionKey, }); - } - ({ handle, meta, runtimeStatus } = await this.reconcileRuntimeSessionIdentifiers({ - cfg: params.cfg, - sessionKey, - runtime, - handle, - meta, - runtimeStatus, - failOnStatusError: true, - })); - const identity = resolveSessionIdentityFromMeta(meta); - return { - sessionKey, - backend: handle.backend || meta.backend, - agent: meta.agent, - ...(identity ? { identity } : {}), - state: meta.state, - mode: meta.mode, - runtimeOptions: resolveRuntimeOptionsFromMeta(meta), - capabilities, - runtimeStatus, - lastActivityAt: meta.lastActivityAt, - lastError: meta.lastError, - }; - }); + if (resolution.kind === "none") { + throw new AcpRuntimeError( + "ACP_SESSION_INIT_FAILED", + `Session is not ACP-enabled: ${sessionKey}`, + ); + } + if (resolution.kind === "stale") { + throw resolution.error; + } + const { + runtime, + handle: ensuredHandle, + meta: ensuredMeta, + } = await this.ensureRuntimeHandle({ + cfg: params.cfg, + sessionKey, + meta: resolution.meta, + }); + let handle = ensuredHandle; + let meta = ensuredMeta; + const capabilities = await this.resolveRuntimeCapabilities({ runtime, handle }); + let runtimeStatus: AcpRuntimeStatus | undefined; + if (runtime.getStatus) { + runtimeStatus = await withAcpRuntimeErrorBoundary({ + run: async () => { + this.throwIfAborted(params.signal); + const status = await runtime.getStatus!({ + handle, + ...(params.signal ? { signal: params.signal } : {}), + }); + this.throwIfAborted(params.signal); + return status; + }, + fallbackCode: "ACP_TURN_FAILED", + fallbackMessage: "Could not read ACP runtime status.", + }); + } + ({ handle, meta, runtimeStatus } = await this.reconcileRuntimeSessionIdentifiers({ + cfg: params.cfg, + sessionKey, + runtime, + handle, + meta, + runtimeStatus, + failOnStatusError: true, + })); + const identity = resolveSessionIdentityFromMeta(meta); + return { + sessionKey, + backend: handle.backend || meta.backend, + agent: meta.agent, + ...(identity ? { identity } : {}), + state: meta.state, + mode: meta.mode, + runtimeOptions: resolveRuntimeOptionsFromMeta(meta), + capabilities, + runtimeStatus, + lastActivityAt: meta.lastActivityAt, + lastError: meta.lastError, + }; + }, + params.signal, + ); } async setSessionRuntimeMode(params: { @@ -1295,9 +1310,23 @@ export class AcpSessionManager { } } - private async withSessionActor(sessionKey: string, op: () => Promise): Promise { + private async withSessionActor( + sessionKey: string, + op: () => Promise, + signal?: AbortSignal, + ): Promise { const actorKey = normalizeActorKey(sessionKey); - return await this.actorQueue.run(actorKey, op); + return await this.actorQueue.run(actorKey, async () => { + this.throwIfAborted(signal); + return await op(); + }); + } + + private throwIfAborted(signal?: AbortSignal): void { + if (!signal?.aborted) { + return; + } + throw new AcpRuntimeError("ACP_TURN_FAILED", "ACP operation aborted."); } private getCachedRuntimeState(sessionKey: string): CachedRuntimeState | null { diff --git a/src/acp/runtime/types.ts b/src/acp/runtime/types.ts index ff4f39a70ee..6a3d3bb3f8e 100644 --- a/src/acp/runtime/types.ts +++ b/src/acp/runtime/types.ts @@ -117,7 +117,7 @@ export interface AcpRuntime { handle?: AcpRuntimeHandle; }): Promise | AcpRuntimeCapabilities; - getStatus?(input: { handle: AcpRuntimeHandle }): Promise; + getStatus?(input: { handle: AcpRuntimeHandle; signal?: AbortSignal }): Promise; setMode?(input: { handle: AcpRuntimeHandle; mode: string }): Promise; diff --git a/src/daemon/systemd-unit.test.ts b/src/daemon/systemd-unit.test.ts index bd65e34bba4..5c5562b25e6 100644 --- a/src/daemon/systemd-unit.test.ts +++ b/src/daemon/systemd-unit.test.ts @@ -12,6 +12,15 @@ describe("buildSystemdUnit", () => { expect(execStart).toBe('ExecStart=/usr/bin/openclaw gateway --name "My Bot"'); }); + it("renders control-group kill mode for child-process cleanup", () => { + const unit = buildSystemdUnit({ + description: "OpenClaw Gateway", + programArguments: ["/usr/bin/openclaw", "gateway", "run"], + environment: {}, + }); + expect(unit).toContain("KillMode=control-group"); + }); + it("rejects environment values with line breaks", () => { expect(() => buildSystemdUnit({ diff --git a/src/daemon/systemd-unit.ts b/src/daemon/systemd-unit.ts index 000f4b64a92..9cddbee24d1 100644 --- a/src/daemon/systemd-unit.ts +++ b/src/daemon/systemd-unit.ts @@ -59,10 +59,9 @@ export function buildSystemdUnit({ `ExecStart=${execStart}`, "Restart=always", "RestartSec=5", - // KillMode=process ensures systemd only waits for the main process to exit. - // Without this, podman's conmon (container monitor) processes block shutdown - // since they run as children of the gateway and stay in the same cgroup. - "KillMode=process", + // Keep service children in the same lifecycle so restarts do not leave + // orphan ACP/runtime workers behind. + "KillMode=control-group", workingDirLine, ...envLines, "", diff --git a/src/discord/monitor.test.ts b/src/discord/monitor.test.ts index 6f555ede67d..50bb52af18d 100644 --- a/src/discord/monitor.test.ts +++ b/src/discord/monitor.test.ts @@ -197,9 +197,9 @@ describe("DiscordMessageListener", () => { // Release the background handler and allow slow-log finalizer to run. deferred.resolve(); - await Promise.resolve(); - - expect(logger.warn).toHaveBeenCalled(); + await vi.waitFor(() => { + expect(logger.warn).toHaveBeenCalled(); + }); const warnMock = logger.warn as unknown as { mock: { calls: unknown[][] } }; const [, meta] = warnMock.mock.calls[0] ?? []; const durationMs = (meta as { durationMs?: number } | undefined)?.durationMs; diff --git a/src/discord/monitor/listeners.test.ts b/src/discord/monitor/listeners.test.ts index 6264ab218db..d1342b3ddb2 100644 --- a/src/discord/monitor/listeners.test.ts +++ b/src/discord/monitor/listeners.test.ts @@ -121,4 +121,110 @@ describe("DiscordMessageListener", () => { ); }); }); + + it("continues same-channel processing after handler timeout", async () => { + vi.useFakeTimers(); + try { + const never = new Promise(() => {}); + const handler = vi.fn(async () => { + if (handler.mock.calls.length === 1) { + await never; + return; + } + }); + const logger = createLogger(); + const listener = new DiscordMessageListener(handler as never, logger as never, undefined, { + timeoutMs: 50, + }); + + await listener.handle(fakeEvent("ch-1"), {} as never); + await listener.handle(fakeEvent("ch-1"), {} as never); + expect(handler).toHaveBeenCalledTimes(1); + + await vi.advanceTimersByTimeAsync(60); + await vi.waitFor(() => { + expect(handler).toHaveBeenCalledTimes(2); + }); + expect(logger.error).toHaveBeenCalledWith(expect.stringContaining("timed out after")); + } finally { + vi.useRealTimers(); + } + }); + + it("aborts timed-out handlers and prevents late side effects", async () => { + vi.useFakeTimers(); + try { + let abortReceived = false; + let lateSideEffect = false; + const handler = vi.fn( + async ( + _data: unknown, + _client: unknown, + options?: { + abortSignal?: AbortSignal; + }, + ) => { + await new Promise((resolve) => { + if (options?.abortSignal?.aborted) { + abortReceived = true; + resolve(); + return; + } + options?.abortSignal?.addEventListener( + "abort", + () => { + abortReceived = true; + resolve(); + }, + { once: true }, + ); + }); + if (options?.abortSignal?.aborted) { + return; + } + lateSideEffect = true; + }, + ); + const logger = createLogger(); + const listener = new DiscordMessageListener(handler as never, logger as never, undefined, { + timeoutMs: 50, + }); + + await listener.handle(fakeEvent("ch-1"), {} as never); + await listener.handle(fakeEvent("ch-1"), {} as never); + + await vi.advanceTimersByTimeAsync(60); + await vi.waitFor(() => { + expect(handler).toHaveBeenCalledTimes(2); + }); + expect(abortReceived).toBe(true); + expect(lateSideEffect).toBe(false); + expect(logger.error).toHaveBeenCalledWith(expect.stringContaining("timed out after")); + } finally { + vi.useRealTimers(); + } + }); + + it("does not emit slow-listener warnings when timeout already fired", async () => { + vi.useFakeTimers(); + try { + const never = new Promise(() => {}); + const handler = vi.fn(async () => { + await never; + }); + const logger = createLogger(); + const listener = new DiscordMessageListener(handler as never, logger as never, undefined, { + timeoutMs: 31_000, + }); + + await listener.handle(fakeEvent("ch-1"), {} as never); + await vi.advanceTimersByTimeAsync(31_100); + await vi.waitFor(() => { + expect(logger.error).toHaveBeenCalledWith(expect.stringContaining("timed out after")); + }); + expect(logger.warn).not.toHaveBeenCalled(); + } finally { + vi.useRealTimers(); + } + }); }); diff --git a/src/discord/monitor/listeners.ts b/src/discord/monitor/listeners.ts index 71d7cfbddf9..5297460e228 100644 --- a/src/discord/monitor/listeners.ts +++ b/src/discord/monitor/listeners.ts @@ -41,7 +41,11 @@ type Logger = ReturnType[0]; -export type DiscordMessageHandler = (data: DiscordMessageEvent, client: Client) => Promise; +export type DiscordMessageHandler = ( + data: DiscordMessageEvent, + client: Client, + options?: { abortSignal?: AbortSignal }, +) => Promise; type DiscordReactionEvent = Parameters[0]; @@ -66,13 +70,50 @@ type DiscordReactionRoutingParams = { }; const DISCORD_SLOW_LISTENER_THRESHOLD_MS = 30_000; +const DISCORD_DEFAULT_LISTENER_TIMEOUT_MS = 120_000; const discordEventQueueLog = createSubsystemLogger("discord/event-queue"); +function normalizeDiscordListenerTimeoutMs(raw: number | undefined): number { + if (!Number.isFinite(raw) || (raw ?? 0) <= 0) { + return DISCORD_DEFAULT_LISTENER_TIMEOUT_MS; + } + return Math.max(1_000, Math.floor(raw!)); +} + +function formatListenerContextValue(value: unknown): string | null { + if (value === undefined || value === null) { + return null; + } + if (typeof value === "string") { + const trimmed = value.trim(); + return trimmed.length > 0 ? trimmed : null; + } + if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") { + return String(value); + } + return null; +} + +function formatListenerContextSuffix(context?: Record): string { + if (!context) { + return ""; + } + const entries = Object.entries(context).flatMap(([key, value]) => { + const formatted = formatListenerContextValue(value); + return formatted ? [`${key}=${formatted}`] : []; + }); + if (entries.length === 0) { + return ""; + } + return ` (${entries.join(" ")})`; +} + function logSlowDiscordListener(params: { logger: Logger | undefined; listener: string; event: string; durationMs: number; + context?: Record; }) { if (params.durationMs < DISCORD_SLOW_LISTENER_THRESHOLD_MS) { return; @@ -88,7 +129,8 @@ function logSlowDiscordListener(params: { event: params.event, durationMs: params.durationMs, duration, - consoleMessage: message, + ...params.context, + consoleMessage: `${message}${formatListenerContextSuffix(params.context)}`, }); } @@ -96,12 +138,59 @@ async function runDiscordListenerWithSlowLog(params: { logger: Logger | undefined; listener: string; event: string; - run: () => Promise; + run: (abortSignal: AbortSignal) => Promise; + timeoutMs?: number; + context?: Record; onError?: (err: unknown) => void; }) { const startedAt = Date.now(); + const timeoutMs = normalizeDiscordListenerTimeoutMs(params.timeoutMs); + let timedOut = false; + let timeoutHandle: ReturnType | null = null; + const logger = params.logger ?? discordEventQueueLog; + const abortController = new AbortController(); + const runPromise = params.run(abortController.signal).catch((err) => { + if (timedOut) { + const errorName = + err && typeof err === "object" && "name" in err ? String(err.name) : undefined; + if (abortController.signal.aborted && errorName === "AbortError") { + logger.warn( + `discord handler canceled after timeout${formatListenerContextSuffix(params.context)}`, + ); + return; + } + logger.error( + danger( + `discord handler failed after timeout: ${String(err)}${formatListenerContextSuffix(params.context)}`, + ), + ); + return; + } + throw err; + }); + try { - await params.run(); + const timeoutPromise = new Promise<"timeout">((resolve) => { + timeoutHandle = setTimeout(() => resolve("timeout"), timeoutMs); + timeoutHandle.unref?.(); + }); + const result = await Promise.race([ + runPromise.then(() => "completed" as const), + timeoutPromise, + ]); + if (result === "timeout") { + timedOut = true; + abortController.abort(); + logger.error( + danger( + `discord handler timed out after ${formatDurationSeconds(timeoutMs, { + decimals: 1, + unit: "seconds", + })}${formatListenerContextSuffix(params.context)}`, + ), + ); + return; + } } catch (err) { if (params.onError) { params.onError(err); @@ -109,12 +198,18 @@ async function runDiscordListenerWithSlowLog(params: { } throw err; } finally { - logSlowDiscordListener({ - logger: params.logger, - listener: params.listener, - event: params.event, - durationMs: Date.now() - startedAt, - }); + if (timeoutHandle) { + clearTimeout(timeoutHandle); + } + if (!timedOut) { + logSlowDiscordListener({ + logger: params.logger, + listener: params.listener, + event: params.event, + durationMs: Date.now() - startedAt, + context: params.context, + }); + } } } @@ -128,18 +223,26 @@ export function registerDiscordListener(listeners: Array, listener: obje export class DiscordMessageListener extends MessageCreateListener { private readonly channelQueue = new KeyedAsyncQueue(); + private readonly listenerTimeoutMs: number; constructor( private handler: DiscordMessageHandler, private logger?: Logger, private onEvent?: () => void, + options?: { timeoutMs?: number }, ) { super(); + this.listenerTimeoutMs = normalizeDiscordListenerTimeoutMs(options?.timeoutMs); } async handle(data: DiscordMessageEvent, client: Client) { this.onEvent?.(); const channelId = data.channel_id; + const context = { + channelId, + messageId: (data as { message?: { id?: string } }).message?.id, + guildId: (data as { guild_id?: string }).guild_id, + } satisfies Record; // Serialize messages within the same channel to preserve ordering, // but allow different channels to proceed in parallel so that // channel-bound agents are not blocked by each other. @@ -148,7 +251,9 @@ export class DiscordMessageListener extends MessageCreateListener { logger: this.logger, listener: this.constructor.name, event: this.type, - run: () => this.handler(data, client), + timeoutMs: this.listenerTimeoutMs, + context, + run: (abortSignal) => this.handler(data, client, { abortSignal }), onError: (err) => { const logger = this.logger ?? discordEventQueueLog; logger.error(danger(`discord handler failed: ${String(err)}`)); @@ -206,7 +311,7 @@ async function runDiscordReactionHandler(params: { logger: params.handlerParams.logger, listener: params.listener, event: params.event, - run: () => + run: async () => handleDiscordReactionEvent({ data: params.data, client: params.client, diff --git a/src/discord/monitor/message-handler.preflight.ts b/src/discord/monitor/message-handler.preflight.ts index 7339caf0604..2aea357d236 100644 --- a/src/discord/monitor/message-handler.preflight.ts +++ b/src/discord/monitor/message-handler.preflight.ts @@ -68,6 +68,10 @@ export type { const DISCORD_BOUND_THREAD_SYSTEM_PREFIXES = ["⚙️", "🤖", "🧰"]; +function isPreflightAborted(abortSignal?: AbortSignal): boolean { + return Boolean(abortSignal?.aborted); +} + function isBoundThreadBotSystemMessage(params: { isBoundThreadSession: boolean; isBotAuthor: boolean; @@ -124,6 +128,9 @@ export function shouldIgnoreBoundThreadWebhookMessage(params: { export async function preflightDiscordMessage( params: DiscordMessagePreflightParams, ): Promise { + if (isPreflightAborted(params.abortSignal)) { + return null; + } const logger = getChildLogger({ module: "discord-auto-reply" }); const message = params.data.message; const author = params.data.author; @@ -157,6 +164,9 @@ export async function preflightDiscordMessage( messageId: message.id, config: pluralkitConfig, }); + if (isPreflightAborted(params.abortSignal)) { + return null; + } } catch (err) { logVerbose(`discord: pluralkit lookup failed for ${message.id}: ${String(err)}`); } @@ -176,6 +186,9 @@ export async function preflightDiscordMessage( const isGuildMessage = Boolean(params.data.guild_id); const channelInfo = await resolveDiscordChannelInfo(params.client, messageChannelId); + if (isPreflightAborted(params.abortSignal)) { + return null; + } const isDirectMessage = channelInfo?.type === ChannelType.DM; const isGroupDm = channelInfo?.type === ChannelType.GroupDM; logDebug( @@ -213,6 +226,9 @@ export async function preflightDiscordMessage( allowNameMatching, useAccessGroups, }); + if (isPreflightAborted(params.abortSignal)) { + return null; + } commandAuthorized = dmAccess.commandAuthorized; if (dmAccess.decision !== "allow") { const allowMatchMeta = formatAllowlistMatchMeta( @@ -300,6 +316,9 @@ export async function preflightDiscordMessage( threadChannel: earlyThreadChannel, channelInfo, }); + if (isPreflightAborted(params.abortSignal)) { + return null; + } earlyThreadParentId = parentInfo.id; earlyThreadParentName = parentInfo.name; earlyThreadParentType = parentInfo.type; @@ -548,7 +567,11 @@ export async function preflightDiscordMessage( shouldRequireMention, mentionRegexes, cfg: params.cfg, + abortSignal: params.abortSignal, }); + if (isPreflightAborted(params.abortSignal)) { + return null; + } const mentionText = hasTypedText ? baseText : ""; const wasMentioned = @@ -727,6 +750,7 @@ export async function preflightDiscordMessage( token: params.token, runtime: params.runtime, botUserId: params.botUserId, + abortSignal: params.abortSignal, guildHistories: params.guildHistories, historyLimit: params.historyLimit, mediaMaxBytes: params.mediaMaxBytes, diff --git a/src/discord/monitor/message-handler.preflight.types.ts b/src/discord/monitor/message-handler.preflight.types.ts index 0cca0cb4085..a2b3c210a1c 100644 --- a/src/discord/monitor/message-handler.preflight.types.ts +++ b/src/discord/monitor/message-handler.preflight.types.ts @@ -25,6 +25,7 @@ export type DiscordMessagePreflightContext = { token: string; runtime: RuntimeEnv; botUserId?: string; + abortSignal?: AbortSignal; guildHistories: Map; historyLimit: number; mediaMaxBytes: number; @@ -95,6 +96,7 @@ export type DiscordMessagePreflightParams = { token: string; runtime: RuntimeEnv; botUserId?: string; + abortSignal?: AbortSignal; guildHistories: Map; historyLimit: number; mediaMaxBytes: number; diff --git a/src/discord/monitor/message-handler.process.test.ts b/src/discord/monitor/message-handler.process.test.ts index 748ee921c72..9bc9cf77498 100644 --- a/src/discord/monitor/message-handler.process.test.ts +++ b/src/discord/monitor/message-handler.process.test.ts @@ -345,6 +345,32 @@ describe("processDiscordMessage ack reactions", () => { expect(emojis).toContain("🟦"); expect(emojis).toContain("🏁"); }); + + it("clears status reactions when dispatch aborts and removeAckAfterReply is enabled", async () => { + const abortController = new AbortController(); + dispatchInboundMessage.mockImplementationOnce(async () => { + abortController.abort(); + throw new Error("aborted"); + }); + + const ctx = await createBaseContext({ + abortSignal: abortController.signal, + cfg: { + messages: { + ackReaction: "👀", + removeAckAfterReply: true, + }, + session: { store: "/tmp/openclaw-discord-process-test-sessions.json" }, + }, + }); + + // oxlint-disable-next-line typescript/no-explicit-any + await processDiscordMessage(ctx as any); + + await vi.waitFor(() => { + expect(sendMocks.removeReactionDiscord).toHaveBeenCalledWith("c1", "m1", "👀", { rest: {} }); + }); + }); }); describe("processDiscordMessage session routing", () => { diff --git a/src/discord/monitor/message-handler.process.ts b/src/discord/monitor/message-handler.process.ts index cf942046ce1..3b7082dc218 100644 --- a/src/discord/monitor/message-handler.process.ts +++ b/src/discord/monitor/message-handler.process.ts @@ -60,6 +60,10 @@ function sleep(ms: number): Promise { const DISCORD_TYPING_MAX_DURATION_MS = 20 * 60_000; +function isProcessAborted(abortSignal?: AbortSignal): boolean { + return Boolean(abortSignal?.aborted); +} + export async function processDiscordMessage(ctx: DiscordMessagePreflightContext) { const { cfg, @@ -105,16 +109,26 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext) route, commandAuthorized, discordRestFetch, + abortSignal, } = ctx; + if (isProcessAborted(abortSignal)) { + return; + } const ssrfPolicy = cfg.browser?.ssrfPolicy; const mediaList = await resolveMediaList(message, mediaMaxBytes, discordRestFetch, ssrfPolicy); + if (isProcessAborted(abortSignal)) { + return; + } const forwardedMediaList = await resolveForwardedMediaList( message, mediaMaxBytes, discordRestFetch, ssrfPolicy, ); + if (isProcessAborted(abortSignal)) { + return; + } mediaList.push(...forwardedMediaList); const text = messageText; if (!text) { @@ -585,6 +599,9 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext) humanDelay: resolveHumanDelayConfig(cfg, route.agentId), typingCallbacks, deliver: async (payload: ReplyPayload, info) => { + if (isProcessAborted(abortSignal)) { + return; + } const isFinal = info.kind === "final"; if (payload.isReasoning) { // Reasoning/thinking payloads should not be delivered to Discord. @@ -607,6 +624,9 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext) if (canFinalizeViaPreviewEdit) { await draftStream.stop(); + if (isProcessAborted(abortSignal)) { + return; + } try { await editMessageDiscord( deliverChannelId, @@ -627,6 +647,9 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext) // Check if stop() flushed a message we can edit if (!finalizedViaPreviewMessage) { await draftStream.stop(); + if (isProcessAborted(abortSignal)) { + return; + } const messageIdAfterStop = draftStream.messageId(); if ( typeof messageIdAfterStop === "string" && @@ -657,6 +680,9 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext) await draftStream.clear(); } } + if (isProcessAborted(abortSignal)) { + return; + } const replyToId = replyReference.use(); await deliverDiscordReply({ @@ -682,6 +708,9 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext) runtime.error?.(danger(`discord ${info.kind} reply failed: ${String(err)}`)); }, onReplyStart: async () => { + if (isProcessAborted(abortSignal)) { + return; + } await typingCallbacks.onReplyStart(); await statusReactions.setThinking(); }, @@ -689,13 +718,19 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext) let dispatchResult: Awaited> | null = null; let dispatchError = false; + let dispatchAborted = false; try { + if (isProcessAborted(abortSignal)) { + dispatchAborted = true; + return; + } dispatchResult = await dispatchInboundMessage({ ctx: ctxPayload, cfg, dispatcher, replyOptions: { ...replyOptions, + abortSignal, skillFilter: channelConfig?.skills, disableBlockStreaming: disableBlockStreamingForDraft ?? @@ -730,11 +765,22 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext) await statusReactions.setThinking(); }, onToolStart: async (payload) => { + if (isProcessAborted(abortSignal)) { + return; + } await statusReactions.setTool(payload.name); }, }, }); + if (isProcessAborted(abortSignal)) { + dispatchAborted = true; + return; + } } catch (err) { + if (isProcessAborted(abortSignal)) { + dispatchAborted = true; + return; + } dispatchError = true; throw err; } finally { @@ -752,21 +798,32 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext) markDispatchIdle(); } if (statusReactionsEnabled) { - if (dispatchError) { - await statusReactions.setError(); + if (dispatchAborted) { + if (removeAckAfterReply) { + void statusReactions.clear(); + } else { + void statusReactions.restoreInitial(); + } } else { - await statusReactions.setDone(); - } - if (removeAckAfterReply) { - void (async () => { - await sleep(dispatchError ? DEFAULT_TIMING.errorHoldMs : DEFAULT_TIMING.doneHoldMs); - await statusReactions.clear(); - })(); - } else { - void statusReactions.restoreInitial(); + if (dispatchError) { + await statusReactions.setError(); + } else { + await statusReactions.setDone(); + } + if (removeAckAfterReply) { + void (async () => { + await sleep(dispatchError ? DEFAULT_TIMING.errorHoldMs : DEFAULT_TIMING.doneHoldMs); + await statusReactions.clear(); + })(); + } else { + void statusReactions.restoreInitial(); + } } } } + if (dispatchAborted) { + return; + } if (!dispatchResult?.queuedFinal) { if (isGuildMessage) { diff --git a/src/discord/monitor/message-handler.queue.test.ts b/src/discord/monitor/message-handler.queue.test.ts index 1424b29d46d..9ab7914adcc 100644 --- a/src/discord/monitor/message-handler.queue.test.ts +++ b/src/discord/monitor/message-handler.queue.test.ts @@ -26,6 +26,7 @@ function createDeferred() { function createHandlerParams(overrides?: { setStatus?: (patch: Record) => void; abortSignal?: AbortSignal; + listenerTimeoutMs?: number; }) { const cfg: OpenClawConfig = { channels: { @@ -64,6 +65,7 @@ function createHandlerParams(overrides?: { threadBindings: createNoopThreadBindingManager("default"), setStatus: overrides?.setStatus, abortSignal: overrides?.abortSignal, + listenerTimeoutMs: overrides?.listenerTimeoutMs, }; } @@ -167,6 +169,55 @@ describe("createDiscordMessageHandler queue behavior", () => { }); }); + it("applies listener timeout to queued runs so stalled runs do not block the queue", async () => { + vi.useFakeTimers(); + try { + preflightDiscordMessageMock.mockReset(); + processDiscordMessageMock.mockReset(); + + processDiscordMessageMock + .mockImplementationOnce(async (ctx: { abortSignal?: AbortSignal }) => { + await new Promise((resolve) => { + if (ctx.abortSignal?.aborted) { + resolve(); + return; + } + ctx.abortSignal?.addEventListener("abort", () => resolve(), { once: true }); + }); + }) + .mockImplementationOnce(async () => undefined); + preflightDiscordMessageMock.mockImplementation( + async (params: { data: { channel_id: string } }) => + createPreflightContext(params.data.channel_id), + ); + + const params = createHandlerParams({ listenerTimeoutMs: 50 }); + const handler = createDiscordMessageHandler(params); + + await expect( + handler(createMessageData("m-1") as never, {} as never), + ).resolves.toBeUndefined(); + await expect( + handler(createMessageData("m-2") as never, {} as never), + ).resolves.toBeUndefined(); + + await vi.advanceTimersByTimeAsync(60); + await vi.waitFor(() => { + expect(processDiscordMessageMock).toHaveBeenCalledTimes(2); + }); + + const firstCtx = processDiscordMessageMock.mock.calls[0]?.[0] as + | { abortSignal?: AbortSignal } + | undefined; + expect(firstCtx?.abortSignal?.aborted).toBe(true); + expect(params.runtime.error).toHaveBeenCalledWith( + expect.stringContaining("discord queued run timed out after"), + ); + } finally { + vi.useRealTimers(); + } + }); + it("refreshes run activity while active runs are in progress", async () => { preflightDiscordMessageMock.mockReset(); processDiscordMessageMock.mockReset(); diff --git a/src/discord/monitor/message-handler.ts b/src/discord/monitor/message-handler.ts index a069a5a52ec..2d8a245c328 100644 --- a/src/discord/monitor/message-handler.ts +++ b/src/discord/monitor/message-handler.ts @@ -6,6 +6,7 @@ import { import { createRunStateMachine } from "../../channels/run-state-machine.js"; import { resolveOpenProviderRuntimeGroupPolicy } from "../../config/runtime-group-policy.js"; import { danger } from "../../globals.js"; +import { formatDurationSeconds } from "../../infra/format-time/format-duration.ts"; import { KeyedAsyncQueue } from "../../plugin-sdk/keyed-async-queue.js"; import type { DiscordMessageEvent, DiscordMessageHandler } from "./listeners.js"; import { preflightDiscordMessage } from "./message-handler.preflight.js"; @@ -27,12 +28,142 @@ type DiscordMessageHandlerParams = Omit< > & { setStatus?: DiscordMonitorStatusSink; abortSignal?: AbortSignal; + listenerTimeoutMs?: number; }; export type DiscordMessageHandlerWithLifecycle = DiscordMessageHandler & { deactivate: () => void; }; +const DEFAULT_DISCORD_RUN_TIMEOUT_MS = 120_000; +const MAX_DISCORD_TIMEOUT_MS = 2_147_483_647; + +function normalizeDiscordRunTimeoutMs(timeoutMs?: number): number { + if (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs) || timeoutMs <= 0) { + return DEFAULT_DISCORD_RUN_TIMEOUT_MS; + } + return Math.max(1, Math.min(Math.floor(timeoutMs), MAX_DISCORD_TIMEOUT_MS)); +} + +function isAbortError(error: unknown): boolean { + if (typeof error !== "object" || error === null) { + return false; + } + return "name" in error && String((error as { name?: unknown }).name) === "AbortError"; +} + +function formatDiscordRunContextSuffix(ctx: DiscordMessagePreflightContext): string { + const eventData = ctx as { + data?: { + channel_id?: string; + message?: { + id?: string; + }; + }; + }; + const channelId = ctx.messageChannelId?.trim() || eventData.data?.channel_id?.trim(); + const messageId = eventData.data?.message?.id?.trim(); + const details = [ + channelId ? `channelId=${channelId}` : null, + messageId ? `messageId=${messageId}` : null, + ].filter((entry): entry is string => Boolean(entry)); + if (details.length === 0) { + return ""; + } + return ` (${details.join(", ")})`; +} + +function mergeAbortSignals(signals: Array): AbortSignal | undefined { + const activeSignals = signals.filter((signal): signal is AbortSignal => Boolean(signal)); + if (activeSignals.length === 0) { + return undefined; + } + if (activeSignals.length === 1) { + return activeSignals[0]; + } + if (typeof AbortSignal.any === "function") { + return AbortSignal.any(activeSignals); + } + const fallbackController = new AbortController(); + for (const signal of activeSignals) { + if (signal.aborted) { + fallbackController.abort(); + return fallbackController.signal; + } + } + const abortFallback = () => { + fallbackController.abort(); + for (const signal of activeSignals) { + signal.removeEventListener("abort", abortFallback); + } + }; + for (const signal of activeSignals) { + signal.addEventListener("abort", abortFallback, { once: true }); + } + return fallbackController.signal; +} + +async function processDiscordRunWithTimeout(params: { + ctx: DiscordMessagePreflightContext; + runtime: DiscordMessagePreflightParams["runtime"]; + lifecycleSignal?: AbortSignal; + timeoutMs?: number; +}) { + const timeoutMs = normalizeDiscordRunTimeoutMs(params.timeoutMs); + const timeoutAbortController = new AbortController(); + const combinedSignal = mergeAbortSignals([ + params.ctx.abortSignal, + params.lifecycleSignal, + timeoutAbortController.signal, + ]); + const processCtx = + combinedSignal && combinedSignal !== params.ctx.abortSignal + ? { ...params.ctx, abortSignal: combinedSignal } + : params.ctx; + const contextSuffix = formatDiscordRunContextSuffix(params.ctx); + let timedOut = false; + let timeoutHandle: ReturnType | null = null; + const processPromise = processDiscordMessage(processCtx).catch((error) => { + if (timedOut) { + if (timeoutAbortController.signal.aborted && isAbortError(error)) { + return; + } + params.runtime.error?.( + danger(`discord queued run failed after timeout: ${String(error)}${contextSuffix}`), + ); + return; + } + throw error; + }); + + try { + const timeoutPromise = new Promise<"timeout">((resolve) => { + timeoutHandle = setTimeout(() => resolve("timeout"), timeoutMs); + timeoutHandle.unref?.(); + }); + const result = await Promise.race([ + processPromise.then(() => "completed" as const), + timeoutPromise, + ]); + if (result === "timeout") { + timedOut = true; + timeoutAbortController.abort(); + params.runtime.error?.( + danger( + `discord queued run timed out after ${formatDurationSeconds(timeoutMs, { + decimals: 1, + unit: "seconds", + })}${contextSuffix}`, + ), + ); + } + } finally { + if (timeoutHandle) { + clearTimeout(timeoutHandle); + } + } +} + function resolveDiscordRunQueueKey(ctx: DiscordMessagePreflightContext): string { const sessionKey = ctx.route.sessionKey?.trim(); if (sessionKey) { @@ -75,7 +206,12 @@ export function createDiscordMessageHandler( if (!runState.isActive()) { return; } - await processDiscordMessage(ctx); + await processDiscordRunWithTimeout({ + ctx, + runtime: params.runtime, + lifecycleSignal: params.abortSignal, + timeoutMs: params.listenerTimeoutMs, + }); } finally { runState.onRunEnd(); } @@ -88,6 +224,7 @@ export function createDiscordMessageHandler( const { debouncer } = createChannelInboundDebouncer<{ data: DiscordMessageEvent; client: Client; + abortSignal?: AbortSignal; }>({ cfg: params.cfg, channel: "discord", @@ -126,11 +263,16 @@ export function createDiscordMessageHandler( if (!last) { return; } + const abortSignal = last.abortSignal; + if (abortSignal?.aborted) { + return; + } if (entries.length === 1) { const ctx = await preflightDiscordMessage({ ...params, ackReactionScope, groupPolicy, + abortSignal, data: last.data, client: last.client, }); @@ -162,6 +304,7 @@ export function createDiscordMessageHandler( ...params, ackReactionScope, groupPolicy, + abortSignal, data: syntheticData, client: last.client, }); @@ -188,19 +331,22 @@ export function createDiscordMessageHandler( }, }); - const handler: DiscordMessageHandlerWithLifecycle = async (data, client) => { - // Filter bot-own messages before they enter the debounce queue. - // The same check exists in preflightDiscordMessage(), but by that point - // the message has already consumed debounce capacity and blocked - // legitimate user messages. On active servers this causes cumulative - // slowdown (see #15874). - const msgAuthorId = data.message?.author?.id ?? data.author?.id; - if (params.botUserId && msgAuthorId === params.botUserId) { - return; - } - + const handler: DiscordMessageHandlerWithLifecycle = async (data, client, options) => { try { - await debouncer.enqueue({ data, client }); + if (options?.abortSignal?.aborted) { + return; + } + // Filter bot-own messages before they enter the debounce queue. + // The same check exists in preflightDiscordMessage(), but by that point + // the message has already consumed debounce capacity and blocked + // legitimate user messages. On active servers this causes cumulative + // slowdown (see #15874). + const msgAuthorId = data.message?.author?.id ?? data.author?.id; + if (params.botUserId && msgAuthorId === params.botUserId) { + return; + } + + await debouncer.enqueue({ data, client, abortSignal: options?.abortSignal }); } catch (err) { params.runtime.error?.(danger(`handler failed: ${String(err)}`)); } diff --git a/src/discord/monitor/preflight-audio.ts b/src/discord/monitor/preflight-audio.ts index 89e4ae8c3e1..307abcc6b43 100644 --- a/src/discord/monitor/preflight-audio.ts +++ b/src/discord/monitor/preflight-audio.ts @@ -24,6 +24,7 @@ export async function resolveDiscordPreflightAudioMentionContext(params: { shouldRequireMention: boolean; mentionRegexes: RegExp[]; cfg: OpenClawConfig; + abortSignal?: AbortSignal; }): Promise<{ hasAudioAttachment: boolean; hasTypedText: boolean; @@ -42,8 +43,20 @@ export async function resolveDiscordPreflightAudioMentionContext(params: { let transcript: string | undefined; if (needsPreflightTranscription) { + if (params.abortSignal?.aborted) { + return { + hasAudioAttachment, + hasTypedText, + }; + } try { const { transcribeFirstAudio } = await import("../../media-understanding/audio-preflight.js"); + if (params.abortSignal?.aborted) { + return { + hasAudioAttachment, + hasTypedText, + }; + } const audioUrls = audioAttachments .map((att) => att.url) .filter((url): url is string => typeof url === "string" && url.length > 0); @@ -58,6 +71,9 @@ export async function resolveDiscordPreflightAudioMentionContext(params: { cfg: params.cfg, agentDir: undefined, }); + if (params.abortSignal?.aborted) { + transcript = undefined; + } } } catch (err) { logVerbose(`discord: audio preflight transcription failed: ${String(err)}`); diff --git a/src/discord/monitor/provider.test.ts b/src/discord/monitor/provider.test.ts index 8481b5356f6..e3bc0ca36c1 100644 --- a/src/discord/monitor/provider.test.ts +++ b/src/discord/monitor/provider.test.ts @@ -1,5 +1,6 @@ import { EventEmitter } from "node:events"; import { beforeEach, describe, expect, it, vi } from "vitest"; +import { AcpRuntimeError } from "../../acp/runtime/errors.js"; import type { OpenClawConfig } from "../../config/config.js"; import type { RuntimeEnv } from "../../runtime.js"; @@ -25,6 +26,7 @@ const { createThreadBindingManagerMock, reconcileAcpThreadBindingsOnStartupMock, createdBindingManagers, + getAcpSessionStatusMock, getPluginCommandSpecsMock, listNativeCommandSpecsForConfigMock, listSkillCommandsForAgentsMock, @@ -63,6 +65,11 @@ const { staleSessionKeys: [], })), createdBindingManagers, + getAcpSessionStatusMock: vi.fn( + async (_params: { cfg: OpenClawConfig; sessionKey: string; signal?: AbortSignal }) => ({ + state: "idle", + }), + ), getPluginCommandSpecsMock: vi.fn<() => PluginCommandSpecMock[]>(() => []), listNativeCommandSpecsForConfigMock: vi.fn<() => NativeCommandSpecMock[]>(() => [ { name: "cmd", description: "built-in", acceptsArgs: false }, @@ -127,6 +134,12 @@ vi.mock("../../auto-reply/chunk.js", () => ({ resolveTextChunkLimit: () => 2000, })); +vi.mock("../../acp/control-plane/manager.js", () => ({ + getAcpSessionManager: () => ({ + getSessionStatus: getAcpSessionStatusMock, + }), +})); + vi.mock("../../auto-reply/commands-registry.js", () => ({ listNativeCommandSpecsForConfig: listNativeCommandSpecsForConfigMock, })); @@ -272,6 +285,21 @@ vi.mock("./thread-bindings.js", () => ({ })); describe("monitorDiscordProvider", () => { + type ReconcileHealthProbeParams = { + cfg: OpenClawConfig; + accountId: string; + sessionKey: string; + binding: unknown; + session: unknown; + }; + + type ReconcileStartupParams = { + cfg: OpenClawConfig; + healthProbe?: ( + params: ReconcileHealthProbeParams, + ) => Promise<{ status: string; reason?: string }>; + }; + const baseRuntime = (): RuntimeEnv => { return { log: vi.fn(), @@ -299,6 +327,16 @@ describe("monitorDiscordProvider", () => { return opts.eventQueue; }; + const getHealthProbe = () => { + expect(reconcileAcpThreadBindingsOnStartupMock).toHaveBeenCalledTimes(1); + const firstCall = reconcileAcpThreadBindingsOnStartupMock.mock.calls.at(0) as + | [ReconcileStartupParams] + | undefined; + const reconcileParams = firstCall?.[0]; + expect(typeof reconcileParams?.healthProbe).toBe("function"); + return reconcileParams?.healthProbe as NonNullable; + }; + beforeEach(() => { clientConstructorOptionsMock.mockClear(); createDiscordAutoPresenceControllerMock.mockClear().mockImplementation(() => ({ @@ -318,6 +356,7 @@ describe("monitorDiscordProvider", () => { removed: 0, staleSessionKeys: [], }); + getAcpSessionStatusMock.mockClear().mockResolvedValue({ state: "idle" }); createdBindingManagers.length = 0; getPluginCommandSpecsMock.mockClear().mockReturnValue([]); listNativeCommandSpecsForConfigMock @@ -368,6 +407,167 @@ describe("monitorDiscordProvider", () => { expect(reconcileAcpThreadBindingsOnStartupMock).toHaveBeenCalledTimes(1); }); + it("treats ACP error status as uncertain during startup thread-binding probes", async () => { + const { monitorDiscordProvider } = await import("./provider.js"); + getAcpSessionStatusMock.mockResolvedValue({ state: "error" }); + + await monitorDiscordProvider({ + config: baseConfig(), + runtime: baseRuntime(), + }); + + const probeResult = await getHealthProbe()({ + cfg: baseConfig(), + accountId: "default", + sessionKey: "agent:codex:acp:error", + binding: {} as never, + session: { + acp: { + state: "error", + lastActivityAt: Date.now(), + }, + } as never, + }); + + expect(probeResult).toEqual({ + status: "uncertain", + reason: "status-error-state", + }); + }); + + it("classifies typed ACP session init failures as stale", async () => { + const { monitorDiscordProvider } = await import("./provider.js"); + getAcpSessionStatusMock.mockRejectedValue( + new AcpRuntimeError("ACP_SESSION_INIT_FAILED", "missing ACP metadata"), + ); + + await monitorDiscordProvider({ + config: baseConfig(), + runtime: baseRuntime(), + }); + + const probeResult = await getHealthProbe()({ + cfg: baseConfig(), + accountId: "default", + sessionKey: "agent:codex:acp:stale", + binding: {} as never, + session: { + acp: { + state: "idle", + lastActivityAt: Date.now(), + }, + } as never, + }); + + expect(probeResult).toEqual({ + status: "stale", + reason: "session-init-failed", + }); + }); + + it("classifies typed non-init ACP errors as uncertain when not stale-running", async () => { + const { monitorDiscordProvider } = await import("./provider.js"); + getAcpSessionStatusMock.mockRejectedValue( + new AcpRuntimeError("ACP_BACKEND_UNAVAILABLE", "runtime unavailable"), + ); + + await monitorDiscordProvider({ + config: baseConfig(), + runtime: baseRuntime(), + }); + + const probeResult = await getHealthProbe()({ + cfg: baseConfig(), + accountId: "default", + sessionKey: "agent:codex:acp:uncertain", + binding: {} as never, + session: { + acp: { + state: "idle", + lastActivityAt: Date.now(), + }, + } as never, + }); + + expect(probeResult).toEqual({ + status: "uncertain", + reason: "status-error", + }); + }); + + it("aborts timed-out ACP status probes during startup thread-binding health checks", async () => { + vi.useFakeTimers(); + try { + const { monitorDiscordProvider } = await import("./provider.js"); + getAcpSessionStatusMock.mockImplementation( + ({ signal }: { signal?: AbortSignal }) => + new Promise((_resolve, reject) => { + signal?.addEventListener("abort", () => reject(new Error("aborted")), { once: true }); + }), + ); + + await monitorDiscordProvider({ + config: baseConfig(), + runtime: baseRuntime(), + }); + + const probePromise = getHealthProbe()({ + cfg: baseConfig(), + accountId: "default", + sessionKey: "agent:codex:acp:timeout", + binding: {} as never, + session: { + acp: { + state: "idle", + lastActivityAt: Date.now(), + }, + } as never, + }); + + await vi.advanceTimersByTimeAsync(8_100); + await expect(probePromise).resolves.toEqual({ + status: "uncertain", + reason: "status-timeout", + }); + + const firstCall = getAcpSessionStatusMock.mock.calls[0]?.[0] as + | { signal?: AbortSignal } + | undefined; + expect(firstCall?.signal).toBeDefined(); + expect(firstCall?.signal?.aborted).toBe(true); + } finally { + vi.useRealTimers(); + } + }); + + it("falls back to legacy missing-session message classification", async () => { + const { monitorDiscordProvider } = await import("./provider.js"); + getAcpSessionStatusMock.mockRejectedValue(new Error("ACP session metadata missing")); + + await monitorDiscordProvider({ + config: baseConfig(), + runtime: baseRuntime(), + }); + + const probeResult = await getHealthProbe()({ + cfg: baseConfig(), + accountId: "default", + sessionKey: "agent:codex:acp:legacy", + binding: {} as never, + session: { + acp: { + state: "idle", + lastActivityAt: Date.now(), + }, + } as never, + }); + + expect(probeResult).toEqual({ + status: "stale", + reason: "session-missing", + }); + }); + it("captures gateway errors emitted before lifecycle wait starts", async () => { const { monitorDiscordProvider } = await import("./provider.js"); const emitter = new EventEmitter(); diff --git a/src/discord/monitor/provider.ts b/src/discord/monitor/provider.ts index d69cc6d163e..a4f5b13f4e5 100644 --- a/src/discord/monitor/provider.ts +++ b/src/discord/monitor/provider.ts @@ -10,6 +10,8 @@ import { import { GatewayCloseCodes, type GatewayPlugin } from "@buape/carbon/gateway"; import { VoicePlugin } from "@buape/carbon/voice"; import { Routes } from "discord-api-types/v10"; +import { getAcpSessionManager } from "../../acp/control-plane/manager.js"; +import { isAcpRuntimeError } from "../../acp/runtime/errors.js"; import { resolveTextChunkLimit } from "../../auto-reply/chunk.js"; import type { NativeCommandSpec } from "../../auto-reply/commands-registry.js"; import { listNativeCommandSpecsForConfig } from "../../auto-reply/commands-registry.js"; @@ -175,6 +177,92 @@ function appendPluginCommandSpecs(params: { return merged; } +const DISCORD_ACP_STATUS_PROBE_TIMEOUT_MS = 8_000; +const DISCORD_ACP_STALE_RUNNING_ACTIVITY_MS = 2 * 60 * 1000; + +function isLegacyMissingSessionError(message: string): boolean { + return ( + message.includes("Session is not ACP-enabled") || + message.includes("ACP session metadata missing") + ); +} + +function classifyAcpStatusProbeError(params: { error: unknown; isStaleRunning: boolean }): { + status: "stale" | "uncertain"; + reason: string; +} { + if (isAcpRuntimeError(params.error) && params.error.code === "ACP_SESSION_INIT_FAILED") { + return { status: "stale", reason: "session-init-failed" }; + } + + const message = params.error instanceof Error ? params.error.message : String(params.error); + if (isLegacyMissingSessionError(message)) { + return { status: "stale", reason: "session-missing" }; + } + + return params.isStaleRunning + ? { status: "stale", reason: "status-error-running-stale" } + : { status: "uncertain", reason: "status-error" }; +} + +async function probeDiscordAcpBindingHealth(params: { + cfg: OpenClawConfig; + sessionKey: string; + storedState?: "idle" | "running" | "error"; + lastActivityAt?: number; +}): Promise<{ status: "healthy" | "stale" | "uncertain"; reason?: string }> { + const manager = getAcpSessionManager(); + const statusProbeAbortController = new AbortController(); + const statusPromise = manager + .getSessionStatus({ + cfg: params.cfg, + sessionKey: params.sessionKey, + signal: statusProbeAbortController.signal, + }) + .then((status) => ({ kind: "status" as const, status })) + .catch((error: unknown) => ({ kind: "error" as const, error })); + + let timeoutTimer: ReturnType | null = null; + const timeoutPromise = new Promise<{ kind: "timeout" }>((resolve) => { + timeoutTimer = setTimeout( + () => resolve({ kind: "timeout" }), + DISCORD_ACP_STATUS_PROBE_TIMEOUT_MS, + ); + timeoutTimer.unref?.(); + }); + const result = await Promise.race([statusPromise, timeoutPromise]); + if (timeoutTimer) { + clearTimeout(timeoutTimer); + } + if (result.kind === "timeout") { + statusProbeAbortController.abort(); + } + const runningForMs = + params.storedState === "running" && Number.isFinite(params.lastActivityAt) + ? Date.now() - Math.max(0, Math.floor(params.lastActivityAt ?? 0)) + : 0; + const isStaleRunning = + params.storedState === "running" && runningForMs >= DISCORD_ACP_STALE_RUNNING_ACTIVITY_MS; + + if (result.kind === "timeout") { + return isStaleRunning + ? { status: "stale", reason: "status-timeout-running-stale" } + : { status: "uncertain", reason: "status-timeout" }; + } + if (result.kind === "error") { + return classifyAcpStatusProbeError({ + error: result.error, + isStaleRunning, + }); + } + if (result.status.state === "error") { + // ACP error state is recoverable (next turn can clear it), so keep the + // binding unless stronger stale signals exist. + return { status: "uncertain", reason: "status-error-state" }; + } + return { status: "healthy" }; +} + async function deployDiscordCommands(params: { client: Client; runtime: RuntimeEnv; @@ -382,14 +470,32 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) { }) : createNoopThreadBindingManager(account.accountId); if (threadBindingsEnabled) { - const reconciliation = reconcileAcpThreadBindingsOnStartup({ + const uncertainProbeKeys = new Set(); + const reconciliation = await reconcileAcpThreadBindingsOnStartup({ cfg, accountId: account.accountId, sendFarewell: false, + healthProbe: async ({ sessionKey, session }) => { + const probe = await probeDiscordAcpBindingHealth({ + cfg, + sessionKey, + storedState: session.acp?.state, + lastActivityAt: session.acp?.lastActivityAt, + }); + if (probe.status === "uncertain") { + uncertainProbeKeys.add(`${sessionKey}${probe.reason ? ` (${probe.reason})` : ""}`); + } + return probe; + }, }); if (reconciliation.removed > 0) { logVerbose( - `discord: removed ${reconciliation.removed}/${reconciliation.checked} stale ACP thread bindings on startup for account ${account.accountId}`, + `discord: removed ${reconciliation.removed}/${reconciliation.checked} stale ACP thread bindings on startup for account ${account.accountId}: ${reconciliation.staleSessionKeys.join(", ")}`, + ); + } + if (uncertainProbeKeys.size > 0) { + logVerbose( + `discord: ACP thread-binding health probe uncertain for account ${account.accountId}: ${[...uncertainProbeKeys].join(", ")}`, ); } } @@ -599,6 +705,7 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) { runtime, setStatus: opts.setStatus, abortSignal: opts.abortSignal, + listenerTimeoutMs: eventQueueOpts.listenerTimeout, botUserId, guildHistories, historyLimit, @@ -623,7 +730,9 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) { registerDiscordListener( client.listeners, - new DiscordMessageListener(messageHandler, logger, trackInboundEvent), + new DiscordMessageListener(messageHandler, logger, trackInboundEvent, { + timeoutMs: eventQueueOpts.listenerTimeout, + }), ); const reactionListenerOptions = { cfg, diff --git a/src/discord/monitor/thread-bindings.lifecycle.test.ts b/src/discord/monitor/thread-bindings.lifecycle.test.ts index 0e5518d928a..b4eeb229f6f 100644 --- a/src/discord/monitor/thread-bindings.lifecycle.test.ts +++ b/src/discord/monitor/thread-bindings.lifecycle.test.ts @@ -811,7 +811,7 @@ describe("thread binding lifecycle", () => { }; }); - const result = reconcileAcpThreadBindingsOnStartup({ + const result = await reconcileAcpThreadBindingsOnStartup({ cfg: {} as OpenClawConfig, accountId: "default", }); @@ -855,7 +855,7 @@ describe("thread binding lifecycle", () => { acp: undefined, }); - const result = reconcileAcpThreadBindingsOnStartup({ + const result = await reconcileAcpThreadBindingsOnStartup({ cfg: {} as OpenClawConfig, accountId: "default", }); @@ -866,6 +866,287 @@ describe("thread binding lifecycle", () => { expect(manager.getByThreadId("thread-acp-uncertain")).toBeDefined(); }); + it("removes ACP bindings when health probe marks running session as stale", async () => { + const manager = createThreadBindingManager({ + accountId: "default", + persist: false, + enableSweeper: false, + idleTimeoutMs: 24 * 60 * 60 * 1000, + maxAgeMs: 0, + }); + + await manager.bindTarget({ + threadId: "thread-acp-running", + channelId: "parent-1", + targetKind: "acp", + targetSessionKey: "agent:codex:acp:running", + agentId: "codex", + webhookId: "wh-1", + webhookToken: "tok-1", + }); + + hoisted.readAcpSessionEntry.mockReturnValue({ + sessionKey: "agent:codex:acp:running", + storeSessionKey: "agent:codex:acp:running", + acp: { + backend: "acpx", + agent: "codex", + runtimeSessionName: "runtime:running", + mode: "persistent", + state: "running", + lastActivityAt: Date.now() - 5 * 60 * 1000, + }, + }); + + const result = await reconcileAcpThreadBindingsOnStartup({ + cfg: {} as OpenClawConfig, + accountId: "default", + healthProbe: async () => ({ status: "stale", reason: "status-timeout-running-stale" }), + }); + + expect(result.checked).toBe(1); + expect(result.removed).toBe(1); + expect(result.staleSessionKeys).toContain("agent:codex:acp:running"); + expect(manager.getByThreadId("thread-acp-running")).toBeUndefined(); + }); + + it("keeps running ACP bindings when health probe is uncertain", async () => { + const manager = createThreadBindingManager({ + accountId: "default", + persist: false, + enableSweeper: false, + idleTimeoutMs: 24 * 60 * 60 * 1000, + maxAgeMs: 0, + }); + + await manager.bindTarget({ + threadId: "thread-acp-running-uncertain", + channelId: "parent-1", + targetKind: "acp", + targetSessionKey: "agent:codex:acp:running-uncertain", + agentId: "codex", + webhookId: "wh-1", + webhookToken: "tok-1", + }); + + hoisted.readAcpSessionEntry.mockReturnValue({ + sessionKey: "agent:codex:acp:running-uncertain", + storeSessionKey: "agent:codex:acp:running-uncertain", + acp: { + backend: "acpx", + agent: "codex", + runtimeSessionName: "runtime:running-uncertain", + mode: "persistent", + state: "running", + lastActivityAt: Date.now(), + }, + }); + + const result = await reconcileAcpThreadBindingsOnStartup({ + cfg: {} as OpenClawConfig, + accountId: "default", + healthProbe: async () => ({ status: "uncertain", reason: "status-timeout" }), + }); + + expect(result.checked).toBe(1); + expect(result.removed).toBe(0); + expect(result.staleSessionKeys).toEqual([]); + expect(manager.getByThreadId("thread-acp-running-uncertain")).toBeDefined(); + }); + + it("keeps ACP bindings in stored error state when no explicit stale probe verdict exists", async () => { + const manager = createThreadBindingManager({ + accountId: "default", + persist: false, + enableSweeper: false, + idleTimeoutMs: 24 * 60 * 60 * 1000, + maxAgeMs: 0, + }); + + await manager.bindTarget({ + threadId: "thread-acp-error", + channelId: "parent-1", + targetKind: "acp", + targetSessionKey: "agent:codex:acp:error", + agentId: "codex", + webhookId: "wh-1", + webhookToken: "tok-1", + }); + + hoisted.readAcpSessionEntry.mockReturnValue({ + sessionKey: "agent:codex:acp:error", + storeSessionKey: "agent:codex:acp:error", + acp: { + backend: "acpx", + agent: "codex", + runtimeSessionName: "runtime:error", + mode: "persistent", + state: "error", + lastActivityAt: Date.now(), + }, + }); + + const result = await reconcileAcpThreadBindingsOnStartup({ + cfg: {} as OpenClawConfig, + accountId: "default", + }); + + expect(result.checked).toBe(1); + expect(result.removed).toBe(0); + expect(result.staleSessionKeys).toEqual([]); + expect(manager.getByThreadId("thread-acp-error")).toBeDefined(); + }); + + it("starts ACP health probes in parallel during startup reconciliation", async () => { + const manager = createThreadBindingManager({ + accountId: "default", + persist: false, + enableSweeper: false, + idleTimeoutMs: 24 * 60 * 60 * 1000, + maxAgeMs: 0, + }); + + await manager.bindTarget({ + threadId: "thread-acp-probe-1", + channelId: "parent-1", + targetKind: "acp", + targetSessionKey: "agent:codex:acp:probe-1", + agentId: "codex", + webhookId: "wh-1", + webhookToken: "tok-1", + }); + await manager.bindTarget({ + threadId: "thread-acp-probe-2", + channelId: "parent-1", + targetKind: "acp", + targetSessionKey: "agent:codex:acp:probe-2", + agentId: "codex", + webhookId: "wh-1", + webhookToken: "tok-1", + }); + + hoisted.readAcpSessionEntry.mockImplementation((paramsUnknown: unknown) => { + const sessionKey = (paramsUnknown as { sessionKey?: string }).sessionKey ?? ""; + return { + sessionKey, + storeSessionKey: sessionKey, + acp: { + backend: "acpx", + agent: "codex", + runtimeSessionName: `runtime:${sessionKey}`, + mode: "persistent", + state: "running", + lastActivityAt: Date.now(), + }, + }; + }); + + let resolveFirstProbe: ((value: { status: "healthy" }) => void) | undefined; + const firstProbe = new Promise<{ status: "healthy" }>((resolve) => { + resolveFirstProbe = resolve; + }); + let probeCallCount = 0; + let secondProbeStartedBeforeFirstResolved = false; + + const reconcilePromise = reconcileAcpThreadBindingsOnStartup({ + cfg: {} as OpenClawConfig, + accountId: "default", + healthProbe: async () => { + probeCallCount += 1; + if (probeCallCount === 1) { + return await firstProbe; + } + secondProbeStartedBeforeFirstResolved = true; + return { status: "healthy" as const }; + }, + }); + + await Promise.resolve(); + await Promise.resolve(); + const observedParallelStart = secondProbeStartedBeforeFirstResolved; + + resolveFirstProbe?.({ status: "healthy" }); + const result = await reconcilePromise; + + expect(observedParallelStart).toBe(true); + expect(result.checked).toBe(2); + expect(result.removed).toBe(0); + }); + + it("caps ACP startup health probe concurrency", async () => { + const manager = createThreadBindingManager({ + accountId: "default", + persist: false, + enableSweeper: false, + idleTimeoutMs: 24 * 60 * 60 * 1000, + maxAgeMs: 0, + }); + + for (let index = 0; index < 12; index += 1) { + const key = `agent:codex:acp:cap-${index}`; + await manager.bindTarget({ + threadId: `thread-acp-cap-${index}`, + channelId: "parent-1", + targetKind: "acp", + targetSessionKey: key, + agentId: "codex", + webhookId: "wh-1", + webhookToken: "tok-1", + }); + } + + hoisted.readAcpSessionEntry.mockImplementation((paramsUnknown: unknown) => { + const sessionKey = (paramsUnknown as { sessionKey?: string }).sessionKey ?? ""; + return { + sessionKey, + storeSessionKey: sessionKey, + acp: { + backend: "acpx", + agent: "codex", + runtimeSessionName: `runtime:${sessionKey}`, + mode: "persistent", + state: "running", + lastActivityAt: Date.now(), + }, + }; + }); + + const PROBE_LIMIT = 8; + let probeCalls = 0; + let inFlight = 0; + let maxInFlight = 0; + let releaseFirstWave: (() => void) | undefined; + const firstWaveGate = new Promise((resolve) => { + releaseFirstWave = resolve; + }); + + const reconcilePromise = reconcileAcpThreadBindingsOnStartup({ + cfg: {} as OpenClawConfig, + accountId: "default", + healthProbe: async () => { + probeCalls += 1; + inFlight += 1; + maxInFlight = Math.max(maxInFlight, inFlight); + if (probeCalls <= PROBE_LIMIT) { + await firstWaveGate; + } + inFlight -= 1; + return { status: "healthy" as const }; + }, + }); + + await vi.waitFor(() => { + expect(probeCalls).toBe(PROBE_LIMIT); + }); + expect(maxInFlight).toBe(PROBE_LIMIT); + + releaseFirstWave?.(); + const result = await reconcilePromise; + expect(result.checked).toBe(12); + expect(result.removed).toBe(0); + expect(maxInFlight).toBeLessThanOrEqual(PROBE_LIMIT); + }); + it("migrates legacy expiresAt bindings to idle/max-age semantics", () => { const previousStateDir = process.env.OPENCLAW_STATE_DIR; const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-thread-bindings-")); diff --git a/src/discord/monitor/thread-bindings.lifecycle.ts b/src/discord/monitor/thread-bindings.lifecycle.ts index bfc6c8513fb..f5beb9a3e6f 100644 --- a/src/discord/monitor/thread-bindings.lifecycle.ts +++ b/src/discord/monitor/thread-bindings.lifecycle.ts @@ -1,4 +1,4 @@ -import { readAcpSessionEntry } from "../../acp/runtime/session-meta.js"; +import { readAcpSessionEntry, type AcpSessionStoreEntry } from "../../acp/runtime/session-meta.js"; import type { OpenClawConfig } from "../../config/config.js"; import { normalizeAccountId } from "../../routing/session-key.js"; import { parseDiscordTarget } from "../targets.js"; @@ -29,6 +29,50 @@ export type AcpThreadBindingReconciliationResult = { staleSessionKeys: string[]; }; +export type AcpThreadBindingHealthStatus = "healthy" | "stale" | "uncertain"; + +export type AcpThreadBindingHealthProbe = (params: { + cfg: OpenClawConfig; + accountId: string; + sessionKey: string; + binding: ThreadBindingRecord; + session: AcpSessionStoreEntry; +}) => Promise<{ + status: AcpThreadBindingHealthStatus; + reason?: string; +}>; + +// Cap startup fan-out so large binding sets do not create unbounded ACP probe spikes. +const ACP_STARTUP_HEALTH_PROBE_CONCURRENCY_LIMIT = 8; + +async function mapWithConcurrency(params: { + items: TItem[]; + limit: number; + worker: (item: TItem, index: number) => Promise; +}): Promise { + if (params.items.length === 0) { + return []; + } + const limit = Math.max(1, Math.floor(params.limit)); + const resultsByIndex = new Map(); + let nextIndex = 0; + + const runWorker = async () => { + for (;;) { + const index = nextIndex; + nextIndex += 1; + if (index >= params.items.length) { + return; + } + resultsByIndex.set(index, await params.worker(params.items[index], index)); + } + }; + + const workers = Array.from({ length: Math.min(limit, params.items.length) }, () => runWorker()); + await Promise.all(workers); + return params.items.map((_item, index) => resultsByIndex.get(index)!); +} + function normalizeNonNegativeMs(raw: number): number { if (!Number.isFinite(raw)) { return 0; @@ -259,11 +303,21 @@ export function setThreadBindingMaxAgeBySessionKey(params: { return updated; } -export function reconcileAcpThreadBindingsOnStartup(params: { +function resolveStoredAcpBindingHealth(params: { + session: AcpSessionStoreEntry; +}): AcpThreadBindingHealthStatus { + if (!params.session.acp) { + return "stale"; + } + return "healthy"; +} + +export async function reconcileAcpThreadBindingsOnStartup(params: { cfg: OpenClawConfig; accountId?: string; sendFarewell?: boolean; -}): AcpThreadBindingReconciliationResult { + healthProbe?: AcpThreadBindingHealthProbe; +}): Promise { const manager = getThreadBindingManager(params.accountId); if (!manager) { return { @@ -274,21 +328,77 @@ export function reconcileAcpThreadBindingsOnStartup(params: { } const acpBindings = manager.listBindings().filter((binding) => binding.targetKind === "acp"); - const staleBindings = acpBindings.filter((binding) => { + const staleBindings: ThreadBindingRecord[] = []; + const probeTargets: Array<{ + binding: ThreadBindingRecord; + sessionKey: string; + session: AcpSessionStoreEntry; + }> = []; + + for (const binding of acpBindings) { const sessionKey = binding.targetSessionKey.trim(); if (!sessionKey) { - return true; + staleBindings.push(binding); + continue; } const session = readAcpSessionEntry({ cfg: params.cfg, sessionKey, }); - // Session store read failures are transient; never auto-unbind on uncertain reads. - if (session?.storeReadFailed) { - return false; + if (!session) { + staleBindings.push(binding); + continue; } - return !session?.acp; - }); + // Session store read failures are transient; never auto-unbind on uncertain reads. + if (session.storeReadFailed) { + continue; + } + + if (resolveStoredAcpBindingHealth({ session }) === "stale") { + staleBindings.push(binding); + continue; + } + + if (!params.healthProbe) { + continue; + } + probeTargets.push({ binding, sessionKey, session }); + } + + if (params.healthProbe && probeTargets.length > 0) { + const probeResults = await mapWithConcurrency({ + items: probeTargets, + limit: ACP_STARTUP_HEALTH_PROBE_CONCURRENCY_LIMIT, + worker: async ({ binding, sessionKey, session }) => { + try { + const result = await params.healthProbe?.({ + cfg: params.cfg, + accountId: manager.accountId, + sessionKey, + binding, + session, + }); + return { + binding, + status: result?.status ?? ("uncertain" satisfies AcpThreadBindingHealthStatus), + }; + } catch { + // Treat probe failures as uncertain and keep the binding. + return { + binding, + status: "uncertain" satisfies AcpThreadBindingHealthStatus, + }; + } + }, + }); + + for (const probeResult of probeResults) { + if (probeResult.status === "stale") { + staleBindings.push(probeResult.binding); + } + } + } + if (staleBindings.length === 0) { return { checked: acpBindings.length, diff --git a/src/gateway/server-methods/agent-job.ts b/src/gateway/server-methods/agent-job.ts index 1acd1bea175..2c7e7a6aeba 100644 --- a/src/gateway/server-methods/agent-job.ts +++ b/src/gateway/server-methods/agent-job.ts @@ -144,20 +144,23 @@ function getCachedAgentRun(runId: string) { export async function waitForAgentJob(params: { runId: string; timeoutMs: number; + signal?: AbortSignal; + ignoreCachedSnapshot?: boolean; }): Promise { - const { runId, timeoutMs } = params; + const { runId, timeoutMs, signal, ignoreCachedSnapshot = false } = params; ensureAgentRunListener(); - const cached = getCachedAgentRun(runId); + const cached = ignoreCachedSnapshot ? undefined : getCachedAgentRun(runId); if (cached) { return cached; } - if (timeoutMs <= 0) { + if (timeoutMs <= 0 || signal?.aborted) { return null; } return await new Promise((resolve) => { let settled = false; let pendingErrorTimer: NodeJS.Timeout | undefined; + let onAbort: (() => void) | undefined; const clearPendingErrorTimer = () => { if (!pendingErrorTimer) { @@ -175,6 +178,9 @@ export async function waitForAgentJob(params: { clearTimeout(timer); clearPendingErrorTimer(); unsubscribe(); + if (onAbort) { + signal?.removeEventListener("abort", onAbort); + } resolve(entry); }; @@ -185,7 +191,7 @@ export async function waitForAgentJob(params: { clearPendingErrorTimer(); const effectiveDelay = Math.max(1, Math.min(Math.floor(delayMs), 2_147_483_647)); pendingErrorTimer = setTimeout(() => { - const latest = getCachedAgentRun(runId); + const latest = ignoreCachedSnapshot ? undefined : getCachedAgentRun(runId); if (latest) { finish(latest); return; @@ -196,9 +202,11 @@ export async function waitForAgentJob(params: { pendingErrorTimer.unref?.(); }; - const pending = getPendingAgentRunError(runId); - if (pending) { - scheduleErrorFinish(pending.snapshot, pending.dueAt - Date.now()); + if (!ignoreCachedSnapshot) { + const pending = getPendingAgentRunError(runId); + if (pending) { + scheduleErrorFinish(pending.snapshot, pending.dueAt - Date.now()); + } } const unsubscribe = onAgentEvent((evt) => { @@ -216,7 +224,7 @@ export async function waitForAgentJob(params: { if (phase !== "end" && phase !== "error") { return; } - const latest = getCachedAgentRun(runId); + const latest = ignoreCachedSnapshot ? undefined : getCachedAgentRun(runId); if (latest) { finish(latest); return; @@ -236,6 +244,8 @@ export async function waitForAgentJob(params: { const timerDelayMs = Math.max(1, Math.min(Math.floor(timeoutMs), 2_147_483_647)); const timer = setTimeout(() => finish(null), timerDelayMs); + onAbort = () => finish(null); + signal?.addEventListener("abort", onAbort, { once: true }); }); } diff --git a/src/gateway/server-methods/agent-wait-dedupe.test.ts b/src/gateway/server-methods/agent-wait-dedupe.test.ts new file mode 100644 index 00000000000..e9a1899c88b --- /dev/null +++ b/src/gateway/server-methods/agent-wait-dedupe.test.ts @@ -0,0 +1,323 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { + __testing, + readTerminalSnapshotFromGatewayDedupe, + setGatewayDedupeEntry, + waitForTerminalGatewayDedupe, +} from "./agent-wait-dedupe.js"; + +describe("agent wait dedupe helper", () => { + beforeEach(() => { + __testing.resetWaiters(); + vi.useFakeTimers(); + }); + + afterEach(() => { + __testing.resetWaiters(); + vi.useRealTimers(); + }); + + it("unblocks waiters when a terminal chat dedupe entry is written", async () => { + const dedupe = new Map(); + const runId = "run-chat-terminal"; + const waiter = waitForTerminalGatewayDedupe({ + dedupe, + runId, + timeoutMs: 1_000, + }); + + await Promise.resolve(); + expect(__testing.getWaiterCount(runId)).toBe(1); + + setGatewayDedupeEntry({ + dedupe, + key: `chat:${runId}`, + entry: { + ts: Date.now(), + ok: true, + payload: { + runId, + status: "ok", + startedAt: 100, + endedAt: 200, + }, + }, + }); + + await expect(waiter).resolves.toEqual({ + status: "ok", + startedAt: 100, + endedAt: 200, + error: undefined, + }); + expect(__testing.getWaiterCount(runId)).toBe(0); + }); + + it("keeps stale chat dedupe blocked while agent dedupe is in-flight", async () => { + const dedupe = new Map(); + const runId = "run-stale-chat"; + setGatewayDedupeEntry({ + dedupe, + key: `chat:${runId}`, + entry: { + ts: Date.now(), + ok: true, + payload: { + runId, + status: "ok", + }, + }, + }); + setGatewayDedupeEntry({ + dedupe, + key: `agent:${runId}`, + entry: { + ts: Date.now(), + ok: true, + payload: { + runId, + status: "accepted", + }, + }, + }); + + const snapshot = readTerminalSnapshotFromGatewayDedupe({ + dedupe, + runId, + }); + expect(snapshot).toBeNull(); + + const blockedWait = waitForTerminalGatewayDedupe({ + dedupe, + runId, + timeoutMs: 25, + }); + await vi.advanceTimersByTimeAsync(30); + await expect(blockedWait).resolves.toBeNull(); + expect(__testing.getWaiterCount(runId)).toBe(0); + }); + + it("uses newer terminal chat snapshot when agent entry is non-terminal", () => { + const dedupe = new Map(); + const runId = "run-nonterminal-agent-with-newer-chat"; + setGatewayDedupeEntry({ + dedupe, + key: `agent:${runId}`, + entry: { + ts: 100, + ok: true, + payload: { + runId, + status: "accepted", + }, + }, + }); + setGatewayDedupeEntry({ + dedupe, + key: `chat:${runId}`, + entry: { + ts: 200, + ok: true, + payload: { + runId, + status: "ok", + startedAt: 1, + endedAt: 2, + }, + }, + }); + + expect( + readTerminalSnapshotFromGatewayDedupe({ + dedupe, + runId, + }), + ).toEqual({ + status: "ok", + startedAt: 1, + endedAt: 2, + error: undefined, + }); + }); + + it("ignores stale agent snapshots when waiting for an active chat run", async () => { + const dedupe = new Map(); + const runId = "run-chat-active-ignore-agent"; + setGatewayDedupeEntry({ + dedupe, + key: `agent:${runId}`, + entry: { + ts: Date.now(), + ok: true, + payload: { + runId, + status: "ok", + }, + }, + }); + + expect( + readTerminalSnapshotFromGatewayDedupe({ + dedupe, + runId, + ignoreAgentTerminalSnapshot: true, + }), + ).toBeNull(); + + const wait = waitForTerminalGatewayDedupe({ + dedupe, + runId, + timeoutMs: 1_000, + ignoreAgentTerminalSnapshot: true, + }); + await Promise.resolve(); + expect(__testing.getWaiterCount(runId)).toBe(1); + + setGatewayDedupeEntry({ + dedupe, + key: `chat:${runId}`, + entry: { + ts: Date.now(), + ok: true, + payload: { + runId, + status: "ok", + startedAt: 123, + endedAt: 456, + }, + }, + }); + + await expect(wait).resolves.toEqual({ + status: "ok", + startedAt: 123, + endedAt: 456, + error: undefined, + }); + }); + + it("prefers the freshest terminal snapshot when agent/chat dedupe keys collide", () => { + const runId = "run-collision"; + const dedupe = new Map(); + + setGatewayDedupeEntry({ + dedupe, + key: `agent:${runId}`, + entry: { + ts: 100, + ok: true, + payload: { runId, status: "ok", startedAt: 10, endedAt: 20 }, + }, + }); + setGatewayDedupeEntry({ + dedupe, + key: `chat:${runId}`, + entry: { + ts: 200, + ok: false, + payload: { runId, status: "error", startedAt: 30, endedAt: 40, error: "chat failed" }, + }, + }); + + expect( + readTerminalSnapshotFromGatewayDedupe({ + dedupe, + runId, + }), + ).toEqual({ + status: "error", + startedAt: 30, + endedAt: 40, + error: "chat failed", + }); + + const dedupeReverse = new Map(); + setGatewayDedupeEntry({ + dedupe: dedupeReverse, + key: `chat:${runId}`, + entry: { + ts: 100, + ok: true, + payload: { runId, status: "ok", startedAt: 1, endedAt: 2 }, + }, + }); + setGatewayDedupeEntry({ + dedupe: dedupeReverse, + key: `agent:${runId}`, + entry: { + ts: 200, + ok: true, + payload: { runId, status: "timeout", startedAt: 3, endedAt: 4, error: "still running" }, + }, + }); + + expect( + readTerminalSnapshotFromGatewayDedupe({ + dedupe: dedupeReverse, + runId, + }), + ).toEqual({ + status: "timeout", + startedAt: 3, + endedAt: 4, + error: "still running", + }); + }); + + it("resolves multiple waiters for the same run id", async () => { + const dedupe = new Map(); + const runId = "run-multi"; + const first = waitForTerminalGatewayDedupe({ + dedupe, + runId, + timeoutMs: 1_000, + }); + const second = waitForTerminalGatewayDedupe({ + dedupe, + runId, + timeoutMs: 1_000, + }); + + await Promise.resolve(); + expect(__testing.getWaiterCount(runId)).toBe(2); + + setGatewayDedupeEntry({ + dedupe, + key: `chat:${runId}`, + entry: { + ts: Date.now(), + ok: true, + payload: { runId, status: "ok" }, + }, + }); + + await expect(first).resolves.toEqual( + expect.objectContaining({ + status: "ok", + }), + ); + await expect(second).resolves.toEqual( + expect.objectContaining({ + status: "ok", + }), + ); + expect(__testing.getWaiterCount(runId)).toBe(0); + }); + + it("cleans up waiter registration on timeout", async () => { + const dedupe = new Map(); + const runId = "run-timeout"; + const wait = waitForTerminalGatewayDedupe({ + dedupe, + runId, + timeoutMs: 20, + }); + + await Promise.resolve(); + expect(__testing.getWaiterCount(runId)).toBe(1); + + await vi.advanceTimersByTimeAsync(25); + await expect(wait).resolves.toBeNull(); + expect(__testing.getWaiterCount(runId)).toBe(0); + }); +}); diff --git a/src/gateway/server-methods/agent-wait-dedupe.ts b/src/gateway/server-methods/agent-wait-dedupe.ts new file mode 100644 index 00000000000..98d0df72fa3 --- /dev/null +++ b/src/gateway/server-methods/agent-wait-dedupe.ts @@ -0,0 +1,244 @@ +import type { DedupeEntry } from "../server-shared.js"; + +export type AgentWaitTerminalSnapshot = { + status: "ok" | "error" | "timeout"; + startedAt?: number; + endedAt?: number; + error?: string; +}; + +const AGENT_WAITERS_BY_RUN_ID = new Map void>>(); + +function parseRunIdFromDedupeKey(key: string): string | null { + if (key.startsWith("agent:")) { + return key.slice("agent:".length) || null; + } + if (key.startsWith("chat:")) { + return key.slice("chat:".length) || null; + } + return null; +} + +function asFiniteNumber(value: unknown): number | undefined { + return typeof value === "number" && Number.isFinite(value) ? value : undefined; +} + +function addWaiter(runId: string, waiter: () => void): () => void { + const normalizedRunId = runId.trim(); + if (!normalizedRunId) { + return () => {}; + } + const existing = AGENT_WAITERS_BY_RUN_ID.get(normalizedRunId); + if (existing) { + existing.add(waiter); + return () => { + const waiters = AGENT_WAITERS_BY_RUN_ID.get(normalizedRunId); + if (!waiters) { + return; + } + waiters.delete(waiter); + if (waiters.size === 0) { + AGENT_WAITERS_BY_RUN_ID.delete(normalizedRunId); + } + }; + } + AGENT_WAITERS_BY_RUN_ID.set(normalizedRunId, new Set([waiter])); + return () => { + const waiters = AGENT_WAITERS_BY_RUN_ID.get(normalizedRunId); + if (!waiters) { + return; + } + waiters.delete(waiter); + if (waiters.size === 0) { + AGENT_WAITERS_BY_RUN_ID.delete(normalizedRunId); + } + }; +} + +function notifyWaiters(runId: string): void { + const normalizedRunId = runId.trim(); + if (!normalizedRunId) { + return; + } + const waiters = AGENT_WAITERS_BY_RUN_ID.get(normalizedRunId); + if (!waiters || waiters.size === 0) { + return; + } + for (const waiter of waiters) { + waiter(); + } +} + +export function readTerminalSnapshotFromDedupeEntry( + entry: DedupeEntry, +): AgentWaitTerminalSnapshot | null { + const payload = entry.payload as + | { + status?: unknown; + startedAt?: unknown; + endedAt?: unknown; + error?: unknown; + summary?: unknown; + } + | undefined; + const status = typeof payload?.status === "string" ? payload.status : undefined; + if (status === "accepted" || status === "started" || status === "in_flight") { + return null; + } + + const startedAt = asFiniteNumber(payload?.startedAt); + const endedAt = asFiniteNumber(payload?.endedAt) ?? entry.ts; + const errorMessage = + typeof payload?.error === "string" + ? payload.error + : typeof payload?.summary === "string" + ? payload.summary + : entry.error?.message; + + if (status === "ok" || status === "timeout") { + return { + status, + startedAt, + endedAt, + error: status === "timeout" ? errorMessage : undefined, + }; + } + if (status === "error" || !entry.ok) { + return { + status: "error", + startedAt, + endedAt, + error: errorMessage, + }; + } + return null; +} + +export function readTerminalSnapshotFromGatewayDedupe(params: { + dedupe: Map; + runId: string; + ignoreAgentTerminalSnapshot?: boolean; +}): AgentWaitTerminalSnapshot | null { + if (params.ignoreAgentTerminalSnapshot) { + const chatEntry = params.dedupe.get(`chat:${params.runId}`); + if (!chatEntry) { + return null; + } + return readTerminalSnapshotFromDedupeEntry(chatEntry); + } + + const chatEntry = params.dedupe.get(`chat:${params.runId}`); + const chatSnapshot = chatEntry ? readTerminalSnapshotFromDedupeEntry(chatEntry) : null; + + const agentEntry = params.dedupe.get(`agent:${params.runId}`); + const agentSnapshot = agentEntry ? readTerminalSnapshotFromDedupeEntry(agentEntry) : null; + if (agentEntry) { + if (!agentSnapshot) { + // If agent is still in-flight, only trust chat if it was written after + // this agent entry (indicating a newer completed chat run reused runId). + if (chatSnapshot && chatEntry && chatEntry.ts > agentEntry.ts) { + return chatSnapshot; + } + return null; + } + } + + if (agentSnapshot && chatSnapshot && agentEntry && chatEntry) { + // Reused idempotency keys can leave both records present. Prefer the + // freshest terminal snapshot so callers observe the latest run outcome. + return chatEntry.ts > agentEntry.ts ? chatSnapshot : agentSnapshot; + } + + return agentSnapshot ?? chatSnapshot; +} + +export async function waitForTerminalGatewayDedupe(params: { + dedupe: Map; + runId: string; + timeoutMs: number; + signal?: AbortSignal; + ignoreAgentTerminalSnapshot?: boolean; +}): Promise { + const initial = readTerminalSnapshotFromGatewayDedupe(params); + if (initial) { + return initial; + } + if (params.timeoutMs <= 0 || params.signal?.aborted) { + return null; + } + + return await new Promise((resolve) => { + let settled = false; + let timeoutHandle: NodeJS.Timeout | undefined; + let onAbort: (() => void) | undefined; + let removeWaiter: (() => void) | undefined; + + const finish = (snapshot: AgentWaitTerminalSnapshot | null) => { + if (settled) { + return; + } + settled = true; + if (timeoutHandle) { + clearTimeout(timeoutHandle); + } + if (onAbort) { + params.signal?.removeEventListener("abort", onAbort); + } + removeWaiter?.(); + resolve(snapshot); + }; + + const onWake = () => { + const snapshot = readTerminalSnapshotFromGatewayDedupe(params); + if (snapshot) { + finish(snapshot); + } + }; + + removeWaiter = addWaiter(params.runId, onWake); + onWake(); + if (settled) { + return; + } + + const timeoutDelayMs = Math.max(1, Math.min(Math.floor(params.timeoutMs), 2_147_483_647)); + timeoutHandle = setTimeout(() => finish(null), timeoutDelayMs); + timeoutHandle.unref?.(); + + onAbort = () => finish(null); + params.signal?.addEventListener("abort", onAbort, { once: true }); + }); +} + +export function setGatewayDedupeEntry(params: { + dedupe: Map; + key: string; + entry: DedupeEntry; +}) { + params.dedupe.set(params.key, params.entry); + const runId = parseRunIdFromDedupeKey(params.key); + if (!runId) { + return; + } + const snapshot = readTerminalSnapshotFromDedupeEntry(params.entry); + if (!snapshot) { + return; + } + notifyWaiters(runId); +} + +export const __testing = { + getWaiterCount(runId?: string): number { + if (runId) { + return AGENT_WAITERS_BY_RUN_ID.get(runId)?.size ?? 0; + } + let total = 0; + for (const waiters of AGENT_WAITERS_BY_RUN_ID.values()) { + total += waiters.size; + } + return total; + }, + resetWaiters() { + AGENT_WAITERS_BY_RUN_ID.clear(); + }, +}; diff --git a/src/gateway/server-methods/agent.ts b/src/gateway/server-methods/agent.ts index 41228b4ffae..aa56b857aca 100644 --- a/src/gateway/server-methods/agent.ts +++ b/src/gateway/server-methods/agent.ts @@ -51,6 +51,12 @@ import { import { formatForLog } from "../ws-log.js"; import { waitForAgentJob } from "./agent-job.js"; import { injectTimestamp, timestampOptsFromConfig } from "./agent-timestamp.js"; +import { + readTerminalSnapshotFromGatewayDedupe, + setGatewayDedupeEntry, + type AgentWaitTerminalSnapshot, + waitForTerminalGatewayDedupe, +} from "./agent-wait-dedupe.js"; import { normalizeRpcAttachmentsToChatAttachments } from "./attachment-normalize.js"; import { sessionsHandlers } from "./sessions.js"; import type { GatewayRequestHandlerOptions, GatewayRequestHandlers } from "./types.js"; @@ -593,10 +599,14 @@ export const agentHandlers: GatewayRequestHandlers = { acceptedAt: Date.now(), }; // Store an in-flight ack so retries do not spawn a second run. - context.dedupe.set(`agent:${idem}`, { - ts: Date.now(), - ok: true, - payload: accepted, + setGatewayDedupeEntry({ + dedupe: context.dedupe, + key: `agent:${idem}`, + entry: { + ts: Date.now(), + ok: true, + payload: accepted, + }, }); respond(true, accepted, undefined, { runId }); @@ -647,10 +657,14 @@ export const agentHandlers: GatewayRequestHandlers = { summary: "completed", result, }; - context.dedupe.set(`agent:${idem}`, { - ts: Date.now(), - ok: true, - payload, + setGatewayDedupeEntry({ + dedupe: context.dedupe, + key: `agent:${idem}`, + entry: { + ts: Date.now(), + ok: true, + payload, + }, }); // Send a second res frame (same id) so TS clients with expectFinal can wait. // Swift clients will typically treat the first res as the result and ignore this. @@ -663,11 +677,15 @@ export const agentHandlers: GatewayRequestHandlers = { status: "error" as const, summary: String(err), }; - context.dedupe.set(`agent:${idem}`, { - ts: Date.now(), - ok: false, - payload, - error, + setGatewayDedupeEntry({ + dedupe: context.dedupe, + key: `agent:${idem}`, + entry: { + ts: Date.now(), + ok: false, + payload, + error, + }, }); respond(false, payload, error, { runId, @@ -729,7 +747,7 @@ export const agentHandlers: GatewayRequestHandlers = { }) ?? identity.avatar; respond(true, { ...identity, avatar: avatarValue }, undefined); }, - "agent.wait": async ({ params, respond }) => { + "agent.wait": async ({ params, respond, context }) => { if (!validateAgentWaitParams(params)) { respond( false, @@ -747,11 +765,61 @@ export const agentHandlers: GatewayRequestHandlers = { typeof p.timeoutMs === "number" && Number.isFinite(p.timeoutMs) ? Math.max(0, Math.floor(p.timeoutMs)) : 30_000; + const hasActiveChatRun = context.chatAbortControllers.has(runId); - const snapshot = await waitForAgentJob({ + const cachedGatewaySnapshot = readTerminalSnapshotFromGatewayDedupe({ + dedupe: context.dedupe, + runId, + ignoreAgentTerminalSnapshot: hasActiveChatRun, + }); + if (cachedGatewaySnapshot) { + respond(true, { + runId, + status: cachedGatewaySnapshot.status, + startedAt: cachedGatewaySnapshot.startedAt, + endedAt: cachedGatewaySnapshot.endedAt, + error: cachedGatewaySnapshot.error, + }); + return; + } + + const lifecycleAbortController = new AbortController(); + const dedupeAbortController = new AbortController(); + const lifecyclePromise = waitForAgentJob({ runId, timeoutMs, + signal: lifecycleAbortController.signal, + // When chat.send is active with the same runId, ignore cached lifecycle + // snapshots so stale agent results do not preempt the active chat run. + ignoreCachedSnapshot: hasActiveChatRun, }); + const dedupePromise = waitForTerminalGatewayDedupe({ + dedupe: context.dedupe, + runId, + timeoutMs, + signal: dedupeAbortController.signal, + ignoreAgentTerminalSnapshot: hasActiveChatRun, + }); + + const first = await Promise.race([ + lifecyclePromise.then((snapshot) => ({ source: "lifecycle" as const, snapshot })), + dedupePromise.then((snapshot) => ({ source: "dedupe" as const, snapshot })), + ]); + + let snapshot: AgentWaitTerminalSnapshot | Awaited> = + first.snapshot; + if (snapshot) { + if (first.source === "lifecycle") { + dedupeAbortController.abort(); + } else { + lifecycleAbortController.abort(); + } + } else { + snapshot = first.source === "lifecycle" ? await dedupePromise : await lifecyclePromise; + lifecycleAbortController.abort(); + dedupeAbortController.abort(); + } + if (!snapshot) { respond(true, { runId, diff --git a/src/gateway/server-methods/chat.ts b/src/gateway/server-methods/chat.ts index db78d79666a..13feee2d131 100644 --- a/src/gateway/server-methods/chat.ts +++ b/src/gateway/server-methods/chat.ts @@ -47,6 +47,7 @@ import { } from "../session-utils.js"; import { formatForLog } from "../ws-log.js"; import { injectTimestamp, timestampOptsFromConfig } from "./agent-timestamp.js"; +import { setGatewayDedupeEntry } from "./agent-wait-dedupe.js"; import { normalizeRpcAttachmentsToChatAttachments } from "./attachment-normalize.js"; import { appendInjectedAssistantMessageToTranscript } from "./chat-transcript-inject.js"; import type { GatewayRequestContext, GatewayRequestHandlers } from "./types.js"; @@ -1030,23 +1031,31 @@ export const chatHandlers: GatewayRequestHandlers = { message, }); } - context.dedupe.set(`chat:${clientRunId}`, { - ts: Date.now(), - ok: true, - payload: { runId: clientRunId, status: "ok" as const }, + setGatewayDedupeEntry({ + dedupe: context.dedupe, + key: `chat:${clientRunId}`, + entry: { + ts: Date.now(), + ok: true, + payload: { runId: clientRunId, status: "ok" as const }, + }, }); }) .catch((err) => { const error = errorShape(ErrorCodes.UNAVAILABLE, String(err)); - context.dedupe.set(`chat:${clientRunId}`, { - ts: Date.now(), - ok: false, - payload: { - runId: clientRunId, - status: "error" as const, - summary: String(err), + setGatewayDedupeEntry({ + dedupe: context.dedupe, + key: `chat:${clientRunId}`, + entry: { + ts: Date.now(), + ok: false, + payload: { + runId: clientRunId, + status: "error" as const, + summary: String(err), + }, + error, }, - error, }); broadcastChatError({ context, @@ -1065,11 +1074,15 @@ export const chatHandlers: GatewayRequestHandlers = { status: "error" as const, summary: String(err), }; - context.dedupe.set(`chat:${clientRunId}`, { - ts: Date.now(), - ok: false, - payload, - error, + setGatewayDedupeEntry({ + dedupe: context.dedupe, + key: `chat:${clientRunId}`, + entry: { + ts: Date.now(), + ok: false, + payload, + error, + }, }); respond(false, payload, error, { runId: clientRunId, diff --git a/src/gateway/server-methods/server-methods.test.ts b/src/gateway/server-methods/server-methods.test.ts index 920d51b0400..4ea91ea247f 100644 --- a/src/gateway/server-methods/server-methods.test.ts +++ b/src/gateway/server-methods/server-methods.test.ts @@ -69,6 +69,43 @@ describe("waitForAgentJob", () => { expect(snapshot?.startedAt).toBe(300); expect(snapshot?.endedAt).toBe(400); }); + + it("can ignore cached snapshots and wait for fresh lifecycle events", async () => { + const runId = `run-ignore-cache-${Date.now()}-${Math.random().toString(36).slice(2)}`; + emitAgentEvent({ + runId, + stream: "lifecycle", + data: { phase: "end", startedAt: 100, endedAt: 110 }, + }); + + const cached = await waitForAgentJob({ runId, timeoutMs: 1_000 }); + expect(cached?.status).toBe("ok"); + expect(cached?.startedAt).toBe(100); + expect(cached?.endedAt).toBe(110); + + const freshWait = waitForAgentJob({ + runId, + timeoutMs: 1_000, + ignoreCachedSnapshot: true, + }); + queueMicrotask(() => { + emitAgentEvent({ + runId, + stream: "lifecycle", + data: { phase: "start", startedAt: 200 }, + }); + emitAgentEvent({ + runId, + stream: "lifecycle", + data: { phase: "end", startedAt: 200, endedAt: 210 }, + }); + }); + + const fresh = await freshWait; + expect(fresh?.status).toBe("ok"); + expect(fresh?.startedAt).toBe(200); + expect(fresh?.endedAt).toBe(210); + }); }); describe("injectTimestamp", () => { diff --git a/src/gateway/server.chat.gateway-server-chat.test.ts b/src/gateway/server.chat.gateway-server-chat.test.ts index e110ace1d73..7a5d84e62d8 100644 --- a/src/gateway/server.chat.gateway-server-chat.test.ts +++ b/src/gateway/server.chat.gateway-server-chat.test.ts @@ -466,6 +466,245 @@ describe("gateway server chat", () => { ]); }); + test("agent.wait resolves chat.send runs that finish without lifecycle events", async () => { + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gw-")); + try { + testState.sessionStorePath = path.join(dir, "sessions.json"); + await writeSessionStore({ + entries: { + main: { + sessionId: "sess-main", + updatedAt: Date.now(), + }, + }, + }); + + const runId = "idem-wait-chat-1"; + const sendRes = await rpcReq(ws, "chat.send", { + sessionKey: "main", + message: "/context list", + idempotencyKey: runId, + }); + expect(sendRes.ok).toBe(true); + expect(sendRes.payload?.status).toBe("started"); + + const waitRes = await rpcReq(ws, "agent.wait", { + runId, + timeoutMs: 1_000, + }); + expect(waitRes.ok).toBe(true); + expect(waitRes.payload?.status).toBe("ok"); + } finally { + testState.sessionStorePath = undefined; + await fs.rm(dir, { recursive: true, force: true }); + } + }); + + test("agent.wait ignores stale chat dedupe when an agent run with the same runId is in flight", async () => { + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gw-")); + let resolveAgentRun: (() => void) | undefined; + const blockedAgentRun = new Promise((resolve) => { + resolveAgentRun = resolve; + }); + const agentSpy = vi.mocked(agentCommand); + agentSpy.mockImplementationOnce(async () => { + await blockedAgentRun; + return undefined; + }); + + try { + testState.sessionStorePath = path.join(dir, "sessions.json"); + await writeSessionStore({ + entries: { + main: { + sessionId: "sess-main", + updatedAt: Date.now(), + }, + }, + }); + + const runId = "idem-wait-chat-vs-agent"; + const sendRes = await rpcReq(ws, "chat.send", { + sessionKey: "main", + message: "/context list", + idempotencyKey: runId, + }); + expect(sendRes.ok).toBe(true); + expect(sendRes.payload?.status).toBe("started"); + + const chatWaitRes = await rpcReq(ws, "agent.wait", { + runId, + timeoutMs: 1_000, + }); + expect(chatWaitRes.ok).toBe(true); + expect(chatWaitRes.payload?.status).toBe("ok"); + + const agentRes = await rpcReq(ws, "agent", { + sessionKey: "main", + message: "hold this run open", + idempotencyKey: runId, + }); + expect(agentRes.ok).toBe(true); + expect(agentRes.payload?.status).toBe("accepted"); + + const waitWhileAgentInFlight = await rpcReq(ws, "agent.wait", { + runId, + timeoutMs: 40, + }); + expectAgentWaitTimeout(waitWhileAgentInFlight); + + resolveAgentRun?.(); + const waitAfterAgentCompletion = await rpcReq(ws, "agent.wait", { + runId, + timeoutMs: 1_000, + }); + expect(waitAfterAgentCompletion.ok).toBe(true); + expect(waitAfterAgentCompletion.payload?.status).toBe("ok"); + } finally { + resolveAgentRun?.(); + testState.sessionStorePath = undefined; + await fs.rm(dir, { recursive: true, force: true }); + } + }); + + test("agent.wait ignores stale agent snapshots while same-runId chat.send is active", async () => { + await withMainSessionStore(async () => { + const runId = "idem-wait-chat-active-vs-stale-agent"; + const seedAgentRes = await rpcReq(ws, "agent", { + sessionKey: "main", + message: "seed stale agent snapshot", + idempotencyKey: runId, + }); + expect(seedAgentRes.ok).toBe(true); + expect(seedAgentRes.payload?.status).toBe("accepted"); + + const seedWaitRes = await rpcReq(ws, "agent.wait", { + runId, + timeoutMs: 1_000, + }); + expect(seedWaitRes.ok).toBe(true); + expect(seedWaitRes.payload?.status).toBe("ok"); + + let releaseBlockedReply: (() => void) | undefined; + const blockedReply = new Promise((resolve) => { + releaseBlockedReply = resolve; + }); + const replySpy = vi.mocked(getReplyFromConfig); + replySpy.mockImplementationOnce(async (_ctx, opts) => { + await new Promise((resolve) => { + let settled = false; + const finish = () => { + if (settled) { + return; + } + settled = true; + resolve(); + }; + void blockedReply.then(finish); + if (opts?.abortSignal?.aborted) { + finish(); + return; + } + opts?.abortSignal?.addEventListener("abort", finish, { once: true }); + }); + return undefined; + }); + + try { + const chatRes = await rpcReq(ws, "chat.send", { + sessionKey: "main", + message: "hold chat run open", + idempotencyKey: runId, + }); + expect(chatRes.ok).toBe(true); + expect(chatRes.payload?.status).toBe("started"); + + const waitWhileChatActive = await rpcReq(ws, "agent.wait", { + runId, + timeoutMs: 40, + }); + expectAgentWaitTimeout(waitWhileChatActive); + + const abortRes = await rpcReq(ws, "chat.abort", { + sessionKey: "main", + runId, + }); + expect(abortRes.ok).toBe(true); + } finally { + releaseBlockedReply?.(); + } + }); + }); + + test("agent.wait keeps lifecycle wait active while same-runId chat.send is active", async () => { + await withMainSessionStore(async () => { + const runId = "idem-wait-chat-active-with-agent-lifecycle"; + let releaseBlockedReply: (() => void) | undefined; + const blockedReply = new Promise((resolve) => { + releaseBlockedReply = resolve; + }); + const replySpy = vi.mocked(getReplyFromConfig); + replySpy.mockImplementationOnce(async (_ctx, opts) => { + await new Promise((resolve) => { + let settled = false; + const finish = () => { + if (settled) { + return; + } + settled = true; + resolve(); + }; + void blockedReply.then(finish); + if (opts?.abortSignal?.aborted) { + finish(); + return; + } + opts?.abortSignal?.addEventListener("abort", finish, { once: true }); + }); + return undefined; + }); + + try { + const chatRes = await rpcReq(ws, "chat.send", { + sessionKey: "main", + message: "hold chat run open", + idempotencyKey: runId, + }); + expect(chatRes.ok).toBe(true); + expect(chatRes.payload?.status).toBe("started"); + + const waitP = rpcReq(ws, "agent.wait", { + runId, + timeoutMs: 1_000, + }); + + await new Promise((resolve) => setTimeout(resolve, 20)); + emitAgentEvent({ + runId, + stream: "lifecycle", + data: { phase: "start", startedAt: 1 }, + }); + emitAgentEvent({ + runId, + stream: "lifecycle", + data: { phase: "end", startedAt: 1, endedAt: 2 }, + }); + + const waitRes = await waitP; + expect(waitRes.ok).toBe(true); + expect(waitRes.payload?.status).toBe("ok"); + + const abortRes = await rpcReq(ws, "chat.abort", { + sessionKey: "main", + runId, + }); + expect(abortRes.ok).toBe(true); + } finally { + releaseBlockedReply?.(); + } + }); + }); + test("agent events include sessionKey and agent.wait covers lifecycle flows", async () => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); diff --git a/src/plugin-sdk/root-alias.cjs b/src/plugin-sdk/root-alias.cjs index 37626deebaf..aa2127bdc9a 100644 --- a/src/plugin-sdk/root-alias.cjs +++ b/src/plugin-sdk/root-alias.cjs @@ -4,6 +4,7 @@ const path = require("node:path"); const fs = require("node:fs"); let monolithicSdk = null; +let jitiLoader = null; function emptyPluginConfigSchema() { function error(message) { @@ -31,16 +32,54 @@ function emptyPluginConfigSchema() { }; } +function resolveCommandAuthorizedFromAuthorizers(params) { + const { useAccessGroups, authorizers } = params; + const mode = params.modeWhenAccessGroupsOff ?? "allow"; + if (!useAccessGroups) { + if (mode === "allow") { + return true; + } + if (mode === "deny") { + return false; + } + const anyConfigured = authorizers.some((entry) => entry.configured); + if (!anyConfigured) { + return true; + } + return authorizers.some((entry) => entry.configured && entry.allowed); + } + return authorizers.some((entry) => entry.configured && entry.allowed); +} + +function resolveControlCommandGate(params) { + const commandAuthorized = resolveCommandAuthorizedFromAuthorizers({ + useAccessGroups: params.useAccessGroups, + authorizers: params.authorizers, + modeWhenAccessGroupsOff: params.modeWhenAccessGroupsOff, + }); + const shouldBlock = params.allowTextCommands && params.hasControlCommand && !commandAuthorized; + return { commandAuthorized, shouldBlock }; +} + +function getJiti() { + if (jitiLoader) { + return jitiLoader; + } + + const { createJiti } = require("jiti"); + jitiLoader = createJiti(__filename, { + interopDefault: true, + extensions: [".ts", ".tsx", ".mts", ".cts", ".mtsx", ".ctsx", ".js", ".mjs", ".cjs", ".json"], + }); + return jitiLoader; +} + function loadMonolithicSdk() { if (monolithicSdk) { return monolithicSdk; } - const { createJiti } = require("jiti"); - const jiti = createJiti(__filename, { - interopDefault: true, - extensions: [".ts", ".tsx", ".mts", ".cts", ".mtsx", ".ctsx", ".js", ".mjs", ".cjs", ".json"], - }); + const jiti = getJiti(); const distCandidate = path.resolve(__dirname, "..", "..", "dist", "plugin-sdk", "index.js"); if (fs.existsSync(distCandidate)) { @@ -56,8 +95,17 @@ function loadMonolithicSdk() { return monolithicSdk; } +function tryLoadMonolithicSdk() { + try { + return loadMonolithicSdk(); + } catch { + return null; + } +} + const fastExports = { emptyPluginConfigSchema, + resolveControlCommandGate, }; const rootProxy = new Proxy(fastExports, { @@ -80,15 +128,18 @@ const rootProxy = new Proxy(fastExports, { if (Reflect.has(target, prop)) { return true; } - return prop in loadMonolithicSdk(); + const monolithic = tryLoadMonolithicSdk(); + return monolithic ? prop in monolithic : false; }, ownKeys(target) { - const keys = new Set([ - ...Reflect.ownKeys(target), - ...Reflect.ownKeys(loadMonolithicSdk()), - "default", - "__esModule", - ]); + const keys = new Set([...Reflect.ownKeys(target), "default", "__esModule"]); + // Keep Object.keys/property reflection fast and deterministic. + // Only expose monolithic keys if it was already loaded by direct access. + if (monolithicSdk) { + for (const key of Reflect.ownKeys(monolithicSdk)) { + keys.add(key); + } + } return [...keys]; }, getOwnPropertyDescriptor(target, prop) { @@ -112,12 +163,15 @@ const rootProxy = new Proxy(fastExports, { if (own) { return own; } - const descriptor = Object.getOwnPropertyDescriptor(loadMonolithicSdk(), prop); + const monolithic = tryLoadMonolithicSdk(); + if (!monolithic) { + return undefined; + } + const descriptor = Object.getOwnPropertyDescriptor(monolithic, prop); if (!descriptor) { return undefined; } if (descriptor.get || descriptor.set) { - const monolithic = loadMonolithicSdk(); return { configurable: true, enumerable: descriptor.enumerable ?? true, From 257e2f5338d13ca634869670c88c7baa73d8d059 Mon Sep 17 00:00:00 2001 From: Bob Date: Wed, 4 Mar 2026 11:44:20 +0100 Subject: [PATCH 69/74] fix: relay ACP sessions_spawn parent streaming (#34310) (thanks @vincentkoc) (#34310) Co-authored-by: Onur Solmaz <2453968+osolmaz@users.noreply.github.com> --- CHANGELOG.md | 3 + docs/tools/acp-agents.md | 2 + docs/tools/index.md | 4 +- src/agents/acp-spawn-parent-stream.test.ts | 242 ++++++++++++ src/agents/acp-spawn-parent-stream.ts | 376 +++++++++++++++++++ src/agents/acp-spawn.test.ts | 170 +++++++++ src/agents/acp-spawn.ts | 63 +++- src/agents/openclaw-tools.sessions.test.ts | 1 + src/agents/tools/sessions-spawn-tool.test.ts | 23 ++ src/agents/tools/sessions-spawn-tool.ts | 12 +- 10 files changed, 893 insertions(+), 3 deletions(-) create mode 100644 src/agents/acp-spawn-parent-stream.test.ts create mode 100644 src/agents/acp-spawn-parent-stream.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index db6e5f310e8..563deedf307 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,6 +69,9 @@ Docs: https://docs.openclaw.ai - iOS/TTS playback fallback: keep voice playback resilient by switching from PCM to MP3 when provider format support is unavailable, while avoiding sticky fallback on generic local playback errors. (#33032) thanks @mbelinky. - Telegram/multi-account default routing clarity: warn only for ambiguous (2+) account setups without an explicit default, add `openclaw doctor` warnings for missing/invalid multi-account defaults across channels, and document explicit-default guidance for channel routing and Telegram config. (#32544) thanks @Sid-Qin. - Telegram/plugin outbound hook parity: run `message_sending` + `message_sent` in Telegram reply delivery, include reply-path hook metadata (`mediaUrls`, `threadId`), and report `message_sent.success=false` when hooks blank text and no outbound message is delivered. (#32649) Thanks @KimGLee. +- CLI/Coding-agent reliability: switch default `claude-cli` non-interactive args to `--permission-mode bypassPermissions`, auto-normalize legacy `--dangerously-skip-permissions` backend overrides to the modern permission-mode form, align coding-agent + live-test docs with the non-PTY Claude path, and emit session system-event heartbeat notices when CLI watchdog no-output timeouts terminate runs. (#28610, #31149, #34055). Thanks @niceysam, @cryptomaltese and @vincentkoc. +- ACP/ACPX session bootstrap: retry with `sessions new` when `sessions ensure` returns no session identifiers so ACP spawns avoid `NO_SESSION`/`ACP_TURN_FAILED` failures on affected agents. (#28786, #31338, #34055). Thanks @Sid-Qin and @vincentkoc. +- ACP/sessions_spawn parent stream visibility: add `streamTo: "parent"` for `runtime: "acp"` to forward initial child-run progress/no-output/completion updates back into the requester session as system events (instead of direct child delivery), and emit a tail-able session-scoped relay log (`.acp-stream.jsonl`, returned as `streamLogPath` when available), improving orchestrator visibility for blocked or long-running harness turns. (#34310, #29909; reopened from #34055). Thanks @vincentkoc. - Agents/bootstrap truncation warning handling: unify bootstrap budget/truncation analysis across embedded + CLI runtime, `/context`, and `openclaw doctor`; add `agents.defaults.bootstrapPromptTruncationWarning` (`off|once|always`, default `once`) and persist warning-signature metadata so truncation warnings are consistent and deduped across turns. (#32769) Thanks @gumadeiras. - Agents/Skills runtime loading: propagate run config into embedded attempt and compaction skill-entry loading so explicitly enabled bundled companion skills are discovered consistently when skill snapshots do not already provide resolved entries. Thanks @gumadeiras. - Agents/Session startup date grounding: substitute `YYYY-MM-DD` placeholders in startup/post-compaction AGENTS context and append runtime current-time lines for `/new` and `/reset` prompts so daily-memory references resolve correctly. (#32381) Thanks @chengzhichao-xydt. diff --git a/docs/tools/acp-agents.md b/docs/tools/acp-agents.md index d16bfc3868b..f6c1d5734cb 100644 --- a/docs/tools/acp-agents.md +++ b/docs/tools/acp-agents.md @@ -119,6 +119,8 @@ Interface details: - `mode: "session"` requires `thread: true` - `cwd` (optional): requested runtime working directory (validated by backend/runtime policy). - `label` (optional): operator-facing label used in session/banner text. +- `streamTo` (optional): `"parent"` streams initial ACP run progress summaries back to the requester session as system events. + - When available, accepted responses include `streamLogPath` pointing to a session-scoped JSONL log (`.acp-stream.jsonl`) you can tail for full relay history. ## Sandbox compatibility diff --git a/docs/tools/index.md b/docs/tools/index.md index fdbc0250833..47366f25e3a 100644 --- a/docs/tools/index.md +++ b/docs/tools/index.md @@ -472,7 +472,7 @@ Core parameters: - `sessions_list`: `kinds?`, `limit?`, `activeMinutes?`, `messageLimit?` (0 = none) - `sessions_history`: `sessionKey` (or `sessionId`), `limit?`, `includeTools?` - `sessions_send`: `sessionKey` (or `sessionId`), `message`, `timeoutSeconds?` (0 = fire-and-forget) -- `sessions_spawn`: `task`, `label?`, `runtime?`, `agentId?`, `model?`, `thinking?`, `cwd?`, `runTimeoutSeconds?`, `thread?`, `mode?`, `cleanup?`, `sandbox?`, `attachments?`, `attachAs?` +- `sessions_spawn`: `task`, `label?`, `runtime?`, `agentId?`, `model?`, `thinking?`, `cwd?`, `runTimeoutSeconds?`, `thread?`, `mode?`, `cleanup?`, `sandbox?`, `streamTo?`, `attachments?`, `attachAs?` - `session_status`: `sessionKey?` (default current; accepts `sessionId`), `model?` (`default` clears override) Notes: @@ -483,6 +483,7 @@ Notes: - `sessions_send` waits for final completion when `timeoutSeconds > 0`. - Delivery/announce happens after completion and is best-effort; `status: "ok"` confirms the agent run finished, not that the announce was delivered. - `sessions_spawn` supports `runtime: "subagent" | "acp"` (`subagent` default). For ACP runtime behavior, see [ACP Agents](/tools/acp-agents). +- For ACP runtime, `streamTo: "parent"` routes initial-run progress summaries back to the requester session as system events instead of direct child delivery. - `sessions_spawn` starts a sub-agent run and posts an announce reply back to the requester chat. - Supports one-shot mode (`mode: "run"`) and persistent thread-bound mode (`mode: "session"` with `thread: true`). - If `thread: true` and `mode` is omitted, mode defaults to `session`. @@ -496,6 +497,7 @@ Notes: - Configure limits via `tools.sessions_spawn.attachments` (`enabled`, `maxTotalBytes`, `maxFiles`, `maxFileBytes`, `retainOnSessionKeep`). - `attachAs.mountPath` is a reserved hint for future mount implementations. - `sessions_spawn` is non-blocking and returns `status: "accepted"` immediately. +- ACP `streamTo: "parent"` responses may include `streamLogPath` (session-scoped `*.acp-stream.jsonl`) for tailing progress history. - `sessions_send` runs a reply‑back ping‑pong (reply `REPLY_SKIP` to stop; max turns via `session.agentToAgent.maxPingPongTurns`, 0–5). - After the ping‑pong, the target agent runs an **announce step**; reply `ANNOUNCE_SKIP` to suppress the announcement. - Sandbox clamp: when the current session is sandboxed and `agents.defaults.sandbox.sessionToolsVisibility: "spawned"`, OpenClaw clamps `tools.sessions.visibility` to `tree`. diff --git a/src/agents/acp-spawn-parent-stream.test.ts b/src/agents/acp-spawn-parent-stream.test.ts new file mode 100644 index 00000000000..010cd596e7f --- /dev/null +++ b/src/agents/acp-spawn-parent-stream.test.ts @@ -0,0 +1,242 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { emitAgentEvent } from "../infra/agent-events.js"; +import { + resolveAcpSpawnStreamLogPath, + startAcpSpawnParentStreamRelay, +} from "./acp-spawn-parent-stream.js"; + +const enqueueSystemEventMock = vi.fn(); +const requestHeartbeatNowMock = vi.fn(); +const readAcpSessionEntryMock = vi.fn(); +const resolveSessionFilePathMock = vi.fn(); +const resolveSessionFilePathOptionsMock = vi.fn(); + +vi.mock("../infra/system-events.js", () => ({ + enqueueSystemEvent: (...args: unknown[]) => enqueueSystemEventMock(...args), +})); + +vi.mock("../infra/heartbeat-wake.js", () => ({ + requestHeartbeatNow: (...args: unknown[]) => requestHeartbeatNowMock(...args), +})); + +vi.mock("../acp/runtime/session-meta.js", () => ({ + readAcpSessionEntry: (...args: unknown[]) => readAcpSessionEntryMock(...args), +})); + +vi.mock("../config/sessions/paths.js", () => ({ + resolveSessionFilePath: (...args: unknown[]) => resolveSessionFilePathMock(...args), + resolveSessionFilePathOptions: (...args: unknown[]) => resolveSessionFilePathOptionsMock(...args), +})); + +function collectedTexts() { + return enqueueSystemEventMock.mock.calls.map((call) => String(call[0] ?? "")); +} + +describe("startAcpSpawnParentStreamRelay", () => { + beforeEach(() => { + enqueueSystemEventMock.mockClear(); + requestHeartbeatNowMock.mockClear(); + readAcpSessionEntryMock.mockReset(); + resolveSessionFilePathMock.mockReset(); + resolveSessionFilePathOptionsMock.mockReset(); + resolveSessionFilePathOptionsMock.mockImplementation((value: unknown) => value); + vi.useFakeTimers(); + vi.setSystemTime(new Date("2026-03-04T01:00:00.000Z")); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it("relays assistant progress and completion to the parent session", () => { + const relay = startAcpSpawnParentStreamRelay({ + runId: "run-1", + parentSessionKey: "agent:main:main", + childSessionKey: "agent:codex:acp:child-1", + agentId: "codex", + streamFlushMs: 10, + noOutputNoticeMs: 120_000, + }); + + emitAgentEvent({ + runId: "run-1", + stream: "assistant", + data: { + delta: "hello from child", + }, + }); + vi.advanceTimersByTime(15); + + emitAgentEvent({ + runId: "run-1", + stream: "lifecycle", + data: { + phase: "end", + startedAt: 1_000, + endedAt: 3_100, + }, + }); + + const texts = collectedTexts(); + expect(texts.some((text) => text.includes("Started codex session"))).toBe(true); + expect(texts.some((text) => text.includes("codex: hello from child"))).toBe(true); + expect(texts.some((text) => text.includes("codex run completed in 2s"))).toBe(true); + expect(requestHeartbeatNowMock).toHaveBeenCalledWith( + expect.objectContaining({ + reason: "acp:spawn:stream", + sessionKey: "agent:main:main", + }), + ); + relay.dispose(); + }); + + it("emits a no-output notice and a resumed notice when output returns", () => { + const relay = startAcpSpawnParentStreamRelay({ + runId: "run-2", + parentSessionKey: "agent:main:main", + childSessionKey: "agent:codex:acp:child-2", + agentId: "codex", + streamFlushMs: 1, + noOutputNoticeMs: 1_000, + noOutputPollMs: 250, + }); + + vi.advanceTimersByTime(1_500); + expect(collectedTexts().some((text) => text.includes("has produced no output for 1s"))).toBe( + true, + ); + + emitAgentEvent({ + runId: "run-2", + stream: "assistant", + data: { + delta: "resumed output", + }, + }); + vi.advanceTimersByTime(5); + + const texts = collectedTexts(); + expect(texts.some((text) => text.includes("resumed output."))).toBe(true); + expect(texts.some((text) => text.includes("codex: resumed output"))).toBe(true); + + emitAgentEvent({ + runId: "run-2", + stream: "lifecycle", + data: { + phase: "error", + error: "boom", + }, + }); + expect(collectedTexts().some((text) => text.includes("run failed: boom"))).toBe(true); + relay.dispose(); + }); + + it("auto-disposes stale relays after max lifetime timeout", () => { + const relay = startAcpSpawnParentStreamRelay({ + runId: "run-3", + parentSessionKey: "agent:main:main", + childSessionKey: "agent:codex:acp:child-3", + agentId: "codex", + streamFlushMs: 1, + noOutputNoticeMs: 0, + maxRelayLifetimeMs: 1_000, + }); + + vi.advanceTimersByTime(1_001); + expect(collectedTexts().some((text) => text.includes("stream relay timed out after 1s"))).toBe( + true, + ); + + const before = enqueueSystemEventMock.mock.calls.length; + emitAgentEvent({ + runId: "run-3", + stream: "assistant", + data: { + delta: "late output", + }, + }); + vi.advanceTimersByTime(5); + + expect(enqueueSystemEventMock.mock.calls).toHaveLength(before); + relay.dispose(); + }); + + it("supports delayed start notices", () => { + const relay = startAcpSpawnParentStreamRelay({ + runId: "run-4", + parentSessionKey: "agent:main:main", + childSessionKey: "agent:codex:acp:child-4", + agentId: "codex", + emitStartNotice: false, + }); + + expect(collectedTexts().some((text) => text.includes("Started codex session"))).toBe(false); + + relay.notifyStarted(); + + expect(collectedTexts().some((text) => text.includes("Started codex session"))).toBe(true); + relay.dispose(); + }); + + it("preserves delta whitespace boundaries in progress relays", () => { + const relay = startAcpSpawnParentStreamRelay({ + runId: "run-5", + parentSessionKey: "agent:main:main", + childSessionKey: "agent:codex:acp:child-5", + agentId: "codex", + streamFlushMs: 10, + noOutputNoticeMs: 120_000, + }); + + emitAgentEvent({ + runId: "run-5", + stream: "assistant", + data: { + delta: "hello", + }, + }); + emitAgentEvent({ + runId: "run-5", + stream: "assistant", + data: { + delta: " world", + }, + }); + vi.advanceTimersByTime(15); + + const texts = collectedTexts(); + expect(texts.some((text) => text.includes("codex: hello world"))).toBe(true); + relay.dispose(); + }); + + it("resolves ACP spawn stream log path from session metadata", () => { + readAcpSessionEntryMock.mockReturnValue({ + storePath: "/tmp/openclaw/agents/codex/sessions/sessions.json", + entry: { + sessionId: "sess-123", + sessionFile: "/tmp/openclaw/agents/codex/sessions/sess-123.jsonl", + }, + }); + resolveSessionFilePathMock.mockReturnValue( + "/tmp/openclaw/agents/codex/sessions/sess-123.jsonl", + ); + + const resolved = resolveAcpSpawnStreamLogPath({ + childSessionKey: "agent:codex:acp:child-1", + }); + + expect(resolved).toBe("/tmp/openclaw/agents/codex/sessions/sess-123.acp-stream.jsonl"); + expect(readAcpSessionEntryMock).toHaveBeenCalledWith({ + sessionKey: "agent:codex:acp:child-1", + }); + expect(resolveSessionFilePathMock).toHaveBeenCalledWith( + "sess-123", + expect.objectContaining({ + sessionId: "sess-123", + }), + expect.objectContaining({ + storePath: "/tmp/openclaw/agents/codex/sessions/sessions.json", + }), + ); + }); +}); diff --git a/src/agents/acp-spawn-parent-stream.ts b/src/agents/acp-spawn-parent-stream.ts new file mode 100644 index 00000000000..94f04ce3940 --- /dev/null +++ b/src/agents/acp-spawn-parent-stream.ts @@ -0,0 +1,376 @@ +import { appendFile, mkdir } from "node:fs/promises"; +import path from "node:path"; +import { readAcpSessionEntry } from "../acp/runtime/session-meta.js"; +import { resolveSessionFilePath, resolveSessionFilePathOptions } from "../config/sessions/paths.js"; +import { onAgentEvent } from "../infra/agent-events.js"; +import { requestHeartbeatNow } from "../infra/heartbeat-wake.js"; +import { enqueueSystemEvent } from "../infra/system-events.js"; +import { scopedHeartbeatWakeOptions } from "../routing/session-key.js"; + +const DEFAULT_STREAM_FLUSH_MS = 2_500; +const DEFAULT_NO_OUTPUT_NOTICE_MS = 60_000; +const DEFAULT_NO_OUTPUT_POLL_MS = 15_000; +const DEFAULT_MAX_RELAY_LIFETIME_MS = 6 * 60 * 60 * 1000; +const STREAM_BUFFER_MAX_CHARS = 4_000; +const STREAM_SNIPPET_MAX_CHARS = 220; + +function compactWhitespace(value: string): string { + return value.replace(/\s+/g, " ").trim(); +} + +function truncate(value: string, maxChars: number): string { + if (value.length <= maxChars) { + return value; + } + if (maxChars <= 1) { + return value.slice(0, maxChars); + } + return `${value.slice(0, maxChars - 1)}…`; +} + +function toTrimmedString(value: unknown): string | undefined { + if (typeof value !== "string") { + return undefined; + } + const trimmed = value.trim(); + return trimmed || undefined; +} + +function toFiniteNumber(value: unknown): number | undefined { + return typeof value === "number" && Number.isFinite(value) ? value : undefined; +} + +function resolveAcpStreamLogPathFromSessionFile(sessionFile: string, sessionId: string): string { + const baseDir = path.dirname(path.resolve(sessionFile)); + return path.join(baseDir, `${sessionId}.acp-stream.jsonl`); +} + +export function resolveAcpSpawnStreamLogPath(params: { + childSessionKey: string; +}): string | undefined { + const childSessionKey = params.childSessionKey.trim(); + if (!childSessionKey) { + return undefined; + } + const storeEntry = readAcpSessionEntry({ + sessionKey: childSessionKey, + }); + const sessionId = storeEntry?.entry?.sessionId?.trim(); + if (!storeEntry || !sessionId) { + return undefined; + } + try { + const sessionFile = resolveSessionFilePath( + sessionId, + storeEntry.entry, + resolveSessionFilePathOptions({ + storePath: storeEntry.storePath, + }), + ); + return resolveAcpStreamLogPathFromSessionFile(sessionFile, sessionId); + } catch { + return undefined; + } +} + +export function startAcpSpawnParentStreamRelay(params: { + runId: string; + parentSessionKey: string; + childSessionKey: string; + agentId: string; + logPath?: string; + streamFlushMs?: number; + noOutputNoticeMs?: number; + noOutputPollMs?: number; + maxRelayLifetimeMs?: number; + emitStartNotice?: boolean; +}): AcpSpawnParentRelayHandle { + const runId = params.runId.trim(); + const parentSessionKey = params.parentSessionKey.trim(); + if (!runId || !parentSessionKey) { + return { + dispose: () => {}, + notifyStarted: () => {}, + }; + } + + const streamFlushMs = + typeof params.streamFlushMs === "number" && Number.isFinite(params.streamFlushMs) + ? Math.max(0, Math.floor(params.streamFlushMs)) + : DEFAULT_STREAM_FLUSH_MS; + const noOutputNoticeMs = + typeof params.noOutputNoticeMs === "number" && Number.isFinite(params.noOutputNoticeMs) + ? Math.max(0, Math.floor(params.noOutputNoticeMs)) + : DEFAULT_NO_OUTPUT_NOTICE_MS; + const noOutputPollMs = + typeof params.noOutputPollMs === "number" && Number.isFinite(params.noOutputPollMs) + ? Math.max(250, Math.floor(params.noOutputPollMs)) + : DEFAULT_NO_OUTPUT_POLL_MS; + const maxRelayLifetimeMs = + typeof params.maxRelayLifetimeMs === "number" && Number.isFinite(params.maxRelayLifetimeMs) + ? Math.max(1_000, Math.floor(params.maxRelayLifetimeMs)) + : DEFAULT_MAX_RELAY_LIFETIME_MS; + + const relayLabel = truncate(compactWhitespace(params.agentId), 40) || "ACP child"; + const contextPrefix = `acp-spawn:${runId}`; + const logPath = toTrimmedString(params.logPath); + let logDirReady = false; + let pendingLogLines = ""; + let logFlushScheduled = false; + let logWriteChain: Promise = Promise.resolve(); + const flushLogBuffer = () => { + if (!logPath || !pendingLogLines) { + return; + } + const chunk = pendingLogLines; + pendingLogLines = ""; + logWriteChain = logWriteChain + .then(async () => { + if (!logDirReady) { + await mkdir(path.dirname(logPath), { + recursive: true, + }); + logDirReady = true; + } + await appendFile(logPath, chunk, { + encoding: "utf-8", + mode: 0o600, + }); + }) + .catch(() => { + // Best-effort diagnostics; never break relay flow. + }); + }; + const scheduleLogFlush = () => { + if (!logPath || logFlushScheduled) { + return; + } + logFlushScheduled = true; + queueMicrotask(() => { + logFlushScheduled = false; + flushLogBuffer(); + }); + }; + const writeLogLine = (entry: Record) => { + if (!logPath) { + return; + } + try { + pendingLogLines += `${JSON.stringify(entry)}\n`; + if (pendingLogLines.length >= 16_384) { + flushLogBuffer(); + return; + } + scheduleLogFlush(); + } catch { + // Best-effort diagnostics; never break relay flow. + } + }; + const logEvent = (kind: string, fields?: Record) => { + writeLogLine({ + ts: new Date().toISOString(), + epochMs: Date.now(), + runId, + parentSessionKey, + childSessionKey: params.childSessionKey, + agentId: params.agentId, + kind, + ...fields, + }); + }; + const wake = () => { + requestHeartbeatNow( + scopedHeartbeatWakeOptions(parentSessionKey, { reason: "acp:spawn:stream" }), + ); + }; + const emit = (text: string, contextKey: string) => { + const cleaned = text.trim(); + if (!cleaned) { + return; + } + logEvent("system_event", { contextKey, text: cleaned }); + enqueueSystemEvent(cleaned, { sessionKey: parentSessionKey, contextKey }); + wake(); + }; + const emitStartNotice = () => { + emit( + `Started ${relayLabel} session ${params.childSessionKey}. Streaming progress updates to parent session.`, + `${contextPrefix}:start`, + ); + }; + + let disposed = false; + let pendingText = ""; + let lastProgressAt = Date.now(); + let stallNotified = false; + let flushTimer: NodeJS.Timeout | undefined; + let relayLifetimeTimer: NodeJS.Timeout | undefined; + + const clearFlushTimer = () => { + if (!flushTimer) { + return; + } + clearTimeout(flushTimer); + flushTimer = undefined; + }; + const clearRelayLifetimeTimer = () => { + if (!relayLifetimeTimer) { + return; + } + clearTimeout(relayLifetimeTimer); + relayLifetimeTimer = undefined; + }; + + const flushPending = () => { + clearFlushTimer(); + if (!pendingText) { + return; + } + const snippet = truncate(compactWhitespace(pendingText), STREAM_SNIPPET_MAX_CHARS); + pendingText = ""; + if (!snippet) { + return; + } + emit(`${relayLabel}: ${snippet}`, `${contextPrefix}:progress`); + }; + + const scheduleFlush = () => { + if (disposed || flushTimer || streamFlushMs <= 0) { + return; + } + flushTimer = setTimeout(() => { + flushPending(); + }, streamFlushMs); + flushTimer.unref?.(); + }; + + const noOutputWatcherTimer = setInterval(() => { + if (disposed || noOutputNoticeMs <= 0) { + return; + } + if (stallNotified) { + return; + } + if (Date.now() - lastProgressAt < noOutputNoticeMs) { + return; + } + stallNotified = true; + emit( + `${relayLabel} has produced no output for ${Math.round(noOutputNoticeMs / 1000)}s. It may be waiting for interactive input.`, + `${contextPrefix}:stall`, + ); + }, noOutputPollMs); + noOutputWatcherTimer.unref?.(); + + relayLifetimeTimer = setTimeout(() => { + if (disposed) { + return; + } + emit( + `${relayLabel} stream relay timed out after ${Math.max(1, Math.round(maxRelayLifetimeMs / 1000))}s without completion.`, + `${contextPrefix}:timeout`, + ); + dispose(); + }, maxRelayLifetimeMs); + relayLifetimeTimer.unref?.(); + + if (params.emitStartNotice !== false) { + emitStartNotice(); + } + + const unsubscribe = onAgentEvent((event) => { + if (disposed || event.runId !== runId) { + return; + } + + if (event.stream === "assistant") { + const data = event.data; + const deltaCandidate = + (data as { delta?: unknown } | undefined)?.delta ?? + (data as { text?: unknown } | undefined)?.text; + const delta = typeof deltaCandidate === "string" ? deltaCandidate : undefined; + if (!delta || !delta.trim()) { + return; + } + logEvent("assistant_delta", { delta }); + + if (stallNotified) { + stallNotified = false; + emit(`${relayLabel} resumed output.`, `${contextPrefix}:resumed`); + } + + lastProgressAt = Date.now(); + pendingText += delta; + if (pendingText.length > STREAM_BUFFER_MAX_CHARS) { + pendingText = pendingText.slice(-STREAM_BUFFER_MAX_CHARS); + } + if (pendingText.length >= STREAM_SNIPPET_MAX_CHARS || delta.includes("\n\n")) { + flushPending(); + return; + } + scheduleFlush(); + return; + } + + if (event.stream !== "lifecycle") { + return; + } + + const phase = toTrimmedString((event.data as { phase?: unknown } | undefined)?.phase); + logEvent("lifecycle", { phase: phase ?? "unknown", data: event.data }); + if (phase === "end") { + flushPending(); + const startedAt = toFiniteNumber( + (event.data as { startedAt?: unknown } | undefined)?.startedAt, + ); + const endedAt = toFiniteNumber((event.data as { endedAt?: unknown } | undefined)?.endedAt); + const durationMs = + startedAt != null && endedAt != null && endedAt >= startedAt + ? endedAt - startedAt + : undefined; + if (durationMs != null) { + emit( + `${relayLabel} run completed in ${Math.max(1, Math.round(durationMs / 1000))}s.`, + `${contextPrefix}:done`, + ); + } else { + emit(`${relayLabel} run completed.`, `${contextPrefix}:done`); + } + dispose(); + return; + } + + if (phase === "error") { + flushPending(); + const errorText = toTrimmedString((event.data as { error?: unknown } | undefined)?.error); + if (errorText) { + emit(`${relayLabel} run failed: ${errorText}`, `${contextPrefix}:error`); + } else { + emit(`${relayLabel} run failed.`, `${contextPrefix}:error`); + } + dispose(); + } + }); + + const dispose = () => { + if (disposed) { + return; + } + disposed = true; + clearFlushTimer(); + clearRelayLifetimeTimer(); + flushLogBuffer(); + clearInterval(noOutputWatcherTimer); + unsubscribe(); + }; + + return { + dispose, + notifyStarted: emitStartNotice, + }; +} + +export type AcpSpawnParentRelayHandle = { + dispose: () => void; + notifyStarted: () => void; +}; diff --git a/src/agents/acp-spawn.test.ts b/src/agents/acp-spawn.test.ts index 732a465142d..b9b768361b2 100644 --- a/src/agents/acp-spawn.test.ts +++ b/src/agents/acp-spawn.test.ts @@ -33,6 +33,8 @@ const hoisted = vi.hoisted(() => { const sessionBindingListBySessionMock = vi.fn(); const closeSessionMock = vi.fn(); const initializeSessionMock = vi.fn(); + const startAcpSpawnParentStreamRelayMock = vi.fn(); + const resolveAcpSpawnStreamLogPathMock = vi.fn(); const state = { cfg: createDefaultSpawnConfig(), }; @@ -45,6 +47,8 @@ const hoisted = vi.hoisted(() => { sessionBindingListBySessionMock, closeSessionMock, initializeSessionMock, + startAcpSpawnParentStreamRelayMock, + resolveAcpSpawnStreamLogPathMock, state, }; }); @@ -100,6 +104,13 @@ vi.mock("../infra/outbound/session-binding-service.js", async (importOriginal) = }; }); +vi.mock("./acp-spawn-parent-stream.js", () => ({ + startAcpSpawnParentStreamRelay: (...args: unknown[]) => + hoisted.startAcpSpawnParentStreamRelayMock(...args), + resolveAcpSpawnStreamLogPath: (...args: unknown[]) => + hoisted.resolveAcpSpawnStreamLogPathMock(...args), +})); + const { spawnAcpDirect } = await import("./acp-spawn.js"); function createSessionBindingCapabilities() { @@ -132,6 +143,16 @@ function createSessionBinding(overrides?: Partial): Sessio }; } +function createRelayHandle(overrides?: { + dispose?: ReturnType; + notifyStarted?: ReturnType; +}) { + return { + dispose: overrides?.dispose ?? vi.fn(), + notifyStarted: overrides?.notifyStarted ?? vi.fn(), + }; +} + function expectResolvedIntroTextInBindMetadata(): void { const callWithMetadata = hoisted.sessionBindingBindMock.mock.calls.find( (call: unknown[]) => @@ -236,6 +257,12 @@ describe("spawnAcpDirect", () => { hoisted.sessionBindingResolveByConversationMock.mockReset().mockReturnValue(null); hoisted.sessionBindingListBySessionMock.mockReset().mockReturnValue([]); hoisted.sessionBindingUnbindMock.mockReset().mockResolvedValue([]); + hoisted.startAcpSpawnParentStreamRelayMock + .mockReset() + .mockImplementation(() => createRelayHandle()); + hoisted.resolveAcpSpawnStreamLogPathMock + .mockReset() + .mockReturnValue("/tmp/sess-main.acp-stream.jsonl"); }); it("spawns ACP session, binds a new thread, and dispatches initial task", async () => { @@ -423,4 +450,147 @@ describe("spawnAcpDirect", () => { expect(hoisted.callGatewayMock).not.toHaveBeenCalled(); expect(hoisted.initializeSessionMock).not.toHaveBeenCalled(); }); + + it('streams ACP progress to parent when streamTo="parent"', async () => { + const firstHandle = createRelayHandle(); + const secondHandle = createRelayHandle(); + hoisted.startAcpSpawnParentStreamRelayMock + .mockReset() + .mockReturnValueOnce(firstHandle) + .mockReturnValueOnce(secondHandle); + + const result = await spawnAcpDirect( + { + task: "Investigate flaky tests", + agentId: "codex", + streamTo: "parent", + }, + { + agentSessionKey: "agent:main:main", + agentChannel: "discord", + agentAccountId: "default", + agentTo: "channel:parent-channel", + }, + ); + + expect(result.status).toBe("accepted"); + expect(result.streamLogPath).toBe("/tmp/sess-main.acp-stream.jsonl"); + const agentCall = hoisted.callGatewayMock.mock.calls + .map((call: unknown[]) => call[0] as { method?: string; params?: Record }) + .find((request) => request.method === "agent"); + const agentCallIndex = hoisted.callGatewayMock.mock.calls.findIndex( + (call: unknown[]) => (call[0] as { method?: string }).method === "agent", + ); + const relayCallOrder = hoisted.startAcpSpawnParentStreamRelayMock.mock.invocationCallOrder[0]; + const agentCallOrder = hoisted.callGatewayMock.mock.invocationCallOrder[agentCallIndex]; + expect(agentCall?.params?.deliver).toBe(false); + expect(typeof relayCallOrder).toBe("number"); + expect(typeof agentCallOrder).toBe("number"); + expect(relayCallOrder < agentCallOrder).toBe(true); + expect(hoisted.startAcpSpawnParentStreamRelayMock).toHaveBeenCalledWith( + expect.objectContaining({ + parentSessionKey: "agent:main:main", + agentId: "codex", + logPath: "/tmp/sess-main.acp-stream.jsonl", + emitStartNotice: false, + }), + ); + const relayRuns = hoisted.startAcpSpawnParentStreamRelayMock.mock.calls.map( + (call: unknown[]) => (call[0] as { runId?: string }).runId, + ); + expect(relayRuns).toContain(agentCall?.params?.idempotencyKey); + expect(relayRuns).toContain(result.runId); + expect(hoisted.resolveAcpSpawnStreamLogPathMock).toHaveBeenCalledWith({ + childSessionKey: expect.stringMatching(/^agent:codex:acp:/), + }); + expect(firstHandle.dispose).toHaveBeenCalledTimes(1); + expect(firstHandle.notifyStarted).not.toHaveBeenCalled(); + expect(secondHandle.notifyStarted).toHaveBeenCalledTimes(1); + }); + + it("announces parent relay start only after successful child dispatch", async () => { + const firstHandle = createRelayHandle(); + const secondHandle = createRelayHandle(); + hoisted.startAcpSpawnParentStreamRelayMock + .mockReset() + .mockReturnValueOnce(firstHandle) + .mockReturnValueOnce(secondHandle); + + const result = await spawnAcpDirect( + { + task: "Investigate flaky tests", + agentId: "codex", + streamTo: "parent", + }, + { + agentSessionKey: "agent:main:main", + }, + ); + + expect(result.status).toBe("accepted"); + expect(firstHandle.notifyStarted).not.toHaveBeenCalled(); + expect(secondHandle.notifyStarted).toHaveBeenCalledTimes(1); + const notifyOrder = secondHandle.notifyStarted.mock.invocationCallOrder; + const agentCallIndex = hoisted.callGatewayMock.mock.calls.findIndex( + (call: unknown[]) => (call[0] as { method?: string }).method === "agent", + ); + const agentCallOrder = hoisted.callGatewayMock.mock.invocationCallOrder[agentCallIndex]; + expect(typeof agentCallOrder).toBe("number"); + expect(typeof notifyOrder[0]).toBe("number"); + expect(notifyOrder[0] > agentCallOrder).toBe(true); + }); + + it("disposes pre-registered parent relay when initial ACP dispatch fails", async () => { + const relayHandle = createRelayHandle(); + hoisted.startAcpSpawnParentStreamRelayMock.mockReturnValueOnce(relayHandle); + hoisted.callGatewayMock.mockImplementation(async (argsUnknown: unknown) => { + const args = argsUnknown as { method?: string }; + if (args.method === "sessions.patch") { + return { ok: true }; + } + if (args.method === "agent") { + throw new Error("agent dispatch failed"); + } + if (args.method === "sessions.delete") { + return { ok: true }; + } + return {}; + }); + + const result = await spawnAcpDirect( + { + task: "Investigate flaky tests", + agentId: "codex", + streamTo: "parent", + }, + { + agentSessionKey: "agent:main:main", + }, + ); + + expect(result.status).toBe("error"); + expect(result.error).toContain("agent dispatch failed"); + expect(relayHandle.dispose).toHaveBeenCalledTimes(1); + expect(relayHandle.notifyStarted).not.toHaveBeenCalled(); + }); + + it('rejects streamTo="parent" without requester session context', async () => { + const result = await spawnAcpDirect( + { + task: "Investigate flaky tests", + agentId: "codex", + streamTo: "parent", + }, + { + agentChannel: "discord", + agentAccountId: "default", + agentTo: "channel:parent-channel", + }, + ); + + expect(result.status).toBe("error"); + expect(result.error).toContain('streamTo="parent"'); + expect(hoisted.callGatewayMock).not.toHaveBeenCalled(); + expect(hoisted.startAcpSpawnParentStreamRelayMock).not.toHaveBeenCalled(); + }); }); diff --git a/src/agents/acp-spawn.ts b/src/agents/acp-spawn.ts index ff475e54ebf..d5da9d199d8 100644 --- a/src/agents/acp-spawn.ts +++ b/src/agents/acp-spawn.ts @@ -32,12 +32,19 @@ import { } from "../infra/outbound/session-binding-service.js"; import { normalizeAgentId } from "../routing/session-key.js"; import { normalizeDeliveryContext } from "../utils/delivery-context.js"; +import { + type AcpSpawnParentRelayHandle, + resolveAcpSpawnStreamLogPath, + startAcpSpawnParentStreamRelay, +} from "./acp-spawn-parent-stream.js"; import { resolveSandboxRuntimeStatus } from "./sandbox/runtime-status.js"; export const ACP_SPAWN_MODES = ["run", "session"] as const; export type SpawnAcpMode = (typeof ACP_SPAWN_MODES)[number]; export const ACP_SPAWN_SANDBOX_MODES = ["inherit", "require"] as const; export type SpawnAcpSandboxMode = (typeof ACP_SPAWN_SANDBOX_MODES)[number]; +export const ACP_SPAWN_STREAM_TARGETS = ["parent"] as const; +export type SpawnAcpStreamTarget = (typeof ACP_SPAWN_STREAM_TARGETS)[number]; export type SpawnAcpParams = { task: string; @@ -47,6 +54,7 @@ export type SpawnAcpParams = { mode?: SpawnAcpMode; thread?: boolean; sandbox?: SpawnAcpSandboxMode; + streamTo?: SpawnAcpStreamTarget; }; export type SpawnAcpContext = { @@ -63,6 +71,7 @@ export type SpawnAcpResult = { childSessionKey?: string; runId?: string; mode?: SpawnAcpMode; + streamLogPath?: string; note?: string; error?: string; }; @@ -234,6 +243,14 @@ export async function spawnAcpDirect( }; } const sandboxMode = params.sandbox === "require" ? "require" : "inherit"; + const streamToParentRequested = params.streamTo === "parent"; + const parentSessionKey = ctx.agentSessionKey?.trim(); + if (streamToParentRequested && !parentSessionKey) { + return { + status: "error", + error: 'sessions_spawn streamTo="parent" requires an active requester session context.', + }; + } const requesterRuntime = resolveSandboxRuntimeStatus({ cfg, sessionKey: ctx.agentSessionKey, @@ -410,8 +427,27 @@ export async function spawnAcpDirect( ? `channel:${boundThreadId}` : requesterOrigin?.to?.trim() || (deliveryThreadId ? `channel:${deliveryThreadId}` : undefined); const hasDeliveryTarget = Boolean(requesterOrigin?.channel && inferredDeliveryTo); + const deliverToBoundTarget = hasDeliveryTarget && !streamToParentRequested; const childIdem = crypto.randomUUID(); let childRunId: string = childIdem; + const streamLogPath = + streamToParentRequested && parentSessionKey + ? resolveAcpSpawnStreamLogPath({ + childSessionKey: sessionKey, + }) + : undefined; + let parentRelay: AcpSpawnParentRelayHandle | undefined; + if (streamToParentRequested && parentSessionKey) { + // Register relay before dispatch so fast lifecycle failures are not missed. + parentRelay = startAcpSpawnParentStreamRelay({ + runId: childIdem, + parentSessionKey, + childSessionKey: sessionKey, + agentId: targetAgentId, + logPath: streamLogPath, + emitStartNotice: false, + }); + } try { const response = await callGateway<{ runId?: string }>({ method: "agent", @@ -423,7 +459,7 @@ export async function spawnAcpDirect( accountId: hasDeliveryTarget ? (requesterOrigin?.accountId ?? undefined) : undefined, threadId: hasDeliveryTarget ? deliveryThreadId : undefined, idempotencyKey: childIdem, - deliver: hasDeliveryTarget, + deliver: deliverToBoundTarget, label: params.label || undefined, }, timeoutMs: 10_000, @@ -432,6 +468,7 @@ export async function spawnAcpDirect( childRunId = response.runId.trim(); } } catch (err) { + parentRelay?.dispose(); await cleanupFailedAcpSpawn({ cfg, sessionKey, @@ -445,6 +482,30 @@ export async function spawnAcpDirect( }; } + if (streamToParentRequested && parentSessionKey) { + if (parentRelay && childRunId !== childIdem) { + parentRelay.dispose(); + // Defensive fallback if gateway returns a runId that differs from idempotency key. + parentRelay = startAcpSpawnParentStreamRelay({ + runId: childRunId, + parentSessionKey, + childSessionKey: sessionKey, + agentId: targetAgentId, + logPath: streamLogPath, + emitStartNotice: false, + }); + } + parentRelay?.notifyStarted(); + return { + status: "accepted", + childSessionKey: sessionKey, + runId: childRunId, + mode: spawnMode, + ...(streamLogPath ? { streamLogPath } : {}), + note: spawnMode === "session" ? ACP_SPAWN_SESSION_ACCEPTED_NOTE : ACP_SPAWN_ACCEPTED_NOTE, + }; + } + return { status: "accepted", childSessionKey: sessionKey, diff --git a/src/agents/openclaw-tools.sessions.test.ts b/src/agents/openclaw-tools.sessions.test.ts index 9b07fafc4da..36c1f420af4 100644 --- a/src/agents/openclaw-tools.sessions.test.ts +++ b/src/agents/openclaw-tools.sessions.test.ts @@ -93,6 +93,7 @@ describe("sessions tools", () => { expect(schemaProp("sessions_spawn", "thread").type).toBe("boolean"); expect(schemaProp("sessions_spawn", "mode").type).toBe("string"); expect(schemaProp("sessions_spawn", "sandbox").type).toBe("string"); + expect(schemaProp("sessions_spawn", "streamTo").type).toBe("string"); expect(schemaProp("sessions_spawn", "runtime").type).toBe("string"); expect(schemaProp("sessions_spawn", "cwd").type).toBe("string"); expect(schemaProp("subagents", "recentMinutes").type).toBe("number"); diff --git a/src/agents/tools/sessions-spawn-tool.test.ts b/src/agents/tools/sessions-spawn-tool.test.ts index 3b6b67dbe47..a000000f1ee 100644 --- a/src/agents/tools/sessions-spawn-tool.test.ts +++ b/src/agents/tools/sessions-spawn-tool.test.ts @@ -16,6 +16,7 @@ vi.mock("../subagent-spawn.js", () => ({ vi.mock("../acp-spawn.js", () => ({ ACP_SPAWN_MODES: ["run", "session"], + ACP_SPAWN_STREAM_TARGETS: ["parent"], spawnAcpDirect: (...args: unknown[]) => hoisted.spawnAcpDirectMock(...args), })); @@ -94,6 +95,7 @@ describe("sessions_spawn tool", () => { cwd: "/workspace", thread: true, mode: "session", + streamTo: "parent", }); expect(result.details).toMatchObject({ @@ -108,6 +110,7 @@ describe("sessions_spawn tool", () => { cwd: "/workspace", thread: true, mode: "session", + streamTo: "parent", }), expect.objectContaining({ agentSessionKey: "agent:main:main", @@ -165,6 +168,26 @@ describe("sessions_spawn tool", () => { expect(hoisted.spawnSubagentDirectMock).not.toHaveBeenCalled(); }); + it('rejects streamTo when runtime is not "acp"', async () => { + const tool = createSessionsSpawnTool({ + agentSessionKey: "agent:main:main", + }); + + const result = await tool.execute("call-3b", { + runtime: "subagent", + task: "analyze file", + streamTo: "parent", + }); + + expect(result.details).toMatchObject({ + status: "error", + }); + const details = result.details as { error?: string }; + expect(details.error).toContain("streamTo is only supported for runtime=acp"); + expect(hoisted.spawnAcpDirectMock).not.toHaveBeenCalled(); + expect(hoisted.spawnSubagentDirectMock).not.toHaveBeenCalled(); + }); + it("keeps attachment content schema unconstrained for llama.cpp grammar safety", () => { const tool = createSessionsSpawnTool(); const schema = tool.parameters as { diff --git a/src/agents/tools/sessions-spawn-tool.ts b/src/agents/tools/sessions-spawn-tool.ts index 7ea48ded44f..03a138e8a0f 100644 --- a/src/agents/tools/sessions-spawn-tool.ts +++ b/src/agents/tools/sessions-spawn-tool.ts @@ -1,6 +1,6 @@ import { Type } from "@sinclair/typebox"; import type { GatewayMessageChannel } from "../../utils/message-channel.js"; -import { ACP_SPAWN_MODES, spawnAcpDirect } from "../acp-spawn.js"; +import { ACP_SPAWN_MODES, ACP_SPAWN_STREAM_TARGETS, spawnAcpDirect } from "../acp-spawn.js"; import { optionalStringEnum } from "../schema/typebox.js"; import { SUBAGENT_SPAWN_MODES, spawnSubagentDirect } from "../subagent-spawn.js"; import type { AnyAgentTool } from "./common.js"; @@ -34,6 +34,7 @@ const SessionsSpawnToolSchema = Type.Object({ mode: optionalStringEnum(SUBAGENT_SPAWN_MODES), cleanup: optionalStringEnum(["delete", "keep"] as const), sandbox: optionalStringEnum(SESSIONS_SPAWN_SANDBOX_MODES), + streamTo: optionalStringEnum(ACP_SPAWN_STREAM_TARGETS), // Inline attachments (snapshot-by-value). // NOTE: Attachment contents are redacted from transcript persistence by sanitizeToolCallInputs. @@ -97,6 +98,7 @@ export function createSessionsSpawnTool(opts?: { const cleanup = params.cleanup === "keep" || params.cleanup === "delete" ? params.cleanup : "keep"; const sandbox = params.sandbox === "require" ? "require" : "inherit"; + const streamTo = params.streamTo === "parent" ? "parent" : undefined; // Back-compat: older callers used timeoutSeconds for this tool. const timeoutSecondsCandidate = typeof params.runTimeoutSeconds === "number" @@ -118,6 +120,13 @@ export function createSessionsSpawnTool(opts?: { }>) : undefined; + if (streamTo && runtime !== "acp") { + return jsonResult({ + status: "error", + error: `streamTo is only supported for runtime=acp; got runtime=${runtime}`, + }); + } + if (runtime === "acp") { if (Array.isArray(attachments) && attachments.length > 0) { return jsonResult({ @@ -135,6 +144,7 @@ export function createSessionsSpawnTool(opts?: { mode: mode && ACP_SPAWN_MODES.includes(mode) ? mode : undefined, thread, sandbox, + streamTo, }, { agentSessionKey: opts?.agentSessionKey, From 3cc1d5a92f481a6d93c686d7d013dfc5438ba868 Mon Sep 17 00:00:00 2001 From: Ayaan Zaidi Date: Wed, 4 Mar 2026 16:15:18 +0530 Subject: [PATCH 70/74] fix(telegram): materialize dm draft final to avoid duplicates --- src/telegram/lane-delivery.test.ts | 49 ++++++++++++++++++++++-------- src/telegram/lane-delivery.ts | 46 ++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 12 deletions(-) diff --git a/src/telegram/lane-delivery.test.ts b/src/telegram/lane-delivery.test.ts index 15344f85653..5259a99f6c7 100644 --- a/src/telegram/lane-delivery.test.ts +++ b/src/telegram/lane-delivery.test.ts @@ -215,8 +215,9 @@ describe("createLaneTextDeliverer", () => { expect(harness.log).toHaveBeenCalledWith(expect.stringContaining("preview final too long")); }); - it("sends a final message after DM draft streaming even when text is unchanged", async () => { - const answerStream = createTestDraftStream({ previewMode: "draft" }); + it("materializes DM draft streaming final even when text is unchanged", async () => { + const answerStream = createTestDraftStream({ previewMode: "draft", messageId: 321 }); + answerStream.materialize.mockResolvedValue(321); answerStream.update.mockImplementation(() => {}); const harness = createHarness({ answerStream: answerStream as DraftLaneState["stream"], @@ -231,18 +232,17 @@ describe("createLaneTextDeliverer", () => { infoKind: "final", }); - expect(result).toBe("sent"); + expect(result).toBe("preview-finalized"); expect(harness.flushDraftLane).toHaveBeenCalled(); - expect(harness.stopDraftLane).toHaveBeenCalled(); - expect(harness.sendPayload).toHaveBeenCalledWith( - expect.objectContaining({ text: "Hello final" }), - ); - expect(harness.markDelivered).not.toHaveBeenCalled(); + expect(answerStream.materialize).toHaveBeenCalledTimes(1); + expect(harness.sendPayload).not.toHaveBeenCalled(); + expect(harness.markDelivered).toHaveBeenCalledTimes(1); }); - it("sends a final message after DM draft streaming when revision changes", async () => { + it("materializes DM draft streaming final when revision changes", async () => { let previewRevision = 3; - const answerStream = createTestDraftStream({ previewMode: "draft" }); + const answerStream = createTestDraftStream({ previewMode: "draft", messageId: 654 }); + answerStream.materialize.mockResolvedValue(654); answerStream.previewRevision.mockImplementation(() => previewRevision); answerStream.update.mockImplementation(() => {}); answerStream.flush.mockImplementation(async () => { @@ -261,11 +261,36 @@ describe("createLaneTextDeliverer", () => { infoKind: "final", }); + expect(result).toBe("preview-finalized"); + expect(answerStream.materialize).toHaveBeenCalledTimes(1); + expect(harness.sendPayload).not.toHaveBeenCalled(); + expect(harness.markDelivered).toHaveBeenCalledTimes(1); + }); + + it("falls back to normal send when draft materialize returns no message id", async () => { + const answerStream = createTestDraftStream({ previewMode: "draft" }); + answerStream.materialize.mockResolvedValue(undefined); + const harness = createHarness({ + answerStream: answerStream as DraftLaneState["stream"], + answerHasStreamedMessage: true, + answerLastPartialText: "Hello final", + }); + + const result = await harness.deliverLaneText({ + laneName: "answer", + text: "Hello final", + payload: { text: "Hello final" }, + infoKind: "final", + }); + expect(result).toBe("sent"); + expect(answerStream.materialize).toHaveBeenCalledTimes(1); expect(harness.sendPayload).toHaveBeenCalledWith( - expect.objectContaining({ text: "Final answer" }), + expect.objectContaining({ text: "Hello final" }), + ); + expect(harness.log).toHaveBeenCalledWith( + expect.stringContaining("draft preview materialize produced no message id"), ); - expect(harness.markDelivered).not.toHaveBeenCalled(); }); it("does not use DM draft final shortcut for media payloads", async () => { diff --git a/src/telegram/lane-delivery.ts b/src/telegram/lane-delivery.ts index 5196b4d9983..b02837d90b0 100644 --- a/src/telegram/lane-delivery.ts +++ b/src/telegram/lane-delivery.ts @@ -156,6 +156,41 @@ function resolvePreviewTarget(params: ResolvePreviewTargetParams): PreviewTarget export function createLaneTextDeliverer(params: CreateLaneTextDelivererParams) { const getLanePreviewText = (lane: DraftLaneState) => lane.lastPartialText; const isDraftPreviewLane = (lane: DraftLaneState) => lane.stream?.previewMode?.() === "draft"; + const canMaterializeDraftFinal = ( + lane: DraftLaneState, + previewButtons?: TelegramInlineButtons, + ) => { + const hasPreviewButtons = Boolean(previewButtons && previewButtons.length > 0); + return ( + isDraftPreviewLane(lane) && + !hasPreviewButtons && + typeof lane.stream?.materialize === "function" + ); + }; + + const tryMaterializeDraftPreviewForFinal = async (args: { + lane: DraftLaneState; + laneName: LaneName; + text: string; + }): Promise => { + const stream = args.lane.stream; + if (!stream || !isDraftPreviewLane(args.lane)) { + return false; + } + // Draft previews have no message_id to edit; materialize the final text + // into a real message and treat that as the finalized delivery. + stream.update(args.text); + const materializedMessageId = await stream.materialize?.(); + if (typeof materializedMessageId !== "number") { + params.log( + `telegram: ${args.laneName} draft preview materialize produced no message id; falling back to standard send`, + ); + return false; + } + args.lane.lastPartialText = args.text; + params.markDelivered(); + return true; + }; const tryEditPreviewMessage = async (args: { laneName: LaneName; @@ -363,6 +398,17 @@ export function createLaneTextDeliverer(params: CreateLaneTextDelivererParams) { return archivedResultAfterFlush; } } + if (canMaterializeDraftFinal(lane, previewButtons)) { + const materialized = await tryMaterializeDraftPreviewForFinal({ + lane, + laneName, + text, + }); + if (materialized) { + params.finalizedPreviewByLane[laneName] = true; + return "preview-finalized"; + } + } const finalized = await tryUpdatePreviewForLane({ lane, laneName, From ed8e0a814609a070f46a0b6942dadc8e06ebf9d6 Mon Sep 17 00:00:00 2001 From: Ayaan Zaidi Date: Wed, 4 Mar 2026 16:27:09 +0530 Subject: [PATCH 71/74] docs(changelog): credit @Brotherinlaw-13 for #34318 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 563deedf307..fd47051f45a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,7 @@ Docs: https://docs.openclaw.ai - Discord/auto presence health signal: add runtime availability-driven presence updates plus connected-state reporting to improve health monitoring and operator visibility. (#33277) Thanks @thewilloftheshadow. - Telegram/draft-stream boundary stability: materialize DM draft previews at assistant-message/tool boundaries, serialize lane-boundary callbacks before final delivery, and scope preview cleanup to the active preview so multi-step Telegram streams no longer lose, overwrite, or leave stale preview bubbles. (#33842) Thanks @ngutman. - Telegram/DM draft finalization reliability: require verified final-text draft emission before treating preview finalization as delivered, and fall back to normal payload send when final draft delivery is not confirmed (preventing missing final responses and preserving media/button delivery). (#32118) Thanks @OpenCils. +- Telegram/DM draft final delivery: materialize text-only `sendMessageDraft` previews into one permanent final message and skip duplicate final payload sends, while preserving fallback behavior when materialization fails. (#34318) Thanks @Brotherinlaw-13. - Telegram/draft preview boundary + silent-token reliability: stabilize answer-lane message boundaries across late-partial/message-start races, preserve/reset finalized preview state at the correct boundaries, and suppress `NO_REPLY` lead-fragment leaks without broad heartbeat-prefix false positives. (#33169) Thanks @obviyus. - Discord/audit wildcard warnings: ignore "\*" wildcard keys when counting unresolved guild channels so doctor/status no longer warns on allow-all configs. (#33125) Thanks @thewilloftheshadow. - Discord/channel resolution: default bare numeric recipients to channels, harden allowlist numeric ID handling with safe fallbacks, and avoid inbound WS heartbeat stalls. (#33142) Thanks @thewilloftheshadow. From ef4fa43df89bf442abd134eca58cdefc18116190 Mon Sep 17 00:00:00 2001 From: Ayaan Zaidi Date: Wed, 4 Mar 2026 16:53:16 +0530 Subject: [PATCH 72/74] fix: prevent nodes media base64 context bloat (#34332) --- CHANGELOG.md | 1 + src/agents/openclaw-tools.camera.test.ts | 228 +++++++++++++++++++++-- src/agents/openclaw-tools.ts | 1 + src/agents/tools/nodes-tool.ts | 123 +++++++++++- 4 files changed, 338 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd47051f45a..f03662be132 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Agents/Nodes media outputs: add dedicated `photos_latest` action handling, block media-returning `nodes invoke` commands, keep metadata-only `camera.list` invoke allowed, and normalize empty `photos_latest` results to a consistent response shape to prevent base64 context bloat. (#34332) Thanks @obviyus. - TUI/session-key canonicalization: normalize `openclaw tui --session` values to lowercase so uppercase session names no longer drop real-time streaming updates due to gateway/TUI key mismatches. (#33866, #34013) thanks @lynnzc. - Outbound/send config threading: pass resolved SecretRef config through outbound adapters and helper send paths so send flows do not reload unresolved runtime config. (#33987) Thanks @joshavant. - Sessions/subagent attachments: remove `attachments[].content.maxLength` from `sessions_spawn` schema to avoid llama.cpp GBNF repetition overflow, and preflight UTF-8 byte size before buffer allocation while keeping runtime file-size enforcement unchanged. (#33648) Thanks @anisoptera. diff --git a/src/agents/openclaw-tools.camera.test.ts b/src/agents/openclaw-tools.camera.test.ts index 5fc01d07a82..9621c55c10b 100644 --- a/src/agents/openclaw-tools.camera.test.ts +++ b/src/agents/openclaw-tools.camera.test.ts @@ -32,16 +32,21 @@ function unexpectedGatewayMethod(method: unknown): never { throw new Error(`unexpected method: ${String(method)}`); } -function getNodesTool() { - const tool = createOpenClawTools().find((candidate) => candidate.name === "nodes"); +function getNodesTool(options?: { modelHasVision?: boolean }) { + const tool = createOpenClawTools( + options?.modelHasVision !== undefined ? { modelHasVision: options.modelHasVision } : {}, + ).find((candidate) => candidate.name === "nodes"); if (!tool) { throw new Error("missing nodes tool"); } return tool; } -async function executeNodes(input: Record) { - return getNodesTool().execute("call1", input as never); +async function executeNodes( + input: Record, + options?: { modelHasVision?: boolean }, +) { + return getNodesTool(options).execute("call1", input as never); } type NodesToolResult = Awaited>; @@ -67,6 +72,11 @@ function expectSingleImage(result: NodesToolResult, params?: { mimeType?: string } } +function expectNoImages(result: NodesToolResult) { + const images = (result.content ?? []).filter((block) => block.type === "image"); + expect(images).toHaveLength(0); +} + function expectFirstTextContains(result: NodesToolResult, expectedText: string) { expect(result.content?.[0]).toMatchObject({ type: "text", @@ -156,10 +166,13 @@ describe("nodes camera_snap", () => { }, }); - const result = await executeNodes({ - action: "camera_snap", - node: NODE_ID, - }); + const result = await executeNodes( + { + action: "camera_snap", + node: NODE_ID, + }, + { modelHasVision: true }, + ); expectSingleImage(result); }); @@ -169,15 +182,39 @@ describe("nodes camera_snap", () => { invokePayload: JPG_PAYLOAD, }); - const result = await executeNodes({ - action: "camera_snap", - node: NODE_ID, - facing: "front", - }); + const result = await executeNodes( + { + action: "camera_snap", + node: NODE_ID, + facing: "front", + }, + { modelHasVision: true }, + ); expectSingleImage(result, { mimeType: "image/jpeg" }); }); + it("omits inline base64 image blocks when model has no vision", async () => { + setupNodeInvokeMock({ + invokePayload: JPG_PAYLOAD, + }); + + const result = await executeNodes( + { + action: "camera_snap", + node: NODE_ID, + facing: "front", + }, + { modelHasVision: false }, + ); + + expectNoImages(result); + expect(result.content?.[0]).toMatchObject({ + type: "text", + text: expect.stringMatching(/^MEDIA:/), + }); + }); + it("passes deviceId when provided", async () => { setupNodeInvokeMock({ onInvoke: (invokeParams) => { @@ -299,6 +336,130 @@ describe("nodes camera_clip", () => { }); }); +describe("nodes photos_latest", () => { + it("returns empty content/details when no photos are available", async () => { + setupNodeInvokeMock({ + onInvoke: (invokeParams) => { + expect(invokeParams).toMatchObject({ + command: "photos.latest", + params: { + limit: 1, + maxWidth: 1600, + quality: 0.85, + }, + }); + return { + payload: { + photos: [], + }, + }; + }, + }); + + const result = await executeNodes( + { + action: "photos_latest", + node: NODE_ID, + }, + { modelHasVision: false }, + ); + + expect(result.content ?? []).toEqual([]); + expect(result.details).toEqual([]); + }); + + it("returns MEDIA paths and no inline images when model has no vision", async () => { + setupNodeInvokeMock({ + remoteIp: "198.51.100.42", + onInvoke: (invokeParams) => { + expect(invokeParams).toMatchObject({ + command: "photos.latest", + params: { + limit: 1, + maxWidth: 1600, + quality: 0.85, + }, + }); + return { + payload: { + photos: [ + { + format: "jpeg", + base64: "aGVsbG8=", + width: 1, + height: 1, + createdAt: "2026-03-04T00:00:00Z", + }, + ], + }, + }; + }, + }); + + const result = await executeNodes( + { + action: "photos_latest", + node: NODE_ID, + }, + { modelHasVision: false }, + ); + + expectNoImages(result); + expect(result.content?.[0]).toMatchObject({ + type: "text", + text: expect.stringMatching(/^MEDIA:/), + }); + const details = Array.isArray(result.details) ? result.details : []; + expect(details[0]).toMatchObject({ + width: 1, + height: 1, + createdAt: "2026-03-04T00:00:00Z", + }); + }); + + it("includes inline image blocks when model has vision", async () => { + setupNodeInvokeMock({ + onInvoke: (invokeParams) => { + expect(invokeParams).toMatchObject({ + command: "photos.latest", + params: { + limit: 1, + maxWidth: 1600, + quality: 0.85, + }, + }); + return { + payload: { + photos: [ + { + format: "jpeg", + base64: "aGVsbG8=", + width: 1, + height: 1, + createdAt: "2026-03-04T00:00:00Z", + }, + ], + }, + }; + }, + }); + + const result = await executeNodes( + { + action: "photos_latest", + node: NODE_ID, + }, + { modelHasVision: true }, + ); + + expect(result.content?.[0]).toMatchObject({ + type: "text", + text: expect.stringMatching(/^MEDIA:/), + }); + expectSingleImage(result, { mimeType: "image/jpeg" }); + }); +}); + describe("nodes notifications_list", () => { it("invokes notifications.list and returns payload", async () => { setupNodeInvokeMock({ @@ -576,3 +737,44 @@ describe("nodes run", () => { ); }); }); + +describe("nodes invoke", () => { + it("allows metadata-only camera.list via generic invoke", async () => { + setupNodeInvokeMock({ + onInvoke: (invokeParams) => { + expect(invokeParams).toMatchObject({ + command: "camera.list", + params: {}, + }); + return { + payload: { + devices: [{ id: "cam-back", name: "Back Camera" }], + }, + }; + }, + }); + + const result = await executeNodes({ + action: "invoke", + node: NODE_ID, + invokeCommand: "camera.list", + }); + + expect(result.details).toMatchObject({ + payload: { + devices: [{ id: "cam-back", name: "Back Camera" }], + }, + }); + }); + + it("blocks media invoke commands to avoid base64 context bloat", async () => { + await expect( + executeNodes({ + action: "invoke", + node: NODE_ID, + invokeCommand: "photos.latest", + invokeParamsJson: '{"limit":1}', + }), + ).rejects.toThrow(/use action="photos_latest"/i); + }); +}); diff --git a/src/agents/openclaw-tools.ts b/src/agents/openclaw-tools.ts index cbd9b7b4140..b09f7821208 100644 --- a/src/agents/openclaw-tools.ts +++ b/src/agents/openclaw-tools.ts @@ -136,6 +136,7 @@ export function createOpenClawTools(options?: { currentChannelId: options?.currentChannelId, currentThreadTs: options?.currentThreadTs, config: options?.config, + modelHasVision: options?.modelHasVision, }), createCronTool({ agentSessionKey: options?.agentSessionKey, diff --git a/src/agents/tools/nodes-tool.ts b/src/agents/tools/nodes-tool.ts index 769fe28e0d9..6572ea41205 100644 --- a/src/agents/tools/nodes-tool.ts +++ b/src/agents/tools/nodes-tool.ts @@ -39,6 +39,7 @@ const NODES_TOOL_ACTIONS = [ "camera_snap", "camera_list", "camera_clip", + "photos_latest", "screen_record", "location_get", "notifications_list", @@ -56,6 +57,12 @@ const NOTIFY_DELIVERIES = ["system", "overlay", "auto"] as const; const NOTIFICATIONS_ACTIONS = ["open", "dismiss", "reply"] as const; const CAMERA_FACING = ["front", "back", "both"] as const; const LOCATION_ACCURACY = ["coarse", "balanced", "precise"] as const; +const MEDIA_INVOKE_ACTIONS = { + "camera.snap": "camera_snap", + "camera.clip": "camera_clip", + "photos.latest": "photos_latest", + "screen.record": "screen_record", +} as const; const NODE_READ_ACTION_COMMANDS = { camera_list: "camera.list", notifications_list: "notifications.list", @@ -118,6 +125,7 @@ const NodesToolSchema = Type.Object({ quality: Type.Optional(Type.Number()), delayMs: Type.Optional(Type.Number()), deviceId: Type.Optional(Type.String()), + limit: Type.Optional(Type.Number()), duration: Type.Optional(Type.String()), durationMs: Type.Optional(Type.Number({ maximum: 300_000 })), includeAudio: Type.Optional(Type.Boolean()), @@ -152,6 +160,7 @@ export function createNodesTool(options?: { currentChannelId?: string; currentThreadTs?: string | number; config?: OpenClawConfig; + modelHasVision?: boolean; }): AnyAgentTool { const sessionKey = options?.agentSessionKey?.trim() || undefined; const turnSourceChannel = options?.agentChannel?.trim() || undefined; @@ -167,7 +176,7 @@ export function createNodesTool(options?: { label: "Nodes", name: "nodes", description: - "Discover and control paired nodes (status/describe/pairing/notify/camera/screen/location/notifications/run/invoke).", + "Discover and control paired nodes (status/describe/pairing/notify/camera/photos/screen/location/notifications/run/invoke).", parameters: NodesToolSchema, execute: async (_toolCallId, args) => { const params = args as Record; @@ -301,7 +310,7 @@ export function createNodesTool(options?: { invalidPayloadMessage: "invalid camera.snap payload", }); content.push({ type: "text", text: `MEDIA:${filePath}` }); - if (payload.base64) { + if (options?.modelHasVision && payload.base64) { content.push({ type: "image", data: payload.base64, @@ -320,6 +329,103 @@ export function createNodesTool(options?: { const result: AgentToolResult = { content, details }; return await sanitizeToolResultImages(result, "nodes:camera_snap", imageSanitization); } + case "photos_latest": { + const node = readStringParam(params, "node", { required: true }); + const resolvedNode = await resolveNode(gatewayOpts, node); + const nodeId = resolvedNode.nodeId; + const limitRaw = + typeof params.limit === "number" && Number.isFinite(params.limit) + ? Math.floor(params.limit) + : DEFAULT_PHOTOS_LIMIT; + const limit = Math.max(1, Math.min(limitRaw, MAX_PHOTOS_LIMIT)); + const maxWidth = + typeof params.maxWidth === "number" && Number.isFinite(params.maxWidth) + ? params.maxWidth + : DEFAULT_PHOTOS_MAX_WIDTH; + const quality = + typeof params.quality === "number" && Number.isFinite(params.quality) + ? params.quality + : DEFAULT_PHOTOS_QUALITY; + const raw = await callGatewayTool<{ payload: unknown }>("node.invoke", gatewayOpts, { + nodeId, + command: "photos.latest", + params: { + limit, + maxWidth, + quality, + }, + idempotencyKey: crypto.randomUUID(), + }); + const payload = + raw?.payload && typeof raw.payload === "object" && !Array.isArray(raw.payload) + ? (raw.payload as Record) + : {}; + const photos = Array.isArray(payload.photos) ? payload.photos : []; + + if (photos.length === 0) { + const result: AgentToolResult = { + content: [], + details: [], + }; + return await sanitizeToolResultImages( + result, + "nodes:photos_latest", + imageSanitization, + ); + } + + const content: AgentToolResult["content"] = []; + const details: Array> = []; + + for (const [index, photoRaw] of photos.entries()) { + const photo = parseCameraSnapPayload(photoRaw); + const normalizedFormat = photo.format.toLowerCase(); + if ( + normalizedFormat !== "jpg" && + normalizedFormat !== "jpeg" && + normalizedFormat !== "png" + ) { + throw new Error(`unsupported photos.latest format: ${photo.format}`); + } + const isJpeg = normalizedFormat === "jpg" || normalizedFormat === "jpeg"; + const filePath = cameraTempPath({ + kind: "snap", + ext: isJpeg ? "jpg" : "png", + id: crypto.randomUUID(), + }); + await writeCameraPayloadToFile({ + filePath, + payload: photo, + expectedHost: resolvedNode.remoteIp, + invalidPayloadMessage: "invalid photos.latest payload", + }); + + content.push({ type: "text", text: `MEDIA:${filePath}` }); + if (options?.modelHasVision && photo.base64) { + content.push({ + type: "image", + data: photo.base64, + mimeType: + imageMimeFromFormat(photo.format) ?? (isJpeg ? "image/jpeg" : "image/png"), + }); + } + + const createdAt = + photoRaw && typeof photoRaw === "object" && !Array.isArray(photoRaw) + ? (photoRaw as Record).createdAt + : undefined; + details.push({ + index, + path: filePath, + width: photo.width, + height: photo.height, + ...(typeof createdAt === "string" ? { createdAt } : {}), + }); + } + + const result: AgentToolResult = { content, details }; + return await sanitizeToolResultImages(result, "nodes:photos_latest", imageSanitization); + } case "camera_list": case "notifications_list": case "device_status": @@ -645,6 +751,14 @@ export function createNodesTool(options?: { const node = readStringParam(params, "node", { required: true }); const nodeId = await resolveNodeId(gatewayOpts, node); const invokeCommand = readStringParam(params, "invokeCommand", { required: true }); + const invokeCommandNormalized = invokeCommand.trim().toLowerCase(); + const dedicatedAction = + MEDIA_INVOKE_ACTIONS[invokeCommandNormalized as keyof typeof MEDIA_INVOKE_ACTIONS]; + if (dedicatedAction) { + throw new Error( + `invokeCommand "${invokeCommand}" returns media payloads and is blocked to prevent base64 context bloat; use action="${dedicatedAction}"`, + ); + } const invokeParamsJson = typeof params.invokeParamsJson === "string" ? params.invokeParamsJson.trim() : ""; let invokeParams: unknown = {}; @@ -695,3 +809,8 @@ export function createNodesTool(options?: { }, }; } + +const DEFAULT_PHOTOS_LIMIT = 1; +const MAX_PHOTOS_LIMIT = 20; +const DEFAULT_PHOTOS_MAX_WIDTH = 1600; +const DEFAULT_PHOTOS_QUALITY = 0.85; From 7b5e64ef2e369258e2a4a613b7a62db3c21e5160 Mon Sep 17 00:00:00 2001 From: Ayaan Zaidi Date: Wed, 4 Mar 2026 17:17:24 +0530 Subject: [PATCH 73/74] fix: preserve raw media invoke for HTTP tool clients (#34365) --- CHANGELOG.md | 1 + src/agents/openclaw-tools.camera.test.ts | 50 +++++++++++++++++++++--- src/agents/openclaw-tools.ts | 3 ++ src/agents/tools/nodes-tool.ts | 3 +- src/gateway/tools-invoke-http.test.ts | 1 + src/gateway/tools-invoke-http.ts | 2 + 6 files changed, 54 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f03662be132..fb53bd78081 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Gateway/HTTP tools invoke media compatibility: preserve raw media payload access for direct `/tools/invoke` clients by allowing media `nodes` invoke commands only in HTTP tool context, while keeping agent-context media invoke blocking to prevent base64 prompt bloat. (#34365) Thanks @obviyus. - Agents/Nodes media outputs: add dedicated `photos_latest` action handling, block media-returning `nodes invoke` commands, keep metadata-only `camera.list` invoke allowed, and normalize empty `photos_latest` results to a consistent response shape to prevent base64 context bloat. (#34332) Thanks @obviyus. - TUI/session-key canonicalization: normalize `openclaw tui --session` values to lowercase so uppercase session names no longer drop real-time streaming updates due to gateway/TUI key mismatches. (#33866, #34013) thanks @lynnzc. - Outbound/send config threading: pass resolved SecretRef config through outbound adapters and helper send paths so send flows do not reload unresolved runtime config. (#33987) Thanks @joshavant. diff --git a/src/agents/openclaw-tools.camera.test.ts b/src/agents/openclaw-tools.camera.test.ts index 9621c55c10b..db41cd2857a 100644 --- a/src/agents/openclaw-tools.camera.test.ts +++ b/src/agents/openclaw-tools.camera.test.ts @@ -32,10 +32,18 @@ function unexpectedGatewayMethod(method: unknown): never { throw new Error(`unexpected method: ${String(method)}`); } -function getNodesTool(options?: { modelHasVision?: boolean }) { - const tool = createOpenClawTools( - options?.modelHasVision !== undefined ? { modelHasVision: options.modelHasVision } : {}, - ).find((candidate) => candidate.name === "nodes"); +function getNodesTool(options?: { modelHasVision?: boolean; allowMediaInvokeCommands?: boolean }) { + const toolOptions: { + modelHasVision?: boolean; + allowMediaInvokeCommands?: boolean; + } = {}; + if (options?.modelHasVision !== undefined) { + toolOptions.modelHasVision = options.modelHasVision; + } + if (options?.allowMediaInvokeCommands !== undefined) { + toolOptions.allowMediaInvokeCommands = options.allowMediaInvokeCommands; + } + const tool = createOpenClawTools(toolOptions).find((candidate) => candidate.name === "nodes"); if (!tool) { throw new Error("missing nodes tool"); } @@ -44,7 +52,7 @@ function getNodesTool(options?: { modelHasVision?: boolean }) { async function executeNodes( input: Record, - options?: { modelHasVision?: boolean }, + options?: { modelHasVision?: boolean; allowMediaInvokeCommands?: boolean }, ) { return getNodesTool(options).execute("call1", input as never); } @@ -777,4 +785,36 @@ describe("nodes invoke", () => { }), ).rejects.toThrow(/use action="photos_latest"/i); }); + + it("allows media invoke commands when explicitly enabled", async () => { + setupNodeInvokeMock({ + onInvoke: (invokeParams) => { + expect(invokeParams).toMatchObject({ + command: "photos.latest", + params: { limit: 1 }, + }); + return { + payload: { + photos: [{ format: "jpg", base64: "aGVsbG8=", width: 1, height: 1 }], + }, + }; + }, + }); + + const result = await executeNodes( + { + action: "invoke", + node: NODE_ID, + invokeCommand: "photos.latest", + invokeParamsJson: '{"limit":1}', + }, + { allowMediaInvokeCommands: true }, + ); + + expect(result.details).toMatchObject({ + payload: { + photos: [{ format: "jpg", base64: "aGVsbG8=", width: 1, height: 1 }], + }, + }); + }); }); diff --git a/src/agents/openclaw-tools.ts b/src/agents/openclaw-tools.ts index b09f7821208..4373bf83c4b 100644 --- a/src/agents/openclaw-tools.ts +++ b/src/agents/openclaw-tools.ts @@ -60,6 +60,8 @@ export function createOpenClawTools(options?: { hasRepliedRef?: { value: boolean }; /** If true, the model has native vision capability */ modelHasVision?: boolean; + /** If true, nodes action="invoke" can call media-returning commands directly. */ + allowMediaInvokeCommands?: boolean; /** Explicit agent ID override for cron/hook sessions. */ requesterAgentIdOverride?: string; /** Require explicit message targets (no implicit last-route sends). */ @@ -137,6 +139,7 @@ export function createOpenClawTools(options?: { currentThreadTs: options?.currentThreadTs, config: options?.config, modelHasVision: options?.modelHasVision, + allowMediaInvokeCommands: options?.allowMediaInvokeCommands, }), createCronTool({ agentSessionKey: options?.agentSessionKey, diff --git a/src/agents/tools/nodes-tool.ts b/src/agents/tools/nodes-tool.ts index 6572ea41205..b90d429119b 100644 --- a/src/agents/tools/nodes-tool.ts +++ b/src/agents/tools/nodes-tool.ts @@ -161,6 +161,7 @@ export function createNodesTool(options?: { currentThreadTs?: string | number; config?: OpenClawConfig; modelHasVision?: boolean; + allowMediaInvokeCommands?: boolean; }): AnyAgentTool { const sessionKey = options?.agentSessionKey?.trim() || undefined; const turnSourceChannel = options?.agentChannel?.trim() || undefined; @@ -754,7 +755,7 @@ export function createNodesTool(options?: { const invokeCommandNormalized = invokeCommand.trim().toLowerCase(); const dedicatedAction = MEDIA_INVOKE_ACTIONS[invokeCommandNormalized as keyof typeof MEDIA_INVOKE_ACTIONS]; - if (dedicatedAction) { + if (dedicatedAction && !options?.allowMediaInvokeCommands) { throw new Error( `invokeCommand "${invokeCommand}" returns media payloads and is blocked to prevent base64 context bloat; use action="${dedicatedAction}"`, ); diff --git a/src/gateway/tools-invoke-http.test.ts b/src/gateway/tools-invoke-http.test.ts index 20a2f2c2c19..66a68bf5d9f 100644 --- a/src/gateway/tools-invoke-http.test.ts +++ b/src/gateway/tools-invoke-http.test.ts @@ -335,6 +335,7 @@ describe("POST /tools/invoke", () => { const body = await res.json(); expect(body.ok).toBe(true); expect(body).toHaveProperty("result"); + expect(lastCreateOpenClawToolsContext?.allowMediaInvokeCommands).toBe(true); }); it("supports tools.alsoAllow in profile and implicit modes", async () => { diff --git a/src/gateway/tools-invoke-http.ts b/src/gateway/tools-invoke-http.ts index caf71c56c3c..88cea7b3845 100644 --- a/src/gateway/tools-invoke-http.ts +++ b/src/gateway/tools-invoke-http.ts @@ -252,6 +252,8 @@ export async function handleToolsInvokeHttpRequest( agentAccountId: accountId, agentTo, agentThreadId, + // HTTP callers consume tool output directly; preserve raw media invoke payloads. + allowMediaInvokeCommands: true, config: cfg, pluginToolAllowlist: collectExplicitAllowlist([ profilePolicy, From c1bb07bd165f636744d9d7ed9d351d96c7fe89c4 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Wed, 4 Mar 2026 05:44:07 -0800 Subject: [PATCH 74/74] fix(slack): route system events to bound agent sessions (#34045) * fix(slack): route system events via binding-aware session keys * fix(slack): pass sender to system event session resolver * fix(slack): include sender context for interaction session routing * fix(slack): include modal submitter in session routing * test(slack): cover binding-aware system event routing * test(slack): update interaction session key assertions * test(slack): assert reaction session routing carries sender * docs(changelog): note slack system event routing fix * Update CHANGELOG.md --- CHANGELOG.md | 1 + src/slack/monitor/context.ts | 24 ++++++++++ .../monitor/events/interactions.modal.ts | 3 ++ src/slack/monitor/events/interactions.test.ts | 3 ++ src/slack/monitor/events/interactions.ts | 1 + src/slack/monitor/events/reactions.test.ts | 22 +++++++++ .../monitor/events/system-event-context.ts | 1 + src/slack/monitor/monitor.test.ts | 47 +++++++++++++++++++ 8 files changed, 102 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb53bd78081..0fb849832b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Slack/system-event session routing: resolve reaction/member/pin/interaction system-event session keys through channel/account bindings (with sender-aware DM routing) so inbound Slack events target the correct agent session in multi-account setups instead of defaulting to `agent:main`. (#34045) Thanks @paulomcg, @daht-mad and @vincentkoc. - Gateway/HTTP tools invoke media compatibility: preserve raw media payload access for direct `/tools/invoke` clients by allowing media `nodes` invoke commands only in HTTP tool context, while keeping agent-context media invoke blocking to prevent base64 prompt bloat. (#34365) Thanks @obviyus. - Agents/Nodes media outputs: add dedicated `photos_latest` action handling, block media-returning `nodes invoke` commands, keep metadata-only `camera.list` invoke allowed, and normalize empty `photos_latest` results to a consistent response shape to prevent base64 context bloat. (#34332) Thanks @obviyus. - TUI/session-key canonicalization: normalize `openclaw tui --session` values to lowercase so uppercase session names no longer drop real-time streaming updates due to gateway/TUI key mismatches. (#33866, #34013) thanks @lynnzc. diff --git a/src/slack/monitor/context.ts b/src/slack/monitor/context.ts index 84633320427..1d75af03650 100644 --- a/src/slack/monitor/context.ts +++ b/src/slack/monitor/context.ts @@ -7,6 +7,7 @@ import type { DmPolicy, GroupPolicy } from "../../config/types.js"; import { logVerbose } from "../../globals.js"; import { createDedupeCache } from "../../infra/dedupe.js"; import { getChildLogger } from "../../logging.js"; +import { resolveAgentRoute } from "../../routing/resolve-route.js"; import type { RuntimeEnv } from "../../runtime.js"; import type { SlackMessageEvent } from "../types.js"; import { normalizeAllowList, normalizeAllowListLower, normalizeSlackSlug } from "./allow-list.js"; @@ -62,6 +63,7 @@ export type SlackMonitorContext = { resolveSlackSystemEventSessionKey: (params: { channelId?: string | null; channelType?: string | null; + senderId?: string | null; }) => string; isChannelAllowed: (params: { channelId?: string; @@ -151,6 +153,7 @@ export function createSlackMonitorContext(params: { const resolveSlackSystemEventSessionKey = (p: { channelId?: string | null; channelType?: string | null; + senderId?: string | null; }) => { const channelId = p.channelId?.trim() ?? ""; if (!channelId) { @@ -165,6 +168,27 @@ export function createSlackMonitorContext(params: { ? `slack:group:${channelId}` : `slack:channel:${channelId}`; const chatType = isDirectMessage ? "direct" : isGroup ? "group" : "channel"; + const senderId = p.senderId?.trim() ?? ""; + + // Resolve through shared channel/account bindings so system events route to + // the same agent session as regular inbound messages. + try { + const peerKind = isDirectMessage ? "direct" : isGroup ? "group" : "channel"; + const peerId = isDirectMessage ? senderId : channelId; + if (peerId) { + const route = resolveAgentRoute({ + cfg: params.cfg, + channel: "slack", + accountId: params.accountId, + teamId: params.teamId, + peer: { kind: peerKind, id: peerId }, + }); + return route.sessionKey; + } + } catch { + // Fall through to legacy key derivation. + } + return resolveSessionKey( params.sessionScope, { From: from, ChatType: chatType, Provider: "slack" }, diff --git a/src/slack/monitor/events/interactions.modal.ts b/src/slack/monitor/events/interactions.modal.ts index 603b1ab79e2..99d1a3711b6 100644 --- a/src/slack/monitor/events/interactions.modal.ts +++ b/src/slack/monitor/events/interactions.modal.ts @@ -77,6 +77,7 @@ type SlackInteractionContextPrefix = "slack:interaction:view" | "slack:interacti function resolveModalSessionRouting(params: { ctx: SlackMonitorContext; metadata: ReturnType; + userId?: string; }): { sessionKey: string; channelId?: string; channelType?: string } { const metadata = params.metadata; if (metadata.sessionKey) { @@ -91,6 +92,7 @@ function resolveModalSessionRouting(params: { sessionKey: params.ctx.resolveSlackSystemEventSessionKey({ channelId: metadata.channelId, channelType: metadata.channelType, + senderId: params.userId, }), channelId: metadata.channelId, channelType: metadata.channelType, @@ -139,6 +141,7 @@ function resolveSlackModalEventBase(params: { const sessionRouting = resolveModalSessionRouting({ ctx: params.ctx, metadata, + userId, }); return { callbackId, diff --git a/src/slack/monitor/events/interactions.test.ts b/src/slack/monitor/events/interactions.test.ts index be47f6ac8a7..21fd6d173d4 100644 --- a/src/slack/monitor/events/interactions.test.ts +++ b/src/slack/monitor/events/interactions.test.ts @@ -223,6 +223,7 @@ describe("registerSlackInteractionEvents", () => { expect(resolveSessionKey).toHaveBeenCalledWith({ channelId: "C1", channelType: "channel", + senderId: "U123", }); expect(app.client.chat.update).toHaveBeenCalledTimes(1); }); @@ -554,6 +555,7 @@ describe("registerSlackInteractionEvents", () => { expect(resolveSessionKey).toHaveBeenCalledWith({ channelId: "C222", channelType: "channel", + senderId: "U111", }); expect(enqueueSystemEventMock).toHaveBeenCalledTimes(1); const [eventText] = enqueueSystemEventMock.mock.calls[0] as [string]; @@ -952,6 +954,7 @@ describe("registerSlackInteractionEvents", () => { expect(resolveSessionKey).toHaveBeenCalledWith({ channelId: "D123", channelType: "im", + senderId: "U777", }); expect(enqueueSystemEventMock).toHaveBeenCalledTimes(1); const [eventText] = enqueueSystemEventMock.mock.calls[0] as [string]; diff --git a/src/slack/monitor/events/interactions.ts b/src/slack/monitor/events/interactions.ts index 3a242652bc9..4f92df32be7 100644 --- a/src/slack/monitor/events/interactions.ts +++ b/src/slack/monitor/events/interactions.ts @@ -571,6 +571,7 @@ export function registerSlackInteractionEvents(params: { ctx: SlackMonitorContex const sessionKey = ctx.resolveSlackSystemEventSessionKey({ channelId: channelId, channelType: auth.channelType, + senderId: userId, }); // Build context key - only include defined values to avoid "unknown" noise diff --git a/src/slack/monitor/events/reactions.test.ts b/src/slack/monitor/events/reactions.test.ts index 8105b2047fc..3581d8b5380 100644 --- a/src/slack/monitor/events/reactions.test.ts +++ b/src/slack/monitor/events/reactions.test.ts @@ -153,4 +153,26 @@ describe("registerSlackReactionEvents", () => { expect(trackEvent).toHaveBeenCalledTimes(1); }); + + it("passes sender context when resolving reaction session keys", async () => { + reactionQueueMock.mockClear(); + reactionAllowMock.mockReset().mockResolvedValue([]); + const harness = createSlackSystemEventTestHarness(); + const resolveSessionKey = vi.fn().mockReturnValue("agent:ops:main"); + harness.ctx.resolveSlackSystemEventSessionKey = resolveSessionKey; + registerSlackReactionEvents({ ctx: harness.ctx }); + const handler = harness.getHandler("reaction_added"); + expect(handler).toBeTruthy(); + + await handler!({ + event: buildReactionEvent({ user: "U777", channel: "D123" }), + body: {}, + }); + + expect(resolveSessionKey).toHaveBeenCalledWith({ + channelId: "D123", + channelType: "im", + senderId: "U777", + }); + }); }); diff --git a/src/slack/monitor/events/system-event-context.ts b/src/slack/monitor/events/system-event-context.ts index 5df48dfd167..0c89ec2ce47 100644 --- a/src/slack/monitor/events/system-event-context.ts +++ b/src/slack/monitor/events/system-event-context.ts @@ -36,6 +36,7 @@ export async function authorizeAndResolveSlackSystemEventContext(params: { const sessionKey = ctx.resolveSlackSystemEventSessionKey({ channelId, channelType: auth.channelType, + senderId, }); return { channelLabel, diff --git a/src/slack/monitor/monitor.test.ts b/src/slack/monitor/monitor.test.ts index c1fac686971..d6e819ca46d 100644 --- a/src/slack/monitor/monitor.test.ts +++ b/src/slack/monitor/monitor.test.ts @@ -184,6 +184,53 @@ describe("resolveSlackSystemEventSessionKey", () => { "agent:main:slack:channel:c123", ); }); + + it("routes channel system events through account bindings", () => { + const ctx = createSlackMonitorContext({ + ...baseParams(), + accountId: "work", + cfg: { + bindings: [ + { + agentId: "ops", + match: { + channel: "slack", + accountId: "work", + }, + }, + ], + }, + }); + expect( + ctx.resolveSlackSystemEventSessionKey({ channelId: "C123", channelType: "channel" }), + ).toBe("agent:ops:slack:channel:c123"); + }); + + it("routes DM system events through direct-peer bindings when sender is known", () => { + const ctx = createSlackMonitorContext({ + ...baseParams(), + accountId: "work", + cfg: { + bindings: [ + { + agentId: "ops-dm", + match: { + channel: "slack", + accountId: "work", + peer: { kind: "direct", id: "U123" }, + }, + }, + ], + }, + }); + expect( + ctx.resolveSlackSystemEventSessionKey({ + channelId: "D123", + channelType: "im", + senderId: "U123", + }), + ).toBe("agent:ops-dm:main"); + }); }); describe("isChannelAllowed with groupPolicy and channelsConfig", () => {