feat: extract group listing into shared module with --agent support (#418)
This commit is contained in:
@@ -195,6 +195,7 @@ Commands:
|
||||
model set <handle> Set model by handle (e.g., anthropic/claude-sonnet-4-5-20250929)
|
||||
channels Manage channels (interactive menu)
|
||||
channels list Show channel status
|
||||
channels list-groups List group/channel IDs for Slack/Discord
|
||||
channels add <ch> Add a channel (telegram, slack, discord, whatsapp, signal)
|
||||
channels remove <ch> Remove a channel
|
||||
logout Logout from Letta Platform (revoke OAuth tokens)
|
||||
@@ -300,7 +301,7 @@ async function main() {
|
||||
case 'channels':
|
||||
case 'channel': {
|
||||
const { channelManagementCommand } = await import('./cli/channel-management.js');
|
||||
await channelManagementCommand(subCommand, args[2]);
|
||||
await channelManagementCommand(subCommand, args[2], args.slice(3));
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
getSetupFunction,
|
||||
type ChannelId
|
||||
} from '../channels/setup.js';
|
||||
import { listGroupsFromArgs } from './group-listing.js';
|
||||
|
||||
// ============================================================================
|
||||
// Status Helpers
|
||||
@@ -258,12 +259,18 @@ export async function removeChannel(channelId?: string): Promise<void> {
|
||||
// Main Command Handler
|
||||
// ============================================================================
|
||||
|
||||
export async function channelManagementCommand(subCommand?: string, channelName?: string): Promise<void> {
|
||||
export async function channelManagementCommand(subCommand?: string, channelName?: string, extraArgs: string[] = []): Promise<void> {
|
||||
switch (subCommand) {
|
||||
case 'list':
|
||||
case 'ls':
|
||||
await listChannels();
|
||||
break;
|
||||
case 'list-groups':
|
||||
case 'groups': {
|
||||
const args = channelName ? [channelName, ...extraArgs] : extraArgs;
|
||||
await listGroupsFromArgs(args);
|
||||
break;
|
||||
}
|
||||
case 'add':
|
||||
await addChannel(channelName);
|
||||
break;
|
||||
|
||||
@@ -3,187 +3,18 @@
|
||||
* lettabot-channels - Discover channels across platforms
|
||||
*
|
||||
* Usage:
|
||||
* lettabot-channels list [--channel discord|slack]
|
||||
* lettabot-channels list [--channel discord|slack] [--agent name]
|
||||
*
|
||||
* The agent can use this CLI via Bash to discover channel IDs
|
||||
* for sending messages with lettabot-message.
|
||||
*/
|
||||
|
||||
// Config loaded from lettabot.yaml
|
||||
// Config loaded from lettabot.yaml (sets env vars for token fallback)
|
||||
import { loadAppConfigOrExit, applyConfigToEnv } from '../config/index.js';
|
||||
const config = loadAppConfigOrExit();
|
||||
applyConfigToEnv(config);
|
||||
|
||||
// Types
|
||||
interface DiscordGuild {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface DiscordChannel {
|
||||
id: string;
|
||||
name: string;
|
||||
type: number;
|
||||
}
|
||||
|
||||
interface SlackChannel {
|
||||
id: string;
|
||||
name: string;
|
||||
is_member: boolean;
|
||||
}
|
||||
|
||||
// Discord channel types that are text-based
|
||||
const DISCORD_TEXT_CHANNEL_TYPES = new Set([
|
||||
0, // GUILD_TEXT
|
||||
2, // GUILD_VOICE
|
||||
5, // GUILD_ANNOUNCEMENT
|
||||
13, // GUILD_STAGE_VOICE
|
||||
15, // GUILD_FORUM
|
||||
]);
|
||||
|
||||
async function listDiscord(): Promise<void> {
|
||||
const token = process.env.DISCORD_BOT_TOKEN;
|
||||
if (!token) {
|
||||
console.error('Discord: DISCORD_BOT_TOKEN not set, skipping.');
|
||||
return;
|
||||
}
|
||||
|
||||
const headers = { Authorization: `Bot ${token}` };
|
||||
|
||||
// Fetch guilds the bot is in
|
||||
const guildsRes = await fetch('https://discord.com/api/v10/users/@me/guilds', { headers });
|
||||
if (!guildsRes.ok) {
|
||||
const error = await guildsRes.text();
|
||||
console.error(`Discord: Failed to fetch guilds: ${error}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const guilds = (await guildsRes.json()) as DiscordGuild[];
|
||||
if (guilds.length === 0) {
|
||||
console.log('Discord:\n (bot is not in any servers)');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Discord:');
|
||||
|
||||
for (const guild of guilds) {
|
||||
const channelsRes = await fetch(`https://discord.com/api/v10/guilds/${guild.id}/channels`, { headers });
|
||||
if (!channelsRes.ok) {
|
||||
console.log(` Server: ${guild.name}`);
|
||||
console.log(` (failed to fetch channels)`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const channels = (await channelsRes.json()) as DiscordChannel[];
|
||||
const textChannels = channels
|
||||
.filter((c) => DISCORD_TEXT_CHANNEL_TYPES.has(c.type))
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
console.log(` Server: ${guild.name}`);
|
||||
if (textChannels.length === 0) {
|
||||
console.log(` (no text channels)`);
|
||||
} else {
|
||||
const maxNameLen = Math.max(...textChannels.map((c) => c.name.length));
|
||||
for (const ch of textChannels) {
|
||||
const padded = ch.name.padEnd(maxNameLen);
|
||||
console.log(` #${padded} (id: ${ch.id})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function listSlack(): Promise<void> {
|
||||
const token = process.env.SLACK_BOT_TOKEN;
|
||||
if (!token) {
|
||||
console.error('Slack: SLACK_BOT_TOKEN not set, skipping.');
|
||||
return;
|
||||
}
|
||||
|
||||
const params = new URLSearchParams({
|
||||
types: 'public_channel,private_channel',
|
||||
limit: '1000',
|
||||
});
|
||||
|
||||
const res = await fetch(`https://slack.com/api/conversations.list?${params}`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
|
||||
const data = (await res.json()) as { ok: boolean; channels?: SlackChannel[]; error?: string };
|
||||
if (!data.ok) {
|
||||
console.error(`Slack: API error: ${data.error}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const channels = (data.channels || []).sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
console.log('Slack:');
|
||||
if (channels.length === 0) {
|
||||
console.log(' (no channels found)');
|
||||
} else {
|
||||
const maxNameLen = Math.max(...channels.map((c) => c.name.length));
|
||||
for (const ch of channels) {
|
||||
const padded = ch.name.padEnd(maxNameLen);
|
||||
console.log(` #${padded} (id: ${ch.id})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function printUnsupported(platform: string): void {
|
||||
console.log(`${platform}: Channel listing not supported (platform does not expose a bot-visible channel list).`);
|
||||
}
|
||||
|
||||
async function listCommand(args: string[]): Promise<void> {
|
||||
let channel = '';
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
const next = args[i + 1];
|
||||
if ((arg === '--channel' || arg === '-c') && next) {
|
||||
channel = next.toLowerCase();
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
if (channel) {
|
||||
switch (channel) {
|
||||
case 'discord':
|
||||
await listDiscord();
|
||||
break;
|
||||
case 'slack':
|
||||
await listSlack();
|
||||
break;
|
||||
case 'telegram':
|
||||
printUnsupported('Telegram');
|
||||
break;
|
||||
case 'whatsapp':
|
||||
printUnsupported('WhatsApp');
|
||||
break;
|
||||
case 'signal':
|
||||
printUnsupported('Signal');
|
||||
break;
|
||||
default:
|
||||
console.error(`Unknown channel: ${channel}. Supported for listing: discord, slack`);
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
// List all configured platforms
|
||||
const hasDiscord = !!process.env.DISCORD_BOT_TOKEN;
|
||||
const hasSlack = !!process.env.SLACK_BOT_TOKEN;
|
||||
|
||||
if (!hasDiscord && !hasSlack) {
|
||||
console.log('No supported platforms configured. Set DISCORD_BOT_TOKEN or SLACK_BOT_TOKEN.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasDiscord) {
|
||||
await listDiscord();
|
||||
}
|
||||
if (hasSlack) {
|
||||
if (hasDiscord) console.log('');
|
||||
await listSlack();
|
||||
}
|
||||
}
|
||||
}
|
||||
import { listGroupsFromArgs } from './group-listing.js';
|
||||
|
||||
function showHelp(): void {
|
||||
console.log(`
|
||||
@@ -194,6 +25,7 @@ Commands:
|
||||
|
||||
List options:
|
||||
--channel, -c <name> Platform to list: discord, slack (default: all configured)
|
||||
--agent <name> Agent name from lettabot.yaml (reads tokens from that agent's config)
|
||||
|
||||
Examples:
|
||||
# List channels for all configured platforms
|
||||
@@ -205,7 +37,10 @@ Examples:
|
||||
# List Slack channels only
|
||||
lettabot-channels list --channel slack
|
||||
|
||||
Environment variables:
|
||||
# List channels for a specific agent (multi-agent setup)
|
||||
lettabot-channels list --agent MyAgent
|
||||
|
||||
Environment variables (used as fallback when --agent is not specified):
|
||||
DISCORD_BOT_TOKEN Required for Discord channel listing
|
||||
SLACK_BOT_TOKEN Required for Slack channel listing
|
||||
|
||||
@@ -219,7 +54,7 @@ const command = args[0];
|
||||
|
||||
switch (command) {
|
||||
case 'list':
|
||||
listCommand(args.slice(1));
|
||||
listGroupsFromArgs(args.slice(1));
|
||||
break;
|
||||
|
||||
case 'help':
|
||||
@@ -232,7 +67,7 @@ switch (command) {
|
||||
if (command) {
|
||||
// Allow `lettabot-channels --channel discord` without 'list'
|
||||
if (command.startsWith('-')) {
|
||||
listCommand(args);
|
||||
listGroupsFromArgs(args);
|
||||
break;
|
||||
}
|
||||
console.error(`Unknown command: ${command}`);
|
||||
|
||||
173
src/cli/group-listing.test.ts
Normal file
173
src/cli/group-listing.test.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { parseChannelArgs, resolveAgentConfig, resolveListingTokens } from './group-listing.js';
|
||||
|
||||
// ── parseChannelArgs ─────────────────────────────────────────────────────────
|
||||
|
||||
describe('parseChannelArgs', () => {
|
||||
it('returns empty result for no args', () => {
|
||||
expect(parseChannelArgs([])).toEqual({});
|
||||
});
|
||||
|
||||
it('parses --channel flag', () => {
|
||||
expect(parseChannelArgs(['--channel', 'discord'])).toEqual({ channel: 'discord' });
|
||||
});
|
||||
|
||||
it('parses -c shorthand', () => {
|
||||
expect(parseChannelArgs(['-c', 'slack'])).toEqual({ channel: 'slack' });
|
||||
});
|
||||
|
||||
it('lowercases the channel value', () => {
|
||||
expect(parseChannelArgs(['--channel', 'Discord'])).toEqual({ channel: 'discord' });
|
||||
});
|
||||
|
||||
it('parses --agent flag', () => {
|
||||
expect(parseChannelArgs(['--agent', 'MyAgent'])).toEqual({ agent: 'MyAgent' });
|
||||
});
|
||||
|
||||
it('parses --channel and --agent together', () => {
|
||||
expect(parseChannelArgs(['--channel', 'discord', '--agent', 'MyAgent'])).toEqual({
|
||||
channel: 'discord',
|
||||
agent: 'MyAgent',
|
||||
});
|
||||
});
|
||||
|
||||
it('accepts a bare positional as channel shorthand', () => {
|
||||
expect(parseChannelArgs(['discord'])).toEqual({ channel: 'discord' });
|
||||
});
|
||||
|
||||
it('returns error for --channel with no value', () => {
|
||||
expect(parseChannelArgs(['--channel'])).toEqual({ error: 'Missing value for --channel' });
|
||||
});
|
||||
|
||||
it('returns error for --channel when next token is another flag', () => {
|
||||
expect(parseChannelArgs(['--channel', '--agent', 'MyAgent'])).toEqual({
|
||||
error: 'Missing value for --channel',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns error for -c with no value', () => {
|
||||
expect(parseChannelArgs(['-c'])).toEqual({ error: 'Missing value for --channel' });
|
||||
});
|
||||
|
||||
it('returns error for --agent with no value', () => {
|
||||
expect(parseChannelArgs(['--agent'])).toEqual({ error: 'Missing value for --agent' });
|
||||
});
|
||||
|
||||
it('returns error for --agent when next token is another flag', () => {
|
||||
expect(parseChannelArgs(['--agent', '--channel', 'slack'])).toEqual({
|
||||
error: 'Missing value for --agent',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns error for unexpected extra positional argument', () => {
|
||||
const result = parseChannelArgs(['discord', 'slack']);
|
||||
expect(result.error).toMatch(/unexpected argument/i);
|
||||
});
|
||||
});
|
||||
|
||||
// ── resolveAgentConfig ───────────────────────────────────────────────────────
|
||||
|
||||
describe('resolveAgentConfig', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
it('returns undefined when no agentName provided', async () => {
|
||||
const { resolveAgentConfig: resolve } = await import('./group-listing.js');
|
||||
expect(resolve(undefined)).toBeUndefined();
|
||||
expect(resolve('')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('finds agent by exact name', async () => {
|
||||
vi.doMock('../config/index.js', () => ({
|
||||
loadAppConfigOrExit: () => ({}),
|
||||
normalizeAgents: () => [
|
||||
{ name: 'Muninn', channels: { discord: { token: 'tok' } } },
|
||||
{ name: 'Other', channels: {} },
|
||||
],
|
||||
}));
|
||||
const { resolveAgentConfig: resolve } = await import('./group-listing.js');
|
||||
const result = resolve('Muninn');
|
||||
expect(result).toBeDefined();
|
||||
expect(result!.name).toBe('Muninn');
|
||||
});
|
||||
|
||||
it('finds agent case-insensitively', async () => {
|
||||
vi.doMock('../config/index.js', () => ({
|
||||
loadAppConfigOrExit: () => ({}),
|
||||
normalizeAgents: () => [{ name: 'Muninn', channels: {} }],
|
||||
}));
|
||||
const { resolveAgentConfig: resolve } = await import('./group-listing.js');
|
||||
const result = resolve('muninn');
|
||||
expect(result).toBeDefined();
|
||||
expect(result!.name).toBe('Muninn');
|
||||
});
|
||||
|
||||
it('exits with error when agent not found', async () => {
|
||||
vi.doMock('../config/index.js', () => ({
|
||||
loadAppConfigOrExit: () => ({}),
|
||||
normalizeAgents: () => [{ name: 'Other', channels: {} }],
|
||||
}));
|
||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => {}) as never);
|
||||
|
||||
const { resolveAgentConfig: resolve } = await import('./group-listing.js');
|
||||
resolve('NonExistent');
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('NonExistent'));
|
||||
expect(exitSpy).toHaveBeenCalledWith(1);
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
exitSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
// ── resolveListingTokens ─────────────────────────────────────────────────────
|
||||
|
||||
describe('resolveListingTokens', () => {
|
||||
beforeEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
it('uses env fallback when no agent is selected', () => {
|
||||
vi.stubEnv('DISCORD_BOT_TOKEN', 'env-discord');
|
||||
vi.stubEnv('SLACK_BOT_TOKEN', 'env-slack');
|
||||
|
||||
const result = resolveListingTokens(undefined, undefined);
|
||||
expect(result).toEqual({
|
||||
discordToken: 'env-discord',
|
||||
slackToken: 'env-slack',
|
||||
});
|
||||
});
|
||||
|
||||
it('does not fall back to env when an agent is selected', () => {
|
||||
vi.stubEnv('DISCORD_BOT_TOKEN', 'env-discord');
|
||||
vi.stubEnv('SLACK_BOT_TOKEN', 'env-slack');
|
||||
|
||||
const result = resolveListingTokens({ name: 'A', channels: {} } as any, 'A');
|
||||
expect(result).toEqual({
|
||||
discordToken: undefined,
|
||||
slackToken: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('uses selected agent tokens when present', () => {
|
||||
vi.stubEnv('DISCORD_BOT_TOKEN', 'env-discord');
|
||||
vi.stubEnv('SLACK_BOT_TOKEN', 'env-slack');
|
||||
|
||||
const result = resolveListingTokens(
|
||||
{
|
||||
name: 'A',
|
||||
channels: {
|
||||
discord: { enabled: true, token: 'agent-discord' },
|
||||
slack: { enabled: true, botToken: 'agent-slack' },
|
||||
},
|
||||
} as any,
|
||||
'A',
|
||||
);
|
||||
expect(result).toEqual({
|
||||
discordToken: 'agent-discord',
|
||||
slackToken: 'agent-slack',
|
||||
});
|
||||
});
|
||||
});
|
||||
278
src/cli/group-listing.ts
Normal file
278
src/cli/group-listing.ts
Normal file
@@ -0,0 +1,278 @@
|
||||
/**
|
||||
* Group Listing Helpers
|
||||
*
|
||||
* Shared module for listing group/channel IDs across platforms.
|
||||
* Used by both the `lettabot channels list-groups` CLI subcommand
|
||||
* and the standalone `lettabot-channels` binary.
|
||||
*/
|
||||
|
||||
import { loadAppConfigOrExit, normalizeAgents, type AgentConfig } from '../config/index.js';
|
||||
|
||||
// ── Types ────────────────────────────────────────────────────────────────────
|
||||
|
||||
interface DiscordGuild {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface DiscordChannel {
|
||||
id: string;
|
||||
name: string;
|
||||
type: number;
|
||||
}
|
||||
|
||||
interface SlackChannel {
|
||||
id: string;
|
||||
name: string;
|
||||
is_member: boolean;
|
||||
}
|
||||
|
||||
// Discord channel types that are text-based
|
||||
const DISCORD_TEXT_CHANNEL_TYPES = new Set([
|
||||
0, // GUILD_TEXT
|
||||
2, // GUILD_VOICE
|
||||
5, // GUILD_ANNOUNCEMENT
|
||||
13, // GUILD_STAGE_VOICE
|
||||
15, // GUILD_FORUM
|
||||
]);
|
||||
|
||||
// ── Platform Listing ─────────────────────────────────────────────────────────
|
||||
|
||||
async function listDiscord(token?: string): Promise<void> {
|
||||
const discordToken = token || process.env.DISCORD_BOT_TOKEN;
|
||||
if (!discordToken) {
|
||||
console.error('Discord: DISCORD_BOT_TOKEN not set, skipping.');
|
||||
return;
|
||||
}
|
||||
|
||||
const headers = { Authorization: `Bot ${discordToken}` };
|
||||
const guildsRes = await fetch('https://discord.com/api/v10/users/@me/guilds', { headers });
|
||||
if (!guildsRes.ok) {
|
||||
const error = await guildsRes.text();
|
||||
console.error(`Discord: Failed to fetch guilds: ${error}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const guilds = (await guildsRes.json()) as DiscordGuild[];
|
||||
if (guilds.length === 0) {
|
||||
console.log('Discord:\n (bot is not in any servers)');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Discord:');
|
||||
for (const guild of guilds) {
|
||||
const channelsRes = await fetch(`https://discord.com/api/v10/guilds/${guild.id}/channels`, { headers });
|
||||
if (!channelsRes.ok) {
|
||||
console.log(` Server: ${guild.name} (id: ${guild.id})`);
|
||||
console.log(' (failed to fetch channels)');
|
||||
continue;
|
||||
}
|
||||
|
||||
const channels = (await channelsRes.json()) as DiscordChannel[];
|
||||
const textChannels = channels
|
||||
.filter((c) => DISCORD_TEXT_CHANNEL_TYPES.has(c.type))
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
console.log(` Server: ${guild.name} (id: ${guild.id})`);
|
||||
if (textChannels.length === 0) {
|
||||
console.log(' (no text channels)');
|
||||
} else {
|
||||
const maxNameLen = Math.max(...textChannels.map((c) => c.name.length));
|
||||
for (const ch of textChannels) {
|
||||
const padded = ch.name.padEnd(maxNameLen);
|
||||
console.log(` #${padded} (id: ${ch.id})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function listSlack(token?: string): Promise<void> {
|
||||
const slackToken = token || process.env.SLACK_BOT_TOKEN;
|
||||
if (!slackToken) {
|
||||
console.error('Slack: SLACK_BOT_TOKEN not set, skipping.');
|
||||
return;
|
||||
}
|
||||
|
||||
const allChannels: SlackChannel[] = [];
|
||||
let cursor = '';
|
||||
|
||||
// Cursor-based pagination for workspaces with >1000 channels
|
||||
while (true) {
|
||||
const params = new URLSearchParams({
|
||||
types: 'public_channel,private_channel',
|
||||
exclude_archived: 'true',
|
||||
limit: '1000',
|
||||
});
|
||||
if (cursor) params.set('cursor', cursor);
|
||||
|
||||
const res = await fetch(`https://slack.com/api/conversations.list?${params}`, {
|
||||
headers: { Authorization: `Bearer ${slackToken}` },
|
||||
});
|
||||
|
||||
const data = (await res.json()) as {
|
||||
ok: boolean;
|
||||
channels?: SlackChannel[];
|
||||
error?: string;
|
||||
response_metadata?: { next_cursor?: string };
|
||||
};
|
||||
if (!data.ok) {
|
||||
console.error(`Slack: API error: ${data.error}`);
|
||||
return;
|
||||
}
|
||||
|
||||
allChannels.push(...(data.channels || []));
|
||||
cursor = data.response_metadata?.next_cursor || '';
|
||||
if (!cursor) break;
|
||||
}
|
||||
|
||||
const channels = allChannels.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
console.log('Slack:');
|
||||
if (channels.length === 0) {
|
||||
console.log(' (no channels found)');
|
||||
} else {
|
||||
const maxNameLen = Math.max(...channels.map((c) => c.name.length));
|
||||
for (const ch of channels) {
|
||||
const padded = ch.name.padEnd(maxNameLen);
|
||||
console.log(` #${padded} (id: ${ch.id})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function printUnsupported(platform: string): void {
|
||||
console.log(`${platform}: Channel listing not supported (platform does not expose a bot-visible channel list).`);
|
||||
}
|
||||
|
||||
// ── Agent Config Resolution ──────────────────────────────────────────────────
|
||||
|
||||
export function resolveAgentConfig(agentName?: string): AgentConfig | undefined {
|
||||
if (!agentName) return undefined;
|
||||
|
||||
const config = loadAppConfigOrExit();
|
||||
const agents = normalizeAgents(config);
|
||||
|
||||
const exact = agents.find(a => a.name === agentName);
|
||||
if (exact) return exact;
|
||||
|
||||
const lower = agentName.toLowerCase();
|
||||
const found = agents.find(a => a.name.toLowerCase() === lower);
|
||||
if (found) return found;
|
||||
|
||||
console.error(`Agent "${agentName}" not found in config`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
export function resolveListingTokens(
|
||||
agentConfig: AgentConfig | undefined,
|
||||
agentName?: string,
|
||||
): { discordToken?: string; slackToken?: string } {
|
||||
// When an agent is explicitly selected, only use that agent's configured tokens.
|
||||
// Do not fall back to global env vars (prevents cross-agent token leakage).
|
||||
if (agentName) {
|
||||
return {
|
||||
discordToken: agentConfig?.channels?.discord?.token,
|
||||
slackToken: agentConfig?.channels?.slack?.botToken,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
discordToken: agentConfig?.channels?.discord?.token || process.env.DISCORD_BOT_TOKEN,
|
||||
slackToken: agentConfig?.channels?.slack?.botToken || process.env.SLACK_BOT_TOKEN,
|
||||
};
|
||||
}
|
||||
|
||||
// ── Arg Parsing ──────────────────────────────────────────────────────────────
|
||||
|
||||
export function parseChannelArgs(args: string[]): { channel?: string; agent?: string; error?: string } {
|
||||
let channel: string | undefined;
|
||||
let agent: string | undefined;
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
const next = args[i + 1];
|
||||
|
||||
if (arg === '--channel' || arg === '-c') {
|
||||
if (!next || next.startsWith('-')) return { error: 'Missing value for --channel' };
|
||||
channel = next.toLowerCase();
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (arg === '--agent') {
|
||||
if (!next || next.startsWith('-')) return { error: 'Missing value for --agent' };
|
||||
agent = next;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (!arg.startsWith('-')) {
|
||||
if (!channel) {
|
||||
channel = arg.toLowerCase();
|
||||
} else {
|
||||
return { error: `Unexpected argument: ${arg}` };
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return { channel, agent };
|
||||
}
|
||||
|
||||
// ── Main Entry Points ────────────────────────────────────────────────────────
|
||||
|
||||
export async function listGroups(channel?: string, agentName?: string): Promise<void> {
|
||||
const agentConfig = resolveAgentConfig(agentName);
|
||||
|
||||
const { discordToken, slackToken } = resolveListingTokens(agentConfig, agentName);
|
||||
|
||||
if (channel) {
|
||||
switch (channel) {
|
||||
case 'discord':
|
||||
await listDiscord(discordToken);
|
||||
break;
|
||||
case 'slack':
|
||||
await listSlack(slackToken);
|
||||
break;
|
||||
case 'telegram':
|
||||
printUnsupported('Telegram');
|
||||
break;
|
||||
case 'whatsapp':
|
||||
printUnsupported('WhatsApp');
|
||||
break;
|
||||
case 'signal':
|
||||
printUnsupported('Signal');
|
||||
break;
|
||||
default:
|
||||
console.error(`Unknown channel: ${channel}. Supported for listing: discord, slack`);
|
||||
process.exit(1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const hasDiscord = !!discordToken;
|
||||
const hasSlack = !!slackToken;
|
||||
|
||||
if (!hasDiscord && !hasSlack) {
|
||||
if (agentName) {
|
||||
console.log(`No Discord or Slack channels configured for agent "${agentName}".`);
|
||||
} else {
|
||||
console.log('No supported platforms configured. Set DISCORD_BOT_TOKEN or SLACK_BOT_TOKEN.');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasDiscord) {
|
||||
await listDiscord(discordToken);
|
||||
}
|
||||
if (hasSlack) {
|
||||
if (hasDiscord) console.log('');
|
||||
await listSlack(slackToken);
|
||||
}
|
||||
}
|
||||
|
||||
export async function listGroupsFromArgs(args: string[]): Promise<void> {
|
||||
const { channel, agent, error } = parseChannelArgs(args);
|
||||
if (error) {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
await listGroups(channel, agent);
|
||||
}
|
||||
Reference in New Issue
Block a user