Merge remote-tracking branch 'origin/main' into feat/add_qwen_official_api

# Conflicts:
#	.agents/skills/parallels-discord-roundtrip/SKILL.md
#	src/commands/auth-choice.apply.api-key-providers.ts
This commit is contained in:
wenmengzhou 2026-03-16 16:59:25 +08:00
commit b532ae75f9
213 changed files with 4795 additions and 3313 deletions

View File

@ -6,26 +6,27 @@ Docs: https://docs.openclaw.ai
### Changes
- Android/mobile: add a system-aware dark theme across onboarding and post-onboarding screens so the app follows the device theme through setup, chat, and voice flows. (#46249) Thanks @sibbl.
- Commands/btw: add `/btw` side questions for quick tool-less answers about the current session without changing future session context, with dismissible in-session TUI answers and explicit BTW replies on external channels. (#45444) Thanks @ngutman.
- Gateway/health monitor: add configurable stale-event thresholds and restart limits, plus per-channel and per-account `healthMonitor.enabled` overrides, while keeping the existing global disable path on `gateway.channelHealthCheckMinutes=0`. (#42107) Thanks @rstar327.
- Feishu/cards: add identity-aware structured card headers and note footers for Feishu replies and direct sends, while keeping that presentation wired through the shared outbound identity path. (#29938) Thanks @nszhsl.
- Feishu/streaming: add `onReasoningStream` and `onReasoningEnd` support to streaming cards, so `/reasoning stream` renders thinking tokens as markdown blockquotes in the same card — matching the Telegram channel's reasoning lane behavior. (#46029)
- Web tools/Firecrawl: add Firecrawl as an `onboard`/configure search provider via a bundled plugin, expose explicit `firecrawl_search` and `firecrawl_scrape` tools, and align core `web_fetch` fallback behavior with Firecrawl base-URL/env fallback plus guarded endpoint fetches.
- Refactor/channels: remove the legacy channel shim directories and point channel-specific imports directly at the extension-owned implementations. (#45967) thanks @scoootscooob.
- Android/nodes: add `callLog.search` plus shared Call Log permission wiring so Android nodes can search recent call history through the gateway. (#44073) Thanks @lxk7280.
- Docs/Zalo: clarify the Marketplace-bot support matrix and config guidance so the Zalo channel docs match current Bot Creator behavior more closely. (#47552) Thanks @No898.
- Install/update: allow package-manager installs from GitHub `main` via `openclaw update --tag main`, installer `--version main`, or direct npm/pnpm git specs.
- Plugins/providers: move OpenRouter, GitHub Copilot, and OpenAI Codex provider/runtime logic into bundled plugins, including dynamic model fallback, runtime auth exchange, stream wrappers, capability hints, and cache-TTL policy.
- Plugins/MiniMax: merge the bundled MiniMax API and MiniMax OAuth plugin surfaces into a single default-on `minimax` plugin, while keeping legacy `minimax-portal-auth` config ids aliased for compatibility.
- Plugins/bundles: add compatible Codex, Claude, and Cursor bundle discovery/install support, map bundle skills into OpenClaw skills, and apply Claude bundle `settings.json` defaults to embedded Pi with shell overrides sanitized.
- Plugins/agent integrations: broaden the plugin surface for app-server integrations with channel-aware commands, interactive callbacks, inbound claims, and Discord/Telegram conversation binding support. (#45318) Thanks @huntharo and @vincentkoc.
- Telegram/actions: add `topic-edit` for forum-topic renames and icon updates while sharing the same Telegram topic-edit transport used by the plugin runtime. (#47798) Thanks @obviyus.
- secrets: harden read-only SecretRef command paths and diagnostics. (#47794) Thanks @joshavant.
- Sandbox/runtime: add pluggable sandbox backends, ship an OpenShell backend with `mirror` and `remote` workspace modes, and make sandbox list/recreate/prune backend-aware instead of Docker-only.
- Sandbox/SSH: add a core SSH sandbox backend with secret-backed key, certificate, and known_hosts inputs, move shared remote exec/filesystem tooling into core, and keep OpenShell focused on sandbox lifecycle plus optional `mirror` mode.
- Feishu/cards: add structured interactive approval and quick-action launcher cards, preserve callback user and conversation context through routing, and keep legacy card-action fallback behavior so common actions can run without typing raw commands. (#47873)
- Web tools/Firecrawl: add Firecrawl as an `onboard`/configure search provider via a bundled plugin, expose explicit `firecrawl_search` and `firecrawl_scrape` tools, and align core `web_fetch` fallback behavior with Firecrawl base-URL/env fallback plus guarded endpoint fetches.
- Plugins/bundles: add compatible Codex, Claude, and Cursor bundle discovery/install support, map bundle skills into OpenClaw skills, and apply Claude bundle `settings.json` defaults to embedded Pi with shell overrides sanitized.
- Plugins/providers: move OpenRouter, GitHub Copilot, and OpenAI Codex provider/runtime logic into bundled plugins, including dynamic model fallback, runtime auth exchange, stream wrappers, capability hints, and cache-TTL policy.
- Plugins/agent integrations: broaden the plugin surface for app-server integrations with channel-aware commands, interactive callbacks, inbound claims, and Discord/Telegram conversation binding support. (#45318) Thanks @huntharo and @vincentkoc.
- Install/update: allow package-manager installs from GitHub `main` via `openclaw update --tag main`, installer `--version main`, or direct npm/pnpm git specs.
- Gateway/health monitor: add configurable stale-event thresholds and restart limits, plus per-channel and per-account `healthMonitor.enabled` overrides, while keeping the existing global disable path on `gateway.channelHealthCheckMinutes=0`. (#42107) Thanks @rstar327.
- Android/mobile: add a system-aware dark theme across onboarding and post-onboarding screens so the app follows the device theme through setup, chat, and voice flows. (#46249) Thanks @sibbl.
- Feishu/ACP: add current-conversation ACP and subagent session binding for supported DMs and topic conversations, including completion delivery back to the originating Feishu conversation. (#46819)
- Plugins/marketplaces: add Claude marketplace registry resolution, `plugin@marketplace` installs, marketplace listing, and update support, plus Docker E2E coverage for local and official marketplace flows. Thanks @vincentkoc.
- Feishu/cards: add structured interactive approval and quick-action launcher cards, preserve callback user and conversation context through routing, and keep legacy card-action fallback behavior so common actions can run without typing raw commands. (#47873)
- Feishu/streaming: add `onReasoningStream` and `onReasoningEnd` support to streaming cards, so `/reasoning stream` renders thinking tokens as markdown blockquotes in the same card — matching the Telegram channel's reasoning lane behavior. (#46029)
- Feishu/cards: add identity-aware structured card headers and note footers for Feishu replies and direct sends, while keeping that presentation wired through the shared outbound identity path. (#29938) Thanks @nszhsl.
- Android/nodes: add `callLog.search` plus shared Call Log permission wiring so Android nodes can search recent call history through the gateway. (#44073) Thanks @lxk7280.
- Plugins/MiniMax: merge the bundled MiniMax API and MiniMax OAuth plugin surfaces into a single default-on `minimax` plugin, while keeping legacy `minimax-portal-auth` config ids aliased for compatibility.
- Telegram/actions: add `topic-edit` for forum-topic renames and icon updates while sharing the same Telegram topic-edit transport used by the plugin runtime. (#47798) Thanks @obviyus.
- Refactor/channels: remove the legacy channel shim directories and point channel-specific imports directly at the extension-owned implementations. (#45967) thanks @scoootscooob.
- Docs/Zalo: clarify the Marketplace-bot support matrix and config guidance so the Zalo channel docs match current Bot Creator behavior more closely. (#47552) Thanks @No898.
- secrets: harden read-only SecretRef command paths and diagnostics. (#47794) Thanks @joshavant.
### Breaking
@ -33,65 +34,64 @@ Docs: https://docs.openclaw.ai
### Fixes
- Group mention gating: reject invalid and unsafe nested-repetition `mentionPatterns`, reuse the shared safe config-regex compiler across mention stripping and detection, and cache strip-time regex compilation so noisy groups avoid repeated recompiles.
- Control UI/chat sessions: show human-readable labels in the grouped session dropdown again, keep unique scoped fallbacks when metadata is missing, and disambiguate duplicate labels only when needed. (#45130) thanks @luzhidong.
- Control UI: scope persisted session selection per gateway, prevent stale session bleed across tokenized gateway opens, and cap stored gateway session history. (#47453) Thanks @sallyom.
- Slack/interactive replies: preserve `channelData.slack.blocks` through live DM delivery and preview-finalized edits so Block Kit button and select directives render instead of falling back to raw text. (#45890) Thanks @vincentkoc.
- Feishu/topic threads: fetch full thread context, including prior bot replies, when starting a topic-thread session so follow-up turns in Feishu topics keep the right conversation state. (#45254) Thanks @Coobiw.
- Feishu/media: keep native image, file, audio, and video/media handling aligned across outbound sends, inbound downloads, thread replies, directory/action aliases, and capability docs so unsupported areas are explicit instead of implied.
- Feishu/actions: expand the runtime action surface with message read/edit, explicit thread replies, pinning, and operator-facing chat/member inspection so Feishu can operate more of the workspace directly.
- Configure/startup: move outbound send-deps resolution into a lightweight helper so `openclaw configure` no longer stalls after the banner while eagerly loading channel plugins. (#46301) thanks @scoootscooob.
- Control UI/dashboard: preserve structured gateway shutdown reasons across restart disconnects so config-triggered restarts no longer fall back to `disconnected (1006): no reason`. (#46532) Thanks @vincentkoc.
- Android/chat: theme the thinking dropdown and TLS trust dialogs explicitly so popup surfaces match the active app theme instead of falling back to mismatched Material defaults.
- Z.AI/onboarding: detect a working default model even for explicit `zai-coding-*` endpoint choices, so Coding Plan setup can keep the selected endpoint while defaulting to `glm-5` when available or `glm-4.7` as fallback. (#45969)
- Models/OpenRouter runtime capabilities: fetch uncatalogued OpenRouter model metadata on first use so newly added vision models keep image input instead of silently degrading to text-only, with top-level capability field fallbacks for `/api/v1/models`. (#45824) Thanks @DJjjjhao.
- Z.AI/onboarding: add `glm-5-turbo` to the default Z.AI provider catalog so onboarding-generated configs expose the new model alongside the existing GLM defaults. (#46670) Thanks @tomsun28.
- Zalo Personal/group gating: stop reapplying `dmPolicy.allowFrom` as a sender gate for already-allowlisted groups when `groupAllowFrom` is unset, so any member of an allowed group can trigger replies while DMs stay restricted. (#40146)
- Channels/plugins: keep shared interactive payloads merge-ready by fixing Slack custom callback routing and repeat-click dedupe, allowing interactive-only sends, and preserving ordered Discord shared text blocks. (#47715) Thanks @vincentkoc.
- Google auth/Node 25: patch `gaxios` to use native fetch without injecting `globalThis.window`, while translating proxy and mTLS transport settings so Google Vertex and Google Chat auth keep working on Node 25. (#47914) Thanks @pdd-cli.
- Gateway/startup: load bundled channel plugins from compiled `dist/extensions` entries in built installs, so gateway boot no longer recompiles bundled extension TypeScript on every startup and WhatsApp-class cold starts drop back to seconds instead of tens of seconds or worse.
- Plugins/context engines: enforce owner-aware context-engine registration on both loader and public SDK paths so plugins cannot spoof privileged ownership, claim the core `legacy` engine id, or overwrite an existing engine id through direct SDK imports. (#47595) Thanks @vincentkoc.
- Browser/remote CDP: honor strict browser SSRF policy during remote CDP reachability and `/json/version` discovery checks, redact sensitive `cdpUrl` tokens from status output, and warn when remote CDP targets private/internal hosts.
- Plugins/install precedence: keep bundled plugins ahead of auto-discovered globals by default, but let an explicitly installed plugin record win its own duplicate-id tie so installed channel plugins load from `~/.openclaw/extensions` after `openclaw plugins install`.
- ACP/acpx: resolve the bundled plugin root from the actual plugin directory so plugin-local installs stay under `dist/extensions/acpx` instead of escaping to `dist/extensions` and failing runtime setup.
- Gateway/auth: ignore spoofed loopback hops in trusted forwarding chains and block device approvals that request scopes above the caller session. Thanks @vincentkoc.
- Gateway/config views: strip embedded credentials from URL-based endpoint fields before returning read-only account and config snapshots. Thanks @vincentkoc.
- Tools/apply-patch: revalidate workspace-only delete and directory targets immediately before mutating host paths. Thanks @vincentkoc.
- Webhooks/runtime: move auth earlier and tighten pre-auth body limits and timeouts across bundled webhook handlers, including slow-body handling for Mattermost slash commands. Thanks @vincentkoc.
- Gateway/plugins: pin runtime webhook routes to the gateway startup registry so channel webhooks keep working across plugin-registry churn, and make plugin auth + dispatch resolve routes from the same live HTTP-route registry. Fixes #46924 and #47041.
- Subagents/follow-ups: require the same controller ownership checks for `/subagents send` as other control actions, so leaf sessions cannot message nested child runs they do not control. Thanks @vincentkoc.
- Inbound policy hardening: tighten callback and webhook sender checks across Mattermost and Google Chat, match Nextcloud Talk rooms by stable room token, and treat explicit empty Twitch allowlists as deny-all. (#46787) Thanks @zpbrent, @ijxpwastaken and @vincentkoc.
- macOS/canvas actions: keep unattended local agent actions on trusted in-app canvas surfaces only, and stop exposing the deep-link fallback key to arbitrary page scripts. (#46790) Thanks @vincentkoc.
- Agents/compaction: extend the enclosing run deadline once while compaction is actively in flight, and abort the underlying SDK compaction on timeout/cancel so large-session compactions stop freezing mid-run. (#46889) Thanks @asyncjason.
- Models/openai-completions: default non-native OpenAI-compatible providers to omit tool-definition `strict` fields unless users explicitly opt back in, so tool calling keeps working on providers that reject that option. (#45497) Thanks @sahancava.
- WhatsApp/reconnect: restore the append recency filter in the extension inbox monitor and handle protobuf `Long` timestamps correctly, so fresh post-reconnect append messages are processed while stale history sync stays suppressed. (#42588) thanks @MonkeyLeeT.
- WhatsApp/login: wait for pending creds writes before reopening after Baileys `515` pairing restarts in both QR login and `channels login` flows, and keep the restart coverage pinned to the real wrapped error shape plus per-account creds queues. (#27910) Thanks @asyncjason.
- Agents/openai-compatible tool calls: deduplicate repeated tool call ids across live assistant messages and replayed history so OpenAI-compatible backends no longer reject duplicate `tool_call_id` values with HTTP 400. (#40996) Thanks @xaeon2026.
- Security/device pairing: harden `device.token.rotate` deny handling by keeping public failures generic while logging internal deny reasons and preserving approved-baseline enforcement. (`GHSA-7jrw-x62h-64p8`)
- Slack/interactive replies: preserve `channelData.slack.blocks` through live DM delivery and preview-finalized edits so Block Kit button and select directives render instead of falling back to raw text. (#45890) Thanks @vincentkoc.
- Gateway/auth: ignore spoofed loopback hops in trusted forwarding chains and block device approvals that request scopes above the caller session. Thanks @vincentkoc.
- Gateway/restart: defer externally signaled unmanaged restarts through the in-process idle drain, and preserve the restored subagent run as remap fallback during orphan recovery so resumed sessions do not duplicate work. (#47719) Thanks @joeykrug.
- Zalo/plugin runtime: export `resolveClientIp` from `openclaw/plugin-sdk/zalo` so installed builds no longer crash on startup when the webhook monitor loads from the packaged extension instead of the monorepo source tree. (#46549) Thanks @No898.
- CI/channel test routing: move the built-in channel suites into `test:channels` and keep them out of `test:extensions`, so extension CI no longer fails after the channel migration while targeted test routing still sends Slack, Signal, and iMessage suites to the right lane. (#46066) Thanks @scoootscooob.
- Browser/profiles: drop the auto-created `chrome-relay` browser profile; users who need the Chrome extension relay must now create their own profile via `openclaw browser create-profile`. (#45777) Thanks @odysseus0.
- Docs/Mintlify: fix MDX marker syntax on Perplexity, Model Providers, Moonshot, and exec approvals pages so local docs preview no longer breaks rendering or leaves stale pages unpublished. (#46695) Thanks @velvet-shark.
- Email/webhook wrapping: sanitize sender and subject metadata before external-content wrapping so metadata fields cannot break the wrapper structure. Thanks @vincentkoc.
- Node/startup: remove leftover debug `console.log("node host PATH: ...")` that printed the resolved PATH on every `openclaw node run` invocation. (#46411)
- Nodes/pending actions: re-check queued foreground actions against the current node command policy before returning them to the node. (#46815) Thanks @zpbrent and @vincentkoc.
- ACP/approvals: use canonical tool identity for prompting decisions and fail closed when conflicting tool identity hints are present. (#46817) Thanks @zpbrent and @vincentkoc.
- Telegram/message send: forward `--force-document` through the `sendPayload` path as well as `sendMedia`, so Telegram payload sends with `channelData` keep uploading images as documents instead of silently falling back to compressed photo sends. (#47119) Thanks @thepagent.
- Telegram/message chunking: preserve spaces, paragraph separators, and word boundaries when HTML overflow rechunking splits formatted replies. (#47274)
- Plugins/scoped ids: preserve scoped plugin ids during install and config keying, and keep bundled plugins ahead of discovered duplicate ids by default so `@scope/name` plugins no longer collide with unscoped installs. Thanks @vincentkoc.
- CLI: avoid loading provider discovery during startup model normalization. (#46522) Thanks @ItsAditya-xyz and @vincentkoc.
- Tlon: honor explicit empty allowlists and defer cite expansion. (#46788) Thanks @zpbrent and @vincentkoc.
- ACP: require admin scope for mutating internal actions. (#46789) Thanks @tdjackey and @vincentkoc.
- Gateway/config validation: stop treating the implicit default memory slot as a required explicit plugin config, so startup no longer fails with `plugins.slots.memory: plugin not found: memory-core` when `memory-core` was only inferred. (#47494) Thanks @ngutman.
- Control UI/session routing: preserve established external delivery routes when webchat views or sends in externally originated sessions, so subagent completions still return to the original channel instead of the dashboard. (#47797) Thanks @brokemac79.
- Configure/startup: move outbound send-deps resolution into a lightweight helper so `openclaw configure` no longer stalls after the banner while eagerly loading channel plugins. (#46301) thanks @scoootscooob.
- CLI/startup: lazy-load channel add and root help startup paths to trim avoidable RSS and help latency on constrained hosts. (#46784) Thanks @vincentkoc.
- CLI/onboarding: import static provider definitions directly for onboarding model/config helpers so those paths no longer pull provider discovery just for built-in defaults. (#47467) Thanks @vincentkoc.
- CLI/auth choice: lazy-load plugin/provider fallback resolution so mapped auth choices stay on the static path and only unknown choices pay the heavy provider load. (#47495) Thanks @vincentkoc.
- CLI/completion: reduce recursive completion-script string churn and fix nested PowerShell command-path matching so generated nested completions resolve on PowerShell too. (#45537) Thanks @yiShanXin and @vincentkoc.
- Gateway/startup: load bundled channel plugins from compiled `dist/extensions` entries in built installs, so gateway boot no longer recompiles bundled extension TypeScript on every startup and WhatsApp-class cold starts drop back to seconds instead of tens of seconds or worse.
- CLI: avoid loading provider discovery during startup model normalization. (#46522) Thanks @ItsAditya-xyz and @vincentkoc.
- Security/device pairing: harden `device.token.rotate` deny handling by keeping public failures generic while logging internal deny reasons and preserving approved-baseline enforcement. (`GHSA-7jrw-x62h-64p8`)
- Inbound policy hardening: tighten callback and webhook sender checks across Mattermost and Google Chat, match Nextcloud Talk rooms by stable room token, and treat explicit empty Twitch allowlists as deny-all. (#46787) Thanks @zpbrent, @ijxpwastaken and @vincentkoc.
- Webhooks/runtime: move auth earlier and tighten pre-auth body limits and timeouts across bundled webhook handlers, including slow-body handling for Mattermost slash commands. Thanks @vincentkoc.
- Email/webhook wrapping: sanitize sender and subject metadata before external-content wrapping so metadata fields cannot break the wrapper structure. Thanks @vincentkoc.
- Tools/apply-patch: revalidate workspace-only delete and directory targets immediately before mutating host paths. Thanks @vincentkoc.
- Gateway/config views: strip embedded credentials from URL-based endpoint fields before returning read-only account and config snapshots. Thanks @vincentkoc.
- ACP/approvals: use canonical tool identity for prompting decisions and fail closed when conflicting tool identity hints are present. (#46817) Thanks @zpbrent and @vincentkoc.
- ACP: require admin scope for mutating internal actions. (#46789) Thanks @tdjackey and @vincentkoc.
- Subagents/follow-ups: require the same controller ownership checks for `/subagents send` as other control actions, so leaf sessions cannot message nested child runs they do not control. Thanks @vincentkoc.
- macOS/canvas actions: keep unattended local agent actions on trusted in-app canvas surfaces only, and stop exposing the deep-link fallback key to arbitrary page scripts. (#46790) Thanks @vincentkoc.
- Agents/compaction: extend the enclosing run deadline once while compaction is actively in flight, and abort the underlying SDK compaction on timeout/cancel so large-session compactions stop freezing mid-run. (#46889) Thanks @asyncjason.
- Agents/openai-compatible tool calls: deduplicate repeated tool call ids across live assistant messages and replayed history so OpenAI-compatible backends no longer reject duplicate `tool_call_id` values with HTTP 400. (#40996) Thanks @xaeon2026.
- Models/openai-completions: default non-native OpenAI-compatible providers to omit tool-definition `strict` fields unless users explicitly opt back in, so tool calling keeps working on providers that reject that option. (#45497) Thanks @sahancava.
- Models/OpenRouter runtime capabilities: fetch uncatalogued OpenRouter model metadata on first use so newly added vision models keep image input instead of silently degrading to text-only, with top-level capability field fallbacks for `/api/v1/models`. (#45824) Thanks @DJjjjhao.
- Channels/plugins: keep shared interactive payloads merge-ready by fixing Slack custom callback routing and repeat-click dedupe, allowing interactive-only sends, and preserving ordered Discord shared text blocks. (#47715) Thanks @vincentkoc.
- Slack/interactive replies: preserve `channelData.slack.blocks` through live DM delivery and preview-finalized edits so Block Kit button and select directives render instead of falling back to raw text. (#45890) Thanks @vincentkoc.
- Slack/interactive replies: preserve `channelData.slack.blocks` through live DM delivery and preview-finalized edits so Block Kit button and select directives render instead of falling back to raw text. (#45890) Thanks @vincentkoc.
- Feishu/actions: expand the runtime action surface with message read/edit, explicit thread replies, pinning, and operator-facing chat/member inspection so Feishu can operate more of the workspace directly.
- Feishu/topic threads: fetch full thread context, including prior bot replies, when starting a topic-thread session so follow-up turns in Feishu topics keep the right conversation state. (#45254) Thanks @Coobiw.
- Feishu/media: keep native image, file, audio, and video/media handling aligned across outbound sends, inbound downloads, thread replies, directory/action aliases, and capability docs so unsupported areas are explicit instead of implied.
- WhatsApp/reconnect: restore the append recency filter in the extension inbox monitor and handle protobuf `Long` timestamps correctly, so fresh post-reconnect append messages are processed while stale history sync stays suppressed. (#42588) thanks @MonkeyLeeT.
- WhatsApp/login: wait for pending creds writes before reopening after Baileys `515` pairing restarts in both QR login and `channels login` flows, and keep the restart coverage pinned to the real wrapped error shape plus per-account creds queues. (#27910) Thanks @asyncjason.
- Telegram/message send: forward `--force-document` through the `sendPayload` path as well as `sendMedia`, so Telegram payload sends with `channelData` keep uploading images as documents instead of silently falling back to compressed photo sends. (#47119) Thanks @thepagent.
- Telegram/message chunking: preserve spaces, paragraph separators, and word boundaries when HTML overflow rechunking splits formatted replies. (#47274)
- Z.AI/onboarding: detect a working default model even for explicit `zai-coding-*` endpoint choices, so Coding Plan setup can keep the selected endpoint while defaulting to `glm-5` when available or `glm-4.7` as fallback. (#45969)
- Z.AI/onboarding: add `glm-5-turbo` to the default Z.AI provider catalog so onboarding-generated configs expose the new model alongside the existing GLM defaults. (#46670) Thanks @tomsun28.
- Zalo Personal/group gating: stop reapplying `dmPolicy.allowFrom` as a sender gate for already-allowlisted groups when `groupAllowFrom` is unset, so any member of an allowed group can trigger replies while DMs stay restricted. (#40146)
- Zalo/plugin runtime: export `resolveClientIp` from `openclaw/plugin-sdk/zalo` so installed builds no longer crash on startup when the webhook monitor loads from the packaged extension instead of the monorepo source tree. (#46549) Thanks @No898.
- Plugins/install precedence: keep bundled plugins ahead of auto-discovered globals by default, but let an explicitly installed plugin record win its own duplicate-id tie so installed channel plugins load from `~/.openclaw/extensions` after `openclaw plugins install`.
- Plugins/scoped ids: preserve scoped plugin ids during install and config keying, and keep bundled plugins ahead of discovered duplicate ids by default so `@scope/name` plugins no longer collide with unscoped installs. Thanks @vincentkoc.
- Gateway/watch mode: restart on bundled-plugin package and manifest metadata changes, rebuild `dist` for extension source and `tsdown.config.ts` changes, and still ignore extension docs. (#47571) thanks @gumadeiras.
- Gateway/watch mode: recreate bundled plugin runtime metadata after clean or stale `dist` states, so `pnpm gateway:watch` no longer fails on missing `dist/extensions/*/openclaw.plugin.json` manifests after a rebuild. Thanks @gumadeiras.
- Plugins/context engines: enforce owner-aware context-engine registration on both loader and public SDK paths so plugins cannot spoof privileged ownership, claim the core `legacy` engine id, or overwrite an existing engine id through direct SDK imports. (#47595) Thanks @vincentkoc.
- Windows/gateway status: accept `schtasks` `Last Result` output as an alias for `Last Run Result`, so running scheduled-task installs no longer show `Runtime: unknown`. (#47844) Thanks @MoerAI.
- Control UI/session routing: preserve established external delivery routes when webchat views or sends in externally originated sessions, so subagent completions still return to the original channel instead of the dashboard. (#47797) Thanks @brokemac79.
- Control UI/chat sessions: show human-readable labels in the grouped session dropdown again, keep unique scoped fallbacks when metadata is missing, and disambiguate duplicate labels only when needed. (#45130) thanks @luzhidong.
- Control UI: scope persisted session selection per gateway, prevent stale session bleed across tokenized gateway opens, and cap stored gateway session history. (#47453) Thanks @sallyom.
- Control UI/dashboard: preserve structured gateway shutdown reasons across restart disconnects so config-triggered restarts no longer fall back to `disconnected (1006): no reason`. (#46532) Thanks @vincentkoc.
- Android/chat: theme the thinking dropdown and TLS trust dialogs explicitly so popup surfaces match the active app theme instead of falling back to mismatched Material defaults.
- Group mention gating: reject invalid and unsafe nested-repetition `mentionPatterns`, reuse the shared safe config-regex compiler across mention stripping and detection, and cache strip-time regex compilation so noisy groups avoid repeated recompiles.
- Browser/profiles: drop the auto-created `chrome-relay` browser profile; users who need the Chrome extension relay must now create their own profile via `openclaw browser create-profile`. (#45777) Thanks @odysseus0.
- CI/channel test routing: move the built-in channel suites into `test:channels` and keep them out of `test:extensions`, so extension CI no longer fails after the channel migration while targeted test routing still sends Slack, Signal, and iMessage suites to the right lane. (#46066) Thanks @scoootscooob.
- Docs/Mintlify: fix MDX marker syntax on Perplexity, Model Providers, Moonshot, and exec approvals pages so local docs preview no longer breaks rendering or leaves stale pages unpublished. (#46695) Thanks @velvet-shark.
- Gateway/config validation: stop treating the implicit default memory slot as a required explicit plugin config, so startup no longer fails with `plugins.slots.memory: plugin not found: memory-core` when `memory-core` was only inferred. (#47494) Thanks @ngutman.
- Tlon: honor explicit empty allowlists and defer cite expansion. (#46788) Thanks @zpbrent and @vincentkoc.
- Nodes/pending actions: re-check queued foreground actions against the current node command policy before returning them to the node. (#46815) Thanks @zpbrent and @vincentkoc.
- Node/startup: remove leftover debug `console.log("node host PATH: ...")` that printed the resolved PATH on every `openclaw node run` invocation. (#46411)
- CLI/completion: reduce recursive completion-script string churn and fix nested PowerShell command-path matching so generated nested completions resolve on PowerShell too. (#45537) Thanks @yiShanXin and @vincentkoc.
## 2026.3.13

View File

@ -8,6 +8,24 @@ final class CanvasA2UIActionMessageHandler: NSObject, WKScriptMessageHandler {
static let messageName = "openclawCanvasA2UIAction"
static let allMessageNames = [messageName]
// Compatibility helper for debug/test shims. Runtime dispatch remains
// limited to in-app canvas schemes in `didReceive`.
static func isLocalNetworkCanvasURL(_ url: URL) -> Bool {
guard let scheme = url.scheme?.lowercased(), scheme == "http" || scheme == "https" else {
return false
}
guard let host = url.host?.lowercased(), !host.isEmpty else {
return false
}
if host == "localhost" {
return true
}
guard let ip = Self.parseIPv4(host) else {
return false
}
return Self.isLocalNetworkIPv4(ip)
}
private let sessionKey: String
init(sessionKey: String) {
@ -104,5 +122,24 @@ final class CanvasA2UIActionMessageHandler: NSObject, WKScriptMessageHandler {
}
}
}
private static func parseIPv4(_ host: String) -> (UInt8, UInt8, UInt8, UInt8)? {
let parts = host.split(separator: ".", omittingEmptySubsequences: false)
guard parts.count == 4 else { return nil }
let bytes = parts.compactMap { UInt8($0) }
guard bytes.count == 4 else { return nil }
return (bytes[0], bytes[1], bytes[2], bytes[3])
}
private static func isLocalNetworkIPv4(_ ip: (UInt8, UInt8, UInt8, UInt8)) -> Bool {
let (a, b, _, _) = ip
if a == 10 { return true }
if a == 172, (16...31).contains(Int(b)) { return true }
if a == 192, b == 168 { return true }
if a == 127 { return true }
if a == 169, b == 254 { return true }
if a == 100, (64...127).contains(Int(b)) { return true }
return false
}
// Formatting helpers live in OpenClawKit (`OpenClawCanvasA2UIAction`).
}

View File

@ -254,6 +254,71 @@ struct CronJob: Identifiable, Codable, Equatable {
case state
}
init(
id: String,
agentId: String?,
name: String,
description: String?,
enabled: Bool,
deleteAfterRun: Bool?,
createdAtMs: Int,
updatedAtMs: Int,
schedule: CronSchedule,
sessionTarget: CronSessionTarget,
wakeMode: CronWakeMode,
payload: CronPayload,
delivery: CronDelivery?,
state: CronJobState)
{
self.init(
id: id,
agentId: agentId,
name: name,
description: description,
enabled: enabled,
deleteAfterRun: deleteAfterRun,
createdAtMs: createdAtMs,
updatedAtMs: updatedAtMs,
schedule: schedule,
sessionTarget: .predefined(sessionTarget),
wakeMode: wakeMode,
payload: payload,
delivery: delivery,
state: state)
}
init(
id: String,
agentId: String?,
name: String,
description: String?,
enabled: Bool,
deleteAfterRun: Bool?,
createdAtMs: Int,
updatedAtMs: Int,
schedule: CronSchedule,
sessionTarget: CronCustomSessionTarget,
wakeMode: CronWakeMode,
payload: CronPayload,
delivery: CronDelivery?,
state: CronJobState)
{
self.id = id
self.agentId = agentId
self.name = name
self.description = description
self.enabled = enabled
self.deleteAfterRun = deleteAfterRun
self.createdAtMs = createdAtMs
self.updatedAtMs = updatedAtMs
self.schedule = schedule
self.sessionTargetRaw = sessionTarget.rawValue
self.wakeMode = wakeMode
self.payload = payload
self.delivery = delivery
self.state = state
}
/// Parsed session target (predefined or custom session ID)
var parsedSessionTarget: CronCustomSessionTarget {
CronCustomSessionTarget.from(self.sessionTargetRaw)

View File

@ -44995,6 +44995,75 @@
"help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.",
"hasChildren": false
},
{
"path": "plugins.entries.amazon-bedrock",
"kind": "plugin",
"type": "object",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [
"advanced"
],
"label": "@openclaw/amazon-bedrock-provider",
"help": "OpenClaw Amazon Bedrock provider plugin (plugin: amazon-bedrock)",
"hasChildren": true
},
{
"path": "plugins.entries.amazon-bedrock.config",
"kind": "plugin",
"type": "object",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [
"advanced"
],
"label": "@openclaw/amazon-bedrock-provider Config",
"help": "Plugin-defined config payload for amazon-bedrock.",
"hasChildren": false
},
{
"path": "plugins.entries.amazon-bedrock.enabled",
"kind": "plugin",
"type": "boolean",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [
"advanced"
],
"label": "Enable @openclaw/amazon-bedrock-provider",
"hasChildren": false
},
{
"path": "plugins.entries.amazon-bedrock.hooks",
"kind": "plugin",
"type": "object",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [
"advanced"
],
"label": "Plugin Hook Policy",
"help": "Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.",
"hasChildren": true
},
{
"path": "plugins.entries.amazon-bedrock.hooks.allowPromptInjection",
"kind": "plugin",
"type": "boolean",
"required": false,
"deprecated": false,
"sensitive": false,
"tags": [
"access"
],
"label": "Allow Prompt Injection Hooks",
"help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.",
"hasChildren": false
},
{
"path": "plugins.entries.anthropic",
"kind": "plugin",

View File

@ -1,4 +1,4 @@
{"generatedBy":"scripts/generate-config-doc-baseline.ts","recordType":"meta","totalPaths":5094}
{"generatedBy":"scripts/generate-config-doc-baseline.ts","recordType":"meta","totalPaths":5098}
{"recordType":"path","path":"acp","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"ACP","help":"ACP runtime controls for enabling dispatch, selecting backends, constraining allowed agent targets, and tuning streamed turn projection behavior.","hasChildren":true}
{"recordType":"path","path":"acp.allowedAgents","kind":"core","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"ACP Allowed Agents","help":"Allowlist of ACP target agent ids permitted for ACP runtime sessions. Empty means no additional allowlist restriction.","hasChildren":true}
{"recordType":"path","path":"acp.allowedAgents.*","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
@ -3986,6 +3986,11 @@
{"recordType":"path","path":"plugins.entries.acpx.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable ACPX Runtime","hasChildren":false}
{"recordType":"path","path":"plugins.entries.acpx.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true}
{"recordType":"path","path":"plugins.entries.acpx.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false}
{"recordType":"path","path":"plugins.entries.amazon-bedrock","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/amazon-bedrock-provider","help":"OpenClaw Amazon Bedrock provider plugin (plugin: amazon-bedrock)","hasChildren":true}
{"recordType":"path","path":"plugins.entries.amazon-bedrock.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/amazon-bedrock-provider Config","help":"Plugin-defined config payload for amazon-bedrock.","hasChildren":false}
{"recordType":"path","path":"plugins.entries.amazon-bedrock.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/amazon-bedrock-provider","hasChildren":false}
{"recordType":"path","path":"plugins.entries.amazon-bedrock.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true}
{"recordType":"path","path":"plugins.entries.amazon-bedrock.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false}
{"recordType":"path","path":"plugins.entries.anthropic","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/anthropic-provider","help":"OpenClaw Anthropic provider plugin (plugin: anthropic)","hasChildren":true}
{"recordType":"path","path":"plugins.entries.anthropic.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/anthropic-provider Config","help":"Plugin-defined config payload for anthropic.","hasChildren":false}
{"recordType":"path","path":"plugins.entries.anthropic.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/anthropic-provider","hasChildren":false}

View File

@ -573,12 +573,12 @@ authoring plugins:
- `openclaw/plugin-sdk/core` for generic plugin APIs, provider auth types, and shared helpers such as routing/session utilities and logger-backed runtimes.
- `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.
- `openclaw/plugin-sdk/signal` for Signal channel plugins.
- `openclaw/plugin-sdk/imessage` for iMessage channel plugins.
- `openclaw/plugin-sdk/whatsapp` for WhatsApp channel plugins.
- `openclaw/plugin-sdk/telegram` for Telegram channel plugin types and shared channel-facing helpers. Built-in Telegram implementation internals stay private to the bundled extension.
- `openclaw/plugin-sdk/discord` for Discord channel plugin types and shared channel-facing helpers. Built-in Discord implementation internals stay private to the bundled extension.
- `openclaw/plugin-sdk/slack` for Slack channel plugin types and shared channel-facing helpers. Built-in Slack implementation internals stay private to the bundled extension.
- `openclaw/plugin-sdk/signal` for Signal channel plugin types and shared channel-facing helpers. Built-in Signal implementation internals stay private to the bundled extension.
- `openclaw/plugin-sdk/imessage` for iMessage channel plugin types and shared channel-facing helpers. Built-in iMessage implementation internals stay private to the bundled extension.
- `openclaw/plugin-sdk/whatsapp` for WhatsApp channel plugin types and shared channel-facing helpers. Built-in WhatsApp implementation internals stay private to the bundled extension.
- `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:

View File

@ -0,0 +1,22 @@
import { describe, expect, it } from "vitest";
import { registerSingleProviderPlugin } from "../../src/test-utils/plugin-registration.js";
import amazonBedrockPlugin from "./index.js";
describe("amazon-bedrock provider plugin", () => {
it("marks Claude 4.6 Bedrock models as adaptive by default", () => {
const provider = registerSingleProviderPlugin(amazonBedrockPlugin);
expect(
provider.resolveDefaultThinkingLevel?.({
provider: "amazon-bedrock",
modelId: "us.anthropic.claude-opus-4-6-v1",
} as never),
).toBe("adaptive");
expect(
provider.resolveDefaultThinkingLevel?.({
provider: "amazon-bedrock",
modelId: "amazon.nova-micro-v1:0",
} as never),
).toBeUndefined();
});
});

View File

@ -0,0 +1,23 @@
import { emptyPluginConfigSchema, type OpenClawPluginApi } from "openclaw/plugin-sdk/core";
const PROVIDER_ID = "amazon-bedrock";
const CLAUDE_46_MODEL_RE = /claude-(?:opus|sonnet)-4(?:\.|-)6(?:$|[-.])/i;
const amazonBedrockPlugin = {
id: PROVIDER_ID,
name: "Amazon Bedrock Provider",
description: "Bundled Amazon Bedrock provider policy plugin",
configSchema: emptyPluginConfigSchema(),
register(api: OpenClawPluginApi) {
api.registerProvider({
id: PROVIDER_ID,
label: "Amazon Bedrock",
docsPath: "/providers/models",
auth: [],
resolveDefaultThinkingLevel: ({ modelId }) =>
CLAUDE_46_MODEL_RE.test(modelId.trim()) ? "adaptive" : undefined,
});
},
};
export default amazonBedrockPlugin;

View File

@ -0,0 +1,9 @@
{
"id": "amazon-bedrock",
"providers": ["amazon-bedrock"],
"configSchema": {
"type": "object",
"additionalProperties": false,
"properties": {}
}
}

View File

@ -0,0 +1,12 @@
{
"name": "@openclaw/amazon-bedrock-provider",
"version": "2026.3.14",
"private": true,
"description": "OpenClaw Amazon Bedrock provider plugin",
"type": "module",
"openclaw": {
"extensions": [
"./index.ts"
]
}
}

View File

@ -143,7 +143,10 @@ const cloudflareAiGatewayPlugin = {
await ensureApiKeyFromOptionEnvOrPrompt({
token: normalizeOptionalSecretInput(ctx.opts?.cloudflareAiGatewayApiKey),
tokenProvider: "cloudflare-ai-gateway",
secretInputMode: ctx.secretInputMode,
secretInputMode:
ctx.allowSecretRefPrompt === false
? (ctx.secretInputMode ?? "plaintext")
: ctx.secretInputMode,
config: ctx.config,
expectedProviders: [PROVIDER_ID],
provider: PROVIDER_ID,

View File

@ -1,5 +1,5 @@
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/discord";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/discord";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
import { discordPlugin } from "./src/channel.js";
import { setDiscordRuntime } from "./src/runtime.js";
import { registerDiscordSubagentHooks } from "./src/subagent-hooks.js";

View File

@ -7,13 +7,15 @@ import {
buildChannelConfigSchema,
DiscordConfigSchema,
getChatChannelMeta,
inspectDiscordAccount,
type ChannelPlugin,
} from "openclaw/plugin-sdk/discord";
import { inspectDiscordAccount } from "./account-inspect.js";
import {
listDiscordAccountIds,
resolveDefaultDiscordAccountId,
resolveDiscordAccount,
type ChannelPlugin,
type ResolvedDiscordAccount,
} from "openclaw/plugin-sdk/discord";
} from "./accounts.js";
import { createDiscordSetupWizardProxy, discordSetupAdapter } from "./setup-core.js";
async function loadDiscordChannelRuntime() {

View File

@ -8,45 +8,54 @@ import {
createScopedAccountConfigAccessors,
formatAllowFromLowercase,
} from "openclaw/plugin-sdk/compat";
import {
buildAgentSessionKey,
resolveThreadSessionKeys,
type RoutePeer,
} from "openclaw/plugin-sdk/core";
import {
buildComputedAccountStatusSnapshot,
buildChannelConfigSchema,
buildTokenChannelStatusSummary,
collectDiscordAuditChannelIds,
collectDiscordStatusIssues,
DEFAULT_ACCOUNT_ID,
DiscordConfigSchema,
getChatChannelMeta,
inspectDiscordAccount,
listDiscordAccountIds,
listDiscordDirectoryGroupsFromConfig,
listDiscordDirectoryPeersFromConfig,
looksLikeDiscordTargetId,
normalizeDiscordMessagingTarget,
normalizeDiscordOutboundTarget,
PAIRING_APPROVED_MESSAGE,
projectCredentialSnapshotFields,
resolveConfiguredFromCredentialStatuses,
resolveDiscordAccount,
resolveDefaultDiscordAccountId,
resolveDiscordGroupRequireMention,
resolveDiscordGroupToolPolicy,
type ChannelMessageActionAdapter,
type ChannelPlugin,
type OpenClawConfig,
type ResolvedDiscordAccount,
} from "openclaw/plugin-sdk/discord";
import { resolveOutboundSendDep } from "../../../src/infra/outbound/send-deps.js";
import { normalizeMessageChannel } from "../../../src/utils/message-channel.js";
import { inspectDiscordAccount } from "./account-inspect.js";
import {
listDiscordAccountIds,
resolveDiscordAccount,
resolveDefaultDiscordAccountId,
type ResolvedDiscordAccount,
} from "./accounts.js";
import { collectDiscordAuditChannelIds } from "./audit.js";
import {
isDiscordExecApprovalClientEnabled,
shouldSuppressLocalDiscordExecApprovalPrompt,
} from "./exec-approvals.js";
import {
looksLikeDiscordTargetId,
normalizeDiscordMessagingTarget,
normalizeDiscordOutboundTarget,
} from "./normalize.js";
import type { DiscordProbe } from "./probe.js";
import { resolveDiscordUserAllowlist } from "./resolve-users.js";
import { getDiscordRuntime } from "./runtime.js";
import { fetchChannelPermissionsDiscord } from "./send.js";
import { createDiscordSetupWizardProxy, discordSetupAdapter } from "./setup-core.js";
import { collectDiscordStatusIssues } from "./status-issues.js";
import { parseDiscordTarget } from "./targets.js";
import { DiscordUiContainer } from "./ui.js";

View File

@ -1,5 +1,5 @@
import type { PluginRuntime } from "openclaw/plugin-sdk";
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/compat";
import type { PluginRuntime } from "openclaw/plugin-sdk/discord";
const { setRuntime: setDiscordRuntime, getRuntime: getDiscordRuntime } =
createPluginRuntimeStore<PluginRuntime>("Discord runtime not initialized");

View File

@ -1,10 +1,10 @@
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/discord";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import { resolveDiscordAccount } from "./accounts.js";
import {
autoBindSpawnedDiscordSubagent,
listThreadBindingsBySessionKey,
resolveDiscordAccount,
unbindThreadBindingsBySessionKey,
} from "openclaw/plugin-sdk/discord";
} from "./monitor/thread-bindings.js";
function summarizeError(err: unknown): string {
if (err instanceof Error) {

View File

@ -1,5 +1,5 @@
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/imessage";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/imessage";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
import { imessagePlugin } from "./src/channel.js";
import { setIMessageRuntime } from "./src/runtime.js";

View File

@ -9,15 +9,17 @@ import {
formatTrimmedAllowFromEntries,
getChatChannelMeta,
IMessageConfigSchema,
listIMessageAccountIds,
resolveDefaultIMessageAccountId,
resolveIMessageAccount,
resolveIMessageConfigAllowFrom,
resolveIMessageConfigDefaultTo,
setAccountEnabledInConfigSection,
type ChannelPlugin,
type ResolvedIMessageAccount,
} from "openclaw/plugin-sdk/imessage";
import {
listIMessageAccountIds,
resolveDefaultIMessageAccountId,
resolveIMessageAccount,
type ResolvedIMessageAccount,
} from "./accounts.js";
import { createIMessageSetupWizardProxy, imessageSetupAdapter } from "./setup-core.js";
async function loadIMessageChannelRuntime() {

View File

@ -3,6 +3,7 @@ import {
buildAccountScopedDmSecurityPolicy,
collectAllowlistProviderRestrictSendersWarnings,
} from "openclaw/plugin-sdk/compat";
import { buildAgentSessionKey, type RoutePeer } from "openclaw/plugin-sdk/core";
import {
buildChannelConfigSchema,
collectStatusIssuesFromLastError,
@ -11,23 +12,25 @@ import {
formatTrimmedAllowFromEntries,
getChatChannelMeta,
IMessageConfigSchema,
listIMessageAccountIds,
looksLikeIMessageTargetId,
normalizeIMessageMessagingTarget,
PAIRING_APPROVED_MESSAGE,
resolveChannelMediaMaxBytes,
resolveDefaultIMessageAccountId,
resolveIMessageAccount,
resolveIMessageConfigAllowFrom,
resolveIMessageConfigDefaultTo,
resolveIMessageGroupRequireMention,
resolveIMessageGroupToolPolicy,
setAccountEnabledInConfigSection,
type ChannelPlugin,
type ResolvedIMessageAccount,
} from "openclaw/plugin-sdk/imessage";
import { resolveOutboundSendDep } from "../../../src/infra/outbound/send-deps.js";
import { buildPassiveProbedChannelStatusSummary } from "../../shared/channel-status-summary.js";
import {
listIMessageAccountIds,
resolveDefaultIMessageAccountId,
resolveIMessageAccount,
type ResolvedIMessageAccount,
} from "./accounts.js";
import { getIMessageRuntime } from "./runtime.js";
import { createIMessageSetupWizardProxy, imessageSetupAdapter } from "./setup-core.js";
import { normalizeIMessageHandle, parseIMessageTarget } from "./targets.js";

View File

@ -1,5 +1,5 @@
import type { PluginRuntime } from "openclaw/plugin-sdk";
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/compat";
import type { PluginRuntime } from "openclaw/plugin-sdk/imessage";
const { setRuntime: setIMessageRuntime, getRuntime: getIMessageRuntime } =
createPluginRuntimeStore<PluginRuntime>("iMessage runtime not initialized");

View File

@ -31,7 +31,11 @@ function getDefaultBaseUrl(region: MiniMaxRegion): string {
return region === "cn" ? DEFAULT_BASE_URL_CN : DEFAULT_BASE_URL_GLOBAL;
}
function modelRef(modelId: string): string {
function apiModelRef(modelId: string): string {
return `${API_PROVIDER_ID}/${modelId}`;
}
function portalModelRef(modelId: string): string {
return `${PORTAL_PROVIDER_ID}/${modelId}`;
}
@ -109,7 +113,7 @@ function createOAuthHandler(region: MiniMaxRegion) {
return buildOauthProviderAuthResult({
providerId: PORTAL_PROVIDER_ID,
defaultModel: modelRef(DEFAULT_MODEL),
defaultModel: portalModelRef(DEFAULT_MODEL),
access: result.access,
refresh: result.refresh,
expires: result.expires,
@ -125,11 +129,11 @@ function createOAuthHandler(region: MiniMaxRegion) {
agents: {
defaults: {
models: {
[modelRef("MiniMax-M2.5")]: { alias: "minimax-m2.5" },
[modelRef("MiniMax-M2.5-highspeed")]: {
[portalModelRef("MiniMax-M2.5")]: { alias: "minimax-m2.5" },
[portalModelRef("MiniMax-M2.5-highspeed")]: {
alias: "minimax-m2.5-highspeed",
},
[modelRef("MiniMax-M2.5-Lightning")]: {
[portalModelRef("MiniMax-M2.5-Lightning")]: {
alias: "minimax-m2.5-lightning",
},
},
@ -177,7 +181,8 @@ const minimaxPlugin = {
promptMessage:
"Enter MiniMax API key (sk-api- or sk-cp-)\nhttps://platform.minimax.io/user-center/basic-information/interface-key",
profileId: "minimax:global",
defaultModel: modelRef(DEFAULT_MODEL),
allowProfile: false,
defaultModel: apiModelRef(DEFAULT_MODEL),
expectedProviders: ["minimax"],
applyConfig: (cfg) => applyMinimaxApiConfig(cfg),
wizard: {
@ -200,7 +205,8 @@ const minimaxPlugin = {
promptMessage:
"Enter MiniMax CN API key (sk-api- or sk-cp-)\nhttps://platform.minimaxi.com/user-center/basic-information/interface-key",
profileId: "minimax:cn",
defaultModel: modelRef(DEFAULT_MODEL),
allowProfile: false,
defaultModel: apiModelRef(DEFAULT_MODEL),
expectedProviders: ["minimax", "minimax-cn"],
applyConfig: (cfg) => applyMinimaxApiConfigCn(cfg),
wizard: {

View File

@ -26,6 +26,7 @@ const opencodeGoPlugin = {
flagName: "--opencode-go-api-key",
envVar: "OPENCODE_API_KEY",
promptMessage: "Enter OpenCode API key",
profileIds: ["opencode:default", "opencode-go:default"],
defaultModel: OPENCODE_GO_DEFAULT_MODEL_REF,
expectedProviders: ["opencode", "opencode-go"],
applyConfig: (cfg) => applyOpencodeGoConfig(cfg),

View File

@ -35,6 +35,7 @@ const opencodePlugin = {
flagName: "--opencode-zen-api-key",
envVar: "OPENCODE_API_KEY",
promptMessage: "Enter OpenCode API key",
profileIds: ["opencode:default", "opencode-go:default"],
defaultModel: OPENCODE_ZEN_DEFAULT_MODEL,
expectedProviders: ["opencode", "opencode-go"],
applyConfig: (cfg) => applyOpencodeZenConfig(cfg),

View File

@ -1,5 +1,5 @@
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/signal";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/signal";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
import { signalPlugin } from "./src/channel.js";
import { setSignalRuntime } from "./src/runtime.js";

View File

@ -8,15 +8,17 @@ import {
DEFAULT_ACCOUNT_ID,
deleteAccountFromConfigSection,
getChatChannelMeta,
listSignalAccountIds,
normalizeE164,
resolveDefaultSignalAccountId,
resolveSignalAccount,
setAccountEnabledInConfigSection,
SignalConfigSchema,
type ChannelPlugin,
type ResolvedSignalAccount,
} from "openclaw/plugin-sdk/signal";
import {
listSignalAccountIds,
resolveDefaultSignalAccountId,
resolveSignalAccount,
type ResolvedSignalAccount,
} from "./accounts.js";
import { createSignalSetupWizardProxy, signalSetupAdapter } from "./setup-core.js";
async function loadSignalChannelRuntime() {

View File

@ -14,23 +14,25 @@ import {
DEFAULT_ACCOUNT_ID,
deleteAccountFromConfigSection,
getChatChannelMeta,
listSignalAccountIds,
looksLikeSignalTargetId,
normalizeE164,
normalizeSignalMessagingTarget,
PAIRING_APPROVED_MESSAGE,
resolveChannelMediaMaxBytes,
resolveDefaultSignalAccountId,
resolveSignalAccount,
setAccountEnabledInConfigSection,
SignalConfigSchema,
type ChannelMessageActionAdapter,
type ChannelPlugin,
type ResolvedSignalAccount,
} from "openclaw/plugin-sdk/signal";
import { resolveTextChunkLimit } from "../../../src/auto-reply/chunk.js";
import { resolveMarkdownTableMode } from "../../../src/config/markdown-tables.js";
import { resolveOutboundSendDep } from "../../../src/infra/outbound/send-deps.js";
import {
listSignalAccountIds,
resolveDefaultSignalAccountId,
resolveSignalAccount,
type ResolvedSignalAccount,
} from "./accounts.js";
import { markdownToSignalTextChunks } from "./format.js";
import {
looksLikeUuid,

View File

@ -1,5 +1,5 @@
import type { PluginRuntime } from "openclaw/plugin-sdk";
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/compat";
import type { PluginRuntime } from "openclaw/plugin-sdk/signal";
const { setRuntime: setSignalRuntime, getRuntime: getSignalRuntime } =
createPluginRuntimeStore<PluginRuntime>("Signal runtime not initialized");

View File

@ -1,5 +1,5 @@
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/slack";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/slack";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
import { slackPlugin } from "./src/channel.js";
import { setSlackRuntime } from "./src/runtime.js";

View File

@ -6,15 +6,17 @@ import {
import {
buildChannelConfigSchema,
getChatChannelMeta,
inspectSlackAccount,
isSlackInteractiveRepliesEnabled,
SlackConfigSchema,
type ChannelPlugin,
} from "openclaw/plugin-sdk/slack";
import { inspectSlackAccount } from "./account-inspect.js";
import {
listSlackAccountIds,
resolveDefaultSlackAccountId,
resolveSlackAccount,
SlackConfigSchema,
type ChannelPlugin,
type ResolvedSlackAccount,
} from "openclaw/plugin-sdk/slack";
} from "./accounts.js";
import { isSlackInteractiveRepliesEnabled } from "./interactive-replies.js";
import { createSlackSetupWizardProxy, slackSetupAdapter } from "./setup-core.js";
async function loadSlackChannelRuntime() {

View File

@ -16,12 +16,7 @@ import {
buildComputedAccountStatusSnapshot,
buildChannelConfigSchema,
DEFAULT_ACCOUNT_ID,
extractSlackToolSend,
getChatChannelMeta,
handleSlackMessageAction,
inspectSlackAccount,
listSlackMessageActions,
listSlackAccountIds,
listSlackDirectoryGroupsFromConfig,
listSlackDirectoryPeersFromConfig,
looksLikeSlackTargetId,
@ -29,22 +24,28 @@ import {
PAIRING_APPROVED_MESSAGE,
projectCredentialSnapshotFields,
resolveConfiguredFromRequiredCredentialStatuses,
resolveDefaultSlackAccountId,
resolveSlackAccount,
resolveSlackReplyToMode,
isSlackInteractiveRepliesEnabled,
resolveSlackGroupRequireMention,
resolveSlackGroupToolPolicy,
buildSlackThreadingToolContext,
SlackConfigSchema,
type ChannelPlugin,
type OpenClawConfig,
type ResolvedSlackAccount,
} from "openclaw/plugin-sdk/slack";
import { resolveOutboundSendDep } from "../../../src/infra/outbound/send-deps.js";
import { buildPassiveProbedChannelStatusSummary } from "../../shared/channel-status-summary.js";
import { inspectSlackAccount } from "./account-inspect.js";
import {
listEnabledSlackAccounts,
listSlackAccountIds,
resolveDefaultSlackAccountId,
resolveSlackAccount,
resolveSlackReplyToMode,
type ResolvedSlackAccount,
} from "./accounts.js";
import { parseSlackBlocksInput } from "./blocks-input.js";
import { createSlackWebClient } from "./client.js";
import { isSlackInteractiveRepliesEnabled } from "./interactive-replies.js";
import { handleSlackMessageAction } from "./message-action-dispatch.js";
import { extractSlackToolSend, listSlackMessageActions } from "./message-actions.js";
import { normalizeAllowListLower } from "./monitor/allow-list.js";
import type { SlackProbe } from "./probe.js";
import { resolveSlackUserAllowlist } from "./resolve-users.js";
@ -52,6 +53,7 @@ import { getSlackRuntime } from "./runtime.js";
import { fetchSlackScopes } from "./scopes.js";
import { createSlackSetupWizardProxy, slackSetupAdapter } from "./setup-core.js";
import { parseSlackTarget } from "./targets.js";
import { buildSlackThreadingToolContext } from "./threading-tool-context.js";
const meta = getChatChannelMeta("slack");
const SLACK_CHANNEL_TYPE_CACHE = new Map<string, "channel" | "group" | "dm" | "unknown">();

View File

@ -0,0 +1,334 @@
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
import type { ChannelMessageActionContext } from "openclaw/plugin-sdk";
import { parseSlackBlocksInput } from "./blocks-input.js";
import { buildSlackInteractiveBlocks } from "./blocks-render.js";
type SlackActionInvoke = (
action: Record<string, unknown>,
cfg: ChannelMessageActionContext["cfg"],
toolContext?: ChannelMessageActionContext["toolContext"],
) => Promise<AgentToolResult<unknown>>;
type InteractiveButtonStyle = "primary" | "secondary" | "success" | "danger";
type InteractiveReplyButton = {
label: string;
value: string;
style?: InteractiveButtonStyle;
};
type InteractiveReplyOption = {
label: string;
value: string;
};
type InteractiveReplyBlock =
| { type: "text"; text: string }
| { type: "buttons"; buttons: InteractiveReplyButton[] }
| { type: "select"; placeholder?: string; options: InteractiveReplyOption[] };
type InteractiveReply = {
blocks: InteractiveReplyBlock[];
};
function readTrimmedString(value: unknown): string | undefined {
if (typeof value !== "string") {
return undefined;
}
const trimmed = value.trim();
return trimmed || undefined;
}
function normalizeButtonStyle(value: unknown): InteractiveButtonStyle | undefined {
const style = readTrimmedString(value)?.toLowerCase();
return style === "primary" || style === "secondary" || style === "success" || style === "danger"
? style
: undefined;
}
function normalizeInteractiveButton(raw: unknown): InteractiveReplyButton | undefined {
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
return undefined;
}
const record = raw as Record<string, unknown>;
const label = readTrimmedString(record.label) ?? readTrimmedString(record.text);
const value =
readTrimmedString(record.value) ??
readTrimmedString(record.callbackData) ??
readTrimmedString(record.callback_data);
if (!label || !value) {
return undefined;
}
return { label, value, style: normalizeButtonStyle(record.style) };
}
function normalizeInteractiveOption(raw: unknown): InteractiveReplyOption | undefined {
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
return undefined;
}
const record = raw as Record<string, unknown>;
const label = readTrimmedString(record.label) ?? readTrimmedString(record.text);
const value = readTrimmedString(record.value);
return label && value ? { label, value } : undefined;
}
function normalizeInteractiveReply(raw: unknown): InteractiveReply | undefined {
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
return undefined;
}
const record = raw as Record<string, unknown>;
const blocks = Array.isArray(record.blocks)
? record.blocks
.map((entry) => {
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
return undefined;
}
const block = entry as Record<string, unknown>;
const type = readTrimmedString(block.type)?.toLowerCase();
if (type === "text") {
const text = readTrimmedString(block.text);
return text ? ({ type: "text", text } as const) : undefined;
}
if (type === "buttons") {
const buttons = Array.isArray(block.buttons)
? block.buttons
.map((button) => normalizeInteractiveButton(button))
.filter((button): button is InteractiveReplyButton => Boolean(button))
: [];
return buttons.length > 0 ? ({ type: "buttons", buttons } as const) : undefined;
}
if (type === "select") {
const options = Array.isArray(block.options)
? block.options
.map((option) => normalizeInteractiveOption(option))
.filter((option): option is InteractiveReplyOption => Boolean(option))
: [];
return options.length > 0
? ({
type: "select",
placeholder: readTrimmedString(block.placeholder),
options,
} as const)
: undefined;
}
return undefined;
})
.filter((entry): entry is InteractiveReplyBlock => Boolean(entry))
: [];
return blocks.length > 0 ? { blocks } : undefined;
}
function readStringParam(
params: Record<string, unknown>,
key: string,
options: { required?: boolean; trim?: boolean; label?: string; allowEmpty?: boolean } = {},
): string | undefined {
const { required = false, trim = true, label = key, allowEmpty = false } = options;
const raw = params[key];
if (typeof raw !== "string") {
if (required) {
throw new Error(`${label} required`);
}
return undefined;
}
const value = trim ? raw.trim() : raw;
if (!value && !allowEmpty) {
if (required) {
throw new Error(`${label} required`);
}
return undefined;
}
return value;
}
function readNumberParam(
params: Record<string, unknown>,
key: string,
options: { required?: boolean; label?: string; integer?: boolean; strict?: boolean } = {},
): number | undefined {
const { required = false, label = key, integer = false, strict = false } = options;
const raw = params[key];
let value: number | undefined;
if (typeof raw === "number" && Number.isFinite(raw)) {
value = raw;
} else if (typeof raw === "string") {
const trimmed = raw.trim();
if (trimmed) {
const parsed = strict ? Number(trimmed) : Number.parseFloat(trimmed);
if (Number.isFinite(parsed)) {
value = parsed;
}
}
}
if (value === undefined) {
if (required) {
throw new Error(`${label} required`);
}
return undefined;
}
return integer ? Math.trunc(value) : value;
}
function readSlackBlocksParam(actionParams: Record<string, unknown>) {
return parseSlackBlocksInput(actionParams.blocks) as Record<string, unknown>[] | undefined;
}
export async function handleSlackMessageAction(params: {
providerId: string;
ctx: ChannelMessageActionContext;
invoke: SlackActionInvoke;
normalizeChannelId?: (channelId: string) => string;
includeReadThreadId?: boolean;
}): Promise<AgentToolResult<unknown>> {
const { providerId, ctx, invoke, normalizeChannelId, includeReadThreadId = false } = params;
const { action, cfg, params: actionParams } = ctx;
const accountId = ctx.accountId ?? undefined;
const resolveChannelId = () => {
const channelId =
readStringParam(actionParams, "channelId") ??
readStringParam(actionParams, "to", { required: true });
if (!channelId) {
throw new Error("channelId required");
}
return normalizeChannelId ? normalizeChannelId(channelId) : channelId;
};
if (action === "send") {
const to = readStringParam(actionParams, "to", { required: true });
const content = readStringParam(actionParams, "message", { allowEmpty: true });
const mediaUrl = readStringParam(actionParams, "media", { trim: false });
const interactive = normalizeInteractiveReply(actionParams.interactive);
const interactiveBlocks = interactive ? buildSlackInteractiveBlocks(interactive) : undefined;
const blocks = readSlackBlocksParam(actionParams) ?? interactiveBlocks;
if (!content && !mediaUrl && !blocks) {
throw new Error("Slack send requires message, blocks, or media.");
}
if (mediaUrl && blocks) {
throw new Error("Slack send does not support blocks with media.");
}
const threadId = readStringParam(actionParams, "threadId");
const replyTo = readStringParam(actionParams, "replyTo");
return await invoke(
{
action: "sendMessage",
to,
content: content ?? "",
mediaUrl: mediaUrl ?? undefined,
accountId,
threadTs: threadId ?? replyTo ?? undefined,
...(blocks ? { blocks } : {}),
},
cfg,
ctx.toolContext,
);
}
if (action === "react") {
const messageId = readStringParam(actionParams, "messageId", { required: true });
const emoji = readStringParam(actionParams, "emoji", { allowEmpty: true });
const remove = typeof actionParams.remove === "boolean" ? actionParams.remove : undefined;
return await invoke(
{ action: "react", channelId: resolveChannelId(), messageId, emoji, remove, accountId },
cfg,
);
}
if (action === "reactions") {
const messageId = readStringParam(actionParams, "messageId", { required: true });
const limit = readNumberParam(actionParams, "limit", { integer: true });
return await invoke(
{ action: "reactions", channelId: resolveChannelId(), messageId, limit, accountId },
cfg,
);
}
if (action === "read") {
const limit = readNumberParam(actionParams, "limit", { integer: true });
const readAction: Record<string, unknown> = {
action: "readMessages",
channelId: resolveChannelId(),
limit,
before: readStringParam(actionParams, "before"),
after: readStringParam(actionParams, "after"),
accountId,
};
if (includeReadThreadId) {
readAction.threadId = readStringParam(actionParams, "threadId");
}
return await invoke(readAction, cfg);
}
if (action === "edit") {
const messageId = readStringParam(actionParams, "messageId", { required: true });
const content = readStringParam(actionParams, "message", { allowEmpty: true });
const blocks = readSlackBlocksParam(actionParams);
if (!content && !blocks) {
throw new Error("Slack edit requires message or blocks.");
}
return await invoke(
{
action: "editMessage",
channelId: resolveChannelId(),
messageId,
content: content ?? "",
blocks,
accountId,
},
cfg,
);
}
if (action === "delete") {
const messageId = readStringParam(actionParams, "messageId", { required: true });
return await invoke(
{ action: "deleteMessage", channelId: resolveChannelId(), messageId, accountId },
cfg,
);
}
if (action === "pin" || action === "unpin" || action === "list-pins") {
const messageId =
action === "list-pins"
? undefined
: readStringParam(actionParams, "messageId", { required: true });
return await invoke(
{
action: action === "pin" ? "pinMessage" : action === "unpin" ? "unpinMessage" : "listPins",
channelId: resolveChannelId(),
messageId,
accountId,
},
cfg,
);
}
if (action === "member-info") {
const userId = readStringParam(actionParams, "userId", { required: true });
return await invoke({ action: "memberInfo", userId, accountId }, cfg);
}
if (action === "emoji-list") {
const limit = readNumberParam(actionParams, "limit", { integer: true });
return await invoke({ action: "emojiList", limit, accountId }, cfg);
}
if (action === "download-file") {
const fileId = readStringParam(actionParams, "fileId", { required: true });
const channelId =
readStringParam(actionParams, "channelId") ?? readStringParam(actionParams, "to");
const threadId =
readStringParam(actionParams, "threadId") ?? readStringParam(actionParams, "replyTo");
return await invoke(
{
action: "downloadFile",
fileId,
channelId: channelId ?? undefined,
threadId: threadId ?? undefined,
accountId,
},
cfg,
);
}
throw new Error(`Action ${action} is not supported for provider ${providerId}.`);
}

View File

@ -1,5 +1,5 @@
import type { PluginRuntime } from "openclaw/plugin-sdk";
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/compat";
import type { PluginRuntime } from "openclaw/plugin-sdk/slack";
const { setRuntime: setSlackRuntime, getRuntime: getSlackRuntime } =
createPluginRuntimeStore<PluginRuntime>("Slack runtime not initialized");

View File

@ -1,5 +1,5 @@
import type { ChannelPlugin, OpenClawPluginApi } from "openclaw/plugin-sdk/telegram";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/telegram";
import type { ChannelPlugin, OpenClawPluginApi } from "openclaw/plugin-sdk";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
import { telegramPlugin } from "./src/channel.js";
import { setTelegramRuntime } from "./src/runtime.js";

View File

@ -6,17 +6,19 @@ import {
import {
buildChannelConfigSchema,
getChatChannelMeta,
inspectTelegramAccount,
listTelegramAccountIds,
normalizeAccountId,
resolveDefaultTelegramAccountId,
resolveTelegramAccount,
TelegramConfigSchema,
type ChannelPlugin,
type OpenClawConfig,
type ResolvedTelegramAccount,
type TelegramProbe,
} from "openclaw/plugin-sdk/telegram";
import { inspectTelegramAccount } from "./account-inspect.js";
import {
listTelegramAccountIds,
resolveDefaultTelegramAccountId,
resolveTelegramAccount,
type ResolvedTelegramAccount,
} from "./accounts.js";
import type { TelegramProbe } from "./probe.js";
import { telegramSetupAdapter } from "./setup-core.js";
import { telegramSetupWizard } from "./setup-surface.js";

View File

@ -3,10 +3,10 @@ import type {
ChannelGatewayContext,
OpenClawConfig,
PluginRuntime,
ResolvedTelegramAccount,
} from "openclaw/plugin-sdk/telegram";
import { describe, expect, it, vi } from "vitest";
import { createRuntimeEnv } from "../../test-utils/runtime-env.js";
import type { ResolvedTelegramAccount } from "./accounts.js";
import { telegramPlugin } from "./channel.js";
import { setTelegramRuntime } from "./runtime.js";

View File

@ -16,32 +16,20 @@ import {
buildChannelConfigSchema,
buildTokenChannelStatusSummary,
clearAccountEntryFields,
collectTelegramStatusIssues,
DEFAULT_ACCOUNT_ID,
getChatChannelMeta,
inspectTelegramAccount,
listTelegramAccountIds,
listTelegramDirectoryGroupsFromConfig,
listTelegramDirectoryPeersFromConfig,
looksLikeTelegramTargetId,
normalizeAccountId,
normalizeTelegramMessagingTarget,
PAIRING_APPROVED_MESSAGE,
parseTelegramReplyToMessageId,
parseTelegramThreadId,
projectCredentialSnapshotFields,
resolveConfiguredFromCredentialStatuses,
resolveDefaultTelegramAccountId,
resolveTelegramAccount,
resolveTelegramGroupRequireMention,
resolveTelegramGroupToolPolicy,
sendTelegramPayloadMessages,
TelegramConfigSchema,
type ChannelMessageActionAdapter,
type ChannelPlugin,
type OpenClawConfig,
type ResolvedTelegramAccount,
type TelegramProbe,
} from "openclaw/plugin-sdk/telegram";
import { parseTelegramTopicConversation } from "../../../src/acp/conversation-id.js";
import { resolveExecApprovalCommandDisplay } from "../../../src/infra/exec-approval-command-display.js";
@ -51,16 +39,28 @@ import {
resolveOutboundSendDep,
} from "../../../src/infra/outbound/send-deps.js";
import { normalizeMessageChannel } from "../../../src/utils/message-channel.js";
import { inspectTelegramAccount } from "./account-inspect.js";
import {
listTelegramAccountIds,
resolveDefaultTelegramAccountId,
resolveTelegramAccount,
type ResolvedTelegramAccount,
} from "./accounts.js";
import { buildTelegramExecApprovalButtons } from "./approval-buttons.js";
import { buildTelegramGroupPeerId } from "./bot/helpers.js";
import {
isTelegramExecApprovalClientEnabled,
resolveTelegramExecApprovalTarget,
} from "./exec-approvals.js";
import { looksLikeTelegramTargetId, normalizeTelegramMessagingTarget } from "./normalize.js";
import { sendTelegramPayloadMessages } from "./outbound-adapter.js";
import { parseTelegramReplyToMessageId, parseTelegramThreadId } from "./outbound-params.js";
import type { TelegramProbe } from "./probe.js";
import { getTelegramRuntime } from "./runtime.js";
import { sendTypingTelegram } from "./send.js";
import { telegramSetupAdapter } from "./setup-core.js";
import { telegramSetupWizard } from "./setup-surface.js";
import { collectTelegramStatusIssues } from "./status-issues.js";
import { parseTelegramTarget } from "./targets.js";
type TelegramSendFn = ReturnType<

View File

@ -1,5 +1,5 @@
import type { PluginRuntime } from "openclaw/plugin-sdk";
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/compat";
import type { PluginRuntime } from "openclaw/plugin-sdk/telegram";
const { setRuntime: setTelegramRuntime, getRuntime: getTelegramRuntime } =
createPluginRuntimeStore<PluginRuntime>("Telegram runtime not initialized");

View File

@ -88,7 +88,7 @@ export async function promptTelegramAllowFromForAccount(params: {
);
}
const { promptResolvedAllowFrom } =
await import("../../../src/channels/plugins/setup-wizard-helpers.js");
await import("../../../src/channels/plugins/setup-wizard-helpers.runtime.js");
const unique = await promptResolvedAllowFrom({
prompter: params.prompter,
existing: resolved.config.allowFrom ?? [],

View File

@ -1,5 +1,5 @@
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/whatsapp";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/whatsapp";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
import { whatsappPlugin } from "./src/channel.js";
import { setWhatsAppRuntime } from "./src/runtime.js";

View File

@ -1,4 +1,5 @@
import { createPluginRuntimeStore, type PluginRuntime } from "openclaw/plugin-sdk/whatsapp";
import type { PluginRuntime } from "openclaw/plugin-sdk";
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/compat";
const { setRuntime: setWhatsAppRuntime, getRuntime: getWhatsAppRuntime } =
createPluginRuntimeStore<PluginRuntime>("WhatsApp runtime not initialized");

21
extensions/zai/detect.ts Normal file
View File

@ -0,0 +1,21 @@
import {
detectZaiEndpoint as detectZaiEndpointCore,
type ZaiDetectedEndpoint,
type ZaiEndpointId,
} from "../../src/commands/zai-endpoint-detect.js";
type DetectZaiEndpointFn = typeof detectZaiEndpointCore;
let detectZaiEndpointImpl: DetectZaiEndpointFn = detectZaiEndpointCore;
export function setDetectZaiEndpointForTesting(fn?: DetectZaiEndpointFn): void {
detectZaiEndpointImpl = fn ?? detectZaiEndpointCore;
}
export async function detectZaiEndpoint(
...args: Parameters<DetectZaiEndpointFn>
): ReturnType<DetectZaiEndpointFn> {
return await detectZaiEndpointImpl(...args);
}
export type { ZaiDetectedEndpoint, ZaiEndpointId };

View File

@ -26,11 +26,11 @@ import {
applyZaiProviderConfig,
ZAI_DEFAULT_MODEL_REF,
} from "../../src/commands/onboard-auth.js";
import { detectZaiEndpoint, type ZaiEndpointId } from "../../src/commands/zai-endpoint-detect.js";
import type { SecretInput } from "../../src/config/types.secrets.js";
import { resolveRequiredHomeDir } from "../../src/infra/home-dir.js";
import { fetchZaiUsage } from "../../src/infra/provider-usage.fetch.js";
import { normalizeOptionalSecretInput } from "../../src/utils/normalize-secret-input.js";
import { detectZaiEndpoint, type ZaiEndpointId } from "./detect.js";
const PROVIDER_ID = "zai";
const GLM5_MODEL_ID = "glm-5";
@ -97,6 +97,27 @@ function resolveZaiDefaultModel(modelIdOverride?: string): string {
return modelIdOverride ? `zai/${modelIdOverride}` : ZAI_DEFAULT_MODEL_REF;
}
async function promptForZaiEndpoint(ctx: ProviderAuthContext): Promise<ZaiEndpointId> {
return await ctx.prompter.select<ZaiEndpointId>({
message: "Select Z.AI endpoint",
initialValue: "global",
options: [
{ value: "global", label: "Global", hint: "Z.AI Global (api.z.ai)" },
{ value: "cn", label: "CN", hint: "Z.AI CN (open.bigmodel.cn)" },
{
value: "coding-global",
label: "Coding-Plan-Global",
hint: "GLM Coding Plan Global (api.z.ai)",
},
{
value: "coding-cn",
label: "Coding-Plan-CN",
hint: "GLM Coding Plan CN (open.bigmodel.cn)",
},
],
});
}
async function runZaiApiKeyAuth(
ctx: ProviderAuthContext,
endpoint?: ZaiEndpointId,
@ -116,7 +137,10 @@ async function runZaiApiKeyAuth(
tokenProvider: normalizeOptionalSecretInput(ctx.opts?.zaiApiKey)
? PROVIDER_ID
: normalizeOptionalSecretInput(ctx.opts?.tokenProvider),
secretInputMode: ctx.secretInputMode,
secretInputMode:
ctx.allowSecretRefPrompt === false
? (ctx.secretInputMode ?? "plaintext")
: ctx.secretInputMode,
config: ctx.config,
expectedProviders: [PROVIDER_ID, "z-ai"],
provider: PROVIDER_ID,
@ -138,7 +162,7 @@ async function runZaiApiKeyAuth(
const detected = await detectZaiEndpoint({ apiKey, ...(endpoint ? { endpoint } : {}) });
const modelIdOverride = detected?.modelId;
const nextEndpoint = detected?.endpoint ?? endpoint;
const nextEndpoint = detected?.endpoint ?? endpoint ?? (await promptForZaiEndpoint(ctx));
return {
profiles: [
{

View File

@ -382,6 +382,7 @@
"dotenv": "^17.3.1",
"express": "^5.2.1",
"file-type": "^21.3.2",
"gaxios": "^7.1.3",
"grammy": "^1.41.1",
"hono": "4.12.7",
"https-proxy-agent": "^8.0.0",

5
pnpm-lock.yaml generated
View File

@ -125,6 +125,9 @@ importers:
file-type:
specifier: 21.3.2
version: 21.3.2
gaxios:
specifier: ^7.1.3
version: 7.1.3
grammy:
specifier: ^1.41.1
version: 1.41.1
@ -271,6 +274,8 @@ importers:
specifier: 0.3.0
version: 0.3.0(zod@4.3.6)
extensions/amazon-bedrock: {}
extensions/anthropic: {}
extensions/bluebubbles:

View File

@ -8,6 +8,8 @@ import {
} from "./runtime-postbuild-shared.mjs";
const GENERATED_BUNDLED_SKILLS_DIR = "bundled-skills";
const TRANSIENT_COPY_ERROR_CODES = new Set(["EEXIST", "ENOENT", "ENOTEMPTY", "EBUSY"]);
const COPY_RETRY_DELAYS_MS = [10, 25, 50];
export function rewritePackageExtensions(entries) {
if (!Array.isArray(entries)) {
@ -82,6 +84,39 @@ function resolveBundledSkillTarget(rawPath) {
};
}
function isTransientCopyError(error) {
return (
!!error &&
typeof error === "object" &&
typeof error.code === "string" &&
TRANSIENT_COPY_ERROR_CODES.has(error.code)
);
}
function sleepSync(ms) {
if (!Number.isFinite(ms) || ms <= 0) {
return;
}
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
}
function copySkillPathWithRetry(params) {
const maxAttempts = COPY_RETRY_DELAYS_MS.length + 1;
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
try {
removePathIfExists(params.targetPath);
fs.mkdirSync(path.dirname(params.targetPath), { recursive: true });
fs.cpSync(params.sourcePath, params.targetPath, params.copyOptions);
return;
} catch (error) {
if (!isTransientCopyError(error) || attempt === maxAttempts - 1) {
throw error;
}
sleepSync(COPY_RETRY_DELAYS_MS[attempt] ?? 0);
}
}
}
function copyDeclaredPluginSkillPaths(params) {
const skills = Array.isArray(params.manifest.skills) ? params.manifest.skills : [];
const copiedSkills = [];
@ -104,21 +139,23 @@ function copyDeclaredPluginSkillPaths(params) {
continue;
}
const targetPath = ensurePathInsideRoot(params.distPluginDir, target.outputPath);
removePathIfExists(targetPath);
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
const shouldExcludeNestedNodeModules = /^node_modules(?:\/|$)/u.test(
normalizeManifestRelativePath(raw),
);
fs.cpSync(sourcePath, targetPath, {
dereference: true,
force: true,
recursive: true,
filter: (candidatePath) => {
if (!shouldExcludeNestedNodeModules || candidatePath === sourcePath) {
return true;
}
const relativeCandidate = path.relative(sourcePath, candidatePath).replaceAll("\\", "/");
return !relativeCandidate.split("/").includes("node_modules");
copySkillPathWithRetry({
sourcePath,
targetPath,
copyOptions: {
dereference: true,
force: true,
recursive: true,
filter: (candidatePath) => {
if (!shouldExcludeNestedNodeModules || candidatePath === sourcePath) {
return true;
}
const relativeCandidate = path.relative(sourcePath, candidatePath).replaceAll("\\", "/");
return !relativeCandidate.split("/").includes("node_modules");
},
},
});
copiedSkills.push(target.manifestPath);

View File

@ -1,38 +1,44 @@
# syntax=docker/dockerfile:1.7
FROM node:24-bookworm@sha256:9f3b13503acdf9bc1e0213ccb25ebe86ac881cad17636733a1da1be1d44509df
FROM node:24-bookworm@sha256:3a09aa6354567619221ef6c45a5051b671f953f0a1924d1f819ffb236e520e6b
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates git \
&& rm -rf /var/lib/apt/lists/*
RUN corepack enable
WORKDIR /app
RUN useradd --create-home --shell /bin/bash appuser \
&& mkdir -p /app \
&& chown appuser:appuser /app
ENV HOME="/home/appuser"
ENV NODE_OPTIONS="--disable-warning=ExperimentalWarning"
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY ui/package.json ./ui/package.json
COPY extensions/memory-core/package.json ./extensions/memory-core/package.json
COPY patches ./patches
USER appuser
WORKDIR /app
RUN --mount=type=cache,id=openclaw-pnpm-store,target=/root/.local/share/pnpm/store,sharing=locked \
COPY --chown=appuser:appuser package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY --chown=appuser:appuser ui/package.json ./ui/package.json
COPY --chown=appuser:appuser extensions/memory-core/package.json ./extensions/memory-core/package.json
COPY --chown=appuser:appuser patches ./patches
RUN --mount=type=cache,id=openclaw-pnpm-store,target=/home/appuser/.local/share/pnpm/store,sharing=locked \
pnpm install --frozen-lockfile
COPY tsconfig.json tsconfig.plugin-sdk.dts.json tsdown.config.ts vitest.config.ts vitest.e2e.config.ts openclaw.mjs ./
COPY src ./src
COPY test ./test
COPY scripts ./scripts
COPY docs ./docs
COPY skills ./skills
COPY ui ./ui
COPY extensions/memory-core ./extensions/memory-core
COPY vendor/a2ui/renderers/lit ./vendor/a2ui/renderers/lit
COPY apps/shared/OpenClawKit/Sources/OpenClawKit/Resources ./apps/shared/OpenClawKit/Sources/OpenClawKit/Resources
COPY apps/shared/OpenClawKit/Tools/CanvasA2UI ./apps/shared/OpenClawKit/Tools/CanvasA2UI
COPY --chown=appuser:appuser tsconfig.json tsconfig.plugin-sdk.dts.json tsdown.config.ts vitest.config.ts vitest.e2e.config.ts openclaw.mjs ./
COPY --chown=appuser:appuser src ./src
COPY --chown=appuser:appuser test ./test
COPY --chown=appuser:appuser scripts ./scripts
COPY --chown=appuser:appuser docs ./docs
COPY --chown=appuser:appuser skills ./skills
COPY --chown=appuser:appuser ui ./ui
COPY --chown=appuser:appuser extensions ./extensions
COPY --chown=appuser:appuser vendor/a2ui/renderers/lit ./vendor/a2ui/renderers/lit
COPY --chown=appuser:appuser apps/shared/OpenClawKit/Sources/OpenClawKit/Resources ./apps/shared/OpenClawKit/Sources/OpenClawKit/Resources
COPY --chown=appuser:appuser apps/shared/OpenClawKit/Tools/CanvasA2UI ./apps/shared/OpenClawKit/Tools/CanvasA2UI
RUN pnpm build
RUN pnpm ui:build
RUN useradd --create-home --shell /bin/bash appuser \
&& chown -R appuser:appuser /app
USER appuser
CMD ["bash"]

View File

@ -1,23 +1,26 @@
# syntax=docker/dockerfile:1.7
FROM node:24-bookworm@sha256:9f3b13503acdf9bc1e0213ccb25ebe86ac881cad17636733a1da1be1d44509df
FROM node:24-bookworm@sha256:3a09aa6354567619221ef6c45a5051b671f953f0a1924d1f819ffb236e520e6b
RUN corepack enable
RUN useradd --create-home --shell /bin/bash appuser \
&& mkdir -p /app \
&& chown appuser:appuser /app
ENV HOME="/home/appuser"
USER appuser
WORKDIR /app
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY ui/package.json ./ui/package.json
COPY patches ./patches
COPY --chown=appuser:appuser package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY --chown=appuser:appuser ui/package.json ./ui/package.json
COPY --chown=appuser:appuser patches ./patches
# This image only exercises the root qrcode-terminal dependency path.
# Keep the pre-install copy set limited to the manifests needed for root
# workspace resolution so unrelated extension edits do not bust the layer.
RUN --mount=type=cache,id=openclaw-pnpm-store,target=/root/.local/share/pnpm/store,sharing=locked \
RUN --mount=type=cache,id=openclaw-pnpm-store,target=/home/appuser/.local/share/pnpm/store,sharing=locked \
pnpm install --frozen-lockfile
COPY . .
RUN useradd --create-home --shell /bin/bash appuser \
&& chown -R appuser:appuser /app
USER appuser
COPY --chown=appuser:appuser . .

View File

@ -8,24 +8,69 @@ echo "Building Docker image..."
docker build -t "$IMAGE_NAME" -f "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR"
echo "Running plugins Docker E2E..."
docker run --rm -t "$IMAGE_NAME" bash -lc '
set -euo pipefail
if [ -f dist/index.mjs ]; then
OPENCLAW_ENTRY="dist/index.mjs"
elif [ -f dist/index.js ]; then
OPENCLAW_ENTRY="dist/index.js"
else
echo "Missing dist/index.(m)js (build output):"
ls -la dist || true
exit 1
fi
export OPENCLAW_ENTRY
docker run --rm -i "$IMAGE_NAME" bash -s <<'EOF'
set -euo pipefail
home_dir=$(mktemp -d "/tmp/openclaw-plugins-e2e.XXXXXX")
export HOME="$home_dir"
mkdir -p "$HOME/.openclaw/extensions/demo-plugin"
if [ -f dist/index.mjs ]; then
OPENCLAW_ENTRY="dist/index.mjs"
elif [ -f dist/index.js ]; then
OPENCLAW_ENTRY="dist/index.js"
else
echo "Missing dist/index.(m)js (build output):"
ls -la dist || true
exit 1
fi
export OPENCLAW_ENTRY
cat > "$HOME/.openclaw/extensions/demo-plugin/index.js" <<'"'"'JS'"'"'
home_dir=$(mktemp -d "/tmp/openclaw-plugins-e2e.XXXXXX")
export HOME="$home_dir"
write_fixture_plugin() {
local dir="$1"
local id="$2"
local version="$3"
local method="$4"
local name="$5"
mkdir -p "$dir"
cat > "$dir/package.json" <<JSON
{
"name": "@openclaw/$id",
"version": "$version",
"openclaw": { "extensions": ["./index.js"] }
}
JSON
cat > "$dir/index.js" <<JS
module.exports = {
id: "$id",
name: "$name",
register(api) {
api.registerGatewayMethod("$method", async () => ({ ok: true }));
},
};
JS
cat > "$dir/openclaw.plugin.json" <<'JSON'
{
"id": "placeholder",
"configSchema": {
"type": "object",
"properties": {}
}
}
JSON
node - <<'NODE' "$dir/openclaw.plugin.json" "$id"
const fs = require("node:fs");
const file = process.argv[2];
const id = process.argv[3];
const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
parsed.id = id;
fs.writeFileSync(file, `${JSON.stringify(parsed, null, 2)}\n`);
NODE
}
mkdir -p "$HOME/.openclaw/extensions/demo-plugin"
cat > "$HOME/.openclaw/extensions/demo-plugin/index.js" <<'JS'
module.exports = {
id: "demo-plugin",
name: "Demo Plugin",
@ -38,7 +83,7 @@ module.exports = {
},
};
JS
cat > "$HOME/.openclaw/extensions/demo-plugin/openclaw.plugin.json" <<'"'"'JSON'"'"'
cat > "$HOME/.openclaw/extensions/demo-plugin/openclaw.plugin.json" <<'JSON'
{
"id": "demo-plugin",
"configSchema": {
@ -48,9 +93,9 @@ JS
}
JSON
node "$OPENCLAW_ENTRY" plugins list --json > /tmp/plugins.json
node "$OPENCLAW_ENTRY" plugins list --json > /tmp/plugins.json
node - <<'"'"'NODE'"'"'
node - <<'NODE'
const fs = require("node:fs");
const data = JSON.parse(fs.readFileSync("/tmp/plugins.json", "utf8"));
@ -79,17 +124,17 @@ if (diagErrors.length > 0) {
console.log("ok");
NODE
echo "Testing tgz install flow..."
pack_dir="$(mktemp -d "/tmp/openclaw-plugin-pack.XXXXXX")"
mkdir -p "$pack_dir/package"
cat > "$pack_dir/package/package.json" <<'"'"'JSON'"'"'
echo "Testing tgz install flow..."
pack_dir="$(mktemp -d "/tmp/openclaw-plugin-pack.XXXXXX")"
mkdir -p "$pack_dir/package"
cat > "$pack_dir/package/package.json" <<'JSON'
{
"name": "@openclaw/demo-plugin-tgz",
"version": "0.0.1",
"openclaw": { "extensions": ["./index.js"] }
}
JSON
cat > "$pack_dir/package/index.js" <<'"'"'JS'"'"'
cat > "$pack_dir/package/index.js" <<'JS'
module.exports = {
id: "demo-plugin-tgz",
name: "Demo Plugin TGZ",
@ -98,7 +143,7 @@ module.exports = {
},
};
JS
cat > "$pack_dir/package/openclaw.plugin.json" <<'"'"'JSON'"'"'
cat > "$pack_dir/package/openclaw.plugin.json" <<'JSON'
{
"id": "demo-plugin-tgz",
"configSchema": {
@ -107,12 +152,12 @@ JS
}
}
JSON
tar -czf /tmp/demo-plugin-tgz.tgz -C "$pack_dir" package
tar -czf /tmp/demo-plugin-tgz.tgz -C "$pack_dir" package
node "$OPENCLAW_ENTRY" plugins install /tmp/demo-plugin-tgz.tgz
node "$OPENCLAW_ENTRY" plugins list --json > /tmp/plugins2.json
node "$OPENCLAW_ENTRY" plugins install /tmp/demo-plugin-tgz.tgz
node "$OPENCLAW_ENTRY" plugins list --json > /tmp/plugins2.json
node - <<'"'"'NODE'"'"'
node - <<'NODE'
const fs = require("node:fs");
const data = JSON.parse(fs.readFileSync("/tmp/plugins2.json", "utf8"));
@ -127,16 +172,16 @@ if (!Array.isArray(plugin.gatewayMethods) || !plugin.gatewayMethods.includes("de
console.log("ok");
NODE
echo "Testing install from local folder (plugins.load.paths)..."
dir_plugin="$(mktemp -d "/tmp/openclaw-plugin-dir.XXXXXX")"
cat > "$dir_plugin/package.json" <<'"'"'JSON'"'"'
echo "Testing install from local folder (plugins.load.paths)..."
dir_plugin="$(mktemp -d "/tmp/openclaw-plugin-dir.XXXXXX")"
cat > "$dir_plugin/package.json" <<'JSON'
{
"name": "@openclaw/demo-plugin-dir",
"version": "0.0.1",
"openclaw": { "extensions": ["./index.js"] }
}
JSON
cat > "$dir_plugin/index.js" <<'"'"'JS'"'"'
cat > "$dir_plugin/index.js" <<'JS'
module.exports = {
id: "demo-plugin-dir",
name: "Demo Plugin DIR",
@ -145,7 +190,7 @@ module.exports = {
},
};
JS
cat > "$dir_plugin/openclaw.plugin.json" <<'"'"'JSON'"'"'
cat > "$dir_plugin/openclaw.plugin.json" <<'JSON'
{
"id": "demo-plugin-dir",
"configSchema": {
@ -155,10 +200,10 @@ JS
}
JSON
node "$OPENCLAW_ENTRY" plugins install "$dir_plugin"
node "$OPENCLAW_ENTRY" plugins list --json > /tmp/plugins3.json
node "$OPENCLAW_ENTRY" plugins install "$dir_plugin"
node "$OPENCLAW_ENTRY" plugins list --json > /tmp/plugins3.json
node - <<'"'"'NODE'"'"'
node - <<'NODE'
const fs = require("node:fs");
const data = JSON.parse(fs.readFileSync("/tmp/plugins3.json", "utf8"));
@ -173,17 +218,17 @@ if (!Array.isArray(plugin.gatewayMethods) || !plugin.gatewayMethods.includes("de
console.log("ok");
NODE
echo "Testing install from npm spec (file:)..."
file_pack_dir="$(mktemp -d "/tmp/openclaw-plugin-filepack.XXXXXX")"
mkdir -p "$file_pack_dir/package"
cat > "$file_pack_dir/package/package.json" <<'"'"'JSON'"'"'
echo "Testing install from npm spec (file:)..."
file_pack_dir="$(mktemp -d "/tmp/openclaw-plugin-filepack.XXXXXX")"
mkdir -p "$file_pack_dir/package"
cat > "$file_pack_dir/package/package.json" <<'JSON'
{
"name": "@openclaw/demo-plugin-file",
"version": "0.0.1",
"openclaw": { "extensions": ["./index.js"] }
}
JSON
cat > "$file_pack_dir/package/index.js" <<'"'"'JS'"'"'
cat > "$file_pack_dir/package/index.js" <<'JS'
module.exports = {
id: "demo-plugin-file",
name: "Demo Plugin FILE",
@ -192,7 +237,7 @@ module.exports = {
},
};
JS
cat > "$file_pack_dir/package/openclaw.plugin.json" <<'"'"'JSON'"'"'
cat > "$file_pack_dir/package/openclaw.plugin.json" <<'JSON'
{
"id": "demo-plugin-file",
"configSchema": {
@ -202,10 +247,10 @@ JS
}
JSON
node "$OPENCLAW_ENTRY" plugins install "file:$file_pack_dir/package"
node "$OPENCLAW_ENTRY" plugins list --json > /tmp/plugins4.json
node "$OPENCLAW_ENTRY" plugins install "file:$file_pack_dir/package"
node "$OPENCLAW_ENTRY" plugins list --json > /tmp/plugins4.json
node - <<'"'"'NODE'"'"'
node - <<'NODE'
const fs = require("node:fs");
const data = JSON.parse(fs.readFileSync("/tmp/plugins4.json", "utf8"));
@ -220,8 +265,155 @@ if (!Array.isArray(plugin.gatewayMethods) || !plugin.gatewayMethods.includes("de
console.log("ok");
NODE
echo "Running bundle MCP CLI-agent e2e..."
pnpm exec vitest run --config vitest.e2e.config.ts src/agents/cli-runner.bundle-mcp.e2e.test.ts
'
echo "Testing marketplace install and update flows..."
marketplace_root="$HOME/.claude/plugins/marketplaces/fixture-marketplace"
mkdir -p "$HOME/.claude/plugins" "$marketplace_root/.claude-plugin"
write_fixture_plugin \
"$marketplace_root/plugins/marketplace-shortcut" \
"marketplace-shortcut" \
"0.0.1" \
"demo.marketplace.shortcut.v1" \
"Marketplace Shortcut"
write_fixture_plugin \
"$marketplace_root/plugins/marketplace-direct" \
"marketplace-direct" \
"0.0.1" \
"demo.marketplace.direct.v1" \
"Marketplace Direct"
cat > "$marketplace_root/.claude-plugin/marketplace.json" <<'JSON'
{
"name": "Fixture Marketplace",
"version": "1.0.0",
"plugins": [
{
"name": "marketplace-shortcut",
"version": "0.0.1",
"description": "Shortcut install fixture",
"source": "./plugins/marketplace-shortcut"
},
{
"name": "marketplace-direct",
"version": "0.0.1",
"description": "Explicit marketplace fixture",
"source": {
"type": "path",
"path": "./plugins/marketplace-direct"
}
}
]
}
JSON
cat > "$HOME/.claude/plugins/known_marketplaces.json" <<JSON
{
"claude-fixtures": {
"installLocation": "$marketplace_root",
"source": {
"type": "github",
"repo": "openclaw/fixture-marketplace"
}
}
}
JSON
node "$OPENCLAW_ENTRY" plugins marketplace list claude-fixtures --json > /tmp/marketplace-list.json
node - <<'NODE'
const fs = require("node:fs");
const data = JSON.parse(fs.readFileSync("/tmp/marketplace-list.json", "utf8"));
const names = (data.plugins || []).map((entry) => entry.name).sort();
if (data.name !== "Fixture Marketplace") {
throw new Error(`unexpected marketplace name: ${data.name}`);
}
if (!names.includes("marketplace-shortcut") || !names.includes("marketplace-direct")) {
throw new Error(`unexpected marketplace plugins: ${names.join(", ")}`);
}
console.log("ok");
NODE
node "$OPENCLAW_ENTRY" plugins install marketplace-shortcut@claude-fixtures
node "$OPENCLAW_ENTRY" plugins install marketplace-direct --marketplace claude-fixtures
node "$OPENCLAW_ENTRY" plugins list --json > /tmp/plugins-marketplace.json
node - <<'NODE'
const fs = require("node:fs");
const data = JSON.parse(fs.readFileSync("/tmp/plugins-marketplace.json", "utf8"));
const getPlugin = (id) => {
const plugin = (data.plugins || []).find((entry) => entry.id === id);
if (!plugin) throw new Error(`plugin not found: ${id}`);
if (plugin.status !== "loaded") {
throw new Error(`unexpected status for ${id}: ${plugin.status}`);
}
return plugin;
};
const shortcut = getPlugin("marketplace-shortcut");
const direct = getPlugin("marketplace-direct");
if (shortcut.version !== "0.0.1") {
throw new Error(`unexpected shortcut version: ${shortcut.version}`);
}
if (direct.version !== "0.0.1") {
throw new Error(`unexpected direct version: ${direct.version}`);
}
if (!shortcut.gatewayMethods.includes("demo.marketplace.shortcut.v1")) {
throw new Error("expected marketplace shortcut gateway method");
}
if (!direct.gatewayMethods.includes("demo.marketplace.direct.v1")) {
throw new Error("expected marketplace direct gateway method");
}
console.log("ok");
NODE
node - <<'NODE'
const fs = require("node:fs");
const path = require("node:path");
const configPath = path.join(process.env.HOME, ".openclaw", "openclaw.json");
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
for (const id of ["marketplace-shortcut", "marketplace-direct"]) {
const record = config.plugins?.installs?.[id];
if (!record) throw new Error(`missing install record for ${id}`);
if (record.source !== "marketplace") {
throw new Error(`unexpected source for ${id}: ${record.source}`);
}
if (record.marketplaceSource !== "claude-fixtures") {
throw new Error(`unexpected marketplace source for ${id}: ${record.marketplaceSource}`);
}
if (record.marketplacePlugin !== id) {
throw new Error(`unexpected marketplace plugin for ${id}: ${record.marketplacePlugin}`);
}
}
console.log("ok");
NODE
write_fixture_plugin \
"$marketplace_root/plugins/marketplace-shortcut" \
"marketplace-shortcut" \
"0.0.2" \
"demo.marketplace.shortcut.v2" \
"Marketplace Shortcut"
node "$OPENCLAW_ENTRY" plugins update marketplace-shortcut --dry-run
node "$OPENCLAW_ENTRY" plugins update marketplace-shortcut
node "$OPENCLAW_ENTRY" plugins list --json > /tmp/plugins-marketplace-updated.json
node - <<'NODE'
const fs = require("node:fs");
const data = JSON.parse(fs.readFileSync("/tmp/plugins-marketplace-updated.json", "utf8"));
const plugin = (data.plugins || []).find((entry) => entry.id === "marketplace-shortcut");
if (!plugin) throw new Error("updated marketplace plugin not found");
if (plugin.version !== "0.0.2") {
throw new Error(`unexpected updated version: ${plugin.version}`);
}
if (!plugin.gatewayMethods.includes("demo.marketplace.shortcut.v2")) {
throw new Error(`expected updated gateway method, got ${plugin.gatewayMethods.join(", ")}`);
}
console.log("ok");
NODE
echo "Running bundle MCP CLI-agent e2e..."
pnpm exec vitest run --config vitest.e2e.config.ts src/agents/cli-runner.bundle-mcp.e2e.test.ts
EOF
echo "OK"

View File

@ -1,8 +1,5 @@
import { formatCliCommand } from "../../cli/command-format.js";
import type { OpenClawConfig } from "../../config/config.js";
import { normalizeProviderId } from "../model-selection.js";
import { listProfilesForProvider } from "./profiles.js";
import { suggestOAuthProfileIdForLegacyDefault } from "./repair.js";
import type { AuthProfileStore } from "./types.js";
let providerRuntimePromise:
@ -34,38 +31,5 @@ export async function formatAuthDoctorHint(params: {
if (typeof pluginHint === "string" && pluginHint.trim()) {
return pluginHint;
}
const providerKey = normalizeProviderId(params.provider);
if (providerKey !== "anthropic") {
return "";
}
const legacyProfileId = params.profileId ?? "anthropic:default";
const suggested = suggestOAuthProfileIdForLegacyDefault({
cfg: params.cfg,
store: params.store,
provider: providerKey,
legacyProfileId,
});
if (!suggested || suggested === legacyProfileId) {
return "";
}
const storeOauthProfiles = listProfilesForProvider(params.store, providerKey)
.filter((id) => params.store.profiles[id]?.type === "oauth")
.join(", ");
const cfgMode = params.cfg?.auth?.profiles?.[legacyProfileId]?.mode;
const cfgProvider = params.cfg?.auth?.profiles?.[legacyProfileId]?.provider;
return [
"Doctor hint (for GitHub issue):",
`- provider: ${providerKey}`,
`- config: ${legacyProfileId}${
cfgProvider || cfgMode ? ` (provider=${cfgProvider ?? "?"}, mode=${cfgMode ?? "?"})` : ""
}`,
`- auth store oauth profiles: ${storeOauthProfiles || "(none)"}`,
`- suggested profile: ${suggested}`,
`Fix: run "${formatCliCommand("openclaw doctor --yes")}"`,
].join("\n");
return "";
}

View File

@ -3,12 +3,20 @@ import { describe, expect, it, vi } from "vitest";
vi.mock("../../plugins/provider-runtime.js", () => ({
resolveProviderCacheTtlEligibility: (params: {
context: { provider: string; modelId: string };
}) =>
params.context.provider === "openrouter"
? ["anthropic/", "moonshot/", "moonshotai/", "zai/"].some((prefix) =>
params.context.modelId.startsWith(prefix),
)
: undefined,
}) => {
if (params.context.provider === "anthropic") {
return true;
}
if (params.context.provider === "moonshot" || params.context.provider === "zai") {
return true;
}
if (params.context.provider === "openrouter") {
return ["anthropic/", "moonshot/", "moonshotai/", "zai/"].some((prefix) =>
params.context.modelId.startsWith(prefix),
);
}
return undefined;
},
}));
import { isCacheTtlEligibleProvider } from "./cache-ttl.js";

View File

@ -10,8 +10,6 @@ export type CacheTtlEntryData = {
modelId?: string;
};
const CACHE_TTL_NATIVE_PROVIDERS = new Set(["moonshot", "zai"]);
export function isCacheTtlEligibleProvider(provider: string, modelId: string): boolean {
const normalizedProvider = provider.toLowerCase();
const normalizedModelId = modelId.toLowerCase();
@ -25,17 +23,6 @@ export function isCacheTtlEligibleProvider(provider: string, modelId: string): b
if (pluginEligibility !== undefined) {
return pluginEligibility;
}
if (normalizedProvider === "kilocode" && normalizedModelId.startsWith("anthropic/")) {
return true;
}
// Legacy fallback for tests / plugin-disabled contexts. The Anthropic plugin
// owns this policy in normal runtime.
if (normalizedProvider === "anthropic") {
return true;
}
if (CACHE_TTL_NATIVE_PROVIDERS.has(normalizedProvider)) {
return true;
}
return false;
}

View File

@ -7,9 +7,6 @@ import {
estimateTokens,
SessionManager,
} from "@mariozechner/pi-coding-agent";
import { resolveSignalReactionLevel } from "../../../extensions/signal/src/reaction-level.js";
import { resolveTelegramInlineButtonsScope } from "../../../extensions/telegram/src/inline-buttons.js";
import { resolveTelegramReactionLevel } from "../../../extensions/telegram/src/reaction-level.js";
import { resolveHeartbeatPrompt } from "../../auto-reply/heartbeat.js";
import type { ReasoningLevel, ThinkLevel } from "../../auto-reply/thinking.js";
import { resolveChannelCapabilities } from "../../config/channel-capabilities.js";
@ -22,6 +19,11 @@ import { createInternalHookEvent, triggerInternalHook } from "../../hooks/intern
import { getMachineDisplayName } from "../../infra/machine-name.js";
import { generateSecureToken } from "../../infra/secure-random.js";
import { getMemorySearchManager } from "../../memory/index.js";
import { resolveSignalReactionLevel } from "../../plugin-sdk-internal/signal.js";
import {
resolveTelegramInlineButtonsScope,
resolveTelegramReactionLevel,
} from "../../plugin-sdk-internal/telegram.js";
import { getGlobalHookRunner } from "../../plugins/hook-runner-global.js";
import { prepareProviderRuntimeAuth } from "../../plugins/provider-runtime.js";
import { type enqueueCommand, enqueueCommandInLane } from "../../process/command-queue.js";

View File

@ -7,9 +7,6 @@ import {
DefaultResourceLoader,
SessionManager,
} from "@mariozechner/pi-coding-agent";
import { resolveSignalReactionLevel } from "../../../../extensions/signal/src/reaction-level.js";
import { resolveTelegramInlineButtonsScope } from "../../../../extensions/telegram/src/inline-buttons.js";
import { resolveTelegramReactionLevel } from "../../../../extensions/telegram/src/reaction-level.js";
import { resolveHeartbeatPrompt } from "../../../auto-reply/heartbeat.js";
import { resolveChannelCapabilities } from "../../../config/channel-capabilities.js";
import type { OpenClawConfig } from "../../../config/config.js";
@ -19,6 +16,11 @@ import {
ensureGlobalUndiciStreamTimeouts,
} from "../../../infra/net/undici-global-dispatcher.js";
import { MAX_IMAGE_BYTES } from "../../../media/constants.js";
import { resolveSignalReactionLevel } from "../../../plugin-sdk-internal/signal.js";
import {
resolveTelegramInlineButtonsScope,
resolveTelegramReactionLevel,
} from "../../../plugin-sdk-internal/telegram.js";
import { getGlobalHookRunner } from "../../../plugins/hook-runner-global.js";
import type {
PluginHookAgentContext,

View File

@ -1,7 +1,7 @@
import path from "node:path";
import { fileURLToPath } from "node:url";
import type { ImageContent } from "@mariozechner/pi-ai";
import { loadWebMedia } from "../../../../extensions/whatsapp/src/media.js";
import { loadWebMedia } from "../../../plugin-sdk/web-media.js";
import { resolveUserPath } from "../../../utils.js";
import type { ImageSanitizationLimits } from "../../image-sanitization.js";
import {

View File

@ -1,5 +1,5 @@
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
import { getPresence } from "../../../extensions/discord/src/monitor/presence-cache.js";
import type { DiscordActionConfig } from "../../config/config.js";
import {
addRoleDiscord,
createChannelDiscord,
@ -19,8 +19,8 @@ import {
setChannelPermissionDiscord,
uploadEmojiDiscord,
uploadStickerDiscord,
} from "../../../extensions/discord/src/send.js";
import type { DiscordActionConfig } from "../../config/config.js";
} from "../../plugin-sdk-internal/discord.js";
import { getPresence } from "../../plugin-sdk-internal/discord.js";
import {
type ActionGate,
jsonResult,

View File

@ -1,5 +1,6 @@
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
import { readDiscordComponentSpec } from "../../../extensions/discord/src/components.js";
import type { DiscordActionConfig } from "../../config/config.js";
import type { OpenClawConfig } from "../../config/config.js";
import {
createThreadDiscord,
deleteMessageDiscord,
@ -21,14 +22,15 @@ import {
sendStickerDiscord,
sendVoiceMessageDiscord,
unpinMessageDiscord,
} from "../../../extensions/discord/src/send.js";
} from "../../plugin-sdk-internal/discord.js";
import type {
DiscordSendComponents,
DiscordSendEmbeds,
} from "../../../extensions/discord/src/send.shared.js";
import { resolveDiscordChannelId } from "../../../extensions/discord/src/targets.js";
import type { DiscordActionConfig } from "../../config/config.js";
import type { OpenClawConfig } from "../../config/config.js";
} from "../../plugin-sdk-internal/discord.js";
import {
readDiscordComponentSpec,
resolveDiscordChannelId,
} from "../../plugin-sdk-internal/discord.js";
import { readBooleanParam } from "../../plugin-sdk/boolean-param.js";
import { resolvePollMaxSelections } from "../../polls.js";
import { withNormalizedTimestamp } from "../date-time.js";

View File

@ -1,11 +1,11 @@
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
import type { DiscordActionConfig } from "../../config/config.js";
import {
banMemberDiscord,
hasAnyGuildPermissionDiscord,
kickMemberDiscord,
timeoutMemberDiscord,
} from "../../../extensions/discord/src/send.js";
import type { DiscordActionConfig } from "../../config/config.js";
} from "../../plugin-sdk-internal/discord.js";
import { type ActionGate, jsonResult, readStringParam } from "./common.js";
import {
isDiscordModerationAction,

View File

@ -1,7 +1,7 @@
import type { Activity, UpdatePresenceData } from "@buape/carbon/gateway";
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
import { getGateway } from "../../../extensions/discord/src/monitor/gateway-registry.js";
import type { DiscordActionConfig } from "../../config/config.js";
import { getGateway } from "../../plugin-sdk-internal/discord.js";
import { type ActionGate, jsonResult, readStringParam } from "./common.js";
const ACTIVITY_TYPE_MAP: Record<string, number> = {

View File

@ -1,6 +1,6 @@
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
import { createDiscordActionGate } from "../../../extensions/discord/src/accounts.js";
import type { OpenClawConfig } from "../../config/config.js";
import { createDiscordActionGate } from "../../plugin-sdk-internal/discord.js";
import { readStringParam } from "./common.js";
import { handleDiscordGuildAction } from "./discord-actions-guild.js";
import { handleDiscordMessagingAction } from "./discord-actions-messaging.js";

View File

@ -1,7 +1,7 @@
import { type Context, complete } from "@mariozechner/pi-ai";
import { Type } from "@sinclair/typebox";
import { loadWebMedia } from "../../../extensions/whatsapp/src/media.js";
import type { OpenClawConfig } from "../../config/config.js";
import { loadWebMedia } from "../../plugin-sdk/web-media.js";
import { resolveUserPath } from "../../utils.js";
import { isMinimaxVlmModel, isMinimaxVlmProvider, minimaxUnderstandImage } from "../minimax-vlm.js";
import {

View File

@ -1,6 +1,6 @@
import { type Api, type Model } from "@mariozechner/pi-ai";
import { getDefaultLocalRoots } from "../../../extensions/whatsapp/src/media.js";
import type { OpenClawConfig } from "../../config/config.js";
import { getDefaultLocalRoots } from "../../plugin-sdk/web-media.js";
import type { ImageModelConfig } from "./image-tool.helpers.js";
import { getApiKeyForModel, normalizeWorkspaceDir, requireApiKey } from "./tool-runtime.helpers.js";

View File

@ -1,8 +1,8 @@
import { type Context, complete } from "@mariozechner/pi-ai";
import { Type } from "@sinclair/typebox";
import { loadWebMediaRaw } from "../../../extensions/whatsapp/src/media.js";
import type { OpenClawConfig } from "../../config/config.js";
import { extractPdfContent, type PdfExtractedContent } from "../../media/pdf-extract.js";
import { loadWebMediaRaw } from "../../plugin-sdk/web-media.js";
import { resolveUserPath } from "../../utils.js";
import {
coerceImageModelConfig,

View File

@ -1,5 +1,5 @@
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
import { resolveSlackAccount } from "../../../extensions/slack/src/accounts.js";
import type { OpenClawConfig } from "../../config/config.js";
import {
deleteSlackMessage,
downloadSlackFile,
@ -15,11 +15,14 @@ import {
removeSlackReaction,
sendSlackMessage,
unpinSlackMessage,
} from "../../../extensions/slack/src/actions.js";
import { parseSlackBlocksInput } from "../../../extensions/slack/src/blocks-input.js";
import { recordSlackThreadParticipation } from "../../../extensions/slack/src/sent-thread-cache.js";
import { parseSlackTarget, resolveSlackChannelId } from "../../../extensions/slack/src/targets.js";
import type { OpenClawConfig } from "../../config/config.js";
} from "../../plugin-sdk-internal/slack.js";
import {
parseSlackBlocksInput,
parseSlackTarget,
recordSlackThreadParticipation,
resolveSlackAccount,
resolveSlackChannelId,
} from "../../plugin-sdk-internal/slack.js";
import { withNormalizedTimestamp } from "../date-time.js";
import {
createActionGate,

View File

@ -1,17 +1,17 @@
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
import type { OpenClawConfig } from "../../config/config.js";
import {
createTelegramActionGate,
resolveTelegramPollActionGateState,
} from "../../../extensions/telegram/src/accounts.js";
} from "../../plugin-sdk-internal/telegram.js";
import type {
TelegramButtonStyle,
TelegramInlineButtons,
} from "../../../extensions/telegram/src/button-types.js";
} from "../../plugin-sdk-internal/telegram.js";
import {
resolveTelegramInlineButtonsScope,
resolveTelegramTargetChatType,
} from "../../../extensions/telegram/src/inline-buttons.js";
import { resolveTelegramReactionLevel } from "../../../extensions/telegram/src/reaction-level.js";
} from "../../plugin-sdk-internal/telegram.js";
import {
createForumTopicTelegram,
deleteMessageTelegram,
@ -21,10 +21,13 @@ import {
sendMessageTelegram,
sendPollTelegram,
sendStickerTelegram,
} from "../../../extensions/telegram/src/send.js";
import { getCacheStats, searchStickers } from "../../../extensions/telegram/src/sticker-cache.js";
import { resolveTelegramToken } from "../../../extensions/telegram/src/token.js";
import type { OpenClawConfig } from "../../config/config.js";
} from "../../plugin-sdk-internal/telegram.js";
import {
getCacheStats,
resolveTelegramReactionLevel,
resolveTelegramToken,
searchStickers,
} from "../../plugin-sdk-internal/telegram.js";
import { readBooleanParam } from "../../plugin-sdk/boolean-param.js";
import { resolvePollMaxSelections } from "../../polls.js";
import {

View File

@ -1,6 +1,6 @@
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
import { sendReactionWhatsApp } from "../../../extensions/whatsapp/src/send.js";
import type { OpenClawConfig } from "../../config/config.js";
import { sendReactionWhatsApp } from "../../plugin-sdk-internal/whatsapp.js";
import { createActionGate, jsonResult, readReactionParams, readStringParam } from "./common.js";
import { resolveAuthorizedWhatsAppOutboundTarget } from "./whatsapp-target-auth.js";

View File

@ -1,5 +1,5 @@
import { resolveWhatsAppAccount } from "../../../extensions/whatsapp/src/accounts.js";
import type { OpenClawConfig } from "../../config/config.js";
import { resolveWhatsAppAccount } from "../../plugin-sdk-internal/whatsapp.js";
import { resolveWhatsAppOutboundTarget } from "../../whatsapp/resolve-outbound-target.js";
import { ToolAuthorizationError } from "./common.js";

View File

@ -1,5 +1,6 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { AcpRuntimeError } from "../../acp/runtime/errors.js";
import { setDefaultChannelPluginRegistryForTests } from "../../commands/channel-test-helpers.js";
import type { OpenClawConfig } from "../../config/config.js";
import type { SessionBindingRecord } from "../../infra/outbound/session-binding-service.js";
import { INTERNAL_MESSAGE_CHANNEL } from "../../utils/message-channel.js";
@ -395,6 +396,7 @@ async function runInternalAcpCommand(params: {
describe("/acp command", () => {
beforeEach(() => {
setDefaultChannelPluginRegistryForTests();
acpManagerTesting.resetAcpSessionManagerForTests();
hoisted.listAcpSessionEntriesMock.mockReset().mockResolvedValue([]);
hoisted.callGatewayMock.mockReset().mockResolvedValue({ ok: true });

View File

@ -1,4 +1,3 @@
import { buildFeishuConversationId } from "../../../../extensions/feishu/src/conversation-id.js";
import {
buildTelegramTopicConversationId,
normalizeConversationText,
@ -7,6 +6,7 @@ import {
import { DISCORD_THREAD_BINDING_CHANNEL } from "../../../channels/thread-bindings-policy.js";
import { resolveConversationIdFromTargets } from "../../../infra/outbound/conversation-id.js";
import { getSessionBindingService } from "../../../infra/outbound/session-binding-service.js";
import { buildFeishuConversationId } from "../../../plugin-sdk/feishu.js";
import { parseAgentSessionKey } from "../../../routing/session-key.js";
import type { HandleCommandsParams } from "../commands-types.js";
import { parseDiscordParentChannelFromSessionKey } from "../discord-parent-channel.js";

View File

@ -3,7 +3,7 @@ import { logVerbose } from "../../globals.js";
import {
isTelegramExecApprovalApprover,
isTelegramExecApprovalClientEnabled,
} from "../../plugin-sdk/telegram.js";
} from "../../plugin-sdk-internal/telegram.js";
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../../utils/message-channel.js";
import { requireGatewayClientScopeForInternalChannel } from "./command-gates.js";
import type { CommandHandler } from "./commands-types.js";

View File

@ -16,7 +16,7 @@ import {
calculateTotalPages,
getModelsPageSize,
type ProviderInfo,
} from "../../plugin-sdk/telegram.js";
} from "../../plugin-sdk-internal/telegram.js";
import type { ReplyPayload } from "../types.js";
import { rejectUnauthorizedCommand } from "./command-gates.js";
import type { CommandHandler } from "./commands-types.js";

View File

@ -22,7 +22,12 @@ const SESSION_COMMAND_PREFIX = "/session";
const SESSION_DURATION_OFF_VALUES = new Set(["off", "disable", "disabled", "none", "0"]);
const SESSION_ACTION_IDLE = "idle";
const SESSION_ACTION_MAX_AGE = "max-age";
const channelRuntime = createPluginRuntime().channel;
let cachedChannelRuntime: ReturnType<typeof createPluginRuntime>["channel"] | undefined;
function getChannelRuntime() {
cachedChannelRuntime ??= createPluginRuntime().channel;
return cachedChannelRuntime;
}
function resolveSessionCommandUsage() {
return "Usage: /session idle <duration|off> | /session max-age <duration|off> (example: /session idle 24h)";
@ -373,6 +378,7 @@ export const handleSessionCommand: CommandHandler = async (params, allowTextComm
const threadId =
params.ctx.MessageThreadId != null ? String(params.ctx.MessageThreadId).trim() : "";
const telegramConversationId = onTelegram ? resolveTelegramConversationId(params) : undefined;
const channelRuntime = getChannelRuntime();
const discordManager = onDiscord
? channelRuntime.discord.threadBindings.getManager(accountId)

View File

@ -8,7 +8,7 @@ import {
} from "../../agents/model-selection.js";
import type { OpenClawConfig } from "../../config/config.js";
import type { SessionEntry } from "../../config/sessions.js";
import { buildBrowseProvidersButton } from "../../plugin-sdk/telegram.js";
import { buildBrowseProvidersButton } from "../../plugin-sdk-internal/telegram.js";
import { shortenHomePath } from "../../utils.js";
import { resolveSelectedAndActiveModel } from "../model-runtime.js";
import type { ReplyPayload } from "../types.js";

View File

@ -1,9 +1,9 @@
import type { StickerMetadata } from "../../extensions/telegram/src/bot/types.js";
import type { ChannelId } from "../channels/plugins/types.js";
import type {
MediaUnderstandingDecision,
MediaUnderstandingOutput,
} from "../media-understanding/types.js";
import type { StickerMetadata } from "../plugin-sdk-internal/telegram.js";
import type { InputProvenance } from "../sessions/input-provenance.js";
import type { InternalMessageChannel } from "../utils/message-channel.js";
import type { CommandArgs } from "./commands-registry.types.js";

View File

@ -0,0 +1,226 @@
export type ThinkLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "adaptive";
export type VerboseLevel = "off" | "on" | "full";
export type NoticeLevel = "off" | "on" | "full";
export type ElevatedLevel = "off" | "on" | "ask" | "full";
export type ElevatedMode = "off" | "ask" | "full";
export type ReasoningLevel = "off" | "on" | "stream";
export type UsageDisplayLevel = "off" | "tokens" | "full";
export type ThinkingCatalogEntry = {
provider: string;
id: string;
reasoning?: boolean;
};
const BASE_THINKING_LEVELS: ThinkLevel[] = ["off", "minimal", "low", "medium", "high", "adaptive"];
export function normalizeProviderId(provider?: string | null): string {
if (!provider) {
return "";
}
const normalized = provider.trim().toLowerCase();
if (normalized === "z.ai" || normalized === "z-ai") {
return "zai";
}
if (normalized === "bedrock" || normalized === "aws-bedrock") {
return "amazon-bedrock";
}
return normalized;
}
export function isBinaryThinkingProvider(provider?: string | null): boolean {
return normalizeProviderId(provider) === "zai";
}
// Normalize user-provided thinking level strings to the canonical enum.
export function normalizeThinkLevel(raw?: string | null): ThinkLevel | undefined {
if (!raw) {
return undefined;
}
const key = raw.trim().toLowerCase();
const collapsed = key.replace(/[\s_-]+/g, "");
if (collapsed === "adaptive" || collapsed === "auto") {
return "adaptive";
}
if (collapsed === "xhigh" || collapsed === "extrahigh") {
return "xhigh";
}
if (["off"].includes(key)) {
return "off";
}
if (["on", "enable", "enabled"].includes(key)) {
return "low";
}
if (["min", "minimal"].includes(key)) {
return "minimal";
}
if (["low", "thinkhard", "think-hard", "think_hard"].includes(key)) {
return "low";
}
if (["mid", "med", "medium", "thinkharder", "think-harder", "harder"].includes(key)) {
return "medium";
}
if (
["high", "ultra", "ultrathink", "think-hard", "thinkhardest", "highest", "max"].includes(key)
) {
return "high";
}
if (["think"].includes(key)) {
return "minimal";
}
return undefined;
}
export function listThinkingLevels(
_provider?: string | null,
_model?: string | null,
): ThinkLevel[] {
return [...BASE_THINKING_LEVELS];
}
export function listThinkingLevelLabels(provider?: string | null, model?: string | null): string[] {
if (isBinaryThinkingProvider(provider)) {
return ["off", "on"];
}
return listThinkingLevels(provider, model);
}
export function formatThinkingLevels(
provider?: string | null,
model?: string | null,
separator = ", ",
): string {
return listThinkingLevelLabels(provider, model).join(separator);
}
export function formatXHighModelHint(): string {
return "provider models that advertise xhigh reasoning";
}
export function resolveThinkingDefaultForModel(params: {
provider: string;
model: string;
catalog?: ThinkingCatalogEntry[];
}): ThinkLevel {
const candidate = params.catalog?.find(
(entry) => entry.provider === params.provider && entry.id === params.model,
);
if (candidate?.reasoning) {
return "low";
}
return "off";
}
type OnOffFullLevel = "off" | "on" | "full";
function normalizeOnOffFullLevel(raw?: string | null): OnOffFullLevel | undefined {
if (!raw) {
return undefined;
}
const key = raw.toLowerCase();
if (["off", "false", "no", "0"].includes(key)) {
return "off";
}
if (["full", "all", "everything"].includes(key)) {
return "full";
}
if (["on", "minimal", "true", "yes", "1"].includes(key)) {
return "on";
}
return undefined;
}
export function normalizeVerboseLevel(raw?: string | null): VerboseLevel | undefined {
return normalizeOnOffFullLevel(raw);
}
export function normalizeNoticeLevel(raw?: string | null): NoticeLevel | undefined {
return normalizeOnOffFullLevel(raw);
}
export function normalizeUsageDisplay(raw?: string | null): UsageDisplayLevel | undefined {
if (!raw) {
return undefined;
}
const key = raw.toLowerCase();
if (["off", "false", "no", "0", "disable", "disabled"].includes(key)) {
return "off";
}
if (["on", "true", "yes", "1", "enable", "enabled"].includes(key)) {
return "tokens";
}
if (["tokens", "token", "tok", "minimal", "min"].includes(key)) {
return "tokens";
}
if (["full", "session"].includes(key)) {
return "full";
}
return undefined;
}
export function resolveResponseUsageMode(raw?: string | null): UsageDisplayLevel {
return normalizeUsageDisplay(raw) ?? "off";
}
export function normalizeFastMode(raw?: string | boolean | null): boolean | undefined {
if (typeof raw === "boolean") {
return raw;
}
if (!raw) {
return undefined;
}
const key = raw.toLowerCase();
if (["off", "false", "no", "0", "disable", "disabled", "normal"].includes(key)) {
return false;
}
if (["on", "true", "yes", "1", "enable", "enabled", "fast"].includes(key)) {
return true;
}
return undefined;
}
export function normalizeElevatedLevel(raw?: string | null): ElevatedLevel | undefined {
if (!raw) {
return undefined;
}
const key = raw.toLowerCase();
if (["off", "false", "no", "0"].includes(key)) {
return "off";
}
if (["full", "auto", "auto-approve", "autoapprove"].includes(key)) {
return "full";
}
if (["ask", "prompt", "approval", "approve"].includes(key)) {
return "ask";
}
if (["on", "true", "yes", "1"].includes(key)) {
return "on";
}
return undefined;
}
export function resolveElevatedMode(level?: ElevatedLevel | null): ElevatedMode {
if (!level || level === "off") {
return "off";
}
if (level === "full") {
return "full";
}
return "ask";
}
export function normalizeReasoningLevel(raw?: string | null): ReasoningLevel | undefined {
if (!raw) {
return undefined;
}
const key = raw.toLowerCase();
if (["off", "false", "no", "0", "hide", "hidden", "disable", "disabled"].includes(key)) {
return "off";
}
if (["on", "true", "yes", "1", "show", "visible", "enable", "enabled"].includes(key)) {
return "on";
}
if (["stream", "streaming", "draft", "live"].includes(key)) {
return "stream";
}
return undefined;
}

View File

@ -118,6 +118,10 @@ describe("listThinkingLevelLabels", () => {
expect(listThinkingLevelLabels("zai", "glm-4.7")).toEqual(["off", "on"]);
});
it("keeps built-in binary thinking fallback without provider runtime", () => {
expect(listThinkingLevelLabels("zai", "glm-4.7")).toEqual(["off", "on"]);
});
it("returns full levels for non-ZAI", () => {
expect(listThinkingLevelLabels("openai", "gpt-4.1-mini")).toContain("low");
expect(listThinkingLevelLabels("openai", "gpt-4.1-mini")).not.toContain("on");
@ -144,7 +148,23 @@ describe("resolveThinkingDefaultForModel", () => {
).toBe("adaptive");
});
it("treats Bedrock Anthropic aliases as adaptive", () => {
it("uses provider-advertised adaptive defaults for Bedrock aliases", () => {
providerRuntimeMocks.resolveProviderDefaultThinkingLevel.mockImplementation(
({ provider, context }) =>
provider === "amazon-bedrock" && context.modelId === "claude-sonnet-4-6"
? "adaptive"
: undefined,
);
expect(
resolveThinkingDefaultForModel({ provider: "aws-bedrock", model: "claude-sonnet-4-6" }),
).toBe("adaptive");
});
it("keeps built-in adaptive defaults without provider runtime", () => {
expect(
resolveThinkingDefaultForModel({ provider: "anthropic", model: "claude-opus-4-6" }),
).toBe("adaptive");
expect(
resolveThinkingDefaultForModel({ provider: "aws-bedrock", model: "claude-sonnet-4-6" }),
).toBe("adaptive");

View File

@ -1,38 +1,40 @@
import {
formatThinkingLevels as formatThinkingLevelsFallback,
isBinaryThinkingProvider as isBinaryThinkingProviderFallback,
listThinkingLevelLabels as listThinkingLevelLabelsFallback,
listThinkingLevels as listThinkingLevelsFallback,
normalizeProviderId,
resolveThinkingDefaultForModel as resolveThinkingDefaultForModelFallback,
} from "./thinking.shared.js";
import type { ThinkLevel, ThinkingCatalogEntry } from "./thinking.shared.js";
export {
formatXHighModelHint,
normalizeElevatedLevel,
normalizeFastMode,
normalizeNoticeLevel,
normalizeReasoningLevel,
normalizeThinkLevel,
normalizeUsageDisplay,
normalizeVerboseLevel,
resolveResponseUsageMode,
resolveElevatedMode,
} from "./thinking.shared.js";
export type {
ElevatedLevel,
ElevatedMode,
NoticeLevel,
ReasoningLevel,
ThinkLevel,
ThinkingCatalogEntry,
UsageDisplayLevel,
VerboseLevel,
} from "./thinking.shared.js";
import {
resolveProviderBinaryThinking,
resolveProviderDefaultThinkingLevel,
resolveProviderXHighThinking,
} from "../plugins/provider-runtime.js";
export type ThinkLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "adaptive";
export type VerboseLevel = "off" | "on" | "full";
export type NoticeLevel = "off" | "on" | "full";
export type ElevatedLevel = "off" | "on" | "ask" | "full";
export type ElevatedMode = "off" | "ask" | "full";
export type ReasoningLevel = "off" | "on" | "stream";
export type UsageDisplayLevel = "off" | "tokens" | "full";
export type ThinkingCatalogEntry = {
provider: string;
id: string;
reasoning?: boolean;
};
const CLAUDE_46_MODEL_RE = /claude-(?:opus|sonnet)-4(?:\.|-)6(?:$|[-.])/i;
function normalizeProviderId(provider?: string | null): string {
if (!provider) {
return "";
}
const normalized = provider.trim().toLowerCase();
if (normalized === "z.ai" || normalized === "z-ai") {
return "zai";
}
if (normalized === "bedrock" || normalized === "aws-bedrock") {
return "amazon-bedrock";
}
return normalized;
}
export function isBinaryThinkingProvider(provider?: string | null, model?: string | null): boolean {
const normalizedProvider = normalizeProviderId(provider);
if (!normalizedProvider) {
@ -49,46 +51,7 @@ export function isBinaryThinkingProvider(provider?: string | null, model?: strin
if (typeof pluginDecision === "boolean") {
return pluginDecision;
}
return false;
}
// Normalize user-provided thinking level strings to the canonical enum.
export function normalizeThinkLevel(raw?: string | null): ThinkLevel | undefined {
if (!raw) {
return undefined;
}
const key = raw.trim().toLowerCase();
const collapsed = key.replace(/[\s_-]+/g, "");
if (collapsed === "adaptive" || collapsed === "auto") {
return "adaptive";
}
if (collapsed === "xhigh" || collapsed === "extrahigh") {
return "xhigh";
}
if (["off"].includes(key)) {
return "off";
}
if (["on", "enable", "enabled"].includes(key)) {
return "low";
}
if (["min", "minimal"].includes(key)) {
return "minimal";
}
if (["low", "thinkhard", "think-hard", "think_hard"].includes(key)) {
return "low";
}
if (["mid", "med", "medium", "thinkharder", "think-harder", "harder"].includes(key)) {
return "medium";
}
if (
["high", "ultra", "ultrathink", "think-hard", "thinkhardest", "highest", "max"].includes(key)
) {
return "high";
}
if (["think"].includes(key)) {
return "minimal";
}
return undefined;
return isBinaryThinkingProviderFallback(provider);
}
export function supportsXHighThinking(provider?: string | null, model?: string | null): boolean {
@ -113,11 +76,10 @@ export function supportsXHighThinking(provider?: string | null, model?: string |
}
export function listThinkingLevels(provider?: string | null, model?: string | null): ThinkLevel[] {
const levels: ThinkLevel[] = ["off", "minimal", "low", "medium", "high"];
const levels = listThinkingLevelsFallback(provider, model);
if (supportsXHighThinking(provider, model)) {
levels.push("xhigh");
levels.splice(levels.length - 1, 0, "xhigh");
}
levels.push("adaptive");
return levels;
}
@ -125,7 +87,7 @@ export function listThinkingLevelLabels(provider?: string | null, model?: string
if (isBinaryThinkingProvider(provider, model)) {
return ["off", "on"];
}
return listThinkingLevels(provider, model);
return listThinkingLevelLabelsFallback(provider, model);
}
export function formatThinkingLevels(
@ -133,11 +95,9 @@ export function formatThinkingLevels(
model?: string | null,
separator = ", ",
): string {
return listThinkingLevelLabels(provider, model).join(separator);
}
export function formatXHighModelHint(): string {
return "provider models that advertise xhigh reasoning";
return supportsXHighThinking(provider, model)
? listThinkingLevelLabels(provider, model).join(separator)
: formatThinkingLevelsFallback(provider, model, separator);
}
export function resolveThinkingDefaultForModel(params: {
@ -146,7 +106,6 @@ export function resolveThinkingDefaultForModel(params: {
catalog?: ThinkingCatalogEntry[];
}): ThinkLevel {
const normalizedProvider = normalizeProviderId(params.provider);
const modelLower = params.model.trim().toLowerCase();
const candidate = params.catalog?.find(
(entry) => entry.provider === params.provider && entry.id === params.model,
);
@ -161,133 +120,5 @@ export function resolveThinkingDefaultForModel(params: {
if (pluginDecision) {
return pluginDecision;
}
if (normalizedProvider === "amazon-bedrock" && CLAUDE_46_MODEL_RE.test(modelLower)) {
return "adaptive";
}
if (candidate?.reasoning) {
return "low";
}
return "off";
}
type OnOffFullLevel = "off" | "on" | "full";
function normalizeOnOffFullLevel(raw?: string | null): OnOffFullLevel | undefined {
if (!raw) {
return undefined;
}
const key = raw.toLowerCase();
if (["off", "false", "no", "0"].includes(key)) {
return "off";
}
if (["full", "all", "everything"].includes(key)) {
return "full";
}
if (["on", "minimal", "true", "yes", "1"].includes(key)) {
return "on";
}
return undefined;
}
// Normalize verbose flags used to toggle agent verbosity.
export function normalizeVerboseLevel(raw?: string | null): VerboseLevel | undefined {
return normalizeOnOffFullLevel(raw);
}
// Normalize system notice flags used to toggle system notifications.
export function normalizeNoticeLevel(raw?: string | null): NoticeLevel | undefined {
return normalizeOnOffFullLevel(raw);
}
// Normalize response-usage display modes used to toggle per-response usage footers.
export function normalizeUsageDisplay(raw?: string | null): UsageDisplayLevel | undefined {
if (!raw) {
return undefined;
}
const key = raw.toLowerCase();
if (["off", "false", "no", "0", "disable", "disabled"].includes(key)) {
return "off";
}
if (["on", "true", "yes", "1", "enable", "enabled"].includes(key)) {
return "tokens";
}
if (["tokens", "token", "tok", "minimal", "min"].includes(key)) {
return "tokens";
}
if (["full", "session"].includes(key)) {
return "full";
}
return undefined;
}
export function resolveResponseUsageMode(raw?: string | null): UsageDisplayLevel {
return normalizeUsageDisplay(raw) ?? "off";
}
// Normalize fast-mode flags used to toggle low-latency model behavior.
export function normalizeFastMode(raw?: string | boolean | null): boolean | undefined {
if (typeof raw === "boolean") {
return raw;
}
if (!raw) {
return undefined;
}
const key = raw.toLowerCase();
if (["off", "false", "no", "0", "disable", "disabled", "normal"].includes(key)) {
return false;
}
if (["on", "true", "yes", "1", "enable", "enabled", "fast"].includes(key)) {
return true;
}
return undefined;
}
// Normalize elevated flags used to toggle elevated bash permissions.
export function normalizeElevatedLevel(raw?: string | null): ElevatedLevel | undefined {
if (!raw) {
return undefined;
}
const key = raw.toLowerCase();
if (["off", "false", "no", "0"].includes(key)) {
return "off";
}
if (["full", "auto", "auto-approve", "autoapprove"].includes(key)) {
return "full";
}
if (["ask", "prompt", "approval", "approve"].includes(key)) {
return "ask";
}
if (["on", "true", "yes", "1"].includes(key)) {
return "on";
}
return undefined;
}
export function resolveElevatedMode(level?: ElevatedLevel | null): ElevatedMode {
if (!level || level === "off") {
return "off";
}
if (level === "full") {
return "full";
}
return "ask";
}
// Normalize reasoning visibility flags used to toggle reasoning exposure.
export function normalizeReasoningLevel(raw?: string | null): ReasoningLevel | undefined {
if (!raw) {
return undefined;
}
const key = raw.toLowerCase();
if (["off", "false", "no", "0", "hide", "hidden", "disable", "disabled"].includes(key)) {
return "off";
}
if (["on", "true", "yes", "1", "show", "visible", "enable", "enabled"].includes(key)) {
return "on";
}
if (["stream", "streaming", "draft", "live"].includes(key)) {
return "stream";
}
return undefined;
return resolveThinkingDefaultForModelFallback(params);
}

View File

@ -7,19 +7,15 @@ export {
monitorWebChannel,
resolveHeartbeatRecipients,
runWebHeartbeatOnce,
type WebChannelStatus,
type WebMonitorTuning,
} from "../extensions/whatsapp/src/auto-reply.js";
} from "./plugin-sdk-internal/whatsapp.js";
export {
extractMediaPlaceholder,
extractText,
monitorWebInbox,
type WebInboundMessage,
type WebListenerCloseReason,
} from "../extensions/whatsapp/src/inbound.js";
export { loginWeb } from "../extensions/whatsapp/src/login.js";
export { loadWebMedia, optimizeImageToJpeg } from "../extensions/whatsapp/src/media.js";
export { sendMessageWhatsApp } from "../extensions/whatsapp/src/send.js";
} from "./plugin-sdk-internal/whatsapp.js";
export { loginWeb } from "./plugin-sdk-internal/whatsapp.js";
export { loadWebMedia, optimizeImageToJpeg } from "./plugin-sdk-internal/whatsapp.js";
export { sendMessageWhatsApp } from "./plugin-sdk-internal/whatsapp.js";
export {
createWaSocket,
formatError,
@ -30,4 +26,4 @@ export {
WA_WEB_AUTH_DIR,
waitForWaConnection,
webAuthExists,
} from "../extensions/whatsapp/src/session.js";
} from "./plugin-sdk-internal/whatsapp.js";

View File

@ -1,2 +1,2 @@
// Shim: re-exports from extension
export * from "../../../../extensions/discord/src/channel-actions.js";
// Public entrypoint for the Discord channel action adapter.
export * from "../../../plugin-sdk-internal/discord.js";

View File

@ -5,7 +5,7 @@ import {
resolveSignalAccount,
resolveSignalReactionLevel,
sendReactionSignal,
} from "../../../plugin-sdk/signal.js";
} from "../../../plugin-sdk-internal/signal.js";
import type { ChannelMessageActionAdapter, ChannelMessageActionName } from "../types.js";
import { resolveReactionMessageId } from "./reaction-message-id.js";

View File

@ -1 +1,2 @@
export * from "../../../../extensions/telegram/src/channel-actions.js";
// Public entrypoint for the Telegram channel action adapter.
export * from "../../../plugin-sdk-internal/telegram.js";

View File

@ -1,2 +1,2 @@
// Shim: re-exports from extensions/whatsapp/src/agent-tools-login.ts
export * from "../../../../extensions/whatsapp/src/agent-tools-login.js";
// Shim: keep legacy import path while the runtime loads the plugin SDK surface.
export * from "../../../plugin-sdk-internal/whatsapp.js";

View File

@ -0,0 +1,88 @@
import { bluebubblesPlugin } from "../../../extensions/bluebubbles/src/channel.js";
import { discordPlugin } from "../../../extensions/discord/src/channel.js";
import { discordSetupPlugin } from "../../../extensions/discord/src/channel.setup.js";
import { setDiscordRuntime } from "../../../extensions/discord/src/runtime.js";
import { feishuPlugin } from "../../../extensions/feishu/src/channel.js";
import { googlechatPlugin } from "../../../extensions/googlechat/src/channel.js";
import { imessagePlugin } from "../../../extensions/imessage/src/channel.js";
import { imessageSetupPlugin } from "../../../extensions/imessage/src/channel.setup.js";
import { ircPlugin } from "../../../extensions/irc/src/channel.js";
import { linePlugin } from "../../../extensions/line/src/channel.js";
import { lineSetupPlugin } from "../../../extensions/line/src/channel.setup.js";
import { setLineRuntime } from "../../../extensions/line/src/runtime.js";
import { matrixPlugin } from "../../../extensions/matrix/src/channel.js";
import { mattermostPlugin } from "../../../extensions/mattermost/src/channel.js";
import { msteamsPlugin } from "../../../extensions/msteams/src/channel.js";
import { nextcloudTalkPlugin } from "../../../extensions/nextcloud-talk/src/channel.js";
import { nostrPlugin } from "../../../extensions/nostr/src/channel.js";
import { signalPlugin } from "../../../extensions/signal/src/channel.js";
import { signalSetupPlugin } from "../../../extensions/signal/src/channel.setup.js";
import { slackPlugin } from "../../../extensions/slack/src/channel.js";
import { slackSetupPlugin } from "../../../extensions/slack/src/channel.setup.js";
import { synologyChatPlugin } from "../../../extensions/synology-chat/src/channel.js";
import { telegramPlugin } from "../../../extensions/telegram/src/channel.js";
import { telegramSetupPlugin } from "../../../extensions/telegram/src/channel.setup.js";
import { setTelegramRuntime } from "../../../extensions/telegram/src/runtime.js";
import { tlonPlugin } from "../../../extensions/tlon/src/channel.js";
import { whatsappPlugin } from "../../../extensions/whatsapp/src/channel.js";
import { whatsappSetupPlugin } from "../../../extensions/whatsapp/src/channel.setup.js";
import { zaloPlugin } from "../../../extensions/zalo/src/channel.js";
import { zalouserPlugin } from "../../../extensions/zalouser/src/channel.js";
import type { ChannelId, ChannelPlugin } from "./types.js";
export const bundledChannelPlugins = [
bluebubblesPlugin,
discordPlugin,
feishuPlugin,
googlechatPlugin,
imessagePlugin,
ircPlugin,
linePlugin,
matrixPlugin,
mattermostPlugin,
msteamsPlugin,
nextcloudTalkPlugin,
nostrPlugin,
signalPlugin,
slackPlugin,
synologyChatPlugin,
telegramPlugin,
tlonPlugin,
whatsappPlugin,
zaloPlugin,
zalouserPlugin,
] as ChannelPlugin[];
export const bundledChannelSetupPlugins = [
telegramSetupPlugin,
whatsappSetupPlugin,
discordSetupPlugin,
ircPlugin,
googlechatPlugin,
slackSetupPlugin,
signalSetupPlugin,
imessageSetupPlugin,
lineSetupPlugin,
] as ChannelPlugin[];
const bundledChannelPluginsById = new Map(
bundledChannelPlugins.map((plugin) => [plugin.id, plugin] as const),
);
export function getBundledChannelPlugin(id: ChannelId): ChannelPlugin | undefined {
return bundledChannelPluginsById.get(id);
}
export function requireBundledChannelPlugin(id: ChannelId): ChannelPlugin {
const plugin = getBundledChannelPlugin(id);
if (!plugin) {
throw new Error(`missing bundled channel plugin: ${id}`);
}
return plugin;
}
export const bundledChannelRuntimeSetters = {
setDiscordRuntime,
setLineRuntime,
setTelegramRuntime,
};

View File

@ -1,33 +1,11 @@
import { expect, vi } from "vitest";
import { bluebubblesPlugin } from "../../../../extensions/bluebubbles/src/channel.js";
import { discordPlugin } from "../../../../extensions/discord/src/channel.js";
import { setDiscordRuntime } from "../../../../extensions/discord/src/runtime.js";
import { feishuPlugin } from "../../../../extensions/feishu/src/channel.js";
import { googlechatPlugin } from "../../../../extensions/googlechat/src/channel.js";
import { imessagePlugin } from "../../../../extensions/imessage/src/channel.js";
import { ircPlugin } from "../../../../extensions/irc/src/channel.js";
import { linePlugin } from "../../../../extensions/line/src/channel.js";
import { setLineRuntime } from "../../../../extensions/line/src/runtime.js";
import { matrixPlugin } from "../../../../extensions/matrix/src/channel.js";
import { mattermostPlugin } from "../../../../extensions/mattermost/src/channel.js";
import { msteamsPlugin } from "../../../../extensions/msteams/src/channel.js";
import { nextcloudTalkPlugin } from "../../../../extensions/nextcloud-talk/src/channel.js";
import { nostrPlugin } from "../../../../extensions/nostr/src/channel.js";
import { signalPlugin } from "../../../../extensions/signal/src/channel.js";
import { slackPlugin } from "../../../../extensions/slack/src/channel.js";
import { synologyChatPlugin } from "../../../../extensions/synology-chat/src/channel.js";
import { telegramPlugin } from "../../../../extensions/telegram/src/channel.js";
import { setTelegramRuntime } from "../../../../extensions/telegram/src/runtime.js";
import { tlonPlugin } from "../../../../extensions/tlon/src/channel.js";
import { whatsappPlugin } from "../../../../extensions/whatsapp/src/channel.js";
import { zaloPlugin } from "../../../../extensions/zalo/src/channel.js";
import { zalouserPlugin } from "../../../../extensions/zalouser/src/channel.js";
import type { OpenClawConfig } from "../../../config/config.js";
import {
resolveDefaultLineAccountId,
resolveLineAccount,
listLineAccountIds,
} from "../../../line/accounts.js";
import { bundledChannelRuntimeSetters, requireBundledChannelPlugin } from "../bundled.js";
import type { ChannelPlugin } from "../types.js";
type PluginContractEntry = {
@ -84,7 +62,7 @@ const telegramGetCapabilitiesMock = vi.fn();
const discordListActionsMock = vi.fn();
const discordGetCapabilitiesMock = vi.fn();
setTelegramRuntime({
bundledChannelRuntimeSetters.setTelegramRuntime({
channel: {
telegram: {
messageActions: {
@ -95,7 +73,7 @@ setTelegramRuntime({
},
} as never);
setDiscordRuntime({
bundledChannelRuntimeSetters.setDiscordRuntime({
channel: {
discord: {
messageActions: {
@ -106,7 +84,7 @@ setDiscordRuntime({
},
} as never);
setLineRuntime({
bundledChannelRuntimeSetters.setLineRuntime({
channel: {
line: {
listLineAccountIds,
@ -118,32 +96,32 @@ setLineRuntime({
} as never);
export const pluginContractRegistry: PluginContractEntry[] = [
{ id: "bluebubbles", plugin: bluebubblesPlugin },
{ id: "discord", plugin: discordPlugin },
{ id: "feishu", plugin: feishuPlugin },
{ id: "googlechat", plugin: googlechatPlugin },
{ id: "imessage", plugin: imessagePlugin },
{ id: "irc", plugin: ircPlugin },
{ id: "line", plugin: linePlugin },
{ id: "matrix", plugin: matrixPlugin },
{ id: "mattermost", plugin: mattermostPlugin },
{ id: "msteams", plugin: msteamsPlugin },
{ id: "nextcloud-talk", plugin: nextcloudTalkPlugin },
{ id: "nostr", plugin: nostrPlugin },
{ id: "signal", plugin: signalPlugin },
{ id: "slack", plugin: slackPlugin },
{ id: "synology-chat", plugin: synologyChatPlugin },
{ id: "telegram", plugin: telegramPlugin },
{ id: "tlon", plugin: tlonPlugin },
{ id: "whatsapp", plugin: whatsappPlugin },
{ id: "zalo", plugin: zaloPlugin },
{ id: "zalouser", plugin: zalouserPlugin },
{ id: "bluebubbles", plugin: requireBundledChannelPlugin("bluebubbles") },
{ id: "discord", plugin: requireBundledChannelPlugin("discord") },
{ id: "feishu", plugin: requireBundledChannelPlugin("feishu") },
{ id: "googlechat", plugin: requireBundledChannelPlugin("googlechat") },
{ id: "imessage", plugin: requireBundledChannelPlugin("imessage") },
{ id: "irc", plugin: requireBundledChannelPlugin("irc") },
{ id: "line", plugin: requireBundledChannelPlugin("line") },
{ id: "matrix", plugin: requireBundledChannelPlugin("matrix") },
{ id: "mattermost", plugin: requireBundledChannelPlugin("mattermost") },
{ id: "msteams", plugin: requireBundledChannelPlugin("msteams") },
{ id: "nextcloud-talk", plugin: requireBundledChannelPlugin("nextcloud-talk") },
{ id: "nostr", plugin: requireBundledChannelPlugin("nostr") },
{ id: "signal", plugin: requireBundledChannelPlugin("signal") },
{ id: "slack", plugin: requireBundledChannelPlugin("slack") },
{ id: "synology-chat", plugin: requireBundledChannelPlugin("synology-chat") },
{ id: "telegram", plugin: requireBundledChannelPlugin("telegram") },
{ id: "tlon", plugin: requireBundledChannelPlugin("tlon") },
{ id: "whatsapp", plugin: requireBundledChannelPlugin("whatsapp") },
{ id: "zalo", plugin: requireBundledChannelPlugin("zalo") },
{ id: "zalouser", plugin: requireBundledChannelPlugin("zalouser") },
];
export const actionContractRegistry: ActionsContractEntry[] = [
{
id: "slack",
plugin: slackPlugin,
plugin: requireBundledChannelPlugin("slack"),
unsupportedAction: "poll",
cases: [
{
@ -217,7 +195,7 @@ export const actionContractRegistry: ActionsContractEntry[] = [
},
{
id: "mattermost",
plugin: mattermostPlugin,
plugin: requireBundledChannelPlugin("mattermost"),
unsupportedAction: "poll",
cases: [
{
@ -265,7 +243,7 @@ export const actionContractRegistry: ActionsContractEntry[] = [
},
{
id: "telegram",
plugin: telegramPlugin,
plugin: requireBundledChannelPlugin("telegram"),
cases: [
{
name: "forwards runtime-backed Telegram actions and capabilities",
@ -283,7 +261,7 @@ export const actionContractRegistry: ActionsContractEntry[] = [
},
{
id: "discord",
plugin: discordPlugin,
plugin: requireBundledChannelPlugin("discord"),
cases: [
{
name: "forwards runtime-backed Discord actions and capabilities",
@ -304,7 +282,7 @@ export const actionContractRegistry: ActionsContractEntry[] = [
export const setupContractRegistry: SetupContractEntry[] = [
{
id: "slack",
plugin: slackPlugin,
plugin: requireBundledChannelPlugin("slack"),
cases: [
{
name: "default account stores tokens and enables the channel",
@ -334,7 +312,7 @@ export const setupContractRegistry: SetupContractEntry[] = [
},
{
id: "mattermost",
plugin: mattermostPlugin,
plugin: requireBundledChannelPlugin("mattermost"),
cases: [
{
name: "default account stores token and normalized base URL",
@ -363,7 +341,7 @@ export const setupContractRegistry: SetupContractEntry[] = [
},
{
id: "line",
plugin: linePlugin,
plugin: requireBundledChannelPlugin("line"),
cases: [
{
name: "default account stores token and secret",
@ -396,7 +374,7 @@ export const setupContractRegistry: SetupContractEntry[] = [
export const statusContractRegistry: StatusContractEntry[] = [
{
id: "slack",
plugin: slackPlugin,
plugin: requireBundledChannelPlugin("slack"),
cases: [
{
name: "configured account produces a configured status snapshot",
@ -424,7 +402,7 @@ export const statusContractRegistry: StatusContractEntry[] = [
},
{
id: "mattermost",
plugin: mattermostPlugin,
plugin: requireBundledChannelPlugin("mattermost"),
cases: [
{
name: "configured account preserves connectivity details in the snapshot",
@ -455,7 +433,7 @@ export const statusContractRegistry: StatusContractEntry[] = [
},
{
id: "line",
plugin: linePlugin,
plugin: requireBundledChannelPlugin("line"),
cases: [
{
name: "configured account produces a webhook status snapshot",

View File

@ -10,7 +10,7 @@ import type {
GroupToolPolicyConfig,
} from "../../config/types.tools.js";
import { resolveExactLineGroupConfigKey } from "../../line/group-keys.js";
import { inspectSlackAccount } from "../../plugin-sdk/slack.js";
import { inspectSlackAccount } from "../../plugin-sdk-internal/slack.js";
import { normalizeAtHashSlug, normalizeHyphenSlug } from "../../shared/string-normalization.js";
import type { ChannelGroupContext } from "./types.js";

View File

@ -1,2 +0,0 @@
// Shim: re-exports from extension
export * from "../../../../extensions/discord/src/normalize.js";

View File

@ -1 +0,0 @@
export * from "../../../../extensions/telegram/src/normalize.js";

View File

@ -1,2 +1,25 @@
// Shim: re-exports from extensions/whatsapp/src/normalize.ts
export * from "../../../../extensions/whatsapp/src/normalize.js";
import { normalizeWhatsAppTarget } from "../../../whatsapp/normalize.js";
import { looksLikeHandleOrPhoneTarget, trimMessagingTarget } from "./shared.js";
export function normalizeWhatsAppMessagingTarget(raw: string): string | undefined {
const trimmed = trimMessagingTarget(raw);
if (!trimmed) {
return undefined;
}
return normalizeWhatsAppTarget(trimmed) ?? undefined;
}
export function normalizeWhatsAppAllowFromEntries(allowFrom: Array<string | number>): string[] {
return allowFrom
.map((entry) => String(entry).trim())
.filter((entry): entry is string => Boolean(entry))
.map((entry) => (entry === "*" ? entry : normalizeWhatsAppTarget(entry)))
.filter((entry): entry is string => Boolean(entry));
}
export function looksLikeWhatsAppTargetId(raw: string): boolean {
return looksLikeHandleOrPhoneTarget({
raw,
prefixPattern: /^whatsapp:/i,
});
}

View File

@ -1,17 +1,9 @@
import { discordSetupPlugin } from "../../../extensions/discord/src/channel.setup.js";
import { googlechatPlugin } from "../../../extensions/googlechat/src/channel.js";
import { imessageSetupPlugin } from "../../../extensions/imessage/src/channel.setup.js";
import { ircPlugin } from "../../../extensions/irc/src/channel.js";
import { lineSetupPlugin } from "../../../extensions/line/src/channel.setup.js";
import { signalSetupPlugin } from "../../../extensions/signal/src/channel.setup.js";
import { slackSetupPlugin } from "../../../extensions/slack/src/channel.setup.js";
import { telegramSetupPlugin } from "../../../extensions/telegram/src/channel.setup.js";
import { whatsappSetupPlugin } from "../../../extensions/whatsapp/src/channel.setup.js";
import {
getActivePluginRegistryVersion,
requireActivePluginRegistry,
} from "../../plugins/runtime.js";
import { CHAT_CHANNEL_ORDER, type ChatChannelId } from "../registry.js";
import { bundledChannelSetupPlugins } from "./bundled.js";
import type { ChannelId, ChannelPlugin } from "./types.js";
type CachedChannelSetupPlugins = {
@ -28,18 +20,6 @@ const EMPTY_CHANNEL_SETUP_CACHE: CachedChannelSetupPlugins = {
let cachedChannelSetupPlugins = EMPTY_CHANNEL_SETUP_CACHE;
const BUNDLED_CHANNEL_SETUP_PLUGINS = [
telegramSetupPlugin,
whatsappSetupPlugin,
discordSetupPlugin,
ircPlugin,
googlechatPlugin,
slackSetupPlugin,
signalSetupPlugin,
imessageSetupPlugin,
lineSetupPlugin,
] as ChannelPlugin[];
function dedupeSetupPlugins(plugins: ChannelPlugin[]): ChannelPlugin[] {
const seen = new Set<string>();
const resolved: ChannelPlugin[] = [];
@ -77,7 +57,7 @@ function resolveCachedChannelSetupPlugins(): CachedChannelSetupPlugins {
const registryPlugins = (registry.channelSetups ?? []).map((entry) => entry.plugin);
const sorted = sortChannelSetupPlugins(
registryPlugins.length > 0 ? registryPlugins : BUNDLED_CHANNEL_SETUP_PLUGINS,
registryPlugins.length > 0 ? registryPlugins : bundledChannelSetupPlugins,
);
const byId = new Map<string, ChannelPlugin>();
for (const plugin of sorted) {

View File

@ -0,0 +1 @@
export { promptResolvedAllowFrom } from "./setup-wizard-helpers.js";

View File

@ -1,11 +1,11 @@
import { handleSlackAction, type SlackActionContext } from "../../agents/tools/slack-actions.js";
import { handleSlackMessageAction } from "../../plugin-sdk/slack-message-actions.js";
import {
extractSlackToolSend,
isSlackInteractiveRepliesEnabled,
listSlackMessageActions,
resolveSlackChannelId,
} from "../../plugin-sdk/slack.js";
} from "../../plugin-sdk-internal/slack.js";
import { handleSlackMessageAction } from "../../plugin-sdk/slack-message-actions.js";
import type { ChannelMessageActionAdapter } from "./types.js";
export function createSlackActions(providerId: string): ChannelMessageActionAdapter {

View File

@ -1,2 +0,0 @@
// Shim: re-exports from extension
export * from "../../../../extensions/discord/src/status-issues.js";

View File

@ -1 +0,0 @@
export * from "../../../../extensions/telegram/src/status-issues.js";

View File

@ -1,2 +0,0 @@
// Shim: re-exports from extensions/whatsapp/src/status-issues.ts
export * from "../../../../extensions/whatsapp/src/status-issues.js";

View File

@ -1,4 +1,2 @@
export {
inspectDiscordAccount,
type InspectedDiscordAccount,
} from "../../extensions/discord/src/account-inspect.js";
export { inspectDiscordAccount } from "../plugin-sdk-internal/discord.js";
export type { InspectedDiscordAccount } from "../plugin-sdk-internal/discord.js";

View File

@ -1,4 +1,2 @@
export {
inspectSlackAccount,
type InspectedSlackAccount,
} from "../../extensions/slack/src/account-inspect.js";
export { inspectSlackAccount } from "../plugin-sdk-internal/slack.js";
export type { InspectedSlackAccount } from "../plugin-sdk-internal/slack.js";

View File

@ -1,4 +1,2 @@
export {
inspectTelegramAccount,
type InspectedTelegramAccount,
} from "../../extensions/telegram/src/account-inspect.js";
export { inspectTelegramAccount } from "../plugin-sdk-internal/telegram.js";
export type { InspectedTelegramAccount } from "../plugin-sdk-internal/telegram.js";

View File

@ -19,32 +19,32 @@ const sendFns = vi.hoisted(() => ({
imessage: vi.fn(async () => ({ messageId: "i1", chatId: "imessage:1" })),
}));
vi.mock("../channels/web/index.js", () => {
vi.mock("../plugin-sdk-internal/whatsapp.js", () => {
moduleLoads.whatsapp();
return { sendMessageWhatsApp: sendFns.whatsapp };
});
vi.mock("../../extensions/telegram/src/send.js", () => {
vi.mock("../plugin-sdk-internal/telegram.js", () => {
moduleLoads.telegram();
return { sendMessageTelegram: sendFns.telegram };
});
vi.mock("../../extensions/discord/src/send.js", () => {
vi.mock("../plugin-sdk-internal/discord.js", () => {
moduleLoads.discord();
return { sendMessageDiscord: sendFns.discord };
});
vi.mock("../../extensions/slack/src/send.js", () => {
vi.mock("../plugin-sdk-internal/slack.js", () => {
moduleLoads.slack();
return { sendMessageSlack: sendFns.slack };
});
vi.mock("../../extensions/signal/src/send.js", () => {
vi.mock("../plugin-sdk-internal/signal.js", () => {
moduleLoads.signal();
return { sendMessageSignal: sendFns.signal };
});
vi.mock("../../extensions/imessage/src/send.js", () => {
vi.mock("../plugin-sdk-internal/imessage.js", () => {
moduleLoads.imessage();
return { sendMessageIMessage: sendFns.imessage };
});

View File

@ -35,32 +35,32 @@ export function createDefaultDeps(): CliDeps {
return {
whatsapp: createLazySender(
"whatsapp",
() => import("../channels/web/index.js") as Promise<Record<string, unknown>>,
() => import("../plugin-sdk-internal/whatsapp.js") as Promise<Record<string, unknown>>,
"sendMessageWhatsApp",
),
telegram: createLazySender(
"telegram",
() => import("../../extensions/telegram/src/send.js") as Promise<Record<string, unknown>>,
() => import("../plugin-sdk-internal/telegram.js") as Promise<Record<string, unknown>>,
"sendMessageTelegram",
),
discord: createLazySender(
"discord",
() => import("../../extensions/discord/src/send.js") as Promise<Record<string, unknown>>,
() => import("../plugin-sdk-internal/discord.js") as Promise<Record<string, unknown>>,
"sendMessageDiscord",
),
slack: createLazySender(
"slack",
() => import("../../extensions/slack/src/send.js") as Promise<Record<string, unknown>>,
() => import("../plugin-sdk-internal/slack.js") as Promise<Record<string, unknown>>,
"sendMessageSlack",
),
signal: createLazySender(
"signal",
() => import("../../extensions/signal/src/send.js") as Promise<Record<string, unknown>>,
() => import("../plugin-sdk-internal/signal.js") as Promise<Record<string, unknown>>,
"sendMessageSignal",
),
imessage: createLazySender(
"imessage",
() => import("../../extensions/imessage/src/send.js") as Promise<Record<string, unknown>>,
() => import("../plugin-sdk-internal/imessage.js") as Promise<Record<string, unknown>>,
"sendMessageIMessage",
),
};
@ -70,4 +70,4 @@ export function createOutboundSendDeps(deps: CliDeps): OutboundSendDeps {
return createOutboundSendDepsFromCliSource(deps);
}
export { logWebSelfId } from "../../extensions/whatsapp/src/auth-store.js";
export { logWebSelfId } from "../plugin-sdk-internal/whatsapp.js";

View File

@ -11,6 +11,11 @@ import { enablePluginInConfig } from "../plugins/enable.js";
import { installPluginFromNpmSpec, installPluginFromPath } from "../plugins/install.js";
import { recordPluginInstall } from "../plugins/installs.js";
import { clearPluginManifestRegistryCache } from "../plugins/manifest-registry.js";
import {
installPluginFromMarketplace,
listMarketplacePlugins,
resolveMarketplaceInstallShortcut,
} from "../plugins/marketplace.js";
import type { PluginRecord } from "../plugins/registry.js";
import { applyExclusiveSlotSelection } from "../plugins/slots.js";
import { resolvePluginSourceRoots, formatPluginSourceForTable } from "../plugins/source-display.js";
@ -46,6 +51,10 @@ export type PluginUpdateOptions = {
dryRun?: boolean;
};
export type PluginMarketplaceListOptions = {
json?: boolean;
};
export type PluginUninstallOptions = {
keepFiles?: boolean;
keepConfig?: boolean;
@ -203,9 +212,65 @@ async function installBundledPluginSource(params: {
async function runPluginInstallCommand(params: {
raw: string;
opts: { link?: boolean; pin?: boolean };
opts: { link?: boolean; pin?: boolean; marketplace?: string };
}) {
const { raw, opts } = params;
const shorthand = !params.opts.marketplace
? await resolveMarketplaceInstallShortcut(params.raw)
: null;
if (shorthand?.ok === false) {
defaultRuntime.error(shorthand.error);
process.exit(1);
}
const raw = shorthand?.ok ? shorthand.plugin : params.raw;
const opts = {
...params.opts,
marketplace:
params.opts.marketplace ?? (shorthand?.ok ? shorthand.marketplaceSource : undefined),
};
if (opts.marketplace) {
if (opts.link) {
defaultRuntime.error("`--link` is not supported with `--marketplace`.");
process.exit(1);
}
if (opts.pin) {
defaultRuntime.error("`--pin` is not supported with `--marketplace`.");
process.exit(1);
}
const cfg = loadConfig();
const result = await installPluginFromMarketplace({
marketplace: opts.marketplace,
plugin: raw,
logger: createPluginInstallLogger(),
});
if (!result.ok) {
defaultRuntime.error(result.error);
process.exit(1);
}
clearPluginManifestRegistryCache();
let next = enablePluginInConfig(cfg, result.pluginId).config;
next = recordPluginInstall(next, {
pluginId: result.pluginId,
source: "marketplace",
installPath: result.targetDir,
version: result.version,
marketplaceName: result.marketplaceName,
marketplaceSource: result.marketplaceSource,
marketplacePlugin: result.marketplacePlugin,
});
const slotResult = applySlotSelectionForPlugin(next, result.pluginId);
next = slotResult.config;
await writeConfigFile(next);
logSlotWarnings(slotResult.warnings);
defaultRuntime.log(`Installed plugin: ${result.pluginId}`);
defaultRuntime.log(`Restart the gateway to load plugins.`);
return;
}
const fileSpec = resolveFileNpmSpecToLocalPath(raw);
if (fileSpec && !fileSpec.ok) {
defaultRuntime.error(fileSpec.error);
@ -734,17 +799,24 @@ export function registerPluginsCli(program: Command) {
plugins
.command("install")
.description("Install a plugin (path, archive, or npm spec)")
.argument("<path-or-spec>", "Path (.ts/.js/.zip/.tgz/.tar.gz) or an npm package spec")
.description("Install a plugin (path, archive, npm spec, or marketplace entry)")
.argument(
"<path-or-spec-or-plugin>",
"Path (.ts/.js/.zip/.tgz/.tar.gz), npm package spec, or marketplace plugin name",
)
.option("-l, --link", "Link a local path instead of copying", false)
.option("--pin", "Record npm installs as exact resolved <name>@<version>", false)
.action(async (raw: string, opts: { link?: boolean; pin?: boolean }) => {
.option(
"--marketplace <source>",
"Install a Claude marketplace plugin from a local repo/path or git/GitHub source",
)
.action(async (raw: string, opts: { link?: boolean; pin?: boolean; marketplace?: string }) => {
await runPluginInstallCommand({ raw, opts });
});
plugins
.command("update")
.description("Update installed plugins (npm installs only)")
.description("Update installed plugins (npm and marketplace installs)")
.argument("[id]", "Plugin id (omit with --all)")
.option("--all", "Update all tracked plugins", false)
.option("--dry-run", "Show what would change without writing", false)
@ -755,7 +827,7 @@ export function registerPluginsCli(program: Command) {
if (targets.length === 0) {
if (opts.all) {
defaultRuntime.log("No npm-installed plugins to update.");
defaultRuntime.log("No tracked plugins to update.");
return;
}
defaultRuntime.error("Provide a plugin id or use --all.");
@ -839,4 +911,54 @@ export function registerPluginsCli(program: Command) {
lines.push(`${theme.muted("Docs:")} ${docs}`);
defaultRuntime.log(lines.join("\n"));
});
const marketplace = plugins
.command("marketplace")
.description("Inspect Claude-compatible plugin marketplaces");
marketplace
.command("list")
.description("List plugins published by a marketplace source")
.argument("<source>", "Local marketplace path/repo or git/GitHub source")
.option("--json", "Print JSON")
.action(async (source: string, opts: PluginMarketplaceListOptions) => {
const result = await listMarketplacePlugins({
marketplace: source,
logger: createPluginInstallLogger(),
});
if (!result.ok) {
defaultRuntime.error(result.error);
process.exit(1);
}
if (opts.json) {
defaultRuntime.log(
JSON.stringify(
{
source: result.sourceLabel,
name: result.manifest.name,
version: result.manifest.version,
plugins: result.manifest.plugins,
},
null,
2,
),
);
return;
}
if (result.manifest.plugins.length === 0) {
defaultRuntime.log(`No plugins found in marketplace ${result.sourceLabel}.`);
return;
}
defaultRuntime.log(
`${theme.heading("Marketplace")} ${theme.muted(result.manifest.name ?? result.sourceLabel)}`,
);
for (const plugin of result.manifest.plugins) {
const suffix = plugin.version ? theme.muted(` v${plugin.version}`) : "";
const desc = plugin.description ? ` - ${theme.muted(plugin.description)}` : "";
defaultRuntime.log(`${theme.command(plugin.name)}${suffix}${desc}`);
}
});
}

View File

@ -1,85 +1,15 @@
import { ensureAuthProfileStore, resolveAuthProfileOrder } from "../agents/auth-profiles.js";
import type { SecretInput } from "../config/types.secrets.js";
import { normalizeApiKeyInput, validateApiKeyInput } from "./auth-choice.api-key.js";
import { ensureApiKeyFromOptionEnvOrPrompt } from "./auth-choice.apply-helpers.js";
import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.js";
import { ensureModelAllowlistEntry } from "./model-allowlist.js";
import { applyPrimaryModel } from "./model-picker.js";
import type { ApiKeyStorageOptions } from "./onboard-auth.credentials.js";
import {
applyAuthProfileConfig,
applyKilocodeConfig,
applyKilocodeProviderConfig,
applyKimiCodeConfig,
applyKimiCodeProviderConfig,
applyLitellmConfig,
applyLitellmProviderConfig,
applyMistralConfig,
applyMistralProviderConfig,
applyModelStudioConfig,
applyModelStudioConfigCn,
applyModelStudioProviderConfig,
applyModelStudioProviderConfigCn,
applyModelStudioStandardConfig,
applyModelStudioStandardConfigCn,
applyModelStudioStandardProviderConfig,
applyModelStudioStandardProviderConfigCn,
applyMoonshotConfig,
applyMoonshotConfigCn,
applyMoonshotProviderConfig,
applyMoonshotProviderConfigCn,
applyOpencodeGoConfig,
applyOpencodeGoProviderConfig,
applyOpencodeZenConfig,
applyOpencodeZenProviderConfig,
applyQianfanConfig,
applyQianfanProviderConfig,
applySyntheticConfig,
applySyntheticProviderConfig,
applyTogetherConfig,
applyTogetherProviderConfig,
applyVeniceConfig,
applyVeniceProviderConfig,
applyVercelAiGatewayConfig,
applyVercelAiGatewayProviderConfig,
applyXaiConfig,
applyXaiProviderConfig,
applyXiaomiConfig,
applyXiaomiProviderConfig,
KILOCODE_DEFAULT_MODEL_REF,
KIMI_CODING_MODEL_REF,
LITELLM_DEFAULT_MODEL_REF,
MISTRAL_DEFAULT_MODEL_REF,
MODELSTUDIO_DEFAULT_MODEL_REF,
MOONSHOT_DEFAULT_MODEL_REF,
QIANFAN_DEFAULT_MODEL_REF,
setKilocodeApiKey,
setKimiCodingApiKey,
setLitellmApiKey,
setMistralApiKey,
setModelStudioApiKey,
setMoonshotApiKey,
setOpencodeGoApiKey,
setOpencodeZenApiKey,
setQianfanApiKey,
setSyntheticApiKey,
setTogetherApiKey,
setVeniceApiKey,
setVercelAiGatewayApiKey,
setVolcengineApiKey,
setByteplusApiKey,
setXaiApiKey,
setXiaomiApiKey,
SYNTHETIC_DEFAULT_MODEL_REF,
TOGETHER_DEFAULT_MODEL_REF,
VENICE_DEFAULT_MODEL_REF,
VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF,
XAI_DEFAULT_MODEL_REF,
XIAOMI_DEFAULT_MODEL_REF,
} from "./onboard-auth.js";
import type { AuthChoice, SecretInputMode } from "./onboard-types.js";
import { OPENCODE_GO_DEFAULT_MODEL_REF } from "./opencode-go-model-default.js";
import { OPENCODE_ZEN_DEFAULT_MODEL } from "./opencode-zen-model-default.js";
import type { SecretInputMode } from "./onboard-types.js";
type ApiKeyProviderConfigApplier = (
config: ApplyAuthChoiceParams["config"],
@ -94,7 +24,7 @@ type ApplyProviderDefaultModel = (args: {
type ApplyApiKeyProviderParams = {
params: ApplyAuthChoiceParams;
authChoice: AuthChoice;
authChoice: string;
config: ApplyAuthChoiceParams["config"];
setConfig: (config: ApplyAuthChoiceParams["config"]) => void;
getConfig: () => ApplyAuthChoiceParams["config"];
@ -104,426 +34,6 @@ type ApplyApiKeyProviderParams = {
getAgentModelOverride: () => string | undefined;
};
type SimpleApiKeyProviderFlow = {
provider: Parameters<typeof ensureApiKeyFromOptionEnvOrPrompt>[0]["provider"];
profileId: string;
expectedProviders: string[];
envLabel: string;
promptMessage: string;
setCredential: (
apiKey: SecretInput,
agentDir?: string,
options?: ApiKeyStorageOptions,
) => void | Promise<void>;
defaultModel: string;
applyDefaultConfig: ApiKeyProviderConfigApplier;
applyProviderConfig: ApiKeyProviderConfigApplier;
tokenProvider?: string;
normalize?: (value: string) => string;
validate?: (value: string) => string | undefined;
noteDefault?: string;
noteMessage?: string;
noteTitle?: string;
};
const VOLCENGINE_DEFAULT_MODEL_REF = "volcengine-plan/ark-code-latest";
const BYTEPLUS_DEFAULT_MODEL_REF = "byteplus-plan/ark-code-latest";
const SIMPLE_API_KEY_PROVIDER_FLOWS: Partial<Record<AuthChoice, SimpleApiKeyProviderFlow>> = {
"ai-gateway-api-key": {
provider: "vercel-ai-gateway",
profileId: "vercel-ai-gateway:default",
expectedProviders: ["vercel-ai-gateway"],
envLabel: "AI_GATEWAY_API_KEY",
promptMessage: "Enter Vercel AI Gateway API key",
setCredential: setVercelAiGatewayApiKey,
defaultModel: VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF,
applyDefaultConfig: applyVercelAiGatewayConfig,
applyProviderConfig: applyVercelAiGatewayProviderConfig,
noteDefault: VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF,
},
"moonshot-api-key": {
provider: "moonshot",
profileId: "moonshot:default",
expectedProviders: ["moonshot"],
envLabel: "MOONSHOT_API_KEY",
promptMessage: "Enter Moonshot API key",
setCredential: setMoonshotApiKey,
defaultModel: MOONSHOT_DEFAULT_MODEL_REF,
applyDefaultConfig: applyMoonshotConfig,
applyProviderConfig: applyMoonshotProviderConfig,
},
"moonshot-api-key-cn": {
provider: "moonshot",
profileId: "moonshot:default",
expectedProviders: ["moonshot"],
envLabel: "MOONSHOT_API_KEY",
promptMessage: "Enter Moonshot API key (.cn)",
setCredential: setMoonshotApiKey,
defaultModel: MOONSHOT_DEFAULT_MODEL_REF,
applyDefaultConfig: applyMoonshotConfigCn,
applyProviderConfig: applyMoonshotProviderConfigCn,
},
"kimi-code-api-key": {
provider: "kimi-coding",
profileId: "kimi-coding:default",
expectedProviders: ["kimi-code", "kimi-coding"],
envLabel: "KIMI_API_KEY",
promptMessage: "Enter Kimi Coding API key",
setCredential: setKimiCodingApiKey,
defaultModel: KIMI_CODING_MODEL_REF,
applyDefaultConfig: applyKimiCodeConfig,
applyProviderConfig: applyKimiCodeProviderConfig,
noteDefault: KIMI_CODING_MODEL_REF,
noteMessage: [
"Kimi Coding uses a dedicated endpoint and API key.",
"Get your API key at: https://www.kimi.com/code/en",
].join("\n"),
noteTitle: "Kimi Coding",
},
"xiaomi-api-key": {
provider: "xiaomi",
profileId: "xiaomi:default",
expectedProviders: ["xiaomi"],
envLabel: "XIAOMI_API_KEY",
promptMessage: "Enter Xiaomi API key",
setCredential: setXiaomiApiKey,
defaultModel: XIAOMI_DEFAULT_MODEL_REF,
applyDefaultConfig: applyXiaomiConfig,
applyProviderConfig: applyXiaomiProviderConfig,
noteDefault: XIAOMI_DEFAULT_MODEL_REF,
},
"xai-api-key": {
provider: "xai",
profileId: "xai:default",
expectedProviders: ["xai"],
envLabel: "XAI_API_KEY",
promptMessage: "Enter xAI API key",
setCredential: setXaiApiKey,
defaultModel: XAI_DEFAULT_MODEL_REF,
applyDefaultConfig: applyXaiConfig,
applyProviderConfig: applyXaiProviderConfig,
noteDefault: XAI_DEFAULT_MODEL_REF,
},
"mistral-api-key": {
provider: "mistral",
profileId: "mistral:default",
expectedProviders: ["mistral"],
envLabel: "MISTRAL_API_KEY",
promptMessage: "Enter Mistral API key",
setCredential: setMistralApiKey,
defaultModel: MISTRAL_DEFAULT_MODEL_REF,
applyDefaultConfig: applyMistralConfig,
applyProviderConfig: applyMistralProviderConfig,
noteDefault: MISTRAL_DEFAULT_MODEL_REF,
},
"venice-api-key": {
provider: "venice",
profileId: "venice:default",
expectedProviders: ["venice"],
envLabel: "VENICE_API_KEY",
promptMessage: "Enter Venice AI API key",
setCredential: setVeniceApiKey,
defaultModel: VENICE_DEFAULT_MODEL_REF,
applyDefaultConfig: applyVeniceConfig,
applyProviderConfig: applyVeniceProviderConfig,
noteDefault: VENICE_DEFAULT_MODEL_REF,
noteMessage: [
"Venice AI provides privacy-focused inference with uncensored models.",
"Get your API key at: https://venice.ai/settings/api",
"Supports 'private' (fully private) and 'anonymized' (proxy) modes.",
].join("\n"),
noteTitle: "Venice AI",
},
"opencode-zen": {
provider: "opencode",
profileId: "opencode:default",
expectedProviders: ["opencode", "opencode-go"],
envLabel: "OPENCODE_API_KEY",
promptMessage: "Enter OpenCode API key",
setCredential: setOpencodeZenApiKey,
defaultModel: OPENCODE_ZEN_DEFAULT_MODEL,
applyDefaultConfig: applyOpencodeZenConfig,
applyProviderConfig: applyOpencodeZenProviderConfig,
noteDefault: OPENCODE_ZEN_DEFAULT_MODEL,
noteMessage: [
"OpenCode uses one API key across the Zen and Go catalogs.",
"Zen provides access to Claude, GPT, Gemini, and more models.",
"Get your API key at: https://opencode.ai/auth",
"Choose the Zen catalog when you want the curated multi-model proxy.",
].join("\n"),
noteTitle: "OpenCode",
},
"opencode-go": {
provider: "opencode-go",
profileId: "opencode-go:default",
expectedProviders: ["opencode", "opencode-go"],
envLabel: "OPENCODE_API_KEY",
promptMessage: "Enter OpenCode API key",
setCredential: setOpencodeGoApiKey,
defaultModel: OPENCODE_GO_DEFAULT_MODEL_REF,
applyDefaultConfig: applyOpencodeGoConfig,
applyProviderConfig: applyOpencodeGoProviderConfig,
noteDefault: OPENCODE_GO_DEFAULT_MODEL_REF,
noteMessage: [
"OpenCode uses one API key across the Zen and Go catalogs.",
"Go provides access to Kimi, GLM, and MiniMax models through the Go catalog.",
"Get your API key at: https://opencode.ai/auth",
"Choose the Go catalog when you want the OpenCode-hosted Kimi/GLM/MiniMax lineup.",
].join("\n"),
noteTitle: "OpenCode",
},
"together-api-key": {
provider: "together",
profileId: "together:default",
expectedProviders: ["together"],
envLabel: "TOGETHER_API_KEY",
promptMessage: "Enter Together AI API key",
setCredential: setTogetherApiKey,
defaultModel: TOGETHER_DEFAULT_MODEL_REF,
applyDefaultConfig: applyTogetherConfig,
applyProviderConfig: applyTogetherProviderConfig,
noteDefault: TOGETHER_DEFAULT_MODEL_REF,
noteMessage: [
"Together AI provides access to leading open-source models including Llama, DeepSeek, Qwen, and more.",
"Get your API key at: https://api.together.xyz/settings/api-keys",
].join("\n"),
noteTitle: "Together AI",
},
"qianfan-api-key": {
provider: "qianfan",
profileId: "qianfan:default",
expectedProviders: ["qianfan"],
envLabel: "QIANFAN_API_KEY",
promptMessage: "Enter QIANFAN API key",
setCredential: setQianfanApiKey,
defaultModel: QIANFAN_DEFAULT_MODEL_REF,
applyDefaultConfig: applyQianfanConfig,
applyProviderConfig: applyQianfanProviderConfig,
noteDefault: QIANFAN_DEFAULT_MODEL_REF,
noteMessage: [
"Get your API key at: https://console.bce.baidu.com/qianfan/ais/console/apiKey",
"API key format: bce-v3/ALTAK-...",
].join("\n"),
noteTitle: "QIANFAN",
},
"kilocode-api-key": {
provider: "kilocode",
profileId: "kilocode:default",
expectedProviders: ["kilocode"],
envLabel: "KILOCODE_API_KEY",
promptMessage: "Enter Kilo Gateway API key",
setCredential: setKilocodeApiKey,
defaultModel: KILOCODE_DEFAULT_MODEL_REF,
applyDefaultConfig: applyKilocodeConfig,
applyProviderConfig: applyKilocodeProviderConfig,
noteDefault: KILOCODE_DEFAULT_MODEL_REF,
},
"modelstudio-api-key-cn": {
provider: "modelstudio",
profileId: "modelstudio:default",
expectedProviders: ["modelstudio"],
envLabel: "MODELSTUDIO_API_KEY",
promptMessage: "Enter Alibaba Cloud Model Studio Coding Plan API key (China)",
setCredential: setModelStudioApiKey,
defaultModel: MODELSTUDIO_DEFAULT_MODEL_REF,
applyDefaultConfig: applyModelStudioConfigCn,
applyProviderConfig: applyModelStudioProviderConfigCn,
noteDefault: MODELSTUDIO_DEFAULT_MODEL_REF,
noteMessage: [
"Get your API key at: https://bailian.console.aliyun.com/",
"Endpoint: coding.dashscope.aliyuncs.com",
"Models: qwen3.5-plus, glm-4.7, kimi-k2.5, MiniMax-M2.5, etc.",
].join("\n"),
noteTitle: "Alibaba Cloud Model Studio Coding Plan (China)",
normalize: (value) => String(value ?? "").trim(),
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
},
"modelstudio-api-key": {
provider: "modelstudio",
profileId: "modelstudio:default",
expectedProviders: ["modelstudio"],
envLabel: "MODELSTUDIO_API_KEY",
promptMessage: "Enter Alibaba Cloud Model Studio Coding Plan API key (Global/Intl)",
setCredential: setModelStudioApiKey,
defaultModel: MODELSTUDIO_DEFAULT_MODEL_REF,
applyDefaultConfig: applyModelStudioConfig,
applyProviderConfig: applyModelStudioProviderConfig,
noteDefault: MODELSTUDIO_DEFAULT_MODEL_REF,
noteMessage: [
"Get your API key at: https://bailian.console.aliyun.com/",
"Endpoint: coding-intl.dashscope.aliyuncs.com",
"Models: qwen3.5-plus, glm-4.7, kimi-k2.5, MiniMax-M2.5, etc.",
].join("\n"),
noteTitle: "Alibaba Cloud Model Studio Coding Plan (Global/Intl)",
normalize: (value) => String(value ?? "").trim(),
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
},
"modelstudio-standard-api-key-cn": {
provider: "modelstudio",
profileId: "modelstudio:default",
expectedProviders: ["modelstudio"],
envLabel: "MODELSTUDIO_API_KEY",
promptMessage: "Enter Alibaba Cloud Model Studio API key (China)",
setCredential: setModelStudioApiKey,
defaultModel: MODELSTUDIO_DEFAULT_MODEL_REF,
applyDefaultConfig: applyModelStudioStandardConfigCn,
applyProviderConfig: applyModelStudioStandardProviderConfigCn,
noteDefault: MODELSTUDIO_DEFAULT_MODEL_REF,
noteMessage: [
"Get your API key at: https://bailian.console.aliyun.com/",
"Endpoint: dashscope.aliyuncs.com/compatible-mode/v1",
"Models: qwen3.5-plus, qwen3.5-flash, qwen3-coder-plus, etc.",
].join("\n"),
noteTitle: "Alibaba Cloud Model Studio (China)",
normalize: (value) => String(value ?? "").trim(),
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
},
"modelstudio-standard-api-key": {
provider: "modelstudio",
profileId: "modelstudio:default",
expectedProviders: ["modelstudio"],
envLabel: "MODELSTUDIO_API_KEY",
promptMessage: "Enter Alibaba Cloud Model Studio API key (Global/Intl)",
setCredential: setModelStudioApiKey,
defaultModel: MODELSTUDIO_DEFAULT_MODEL_REF,
applyDefaultConfig: applyModelStudioStandardConfig,
applyProviderConfig: applyModelStudioStandardProviderConfig,
noteDefault: MODELSTUDIO_DEFAULT_MODEL_REF,
noteMessage: [
"Get your API key at: https://modelstudio.console.alibabacloud.com/",
"Endpoint: dashscope-intl.aliyuncs.com/compatible-mode/v1",
"Models: qwen3.5-plus, qwen3.5-flash, qwen3-coder-plus, etc.",
].join("\n"),
noteTitle: "Alibaba Cloud Model Studio (Global/Intl)",
normalize: (value) => String(value ?? "").trim(),
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
},
"volcengine-api-key": {
provider: "volcengine",
profileId: "volcengine:default",
expectedProviders: ["volcengine"],
envLabel: "VOLCANO_ENGINE_API_KEY",
promptMessage: "Enter Volcano Engine API key",
setCredential: setVolcengineApiKey,
defaultModel: VOLCENGINE_DEFAULT_MODEL_REF,
applyDefaultConfig: (cfg) => applyPrimaryModel(cfg, VOLCENGINE_DEFAULT_MODEL_REF),
applyProviderConfig: (cfg) =>
ensureModelAllowlistEntry({
cfg,
modelRef: VOLCENGINE_DEFAULT_MODEL_REF,
}),
noteDefault: VOLCENGINE_DEFAULT_MODEL_REF,
},
"byteplus-api-key": {
provider: "byteplus",
profileId: "byteplus:default",
expectedProviders: ["byteplus"],
envLabel: "BYTEPLUS_API_KEY",
promptMessage: "Enter BytePlus API key",
setCredential: setByteplusApiKey,
defaultModel: BYTEPLUS_DEFAULT_MODEL_REF,
applyDefaultConfig: (cfg) => applyPrimaryModel(cfg, BYTEPLUS_DEFAULT_MODEL_REF),
applyProviderConfig: (cfg) =>
ensureModelAllowlistEntry({
cfg,
modelRef: BYTEPLUS_DEFAULT_MODEL_REF,
}),
noteDefault: BYTEPLUS_DEFAULT_MODEL_REF,
},
"synthetic-api-key": {
provider: "synthetic",
profileId: "synthetic:default",
expectedProviders: ["synthetic"],
envLabel: "SYNTHETIC_API_KEY",
promptMessage: "Enter Synthetic API key",
setCredential: setSyntheticApiKey,
defaultModel: SYNTHETIC_DEFAULT_MODEL_REF,
applyDefaultConfig: applySyntheticConfig,
applyProviderConfig: applySyntheticProviderConfig,
normalize: (value) => String(value ?? "").trim(),
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
},
};
async function applyApiKeyProviderWithDefaultModel({
params,
config,
setConfig,
getConfig,
normalizedTokenProvider,
requestedSecretInputMode,
applyProviderDefaultModel,
getAgentModelOverride,
provider,
profileId,
expectedProviders,
envLabel,
promptMessage,
setCredential,
defaultModel,
applyDefaultConfig,
applyProviderConfig,
noteMessage,
noteTitle,
tokenProvider = normalizedTokenProvider,
normalize = normalizeApiKeyInput,
validate = validateApiKeyInput,
noteDefault = defaultModel,
}: ApplyApiKeyProviderParams & {
provider: Parameters<typeof ensureApiKeyFromOptionEnvOrPrompt>[0]["provider"];
profileId: string;
expectedProviders: string[];
envLabel: string;
promptMessage: string;
setCredential: (apiKey: SecretInput, mode?: SecretInputMode) => void | Promise<void>;
defaultModel: string;
applyDefaultConfig: ApiKeyProviderConfigApplier;
applyProviderConfig: ApiKeyProviderConfigApplier;
noteMessage?: string;
noteTitle?: string;
tokenProvider?: string;
normalize?: (value: string) => string;
validate?: (value: string) => string | undefined;
noteDefault?: string;
}): Promise<ApplyAuthChoiceResult> {
let nextConfig = config;
await ensureApiKeyFromOptionEnvOrPrompt({
token: params.opts?.token,
provider,
tokenProvider,
secretInputMode: requestedSecretInputMode,
config: nextConfig,
expectedProviders,
envLabel,
promptMessage,
setCredential: async (apiKey, mode) => {
await setCredential(apiKey, mode);
},
noteMessage,
noteTitle,
normalize,
validate,
prompter: params.prompter,
});
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId,
provider,
mode: "api_key",
});
setConfig(nextConfig);
await applyProviderDefaultModel({
defaultModel,
applyDefaultConfig,
applyProviderConfig,
noteDefault,
});
return { config: getConfig(), agentModelOverride: getAgentModelOverride() };
}
export async function applyLiteLlmApiKeyProvider({
params,
authChoice,
@ -588,50 +98,3 @@ export async function applyLiteLlmApiKeyProvider({
});
return { config: getConfig(), agentModelOverride: getAgentModelOverride() };
}
export async function applySimpleAuthChoiceApiProvider({
params,
authChoice,
config,
setConfig,
getConfig,
normalizedTokenProvider,
requestedSecretInputMode,
applyProviderDefaultModel,
getAgentModelOverride,
}: ApplyApiKeyProviderParams): Promise<ApplyAuthChoiceResult | null> {
const simpleApiKeyProviderFlow = SIMPLE_API_KEY_PROVIDER_FLOWS[authChoice];
if (!simpleApiKeyProviderFlow) {
return null;
}
return await applyApiKeyProviderWithDefaultModel({
params,
authChoice,
config,
setConfig,
getConfig,
normalizedTokenProvider,
requestedSecretInputMode,
applyProviderDefaultModel,
getAgentModelOverride,
provider: simpleApiKeyProviderFlow.provider,
profileId: simpleApiKeyProviderFlow.profileId,
expectedProviders: simpleApiKeyProviderFlow.expectedProviders,
envLabel: simpleApiKeyProviderFlow.envLabel,
promptMessage: simpleApiKeyProviderFlow.promptMessage,
setCredential: async (apiKey, mode) =>
simpleApiKeyProviderFlow.setCredential(apiKey, params.agentDir, {
secretInputMode: mode ?? requestedSecretInputMode,
}),
defaultModel: simpleApiKeyProviderFlow.defaultModel,
applyDefaultConfig: simpleApiKeyProviderFlow.applyDefaultConfig,
applyProviderConfig: simpleApiKeyProviderFlow.applyProviderConfig,
noteDefault: simpleApiKeyProviderFlow.noteDefault,
noteMessage: simpleApiKeyProviderFlow.noteMessage,
noteTitle: simpleApiKeyProviderFlow.noteTitle,
tokenProvider: simpleApiKeyProviderFlow.tokenProvider,
normalize: simpleApiKeyProviderFlow.normalize,
validate: simpleApiKeyProviderFlow.validate,
});
}

Some files were not shown because too many files have changed in this diff Show More