elizaos--eliza
426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
541 行
19 KiB
TypeScript
541 行
19 KiB
TypeScript
/**
|
|
* Discord Plugin Settings Banner
|
|
* Beautiful ANSI art display for configuration on startup
|
|
* Includes tiered permission system for invite URLs
|
|
*/
|
|
|
|
import type { IAgentRuntime } from "@elizaos/core";
|
|
import { lifeOpsPassiveConnectorsEnabled } from "@elizaos/core";
|
|
import { listEnabledDiscordAccounts } from "./accounts";
|
|
import { getDiscordSettings } from "./environment";
|
|
import {
|
|
type DiscordPermissionValues,
|
|
getPermissionValues,
|
|
} from "./permissions";
|
|
import type { DiscordSettings } from "./types";
|
|
|
|
/** Per-account reply-config overrides used by the suppression diagnostic. */
|
|
type AccountReplyOverrides = {
|
|
autoReply?: boolean;
|
|
shouldIgnoreDirectMessages?: boolean;
|
|
/** DM block (mirrors `DiscordAccountConfig.dm` in service.ts resolution). */
|
|
dm?: {
|
|
policy?: "open" | "allowlist" | "pairing" | "disabled";
|
|
allowFrom?: Array<string | number>;
|
|
};
|
|
};
|
|
|
|
/** DM policies that silently drop replies from un-allowlisted/unpaired senders. */
|
|
type RestrictiveDmPolicy = "allowlist" | "pairing" | "disabled";
|
|
|
|
/**
|
|
* Resolve an account's effective DM allowlist, mirroring
|
|
* `resolveDiscordSettingsForAccount` in service.ts (per-account `dm.allowFrom`
|
|
* wins when non-empty, otherwise the base `allowFrom`).
|
|
*/
|
|
function resolveEffectiveAllowFrom(
|
|
config: AccountReplyOverrides,
|
|
base: DiscordSettings,
|
|
): string[] {
|
|
const dmAllowFrom = config.dm?.allowFrom
|
|
?.map((value) => String(value).trim())
|
|
.filter((value) => value.length > 0);
|
|
if (dmAllowFrom && dmAllowFrom.length > 0) {
|
|
return dmAllowFrom;
|
|
}
|
|
return base.allowFrom ?? [];
|
|
}
|
|
|
|
/**
|
|
* Build the remediation message for a restrictive DM policy value. Every variant
|
|
* points at DISCORD_DM_POLICY (and the allowFrom escape hatch) so an operator is
|
|
* not misled into flipping only one var.
|
|
*/
|
|
function dmPolicyReason(policy: RestrictiveDmPolicy): string {
|
|
if (policy === "disabled") {
|
|
return (
|
|
"DISCORD_DM_POLICY='disabled' drops ALL direct messages; set " +
|
|
"DISCORD_DM_POLICY=open (or character.settings.discord.dmPolicy) to reply in DMs"
|
|
);
|
|
}
|
|
if (policy === "allowlist") {
|
|
return (
|
|
"DISCORD_DM_POLICY='allowlist' only replies to senders in DISCORD_ALLOW_FROM " +
|
|
"or the dynamic pairing allowlist; set DISCORD_DM_POLICY=open, or add senders to " +
|
|
"DISCORD_ALLOW_FROM (or character.settings.discord.allowFrom), to reply in DMs"
|
|
);
|
|
}
|
|
// "pairing" is the default and is issue #10216 root cause #1.
|
|
return (
|
|
"DISCORD_DM_POLICY='pairing' (the default) requires each new DM sender to " +
|
|
"complete pairing before the agent replies, so unpaired senders are silently " +
|
|
"dropped; set DISCORD_DM_POLICY=open to reply to all DMs, or add allowed senders " +
|
|
"to DISCORD_ALLOW_FROM (or character.settings.discord.allowFrom)"
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Inspect the effective Discord reply configuration and emit up to one startup
|
|
* warning per distinct failure mode, naming the specific active reason(s) and
|
|
* the exact env var(s) an operator must flip to re-enable replies.
|
|
*
|
|
* Two independent diagnostics, because the suppressors live at different scopes:
|
|
*
|
|
* 1. GLOBAL — when EVERY enabled account is globally suppressed (passive mode
|
|
* on, or every account's `autoReply` off) the bot replies to NOTHING. One
|
|
* "will NOT auto-reply to ANY messages" warning, naming passive/autoReply.
|
|
*
|
|
* 2. DM-ONLY — when the bot DOES reply in channels but DM replies are still
|
|
* silently dropped by `shouldIgnoreDirectMessages` and/or a restrictive
|
|
* `dmPolicy` (the default 'pairing' blocks unpaired senders — issue #10216
|
|
* root cause #1). A SEPARATE "channel replies are enabled but DM replies
|
|
* will be suppressed" warning. The two are mutually exclusive: if the bot
|
|
* replies to nothing, the DM detail is moot, so only the global one fires.
|
|
*
|
|
* Diagnostics only: this reads resolved settings and logs. It never mutates
|
|
* configuration or reply behavior. The gates it mirrors live in
|
|
* `MessageManager.handleMessage` (messages.ts): the channel/global gate is
|
|
* `!autoReply || lifeOpsPassiveConnectorsEnabled(runtime)`; the DM gate is
|
|
* `shouldIgnoreDirectMessages` (with an allowFrom/dynamic-allowlist exception)
|
|
* plus the `dmPolicy` access check in `checkDmAccess`.
|
|
*
|
|
* Multi-account aware: per-account configs can override `autoReply`,
|
|
* `shouldIgnoreDirectMessages`, and `dm.policy`/`dm.allowFrom` (mirrors
|
|
* `resolveDiscordSettingsForAccount` in service.ts: `config.X ?? base.X`). DM
|
|
* suppression is only flagged on accounts that actually reply in channels, so a
|
|
* fully-silent account is reported once (global), not twice.
|
|
*
|
|
* @param runtime - The agent runtime used to resolve settings and log.
|
|
*/
|
|
export function warnIfRepliesSuppressed(runtime: IAgentRuntime): void {
|
|
// Skip the diagnostic when no enabled account has a token: index.ts still
|
|
// runs the banner in that path, but the bot can never connect, so a
|
|
// "Discord is connected but will NOT reply" warning would be misleading.
|
|
// listEnabledDiscordAccounts is the same connection gate the service uses
|
|
// (env DISCORD_API_TOKEN/DISCORD_BOT_TOKENS, character.settings.discord, and
|
|
// per-account tokens all flow through it), so this covers every supported
|
|
// credential path. The existing missing-token warning in index.ts owns the
|
|
// no-credentials case.
|
|
const accounts = listEnabledDiscordAccounts(runtime);
|
|
if (accounts.length === 0) {
|
|
return;
|
|
}
|
|
|
|
const base = getDiscordSettings(runtime);
|
|
// Passive mode is a runtime-global gate (not per-account); when on it
|
|
// suppresses replies for every account.
|
|
const passiveEnabled = lifeOpsPassiveConnectorsEnabled(runtime);
|
|
|
|
// Resolve each enabled account's effective reply settings, mirroring
|
|
// resolveDiscordSettingsForAccount's `config.X ?? base.X` precedence.
|
|
let anyAutoReplyOff = false;
|
|
let anyReplyEnabled = false;
|
|
|
|
// DM-suppression tracking, scoped to accounts that DO reply in channels: an
|
|
// account that replies to nothing is covered by the global warning instead.
|
|
let dmIgnoredOnReplyEnabled = false;
|
|
const restrictiveDmPolicies = new Set<RestrictiveDmPolicy>();
|
|
|
|
for (const account of accounts) {
|
|
const config = (account.config ?? {}) as AccountReplyOverrides;
|
|
const effectiveAutoReply = config.autoReply ?? base.autoReply;
|
|
const effectiveIgnoreDms =
|
|
config.shouldIgnoreDirectMessages ?? base.shouldIgnoreDirectMessages;
|
|
const effectiveDmPolicy = config.dm?.policy ?? base.dmPolicy ?? "pairing";
|
|
const hasAllowFrom = resolveEffectiveAllowFrom(config, base).length > 0;
|
|
|
|
if (!effectiveAutoReply) {
|
|
anyAutoReplyOff = true;
|
|
}
|
|
|
|
// An account replies in channels unless passive mode is on or its
|
|
// effective autoReply is off. DM suppression only matters for accounts
|
|
// that reply in channels — otherwise the global gate already silences it.
|
|
const replyEnabledInChannels = !passiveEnabled && effectiveAutoReply;
|
|
if (!replyEnabledInChannels) {
|
|
continue;
|
|
}
|
|
anyReplyEnabled = true;
|
|
|
|
if (effectiveIgnoreDms) {
|
|
dmIgnoredOnReplyEnabled = true;
|
|
}
|
|
// A restrictive dmPolicy with no static allowlist drops unpaired/
|
|
// non-allowlisted senders. With an allowFrom list configured the operator
|
|
// has deliberately scoped DM access, so the policy itself isn't flagged.
|
|
if (effectiveDmPolicy !== "open" && !hasAllowFrom) {
|
|
restrictiveDmPolicies.add(effectiveDmPolicy);
|
|
}
|
|
}
|
|
|
|
// Case 1 (GLOBAL): every enabled account is globally suppressed, so the bot
|
|
// will not reply to ANY message — channel or DM. A lone reply-enabled account
|
|
// means the bot is not silent overall, so this branch is skipped then.
|
|
if (!anyReplyEnabled) {
|
|
const reasons: string[] = [];
|
|
if (passiveEnabled) {
|
|
reasons.push(
|
|
"passive-connectors mode is ON (inbound persisted, replies suppressed); " +
|
|
"set ELIZA_LIFEOPS_PASSIVE_CONNECTORS=false " +
|
|
"(or LIFEOPS_PASSIVE_CONNECTORS=false) to allow replies",
|
|
);
|
|
}
|
|
if (anyAutoReplyOff) {
|
|
reasons.push(
|
|
"autoReply is OFF; set DISCORD_AUTO_REPLY=true " +
|
|
"(or character.settings.discord.autoReply=true, or the per-account " +
|
|
"autoReply) to allow replies",
|
|
);
|
|
}
|
|
|
|
runtime.logger.warn(
|
|
{ src: "plugin:discord", agentId: runtime.agentId },
|
|
`Discord is connected but will NOT auto-reply to any messages. Active reason(s): ${reasons
|
|
.map((r, i) => `(${i + 1}) ${r}`)
|
|
.join("; ")}.`,
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Case 2 (DM-ONLY): channel replies work, but DM replies are silently dropped
|
|
// by the DM-ignore flag and/or a restrictive dmPolicy (the default 'pairing'
|
|
// blocks unpaired senders — issue #10216 root cause #1). Case 1's global gate
|
|
// returns above, so this branch handles the DM-only sub-case it does not cover.
|
|
const dmReasons: string[] = [];
|
|
if (dmIgnoredOnReplyEnabled) {
|
|
dmReasons.push(
|
|
"DISCORD_SHOULD_IGNORE_DIRECT_MESSAGES is ON, so DMs are dropped except " +
|
|
"from senders in DISCORD_ALLOW_FROM or the dynamic pairing allowlist; set " +
|
|
"DISCORD_SHOULD_IGNORE_DIRECT_MESSAGES=false to reply in DMs — note this " +
|
|
"alone is not enough, as DISCORD_DM_POLICY (default 'pairing') still gates " +
|
|
"DMs, so also set DISCORD_DM_POLICY=open or add senders to DISCORD_ALLOW_FROM",
|
|
);
|
|
}
|
|
for (const policy of restrictiveDmPolicies) {
|
|
dmReasons.push(dmPolicyReason(policy));
|
|
}
|
|
|
|
if (dmReasons.length === 0) {
|
|
return;
|
|
}
|
|
|
|
runtime.logger.warn(
|
|
{ src: "plugin:discord", agentId: runtime.agentId },
|
|
`Discord channel replies are enabled but DM replies will be suppressed. Active reason(s): ${dmReasons
|
|
.map((r, i) => `(${i + 1}) ${r}`)
|
|
.join("; ")}.`,
|
|
);
|
|
}
|
|
|
|
const ANSI = {
|
|
reset: "\x1b[0m",
|
|
bold: "\x1b[1m",
|
|
dim: "\x1b[2m",
|
|
blue: "\x1b[34m",
|
|
brightRed: "\x1b[91m",
|
|
brightGreen: "\x1b[92m",
|
|
brightYellow: "\x1b[93m",
|
|
brightBlue: "\x1b[94m",
|
|
brightMagenta: "\x1b[95m",
|
|
brightCyan: "\x1b[96m",
|
|
brightWhite: "\x1b[97m",
|
|
};
|
|
|
|
export interface PluginSetting {
|
|
name: string;
|
|
value: unknown;
|
|
defaultValue?: unknown;
|
|
sensitive?: boolean;
|
|
required?: boolean;
|
|
}
|
|
|
|
export interface BannerOptions {
|
|
pluginName: string;
|
|
description?: string;
|
|
settings: PluginSetting[];
|
|
runtime: IAgentRuntime;
|
|
/** Discord Application ID for generating invite URLs */
|
|
applicationId?: string;
|
|
/** Permission values for the 3x2 tier matrix */
|
|
discordPermissions?: DiscordPermissionValues;
|
|
}
|
|
|
|
function mask(v: string): string {
|
|
if (!v || v.length <= 8) {
|
|
return "••••••••";
|
|
}
|
|
return `${v.slice(0, 4)}${"•".repeat(Math.min(12, v.length - 8))}${v.slice(-4)}`;
|
|
}
|
|
|
|
/**
|
|
* Format a value for display in the banner.
|
|
*
|
|
* @param value - The value to format; may be `undefined`, `null`, or an empty string.
|
|
* @param sensitive - Whether the value should be obfuscated for display.
|
|
* @param maxLen - Maximum allowed length of the returned string; longer values are truncated with an ellipsis.
|
|
* @returns A display string: `'(not set)'` if `value` is `undefined`, `null`, or an empty string; a masked representation if `sensitive` is true; otherwise the stringified value truncated to at most `maxLen` characters (truncated strings end with `'...'`).
|
|
*/
|
|
function fmtVal(value: unknown, sensitive: boolean, maxLen: number): string {
|
|
let s: string;
|
|
if (value === undefined || value === null || value === "") {
|
|
s = "(not set)";
|
|
} else if (sensitive) {
|
|
s = mask(String(value));
|
|
} else {
|
|
s = String(value);
|
|
}
|
|
if (s.length > maxLen) {
|
|
s = `${s.slice(0, maxLen - 3)}...`;
|
|
}
|
|
return s;
|
|
}
|
|
|
|
// biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escape codes are required for terminal formatting
|
|
const ANSI_PATTERN = /\x1b\[[0-9;]*m/g;
|
|
|
|
/**
|
|
* Pads a string with trailing spaces until its visible (ANSI-stripped) length is at least the given width.
|
|
*
|
|
* @param s - The input string which may contain ANSI escape sequences.
|
|
* @param n - The target visible width (number of characters) after padding.
|
|
* @returns The original string if its visible length is >= `n`, otherwise the string with trailing spaces appended so its visible length equals `n`.
|
|
*/
|
|
function pad(s: string, n: number): string {
|
|
const len = s.replace(ANSI_PATTERN, "").length;
|
|
if (len >= n) {
|
|
return s;
|
|
}
|
|
return s + " ".repeat(n - len);
|
|
}
|
|
|
|
function line(content: string): string {
|
|
const len = content.replace(ANSI_PATTERN, "").length;
|
|
|
|
if (len <= 78) {
|
|
return content + " ".repeat(78 - len);
|
|
}
|
|
|
|
// Truncate based on visible character count, not raw string position
|
|
// This avoids cutting in the middle of ANSI escape sequences
|
|
let visibleCount = 0;
|
|
let result = "";
|
|
let i = 0;
|
|
|
|
while (i < content.length && visibleCount < 78) {
|
|
const remaining = content.slice(i);
|
|
// biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escape codes are required for terminal formatting
|
|
const match = remaining.match(/^\x1b\[[0-9;]*m/);
|
|
|
|
if (match) {
|
|
// Include ANSI sequence without counting toward visible length
|
|
result += match[0];
|
|
i += match[0].length;
|
|
} else {
|
|
// Regular visible character
|
|
result += content[i];
|
|
visibleCount++;
|
|
i++;
|
|
}
|
|
}
|
|
|
|
// Reset any unclosed ANSI sequences after truncation
|
|
return result + ANSI.reset;
|
|
}
|
|
|
|
/**
|
|
* Render a framed ANSI banner that displays plugin settings and, when available, tiered Discord invite URLs.
|
|
*
|
|
* The banner lists each setting with masked or truncated values, a status (custom/default/unset/required),
|
|
* and an optional Discord invite section generated from `applicationId` and `discordPermissions`.
|
|
*
|
|
* @param options - Configuration for the banner, including `settings`, the `runtime` used to emit the banner,
|
|
* and optional Discord invite data (`applicationId`, `discordPermissions`).
|
|
*/
|
|
export function printBanner(options: BannerOptions): void {
|
|
const { settings, runtime } = options;
|
|
const R = ANSI.reset,
|
|
D = ANSI.dim,
|
|
B = ANSI.bold;
|
|
const c1 = ANSI.brightBlue,
|
|
c2 = ANSI.brightCyan,
|
|
c3 = ANSI.brightMagenta;
|
|
|
|
const top = `${c1}╔${"═".repeat(78)}╗${R}`;
|
|
const mid = `${c1}╠${"═".repeat(78)}╣${R}`;
|
|
const bot = `${c1}╚${"═".repeat(78)}╝${R}`;
|
|
const row = (s: string) => `${c1}║${R}${line(s)}${c1}║${R}`;
|
|
|
|
const lines: string[] = [""];
|
|
lines.push(top);
|
|
lines.push(row(` ${B}Character: ${runtime.character.name}${R}`));
|
|
lines.push(mid);
|
|
lines.push(
|
|
row(
|
|
`${c2} ██████╗ ██╗███████╗ ██████╗ ██████╗ ██████╗ ██████╗ ${c3}◖ ◗${R}`,
|
|
),
|
|
);
|
|
lines.push(
|
|
row(
|
|
`${c2} ██╔══██╗██║██╔════╝██╔════╝██╔═══██╗██╔══██╗██╔══██╗ ${c3}◖===◗${R}`,
|
|
),
|
|
);
|
|
lines.push(
|
|
row(
|
|
`${c2} ██║ ██║██║███████╗██║ ██║ ██║██████╔╝██║ ██║ ${c3}╰─╯${R}`,
|
|
),
|
|
);
|
|
lines.push(
|
|
row(
|
|
`${c2} ██████╔╝██║╚════██║╚██████╗╚██████╔╝██║ ██║██████╔╝ ${c3}(◠◠)${R}`,
|
|
),
|
|
);
|
|
lines.push(
|
|
row(
|
|
`${c2} ╚═════╝ ╚═╝╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═════╝ ${c3}‿‿${R}`,
|
|
),
|
|
);
|
|
lines.push(
|
|
row(
|
|
`${D} Bot Integration • Servers • Channels • Voice${R}`,
|
|
),
|
|
);
|
|
lines.push(mid);
|
|
|
|
const NW = 34,
|
|
VW = 26,
|
|
SW = 8;
|
|
lines.push(
|
|
row(
|
|
` ${B}${pad("ENV VARIABLE", NW)} ${pad("VALUE", VW)} ${pad("STATUS", SW)}${R}`,
|
|
),
|
|
);
|
|
lines.push(
|
|
row(` ${D}${"-".repeat(NW)} ${"-".repeat(VW)} ${"-".repeat(SW)}${R}`),
|
|
);
|
|
|
|
for (const s of settings) {
|
|
const set = s.value !== undefined && s.value !== null && s.value !== "";
|
|
// Normalize to string for comparison (e.g., boolean false vs string 'false')
|
|
const isDefault =
|
|
set &&
|
|
s.defaultValue !== undefined &&
|
|
String(s.value) === String(s.defaultValue);
|
|
|
|
let ico: string, st: string;
|
|
if (!set && s.required) {
|
|
ico = `${ANSI.brightRed}◆${R}`;
|
|
st = `${ANSI.brightRed}REQUIRED${R}`;
|
|
} else if (!set) {
|
|
ico = `${D}○${R}`;
|
|
st = `${D}unset${R}`;
|
|
} else if (isDefault) {
|
|
ico = `${ANSI.brightBlue}●${R}`;
|
|
st = `${ANSI.brightBlue}default${R}`;
|
|
} else {
|
|
ico = `${ANSI.brightGreen}✓${R}`;
|
|
st = `${ANSI.brightGreen}custom${R}`;
|
|
}
|
|
|
|
const name = pad(s.name, NW - 2);
|
|
const val = pad(
|
|
fmtVal(s.value ?? s.defaultValue, s.sensitive ?? false, VW),
|
|
VW,
|
|
);
|
|
const status = pad(st, SW);
|
|
lines.push(row(` ${ico} ${c2}${name}${R} ${val} ${status}`));
|
|
}
|
|
|
|
lines.push(mid);
|
|
lines.push(
|
|
row(
|
|
` ${D}${ANSI.brightGreen}✓${D} custom ${ANSI.brightBlue}●${D} default ○ unset ${ANSI.brightRed}◆${D} required → Set in .env${R}`,
|
|
),
|
|
);
|
|
lines.push(bot);
|
|
|
|
// Add Discord invite links organized by voice capability
|
|
if (options.applicationId && options.discordPermissions) {
|
|
const p = options.discordPermissions;
|
|
const baseUrl = `https://discord.com/api/oauth2/authorize?client_id=${options.applicationId}&scope=bot%20applications.commands&permissions=`;
|
|
|
|
lines.push("");
|
|
lines.push(`${B}${ANSI.brightCyan}🔗 Discord Bot Invite${R}`);
|
|
lines.push("");
|
|
lines.push(` ${B}🎙️ With Voice:${R}`);
|
|
lines.push(
|
|
` ${ANSI.brightGreen}● Basic${R} ${baseUrl}${p.basicVoice}`,
|
|
);
|
|
lines.push(
|
|
` ${ANSI.brightYellow}● Moderator${R} ${baseUrl}${p.moderatorVoice}`,
|
|
);
|
|
lines.push(
|
|
` ${ANSI.brightRed}● Admin${R} ${baseUrl}${p.adminVoice}`,
|
|
);
|
|
lines.push("");
|
|
lines.push(` ${B}💬 Without Voice:${R}`);
|
|
lines.push(` ${ANSI.brightCyan}○ Basic${R} ${baseUrl}${p.basic}`);
|
|
lines.push(
|
|
` ${ANSI.brightMagenta}○ Moderator${R} ${baseUrl}${p.moderator}`,
|
|
);
|
|
lines.push(` ${ANSI.brightBlue}○ Admin${R} ${baseUrl}${p.admin}`);
|
|
}
|
|
|
|
lines.push("");
|
|
|
|
runtime.logger.info(lines.join("\n"));
|
|
|
|
// Diagnostics: if the effective config will suppress all auto-replies, warn
|
|
// the operator with the exact reason(s) instead of failing silently.
|
|
warnIfRepliesSuppressed(runtime);
|
|
}
|
|
|
|
/**
|
|
* Print the Discord plugin banner with current settings.
|
|
*/
|
|
export function printDiscordBanner(runtime: IAgentRuntime): void {
|
|
// Get settings
|
|
const apiToken = runtime.getSetting("DISCORD_API_TOKEN");
|
|
const applicationId = runtime.getSetting("DISCORD_APPLICATION_ID");
|
|
const ignoreBots = runtime.getSetting("DISCORD_SHOULD_IGNORE_BOT_MESSAGES");
|
|
const ignoreDMs = runtime.getSetting("DISCORD_SHOULD_IGNORE_DIRECT_MESSAGES");
|
|
const onlyMentions = runtime.getSetting(
|
|
"DISCORD_SHOULD_RESPOND_ONLY_TO_MENTIONS",
|
|
);
|
|
const listenChannels = runtime.getSetting("DISCORD_LISTEN_CHANNEL_IDS");
|
|
const voiceChannelId = runtime.getSetting("DISCORD_VOICE_CHANNEL_ID");
|
|
|
|
printBanner({
|
|
pluginName: "plugin-discord",
|
|
description: "Discord bot integration for servers and channels",
|
|
applicationId: applicationId ? String(applicationId) : undefined,
|
|
discordPermissions: applicationId ? getPermissionValues() : undefined,
|
|
settings: [
|
|
{
|
|
name: "DISCORD_API_TOKEN",
|
|
value: apiToken,
|
|
sensitive: true,
|
|
required: true,
|
|
},
|
|
{ name: "DISCORD_APPLICATION_ID", value: applicationId },
|
|
{ name: "DISCORD_VOICE_CHANNEL_ID", value: voiceChannelId },
|
|
{ name: "DISCORD_LISTEN_CHANNEL_IDS", value: listenChannels },
|
|
{
|
|
name: "DISCORD_SHOULD_IGNORE_BOT_MESSAGES",
|
|
value: ignoreBots,
|
|
defaultValue: "true",
|
|
},
|
|
{
|
|
name: "DISCORD_SHOULD_IGNORE_DIRECT_MESSAGES",
|
|
value: ignoreDMs,
|
|
defaultValue: "true",
|
|
},
|
|
{
|
|
name: "DISCORD_SHOULD_RESPOND_ONLY_TO_MENTIONS",
|
|
value: onlyMentions,
|
|
defaultValue: "true",
|
|
},
|
|
],
|
|
runtime,
|
|
});
|
|
}
|