Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled

Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11),
free to diverge. Tree copied sans upstream .git; upstream remote added for
future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19.
Preserves docs/ARCHITECTURE.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
2026-07-05 09:36:54 +00:00
parent 3216769225
commit bedb527145
21108 changed files with 6010766 additions and 0 deletions

View File

@@ -0,0 +1,524 @@
/**
* Twitch setup wizard surface for CLI setup.
*/
import { normalizeOptionalAccountId } from "openclaw/plugin-sdk/account-id";
import { getChatChannelMeta, type ChannelPlugin } from "openclaw/plugin-sdk/core";
import {
formatDocsLink,
type ChannelSetupAdapter,
type ChannelSetupDmPolicy,
type ChannelSetupWizard,
type OpenClawConfig,
type WizardPrompter,
normalizeAccountId,
createSetupTranslator,
} from "openclaw/plugin-sdk/setup";
import { normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
DEFAULT_ACCOUNT_ID,
getAccountConfig,
listAccountIds,
resolveDefaultTwitchAccountId,
resolveTwitchAccountContext,
} from "./config.js";
import type { TwitchAccountConfig, TwitchRole } from "./types.js";
import { isAccountConfigured } from "./utils/twitch.js";
const channel = "twitch" as const;
const t = createSetupTranslator();
const INVALID_ACCOUNT_ID_MESSAGE = "Invalid Twitch account id";
function normalizeRequestedSetupAccountId(accountId: string): string {
const normalized = normalizeOptionalAccountId(accountId);
if (!normalized) {
throw new Error(INVALID_ACCOUNT_ID_MESSAGE);
}
return normalized;
}
function resolveSetupAccountId(cfg: OpenClawConfig, requestedAccountId?: string): string {
const requested = requestedAccountId?.trim();
if (requested) {
return normalizeRequestedSetupAccountId(requested);
}
const preferred = cfg.channels?.twitch?.defaultAccount?.trim();
return preferred ? normalizeAccountId(preferred) : resolveDefaultTwitchAccountId(cfg);
}
export function setTwitchAccount(
cfg: OpenClawConfig,
account: Partial<TwitchAccountConfig>,
accountId: string = resolveSetupAccountId(cfg),
): OpenClawConfig {
const resolvedAccountId = accountId.trim()
? normalizeRequestedSetupAccountId(accountId)
: resolveSetupAccountId(cfg);
const existing = getAccountConfig(cfg, resolvedAccountId);
const merged: TwitchAccountConfig = {
username: account.username ?? existing?.username ?? "",
accessToken: account.accessToken ?? existing?.accessToken ?? "",
clientId: account.clientId ?? existing?.clientId ?? "",
channel: account.channel ?? existing?.channel ?? "",
enabled: account.enabled ?? existing?.enabled ?? true,
allowFrom: account.allowFrom ?? existing?.allowFrom,
allowedRoles: account.allowedRoles ?? existing?.allowedRoles,
requireMention: account.requireMention ?? existing?.requireMention,
clientSecret: account.clientSecret ?? existing?.clientSecret,
refreshToken: account.refreshToken ?? existing?.refreshToken,
expiresIn: account.expiresIn ?? existing?.expiresIn,
obtainmentTimestamp: account.obtainmentTimestamp ?? existing?.obtainmentTimestamp,
};
return {
...cfg,
channels: {
...cfg.channels,
twitch: {
...((cfg.channels as Record<string, unknown>)?.twitch as
| Record<string, unknown>
| undefined),
enabled: true,
accounts: {
...((
(cfg.channels as Record<string, unknown>)?.twitch as Record<string, unknown> | undefined
)?.accounts as Record<string, unknown> | undefined),
[resolvedAccountId]: merged,
},
},
},
};
}
async function noteTwitchSetupHelp(prompter: WizardPrompter): Promise<void> {
await prompter.note(
[
t("wizard.twitch.helpRequiresBot"),
t("wizard.twitch.helpCreateApp"),
t("wizard.twitch.helpGenerateToken"),
t("wizard.twitch.helpTokenTools"),
t("wizard.twitch.helpCopyToken"),
t("wizard.twitch.helpEnvVars"),
`Docs: ${formatDocsLink("/channels/twitch", "channels/twitch")}`,
].join("\n"),
t("wizard.twitch.setupTitle"),
);
}
export async function promptToken(
prompter: WizardPrompter,
account: TwitchAccountConfig | null,
envToken: string | undefined,
): Promise<string> {
const existingToken = account?.accessToken ?? "";
if (existingToken && !envToken) {
const keepToken = await prompter.confirm({
message: t("wizard.twitch.accessTokenKeep"),
initialValue: true,
});
if (keepToken) {
return existingToken;
}
}
return (
await prompter.text({
message: t("wizard.twitch.oauthTokenPrompt"),
initialValue: envToken ?? "",
validate: (value) => {
const raw = value?.trim() ?? "";
if (!raw) {
return "Required";
}
if (!raw.startsWith("oauth:")) {
return "Token should start with 'oauth:'";
}
return undefined;
},
})
).trim();
}
export async function promptUsername(
prompter: WizardPrompter,
account: TwitchAccountConfig | null,
): Promise<string> {
return (
await prompter.text({
message: t("wizard.twitch.botUsernamePrompt"),
initialValue: account?.username ?? "",
validate: (value) => (value?.trim() ? undefined : "Required"),
})
).trim();
}
export async function promptClientId(
prompter: WizardPrompter,
account: TwitchAccountConfig | null,
): Promise<string> {
return (
await prompter.text({
message: t("wizard.twitch.clientIdPrompt"),
initialValue: account?.clientId ?? "",
validate: (value) => (value?.trim() ? undefined : "Required"),
})
).trim();
}
export async function promptChannelName(
prompter: WizardPrompter,
account: TwitchAccountConfig | null,
): Promise<string> {
return (
await prompter.text({
message: t("wizard.twitch.channelJoinPrompt"),
initialValue: account?.channel ?? "",
validate: (value) => (value?.trim() ? undefined : "Required"),
})
).trim();
}
export async function promptRefreshTokenSetup(
prompter: WizardPrompter,
account: TwitchAccountConfig | null,
): Promise<{ clientSecret?: string; refreshToken?: string }> {
const useRefresh = await prompter.confirm({
message: t("wizard.twitch.refreshTokenPrompt"),
initialValue: Boolean(account?.clientSecret && account?.refreshToken),
});
if (!useRefresh) {
return {};
}
const clientSecret =
(
await prompter.text({
message: t("wizard.twitch.clientSecretPrompt"),
initialValue: account?.clientSecret ?? "",
validate: (value) => (value?.trim() ? undefined : "Required"),
})
).trim() || undefined;
const refreshToken =
(
await prompter.text({
message: t("wizard.twitch.refreshTokenInputPrompt"),
initialValue: account?.refreshToken ?? "",
validate: (value) => (value?.trim() ? undefined : "Required"),
})
).trim() || undefined;
return { clientSecret, refreshToken };
}
export async function configureWithEnvToken(
cfg: OpenClawConfig,
prompter: WizardPrompter,
account: TwitchAccountConfig | null,
envToken: string,
forceAllowFrom: boolean,
dmPolicy: ChannelSetupDmPolicy,
accountId: string = resolveSetupAccountId(cfg),
): Promise<{ cfg: OpenClawConfig } | null> {
const resolvedAccountId = accountId.trim()
? normalizeRequestedSetupAccountId(accountId)
: resolveSetupAccountId(cfg);
if (resolvedAccountId !== DEFAULT_ACCOUNT_ID) {
return null;
}
const useEnv = await prompter.confirm({
message: t("wizard.twitch.envPrompt"),
initialValue: true,
});
if (!useEnv) {
return null;
}
const username = await promptUsername(prompter, account);
const clientId = await promptClientId(prompter, account);
const cfgWithAccount = setTwitchAccount(
cfg,
{
username,
clientId,
accessToken: envToken,
enabled: true,
},
resolvedAccountId,
);
if (forceAllowFrom && dmPolicy.promptAllowFrom) {
return {
cfg: await dmPolicy.promptAllowFrom({
cfg: cfgWithAccount,
prompter,
accountId: resolvedAccountId,
}),
};
}
return { cfg: cfgWithAccount };
}
function setTwitchAccessControl(
cfg: OpenClawConfig,
allowedRoles: TwitchRole[],
requireMention: boolean,
accountId?: string,
): OpenClawConfig {
const resolvedAccountId = resolveSetupAccountId(cfg, accountId);
const account = getAccountConfig(cfg, resolvedAccountId);
if (!account) {
return cfg;
}
return setTwitchAccount(
cfg,
{
...account,
allowedRoles,
requireMention,
},
resolvedAccountId,
);
}
function resolveTwitchGroupPolicy(
cfg: OpenClawConfig,
accountId?: string,
): "open" | "allowlist" | "disabled" {
const account = getAccountConfig(cfg, resolveSetupAccountId(cfg, accountId));
if (account?.allowedRoles?.includes("all")) {
return "open";
}
if (account?.allowedRoles?.includes("moderator")) {
return "allowlist";
}
return "disabled";
}
function setTwitchGroupPolicy(
cfg: OpenClawConfig,
policy: "open" | "allowlist" | "disabled",
accountId?: string,
): OpenClawConfig {
const allowedRoles: TwitchRole[] =
policy === "open" ? ["all"] : policy === "allowlist" ? ["moderator", "vip"] : [];
return setTwitchAccessControl(cfg, allowedRoles, true, accountId);
}
const twitchDmPolicy: ChannelSetupDmPolicy = {
label: "Twitch",
channel,
policyKey: "channels.twitch.accounts.default.allowedRoles",
allowFromKey: "channels.twitch.accounts.default.allowFrom",
resolveConfigKeys: (cfg, accountId) => {
const resolvedAccountId = resolveSetupAccountId(cfg, accountId);
return {
policyKey: `channels.twitch.accounts.${resolvedAccountId}.allowedRoles`,
allowFromKey: `channels.twitch.accounts.${resolvedAccountId}.allowFrom`,
};
},
getCurrent: (cfg, accountId) => {
const account = getAccountConfig(cfg, resolveSetupAccountId(cfg, accountId));
if (account?.allowedRoles?.includes("all")) {
return "open";
}
if (account?.allowFrom && account.allowFrom.length > 0) {
return "allowlist";
}
return "disabled";
},
setPolicy: (cfg, policy, accountId) => {
const allowedRoles: TwitchRole[] =
policy === "open" ? ["all"] : policy === "allowlist" ? [] : ["moderator"];
return setTwitchAccessControl(cfg, allowedRoles, true, accountId);
},
promptAllowFrom: async ({ cfg, prompter, accountId }) => {
const resolvedAccountId = resolveSetupAccountId(cfg, accountId);
const account = getAccountConfig(cfg, resolvedAccountId);
const existingAllowFrom = account?.allowFrom ?? [];
const entry = await prompter.text({
message: t("wizard.twitch.allowFromPrompt"),
placeholder: "123456789",
initialValue: existingAllowFrom[0] || undefined,
});
const allowFrom = normalizeStringEntries((entry ?? "").split(/[\n,;]+/g));
return setTwitchAccount(
cfg,
{
...(account ?? undefined),
allowFrom,
},
resolvedAccountId,
);
},
};
const twitchGroupAccess: NonNullable<ChannelSetupWizard["groupAccess"]> = {
label: "Twitch chat",
placeholder: "",
skipAllowlistEntries: true,
currentPolicy: ({ cfg, accountId }) => resolveTwitchGroupPolicy(cfg, accountId),
currentEntries: ({ cfg, accountId }) => {
const account = getAccountConfig(cfg, resolveSetupAccountId(cfg, accountId));
return account?.allowFrom ?? [];
},
updatePrompt: ({ cfg, accountId }) => {
const account = getAccountConfig(cfg, resolveSetupAccountId(cfg, accountId));
return Boolean(account?.allowedRoles?.length || account?.allowFrom?.length);
},
setPolicy: ({ cfg, accountId, policy }) => setTwitchGroupPolicy(cfg, policy, accountId),
resolveAllowlist: async () => [],
applyAllowlist: ({ cfg }) => cfg,
};
export const twitchSetupAdapter: ChannelSetupAdapter = {
resolveAccountId: ({ cfg }) => resolveSetupAccountId(cfg),
applyAccountConfig: ({ cfg, accountId }) =>
setTwitchAccount(
cfg,
{
enabled: true,
},
accountId,
),
};
export const twitchSetupWizard: ChannelSetupWizard = {
channel,
resolveAccountIdForConfigure: ({ cfg, accountOverride }) =>
resolveSetupAccountId(cfg, accountOverride),
resolveShouldPromptAccountIds: () => false,
status: {
configuredLabel: t("wizard.channels.statusConfigured"),
unconfiguredLabel: t("wizard.channels.statusNeedsUsernameTokenClientId"),
configuredHint: t("wizard.channels.statusConfigured"),
unconfiguredHint: t("wizard.channels.statusNeedsSetup"),
resolveConfigured: ({ cfg, accountId }) => {
return resolveTwitchAccountContext(cfg, resolveSetupAccountId(cfg, accountId)).configured;
},
resolveStatusLines: ({ cfg, accountId }) => {
const resolvedAccountId = resolveSetupAccountId(cfg, accountId);
const configured = resolveTwitchAccountContext(cfg, resolvedAccountId).configured;
return [
`Twitch${resolvedAccountId !== DEFAULT_ACCOUNT_ID ? ` (${resolvedAccountId})` : ""}: ${
configured
? t("wizard.channels.statusConfigured")
: t("wizard.channels.statusNeedsUsernameTokenClientId")
}`,
];
},
},
credentials: [],
finalize: async ({ cfg, accountId: requestedAccountId, prompter, forceAllowFrom }) => {
const accountId = resolveSetupAccountId(cfg, requestedAccountId);
const account = getAccountConfig(cfg, accountId);
if (!account || !isAccountConfigured(account)) {
await noteTwitchSetupHelp(prompter);
}
const envToken = process.env.OPENCLAW_TWITCH_ACCESS_TOKEN?.trim();
if (accountId === DEFAULT_ACCOUNT_ID && envToken && !account?.accessToken) {
const envResult = await configureWithEnvToken(
cfg,
prompter,
account,
envToken,
forceAllowFrom,
twitchDmPolicy,
accountId,
);
if (envResult) {
return envResult;
}
}
const username = await promptUsername(prompter, account);
const token = await promptToken(prompter, account, envToken);
const clientId = await promptClientId(prompter, account);
const channelName = await promptChannelName(prompter, account);
const { clientSecret, refreshToken } = await promptRefreshTokenSetup(prompter, account);
const cfgWithAccount = setTwitchAccount(
cfg,
{
username,
accessToken: token,
clientId,
channel: channelName,
clientSecret,
refreshToken,
enabled: true,
},
accountId,
);
const cfgWithAllowFrom =
forceAllowFrom && twitchDmPolicy.promptAllowFrom
? await twitchDmPolicy.promptAllowFrom({ cfg: cfgWithAccount, prompter, accountId })
: cfgWithAccount;
return { cfg: cfgWithAllowFrom };
},
dmPolicy: twitchDmPolicy,
groupAccess: twitchGroupAccess,
disable: (cfg) => {
const twitch = (cfg.channels as Record<string, unknown>)?.twitch as
| Record<string, unknown>
| undefined;
return {
...cfg,
channels: {
...cfg.channels,
twitch: { ...twitch, enabled: false },
},
};
},
};
type ResolvedTwitchAccount = TwitchAccountConfig & { accountId?: string | null };
export const twitchSetupPlugin: ChannelPlugin<ResolvedTwitchAccount> = {
id: channel,
meta: getChatChannelMeta(channel),
capabilities: {
chatTypes: ["group"],
},
config: {
listAccountIds: (cfg) => listAccountIds(cfg),
resolveAccount: (cfg, accountId) => {
const resolvedAccountId = normalizeAccountId(accountId ?? resolveDefaultTwitchAccountId(cfg));
const account = getAccountConfig(cfg, resolvedAccountId);
if (!account) {
return {
accountId: resolvedAccountId,
username: "",
accessToken: "",
clientId: "",
channel: "",
enabled: false,
};
}
return {
accountId: resolvedAccountId,
...account,
};
},
defaultAccountId: (cfg) => resolveDefaultTwitchAccountId(cfg),
isConfigured: (account, cfg) => resolveTwitchAccountContext(cfg, account?.accountId).configured,
isEnabled: (account) => account.enabled !== false,
},
setup: twitchSetupAdapter,
setupWizard: twitchSetupWizard,
};