feat: add channels capabilities command
This commit is contained in:
@@ -5,15 +5,12 @@ Docs: https://docs.clawd.bot
|
|||||||
## 2026.1.17 (Unreleased)
|
## 2026.1.17 (Unreleased)
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
- Telegram: enrich forwarded message context with normalized origin details + legacy fallback. (#1090) — thanks @sleontenko.
|
|
||||||
- macOS: strip prerelease/build suffixes when parsing gateway semver patches. (#1110) — thanks @zerone0x.
|
- macOS: strip prerelease/build suffixes when parsing gateway semver patches. (#1110) — thanks @zerone0x.
|
||||||
- macOS: keep CLI install pinned to the full build suffix. (#1111) — thanks @artuskg.
|
- macOS: keep CLI install pinned to the full build suffix. (#1111) — thanks @artuskg.
|
||||||
- CLI: surface update availability in `clawdbot status`.
|
- CLI: add `channels capabilities` to summarize channel support and provider probes.
|
||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
- Doctor: avoid re-adding WhatsApp ack reaction config when only legacy auth files exist. (#1087) — thanks @YuriNachos.
|
- Doctor: avoid re-adding WhatsApp ack reaction config when only legacy auth files exist. (#1087) — thanks @YuriNachos.
|
||||||
- CLI: add WSL2/systemd unavailable hints in daemon status/doctor output.
|
|
||||||
- Status: show both usage windows with reset hints when usage data is available. (#1101) — thanks @rhjoh.
|
|
||||||
|
|
||||||
## 2026.1.16-2
|
## 2026.1.16-2
|
||||||
|
|
||||||
@@ -57,7 +54,6 @@ Docs: https://docs.clawd.bot
|
|||||||
- Directory: unify `clawdbot directory` across channels and plugin channels.
|
- Directory: unify `clawdbot directory` across channels and plugin channels.
|
||||||
- UI: allow deleting sessions from the Control UI.
|
- UI: allow deleting sessions from the Control UI.
|
||||||
- Memory: add sqlite-vec vector acceleration with CLI status details.
|
- Memory: add sqlite-vec vector acceleration with CLI status details.
|
||||||
- Memory: add experimental session transcript indexing for memory_search (opt-in via memorySearch.experimental.sessionMemory + sources).
|
|
||||||
- Skills: add user-invocable skill commands and expanded skill command registration.
|
- Skills: add user-invocable skill commands and expanded skill command registration.
|
||||||
- Telegram: default reaction level to minimal and enable reaction notifications by default.
|
- Telegram: default reaction level to minimal and enable reaction notifications by default.
|
||||||
- Telegram: allow reply-chain messages to bypass mention gating in groups. (#1038) — thanks @adityashaw2.
|
- Telegram: allow reply-chain messages to bypass mention gating in groups. (#1038) — thanks @adityashaw2.
|
||||||
@@ -77,9 +73,6 @@ Docs: https://docs.clawd.bot
|
|||||||
- macOS: drain subprocess pipes before waiting to avoid deadlocks. (#1081) — thanks @thesash.
|
- macOS: drain subprocess pipes before waiting to avoid deadlocks. (#1081) — thanks @thesash.
|
||||||
- Verbose: wrap tool summaries/output in markdown only for markdown-capable channels.
|
- Verbose: wrap tool summaries/output in markdown only for markdown-capable channels.
|
||||||
- Tools: include provider/session context in elevated exec denial errors.
|
- Tools: include provider/session context in elevated exec denial errors.
|
||||||
- Tools: normalize exec tool alias naming in tool error logs.
|
|
||||||
- Logging: reuse shared ANSI stripping to keep console capture lint-clean.
|
|
||||||
- Logging: prefix nested agent output with session/run/channel context.
|
|
||||||
- Telegram: accept tg/group/telegram prefixes + topic targets for inline button validation. (#1072) — thanks @danielz1z.
|
- Telegram: accept tg/group/telegram prefixes + topic targets for inline button validation. (#1072) — thanks @danielz1z.
|
||||||
- Telegram: split long captions into follow-up messages.
|
- Telegram: split long captions into follow-up messages.
|
||||||
- Config: block startup on invalid config, preserve best-effort doctor config, and keep rolling config backups. (#1083) — thanks @mukhtharcm.
|
- Config: block startup on invalid config, preserve best-effort doctor config, and keep rolling config backups. (#1083) — thanks @mukhtharcm.
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ Related docs:
|
|||||||
```bash
|
```bash
|
||||||
clawdbot channels list
|
clawdbot channels list
|
||||||
clawdbot channels status
|
clawdbot channels status
|
||||||
|
clawdbot channels capabilities
|
||||||
|
clawdbot channels capabilities --channel discord --target channel:123
|
||||||
clawdbot channels logs --channel all
|
clawdbot channels logs --channel all
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -42,3 +44,16 @@ clawdbot channels logout --channel whatsapp
|
|||||||
- Run `clawdbot status --deep` for a broad probe.
|
- Run `clawdbot status --deep` for a broad probe.
|
||||||
- Use `clawdbot doctor` for guided fixes.
|
- Use `clawdbot doctor` for guided fixes.
|
||||||
|
|
||||||
|
## Capabilities probe
|
||||||
|
|
||||||
|
Fetch provider capability hints (intents/scopes where available) plus static feature support:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clawdbot channels capabilities
|
||||||
|
clawdbot channels capabilities --channel discord --target channel:123
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- `--channel` is optional; omit it to list every channel (including extensions).
|
||||||
|
- `--target` accepts `channel:<id>` or a raw numeric channel id and only applies to Discord.
|
||||||
|
- This probes provider APIs where possible (Discord intents, Telegram webhook, Slack auth test, Signal daemon); channels without probes report `Probe: unavailable`.
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { Command } from "commander";
|
|||||||
import { listChannelPlugins } from "../channels/plugins/index.js";
|
import { listChannelPlugins } from "../channels/plugins/index.js";
|
||||||
import {
|
import {
|
||||||
channelsAddCommand,
|
channelsAddCommand,
|
||||||
|
channelsCapabilitiesCommand,
|
||||||
channelsListCommand,
|
channelsListCommand,
|
||||||
channelsLogsCommand,
|
channelsLogsCommand,
|
||||||
channelsRemoveCommand,
|
channelsRemoveCommand,
|
||||||
@@ -87,6 +88,23 @@ export function registerChannelsCli(program: Command) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
channels
|
||||||
|
.command("capabilities")
|
||||||
|
.description("Show provider capabilities (intents/scopes + supported features)")
|
||||||
|
.option("--channel <name>", `Channel (${channelNames}|all)`)
|
||||||
|
.option("--account <id>", "Account id (only with --channel)")
|
||||||
|
.option("--target <dest>", "Channel target for permission audit (Discord channel:<id>)")
|
||||||
|
.option("--timeout <ms>", "Timeout in ms", "10000")
|
||||||
|
.option("--json", "Output JSON", false)
|
||||||
|
.action(async (opts) => {
|
||||||
|
try {
|
||||||
|
await channelsCapabilitiesCommand(opts, defaultRuntime);
|
||||||
|
} catch (err) {
|
||||||
|
defaultRuntime.error(String(err));
|
||||||
|
defaultRuntime.exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
channels
|
channels
|
||||||
.command("logs")
|
.command("logs")
|
||||||
.description("Show recent channel logs from the gateway log file")
|
.description("Show recent channel logs from the gateway log file")
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
export type { ChannelsAddOptions } from "./channels/add.js";
|
export type { ChannelsAddOptions } from "./channels/add.js";
|
||||||
export { channelsAddCommand } from "./channels/add.js";
|
export { channelsAddCommand } from "./channels/add.js";
|
||||||
|
export type { ChannelsCapabilitiesOptions } from "./channels/capabilities.js";
|
||||||
|
export { channelsCapabilitiesCommand } from "./channels/capabilities.js";
|
||||||
export type { ChannelsListOptions } from "./channels/list.js";
|
export type { ChannelsListOptions } from "./channels/list.js";
|
||||||
export { channelsListCommand } from "./channels/list.js";
|
export { channelsListCommand } from "./channels/list.js";
|
||||||
export type { ChannelsLogsOptions } from "./channels/logs.js";
|
export type { ChannelsLogsOptions } from "./channels/logs.js";
|
||||||
|
|||||||
391
src/commands/channels/capabilities.ts
Normal file
391
src/commands/channels/capabilities.ts
Normal file
@@ -0,0 +1,391 @@
|
|||||||
|
import { getChannelPlugin, listChannelPlugins } from "../../channels/plugins/index.js";
|
||||||
|
import { resolveChannelDefaultAccountId } from "../../channels/plugins/helpers.js";
|
||||||
|
import { normalizeDiscordMessagingTarget } from "../../channels/plugins/normalize-target.js";
|
||||||
|
import type { ChannelCapabilities, ChannelPlugin } from "../../channels/plugins/types.js";
|
||||||
|
import { fetchChannelPermissionsDiscord } from "../../discord/send.js";
|
||||||
|
import { danger } from "../../globals.js";
|
||||||
|
import type { ClawdbotConfig } from "../../config/config.js";
|
||||||
|
import { defaultRuntime, type RuntimeEnv } from "../../runtime.js";
|
||||||
|
import { theme } from "../../terminal/theme.js";
|
||||||
|
import { formatChannelAccountLabel, requireValidConfig } from "./shared.js";
|
||||||
|
|
||||||
|
export type ChannelsCapabilitiesOptions = {
|
||||||
|
channel?: string;
|
||||||
|
account?: string;
|
||||||
|
target?: string;
|
||||||
|
timeout?: string;
|
||||||
|
json?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type DiscordTargetSummary = {
|
||||||
|
raw?: string;
|
||||||
|
normalized?: string;
|
||||||
|
kind?: "channel" | "user";
|
||||||
|
channelId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type DiscordPermissionsReport = {
|
||||||
|
channelId?: string;
|
||||||
|
guildId?: string;
|
||||||
|
isDm?: boolean;
|
||||||
|
channelType?: number;
|
||||||
|
permissions?: string[];
|
||||||
|
missingRequired?: string[];
|
||||||
|
raw?: string;
|
||||||
|
error?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ChannelCapabilitiesReport = {
|
||||||
|
channel: string;
|
||||||
|
accountId: string;
|
||||||
|
accountName?: string;
|
||||||
|
configured?: boolean;
|
||||||
|
enabled?: boolean;
|
||||||
|
support?: ChannelCapabilities;
|
||||||
|
probe?: unknown;
|
||||||
|
target?: DiscordTargetSummary;
|
||||||
|
channelPermissions?: DiscordPermissionsReport;
|
||||||
|
};
|
||||||
|
|
||||||
|
const REQUIRED_DISCORD_PERMISSIONS = ["ViewChannel", "SendMessages"] as const;
|
||||||
|
|
||||||
|
function normalizeTimeout(raw: unknown, fallback = 10_000) {
|
||||||
|
const value = typeof raw === "string" ? Number(raw) : Number(raw);
|
||||||
|
if (!Number.isFinite(value) || value <= 0) return fallback;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatSupport(capabilities?: ChannelCapabilities) {
|
||||||
|
if (!capabilities) return "unknown";
|
||||||
|
const bits: string[] = [];
|
||||||
|
if (capabilities.chatTypes?.length) {
|
||||||
|
bits.push(`chatTypes=${capabilities.chatTypes.join(",")}`);
|
||||||
|
}
|
||||||
|
if (capabilities.polls) bits.push("polls");
|
||||||
|
if (capabilities.reactions) bits.push("reactions");
|
||||||
|
if (capabilities.threads) bits.push("threads");
|
||||||
|
if (capabilities.media) bits.push("media");
|
||||||
|
if (capabilities.nativeCommands) bits.push("nativeCommands");
|
||||||
|
if (capabilities.blockStreaming) bits.push("blockStreaming");
|
||||||
|
return bits.length ? bits.join(" ") : "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
function summarizeDiscordTarget(raw?: string): DiscordTargetSummary | undefined {
|
||||||
|
if (!raw) return undefined;
|
||||||
|
const normalized = normalizeDiscordMessagingTarget(raw);
|
||||||
|
if (!normalized) return { raw };
|
||||||
|
if (normalized.startsWith("channel:")) {
|
||||||
|
return {
|
||||||
|
raw,
|
||||||
|
normalized,
|
||||||
|
kind: "channel",
|
||||||
|
channelId: normalized.slice("channel:".length),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (normalized.startsWith("user:")) {
|
||||||
|
return {
|
||||||
|
raw,
|
||||||
|
normalized,
|
||||||
|
kind: "user",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { raw, normalized };
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDiscordIntents(intents?: {
|
||||||
|
messageContent?: string;
|
||||||
|
guildMembers?: string;
|
||||||
|
presence?: string;
|
||||||
|
}) {
|
||||||
|
if (!intents) return "unknown";
|
||||||
|
return [
|
||||||
|
`messageContent=${intents.messageContent ?? "unknown"}`,
|
||||||
|
`guildMembers=${intents.guildMembers ?? "unknown"}`,
|
||||||
|
`presence=${intents.presence ?? "unknown"}`,
|
||||||
|
].join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatProbeLines(channelId: string, probe: unknown): string[] {
|
||||||
|
const lines: string[] = [];
|
||||||
|
if (!probe || typeof probe !== "object") return lines;
|
||||||
|
const probeObj = probe as Record<string, unknown>;
|
||||||
|
|
||||||
|
if (channelId === "discord") {
|
||||||
|
const bot = probeObj.bot as { id?: string | null; username?: string | null } | undefined;
|
||||||
|
if (bot?.username) {
|
||||||
|
const botId = bot.id ? ` (${bot.id})` : "";
|
||||||
|
lines.push(`Bot: ${theme.accent(`@${bot.username}`)}${botId}`);
|
||||||
|
}
|
||||||
|
const app = probeObj.application as { intents?: Record<string, unknown> } | undefined;
|
||||||
|
if (app?.intents) {
|
||||||
|
lines.push(`Intents: ${formatDiscordIntents(app.intents)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (channelId === "telegram") {
|
||||||
|
const bot = probeObj.bot as { username?: string | null; id?: number | null } | undefined;
|
||||||
|
if (bot?.username) {
|
||||||
|
const botId = bot.id ? ` (${bot.id})` : "";
|
||||||
|
lines.push(`Bot: ${theme.accent(`@${bot.username}`)}${botId}`);
|
||||||
|
}
|
||||||
|
const webhook = probeObj.webhook as { url?: string | null } | undefined;
|
||||||
|
if (webhook?.url !== undefined) {
|
||||||
|
lines.push(`Webhook: ${webhook.url || "none"}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (channelId === "slack") {
|
||||||
|
const bot = probeObj.bot as { name?: string } | undefined;
|
||||||
|
const team = probeObj.team as { name?: string; id?: string } | undefined;
|
||||||
|
if (bot?.name) {
|
||||||
|
lines.push(`Bot: ${theme.accent(`@${bot.name}`)}`);
|
||||||
|
}
|
||||||
|
if (team?.name || team?.id) {
|
||||||
|
const id = team?.id ? ` (${team.id})` : "";
|
||||||
|
lines.push(`Team: ${team?.name ?? "unknown"}${id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (channelId === "signal") {
|
||||||
|
const version = probeObj.version as string | null | undefined;
|
||||||
|
if (version) {
|
||||||
|
lines.push(`Signal daemon: ${version}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ok = typeof probeObj.ok === "boolean" ? probeObj.ok : undefined;
|
||||||
|
if (ok === true && lines.length === 0) {
|
||||||
|
lines.push("Probe: ok");
|
||||||
|
}
|
||||||
|
if (ok === false) {
|
||||||
|
const error = typeof probeObj.error === "string" && probeObj.error ? ` (${probeObj.error})` : "";
|
||||||
|
lines.push(`Probe: ${theme.error(`failed${error}`)}`);
|
||||||
|
}
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildDiscordPermissions(params: {
|
||||||
|
account: { token?: string; accountId?: string };
|
||||||
|
target?: string;
|
||||||
|
}): Promise<{ target?: DiscordTargetSummary; report?: DiscordPermissionsReport }> {
|
||||||
|
const target = summarizeDiscordTarget(params.target?.trim());
|
||||||
|
if (!target) return {};
|
||||||
|
if (target.kind !== "channel" || !target.channelId) {
|
||||||
|
return {
|
||||||
|
target,
|
||||||
|
report: {
|
||||||
|
error: "Target looks like a DM user; pass channel:<id> to audit channel permissions.",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const token = params.account.token?.trim();
|
||||||
|
if (!token) {
|
||||||
|
return {
|
||||||
|
target,
|
||||||
|
report: {
|
||||||
|
channelId: target.channelId,
|
||||||
|
error: "Discord bot token missing for permission audit.",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const perms = await fetchChannelPermissionsDiscord(target.channelId, {
|
||||||
|
token,
|
||||||
|
accountId: params.account.accountId ?? undefined,
|
||||||
|
});
|
||||||
|
const missing = REQUIRED_DISCORD_PERMISSIONS.filter(
|
||||||
|
(permission) => !perms.permissions.includes(permission),
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
target,
|
||||||
|
report: {
|
||||||
|
channelId: perms.channelId,
|
||||||
|
guildId: perms.guildId,
|
||||||
|
isDm: perms.isDm,
|
||||||
|
channelType: perms.channelType,
|
||||||
|
permissions: perms.permissions,
|
||||||
|
missingRequired: missing.length ? missing : [],
|
||||||
|
raw: perms.raw,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
target,
|
||||||
|
report: {
|
||||||
|
channelId: target.channelId,
|
||||||
|
error: err instanceof Error ? err.message : String(err),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveChannelReports(params: {
|
||||||
|
plugin: ChannelPlugin;
|
||||||
|
cfg: ClawdbotConfig;
|
||||||
|
timeoutMs: number;
|
||||||
|
accountOverride?: string;
|
||||||
|
target?: string;
|
||||||
|
}): Promise<ChannelCapabilitiesReport[]> {
|
||||||
|
const { plugin, cfg, timeoutMs } = params;
|
||||||
|
const accountIds = params.accountOverride
|
||||||
|
? [params.accountOverride]
|
||||||
|
: (() => {
|
||||||
|
const ids = plugin.config.listAccountIds(cfg);
|
||||||
|
return ids.length > 0
|
||||||
|
? ids
|
||||||
|
: [resolveChannelDefaultAccountId({ plugin, cfg, accountIds: ids })];
|
||||||
|
})();
|
||||||
|
const reports: ChannelCapabilitiesReport[] = [];
|
||||||
|
for (const accountId of accountIds) {
|
||||||
|
const resolvedAccount = plugin.config.resolveAccount(cfg, accountId);
|
||||||
|
const configured = plugin.config.isConfigured
|
||||||
|
? await plugin.config.isConfigured(resolvedAccount, cfg)
|
||||||
|
: Boolean(resolvedAccount);
|
||||||
|
const enabled = plugin.config.isEnabled
|
||||||
|
? plugin.config.isEnabled(resolvedAccount, cfg)
|
||||||
|
: (resolvedAccount as { enabled?: boolean }).enabled !== false;
|
||||||
|
let probe: unknown;
|
||||||
|
if (configured && enabled && plugin.status?.probeAccount) {
|
||||||
|
try {
|
||||||
|
probe = await plugin.status.probeAccount({
|
||||||
|
account: resolvedAccount,
|
||||||
|
timeoutMs,
|
||||||
|
cfg,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
probe = { ok: false, error: err instanceof Error ? err.message : String(err) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let discordTarget: DiscordTargetSummary | undefined;
|
||||||
|
let discordPermissions: DiscordPermissionsReport | undefined;
|
||||||
|
if (plugin.id === "discord" && params.target) {
|
||||||
|
const perms = await buildDiscordPermissions({
|
||||||
|
account: resolvedAccount as { token?: string; accountId?: string },
|
||||||
|
target: params.target,
|
||||||
|
});
|
||||||
|
discordTarget = perms.target;
|
||||||
|
discordPermissions = perms.report;
|
||||||
|
}
|
||||||
|
|
||||||
|
reports.push({
|
||||||
|
channel: plugin.id,
|
||||||
|
accountId,
|
||||||
|
accountName:
|
||||||
|
typeof (resolvedAccount as { name?: string }).name === "string"
|
||||||
|
? (resolvedAccount as { name?: string }).name?.trim() || undefined
|
||||||
|
: undefined,
|
||||||
|
configured,
|
||||||
|
enabled,
|
||||||
|
support: plugin.capabilities,
|
||||||
|
probe,
|
||||||
|
target: discordTarget,
|
||||||
|
channelPermissions: discordPermissions,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return reports;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function channelsCapabilitiesCommand(
|
||||||
|
opts: ChannelsCapabilitiesOptions,
|
||||||
|
runtime: RuntimeEnv = defaultRuntime,
|
||||||
|
) {
|
||||||
|
const cfg = await requireValidConfig(runtime);
|
||||||
|
if (!cfg) return;
|
||||||
|
const timeoutMs = normalizeTimeout(opts.timeout, 10_000);
|
||||||
|
const rawChannel =
|
||||||
|
typeof opts.channel === "string" ? opts.channel.trim().toLowerCase() : "";
|
||||||
|
const rawTarget = typeof opts.target === "string" ? opts.target.trim() : "";
|
||||||
|
|
||||||
|
if (opts.account && (!rawChannel || rawChannel === "all")) {
|
||||||
|
runtime.error(danger("--account requires a specific --channel."));
|
||||||
|
runtime.exit(1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (rawTarget && rawChannel !== "discord") {
|
||||||
|
runtime.error(danger("--target requires --channel discord."));
|
||||||
|
runtime.exit(1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const plugins = listChannelPlugins();
|
||||||
|
const selected =
|
||||||
|
!rawChannel || rawChannel === "all"
|
||||||
|
? plugins
|
||||||
|
: (() => {
|
||||||
|
const plugin = getChannelPlugin(rawChannel);
|
||||||
|
if (!plugin) return null;
|
||||||
|
return [plugin];
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (!selected || selected.length === 0) {
|
||||||
|
runtime.error(danger(`Unknown channel "${rawChannel}".`));
|
||||||
|
runtime.exit(1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const reports: ChannelCapabilitiesReport[] = [];
|
||||||
|
for (const plugin of selected) {
|
||||||
|
const accountOverride = opts.account?.trim() || undefined;
|
||||||
|
reports.push(
|
||||||
|
...(await resolveChannelReports({
|
||||||
|
plugin,
|
||||||
|
cfg,
|
||||||
|
timeoutMs,
|
||||||
|
accountOverride,
|
||||||
|
target: rawTarget && plugin.id === "discord" ? rawTarget : undefined,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.json) {
|
||||||
|
runtime.log(JSON.stringify({ channels: reports }, null, 2));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines: string[] = [];
|
||||||
|
for (const report of reports) {
|
||||||
|
const label = formatChannelAccountLabel({
|
||||||
|
channel: report.channel,
|
||||||
|
accountId: report.accountId,
|
||||||
|
name: report.accountName,
|
||||||
|
channelStyle: theme.accent,
|
||||||
|
accountStyle: theme.heading,
|
||||||
|
});
|
||||||
|
lines.push(theme.heading(label));
|
||||||
|
lines.push(`Support: ${formatSupport(report.support)}`);
|
||||||
|
if (report.configured === false || report.enabled === false) {
|
||||||
|
const configuredLabel = report.configured === false ? "not configured" : "configured";
|
||||||
|
const enabledLabel = report.enabled === false ? "disabled" : "enabled";
|
||||||
|
lines.push(`Status: ${configuredLabel}, ${enabledLabel}`);
|
||||||
|
}
|
||||||
|
const probeLines = formatProbeLines(report.channel, report.probe);
|
||||||
|
if (probeLines.length > 0) {
|
||||||
|
lines.push(...probeLines);
|
||||||
|
} else if (report.configured && report.enabled) {
|
||||||
|
lines.push(theme.muted("Probe: unavailable"));
|
||||||
|
}
|
||||||
|
if (report.channel === "discord" && report.channelPermissions) {
|
||||||
|
const perms = report.channelPermissions;
|
||||||
|
if (perms.error) {
|
||||||
|
lines.push(`Permissions: ${theme.error(perms.error)}`);
|
||||||
|
} else {
|
||||||
|
const list = perms.permissions?.length ? perms.permissions.join(", ") : "none";
|
||||||
|
const label = perms.channelId ? ` (${perms.channelId})` : "";
|
||||||
|
lines.push(`Permissions${label}: ${list}`);
|
||||||
|
if (perms.missingRequired && perms.missingRequired.length > 0) {
|
||||||
|
lines.push(
|
||||||
|
`${theme.warn("Missing required:")} ${perms.missingRequired.join(", ")}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
lines.push(theme.success("Missing required: none"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (report.channel === "discord" && rawTarget && !report.channelPermissions) {
|
||||||
|
lines.push(theme.muted("Permissions: skipped (no target)."));
|
||||||
|
}
|
||||||
|
lines.push("");
|
||||||
|
}
|
||||||
|
|
||||||
|
runtime.log(lines.join("\n").trimEnd());
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user