feat(setup): optional Discord wiring in setup:auto

Mirror of the Telegram flow but without a pairing step — Discord
exposes enough via the bot token that we only need one paste from the
operator, with every other identity field derived:

  GET /users/@me                       → bot username (sanity check)
  GET /oauth2/applications/@me         → application id, verify_key
                                         (public key), owner {id, username}
  POST /users/@me/channels             → DM channel id

After confirming "Is @<owner_username> your Discord account?" the flow
invites the bot to a server (OAuth URL + open + confirm, gating so the
welcome DM can actually reach the operator), installs the adapter, opens
the DM channel, and hands off to init-first-agent with
--channel discord --platform-id discord:@me:<dmChannelId>. The existing
init-first-agent welcome-over-CLI-socket path delivers the greeting
through the normal adapter pipeline — no Discord-specific code in the
welcome logic.

Fallbacks: if the app is team-owned (no owner object) or the operator
declines the confirmation, a Dev Mode walkthrough + user-id paste prompt
takes over.

Adds:
- setup/add-discord.sh (non-interactive installer, mirror of
  add-telegram.sh minus pair-step registration)
- setup/channels/discord.ts (operator-facing flow)
- setup/auto.ts: Discord option in askChannelChoice + dispatch

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gavrielc
2026-04-22 10:45:05 +03:00
parent 1e0f7d631d
commit 9b6e5b24a1
3 changed files with 584 additions and 3 deletions

View File

@@ -25,6 +25,7 @@ import { spawn, spawnSync } from 'child_process';
import * as p from '@clack/prompts';
import k from 'kleur';
import { runDiscordChannel } from './channels/discord.js';
import { runTelegramChannel } from './channels/telegram.js';
import * as setupLog from './logs.js';
import { ensureAnswer, fail, runQuietChild, runQuietStep } from './lib/runner.js';
@@ -195,9 +196,11 @@ async function main(): Promise<void> {
const choice = await askChannelChoice();
if (choice === 'telegram') {
await runTelegramChannel(displayName!);
} else if (choice === 'discord') {
await runDiscordChannel(displayName!);
} else {
p.log.info(
"No messaging app for now. You can add one later (like Telegram, Slack, or Discord).",
"No messaging app for now. You can add one later (like Telegram, Discord, or Slack).",
);
}
}
@@ -372,18 +375,19 @@ async function askDisplayName(fallback: string): Promise<string> {
return value;
}
async function askChannelChoice(): Promise<'telegram' | 'skip'> {
async function askChannelChoice(): Promise<'telegram' | 'discord' | 'skip'> {
const choice = ensureAnswer(
await p.select({
message: 'Want to chat with your assistant from your phone?',
options: [
{ value: 'telegram', label: 'Yes, connect Telegram', hint: 'recommended' },
{ value: 'discord', label: 'Yes, connect Discord' },
{ value: 'skip', label: 'Skip for now', hint: "I'll just use the terminal" },
],
}),
);
setupLog.userInput('channel_choice', String(choice));
return choice as 'telegram' | 'skip';
return choice as 'telegram' | 'discord' | 'skip';
}
// ─── interactive / env helpers ─────────────────────────────────────────