feat(setup): add Slack and iMessage channel flows (experimental)

Slack: interactive driver walks through app creation, validates the
bot token via auth.test, installs the adapter, and prints a
post-install checklist for the webhook URL + Event Subscriptions
config. No welcome DM since Slack needs a public URL before inbound
events work — the driver's own "finish in Slack" note replaces the
outro "check your DMs" banner.

iMessage: picks local (macOS) vs remote (Photon) mode. Local mode
opens the node binary's directory in Finder so the user can drag it
into Full Disk Access. Remote mode prompts for Photon URL + API key.
Asks for the operator's phone/email, then wires the first agent
including a welcome iMessage.

Both marked "(experimental)" in the askChannelChoice picker.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gavrielc
2026-04-23 11:09:30 +03:00
parent d8b1f52f2b
commit 3101f65a72
6 changed files with 891 additions and 11 deletions

View File

@@ -27,6 +27,8 @@ import * as p from '@clack/prompts';
import k from 'kleur';
import { runDiscordChannel } from './channels/discord.js';
import { runIMessageChannel } from './channels/imessage.js';
import { runSlackChannel } from './channels/slack.js';
import { runTeamsChannel } from './channels/teams.js';
import { runTelegramChannel } from './channels/telegram.js';
import { runWhatsAppChannel } from './channels/whatsapp.js';
@@ -48,6 +50,15 @@ import { isValidTimezone } from '../src/timezone.js';
const CLI_AGENT_NAME = 'Terminal Agent';
const RUN_START = Date.now();
type ChannelChoice =
| 'telegram'
| 'discord'
| 'whatsapp'
| 'teams'
| 'slack'
| 'imessage'
| 'skip';
async function main(): Promise<void> {
printIntro();
initProgressionLog();
@@ -295,8 +306,7 @@ async function main(): Promise<void> {
await runTimezoneStep();
}
let channelChoice: 'telegram' | 'discord' | 'whatsapp' | 'teams' | 'skip' =
'skip';
let channelChoice: ChannelChoice = 'skip';
if (!skip.has('channel')) {
channelChoice = await askChannelChoice();
if (channelChoice === 'telegram') {
@@ -307,10 +317,14 @@ async function main(): Promise<void> {
await runWhatsAppChannel(displayName!);
} else if (channelChoice === 'teams') {
await runTeamsChannel(displayName!);
} else if (channelChoice === 'slack') {
await runSlackChannel(displayName!);
} else if (channelChoice === 'imessage') {
await runIMessageChannel(displayName!);
} else {
p.log.info(
wrapForGutter(
'No messaging app for now. You can add one later (like Telegram, Discord, WhatsApp, Teams, or Slack).',
'No messaging app for now. You can add one later (like Telegram, Discord, WhatsApp, Teams, Slack, or iMessage).',
4,
),
);
@@ -420,9 +434,7 @@ async function main(): Promise<void> {
}
}
function channelDmLabel(
choice: 'telegram' | 'discord' | 'whatsapp' | 'teams' | 'skip',
): string | null {
function channelDmLabel(choice: ChannelChoice): string | null {
switch (choice) {
case 'telegram':
return 'Telegram';
@@ -432,6 +444,13 @@ function channelDmLabel(
return 'WhatsApp';
case 'teams':
return 'Teams';
case 'imessage':
return 'iMessage';
case 'slack':
// Slack install doesn't wire an agent or send a welcome DM — the
// driver prints its own "finish in your Slack app" note. Falling
// through to null avoids a misleading "check your Slack DMs" banner.
return null;
default:
return null;
}
@@ -807,16 +826,25 @@ async function askDisplayName(fallback: string): Promise<string> {
return value;
}
async function askChannelChoice(): Promise<
'telegram' | 'discord' | 'whatsapp' | 'teams' | 'skip'
> {
async function askChannelChoice(): Promise<ChannelChoice> {
const isMac = process.platform === 'darwin';
const choice = ensureAnswer(
await brightSelect({
await brightSelect<ChannelChoice>({
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: 'whatsapp', label: 'Yes, connect WhatsApp' },
{
value: 'imessage',
label: 'Yes, connect iMessage (experimental)',
hint: isMac ? 'local macOS mode' : 'remote Photon only',
},
{
value: 'slack',
label: 'Yes, connect Slack (experimental)',
hint: 'needs public URL',
},
{ value: 'teams', label: 'Yes, connect Microsoft Teams', hint: 'complex setup' },
{ value: 'skip', label: 'Skip for now', hint: "I'll just use the terminal" },
],
@@ -824,7 +852,7 @@ async function askChannelChoice(): Promise<
);
setupLog.userInput('channel_choice', String(choice));
phEmit('channel_chosen', { channel: String(choice) });
return choice as 'telegram' | 'discord' | 'whatsapp' | 'teams' | 'skip';
return choice;
}
// ─── interactive / env helpers ─────────────────────────────────────────