Merge remote-tracking branch 'upstream/main' into fix/setup-reuse-existing-env

# Conflicts:
#	setup/channels/imessage.ts
#	setup/channels/telegram.ts
This commit is contained in:
gavrielc
2026-04-29 13:58:21 +03:00
12 changed files with 71 additions and 45 deletions

View File

@@ -31,6 +31,7 @@ import { brightSelect } from '../lib/bright-select.js';
import { confirmThenOpen } from '../lib/browser.js';
import { askOperatorRole } from '../lib/role-prompt.js';
import { ensureAnswer, fail, runQuietChild } from '../lib/runner.js';
import { note } from '../lib/theme.js';
const DEFAULT_AGENT_NAME = 'Nano';
const DISCORD_API = 'https://discord.com/api/v10';
@@ -155,7 +156,7 @@ async function askHasBotToken(): Promise<boolean> {
async function walkThroughBotCreation(): Promise<void> {
const url = 'https://discord.com/developers/applications';
p.note(
note(
[
"You'll create a Discord bot in the Developer Portal. It's free and takes about a minute.",
'',
@@ -184,7 +185,7 @@ function showTokenLocationReminder(hasExistingBot: boolean): void {
// to find it — tokens in the Dev Portal aren't visible after first reveal,
// and "Reset Token" issues a new one.
if (hasExistingBot) {
p.note(
note(
[
"Where to find your bot token:",
'',
@@ -216,7 +217,7 @@ async function walkThroughServerCreation(): Promise<void> {
// the web client and rely on the + button being visible. The steps below
// are the same whether they're in the desktop app or the browser.
const url = 'https://discord.com/channels/@me';
p.note(
note(
[
"A Discord server is just a private space for you and the bot. Free and takes 30 seconds.",
'',
@@ -254,6 +255,7 @@ async function collectDiscordToken(): Promise<string> {
const answer = ensureAnswer(
await p.password({
message: 'Paste your bot token',
clearOnError: true,
validate: (v) => {
const t = (v ?? '').trim();
if (!t) return 'Token is required';
@@ -404,7 +406,7 @@ async function resolveOwnerUserId(
}
async function promptForUserIdWithDevMode(): Promise<string> {
p.note(
note(
[
"To get your Discord user ID:",
'',
@@ -442,7 +444,7 @@ async function promptInviteBot(
`&scope=bot` +
`&permissions=${INVITE_PERMISSIONS}`;
p.note(
note(
[
`@${botUsername} needs to share a server with you before it can DM you.`,
'',

View File

@@ -36,7 +36,7 @@ import * as setupLog from '../logs.js';
import { brightSelect } from '../lib/bright-select.js';
import { askOperatorRole } from '../lib/role-prompt.js';
import { ensureAnswer, fail, runQuietChild } from '../lib/runner.js';
import { wrapForGutter } from '../lib/theme.js';
import { note, wrapForGutter } from '../lib/theme.js';
const DEFAULT_AGENT_NAME = 'Nano';
@@ -189,7 +189,7 @@ async function walkThroughFullDiskAccess(): Promise<void> {
}
const nodeDir = path.dirname(nodePath);
p.note(
note(
wrapForGutter(
[
`iMessage needs Full Disk Access granted to the Node binary:`,
@@ -235,7 +235,7 @@ async function collectRemoteCreds(): Promise<RemoteCreds> {
}
}
p.note(
note(
[
"Photon is a separate service that owns an iMessage account and",
"exposes it over HTTP. NanoClaw will talk to it via its API.",
@@ -263,6 +263,7 @@ async function collectRemoteCreds(): Promise<RemoteCreds> {
const keyAnswer = ensureAnswer(
await p.password({
message: 'Photon API key',
clearOnError: true,
validate: (v) => ((v ?? '').trim() ? undefined : 'API key is required'),
}),
);
@@ -277,7 +278,7 @@ async function collectRemoteCreds(): Promise<RemoteCreds> {
}
async function askOperatorHandle(): Promise<string> {
p.note(
note(
[
"What phone number or email do you iMessage with?",
"That's where your assistant will send its welcome message.",

View File

@@ -44,6 +44,7 @@ import {
writeStepEntry,
} from '../lib/runner.js';
import { askOperatorRole } from '../lib/role-prompt.js';
import { note } from '../lib/theme.js';
const DEFAULT_AGENT_NAME = 'Nano';
@@ -139,7 +140,7 @@ async function ensureSignalCli(): Promise<void> {
if (!probe.error && probe.status === 0) return;
if (process.platform === 'darwin') {
p.note(
note(
[
"NanoClaw talks to Signal through signal-cli, which isn't installed yet.",
'',
@@ -152,7 +153,7 @@ async function ensureSignalCli(): Promise<void> {
'signal-cli not found',
);
} else {
p.note(
note(
[
"NanoClaw talks to Signal through signal-cli, which isn't installed yet.",
'',

View File

@@ -28,7 +28,7 @@ import * as setupLog from '../logs.js';
import { confirmThenOpen } from '../lib/browser.js';
import { askOperatorRole } from '../lib/role-prompt.js';
import { ensureAnswer, fail, runQuietChild } from '../lib/runner.js';
import { wrapForGutter } from '../lib/theme.js';
import { note, wrapForGutter } from '../lib/theme.js';
const SLACK_API = 'https://slack.com/api';
const SLACK_APPS_URL = 'https://api.slack.com/apps';
@@ -121,7 +121,7 @@ export async function runSlackChannel(displayName: string): Promise<void> {
}
async function walkThroughAppCreation(): Promise<void> {
p.note(
note(
[
"You'll create a Slack app that the assistant talks through.",
"Free and stays inside the workspaces you pick.",
@@ -166,6 +166,7 @@ async function collectBotToken(): Promise<string> {
const answer = ensureAnswer(
await p.password({
message: 'Paste your Slack bot token',
clearOnError: true,
validate: (v) => {
const t = (v ?? '').trim();
if (!t) return 'Token is required';
@@ -199,6 +200,7 @@ async function collectSigningSecret(): Promise<string> {
const answer = ensureAnswer(
await p.password({
message: 'Paste your Slack signing secret',
clearOnError: true,
validate: (v) => {
const t = (v ?? '').trim();
if (!t) return 'Signing secret is required';
@@ -286,7 +288,7 @@ async function validateSlackToken(token: string): Promise<WorkspaceInfo> {
}
async function collectSlackUserId(): Promise<string> {
p.note(
note(
[
"To get your Slack member ID:",
'',
@@ -391,7 +393,7 @@ async function resolveAgentName(): Promise<string> {
}
function showPostInstallChecklist(info: WorkspaceInfo): void {
p.note(
note(
wrapForGutter(
[
`Your agent is wired to Slack and a welcome DM is on its way.`,

View File

@@ -40,6 +40,7 @@ import {
} from '../lib/claude-handoff.js';
import { ensureAnswer, fail, runQuietChild } from '../lib/runner.js';
import { buildTeamsAppPackage } from '../lib/teams-manifest.js';
import { note } from '../lib/theme.js';
import * as setupLog from '../logs.js';
const CHANNEL = 'teams';
@@ -101,7 +102,7 @@ export async function runTeamsChannel(_displayName: string): Promise<void> {
// ─── step: intro / prereqs ──────────────────────────────────────────────
function printIntro(): void {
p.note(
note(
[
'Setting up Teams is more involved than the other channels — about',
'7 steps across the Azure portal and Teams admin.',
@@ -115,7 +116,7 @@ function printIntro(): void {
}
async function confirmPrereqs(args: { collected: Collected; completed: string[] }): Promise<void> {
p.note(
note(
[
'Before we start, confirm you have:',
'',
@@ -141,7 +142,7 @@ async function confirmPrereqs(args: { collected: Collected; completed: string[]
// ─── step: public URL ──────────────────────────────────────────────────
async function stepPublicUrl(args: { collected: Collected; completed: string[] }): Promise<void> {
p.note(
note(
[
"Azure Bot Service delivers messages to an HTTPS endpoint you",
"control. The endpoint needs to reach this machine's webhook",
@@ -197,7 +198,7 @@ async function stepAppRegistration(args: {
collected: Collected;
completed: string[];
}): Promise<void> {
p.note(
note(
[
`1. In ${AZURE_PORTAL_URL}, search "App registrations" → "New registration"`,
'2. Name it (e.g. "NanoClaw")',
@@ -281,7 +282,7 @@ async function stepClientSecret(args: {
collected: Collected;
completed: string[];
}): Promise<void> {
p.note(
note(
[
`1. In your app registration, open "Certificates & secrets"`,
'2. Click "New client secret"',
@@ -298,6 +299,7 @@ async function stepClientSecret(args: {
const answer = ensureAnswer(
await p.password({
message: 'Paste the client secret Value',
clearOnError: true,
validate: validateWithHelpEscape((v) => {
const t = (v ?? '').trim();
if (!t) return 'Required';
@@ -350,7 +352,7 @@ async function stepAzureBot(args: {
` --appid ${args.collected.appId} \\\n` +
` ${tenantFlag}--endpoint "${endpoint}"`;
p.note(
note(
[
`In ${AZURE_PORTAL_URL}, search "Azure Bot" → Create.`,
'',
@@ -387,7 +389,7 @@ async function stepEnableTeamsChannel(args: {
collected: Collected;
completed: string[];
}): Promise<void> {
p.note(
note(
[
'1. Open your Azure Bot resource → Channels',
'2. Click Microsoft Teams → Accept terms → Apply',
@@ -457,7 +459,7 @@ async function stepSideload(args: {
completed: string[];
zipPath: string;
}): Promise<void> {
p.note(
note(
[
'1. Open Microsoft Teams',
'2. Go to Apps → Manage your apps → Upload an app',
@@ -523,7 +525,7 @@ async function finishWithHandoff(
collected: Collected,
completed: string[],
): Promise<void> {
p.note(
note(
[
'The Teams adapter is live and the service is running.',
'',
@@ -552,7 +554,7 @@ async function finishWithHandoff(
);
if (choice === 'self') {
p.note(
note(
[
' 1. Find your bot in Teams (search by name, or via the sideloaded',
' app) and send it a message ("hi" is fine)',

View File

@@ -33,7 +33,7 @@ import {
spawnStep,
writeStepEntry,
} from '../lib/runner.js';
import { brandBold } from '../lib/theme.js';
import { brandBold, note } from '../lib/theme.js';
const DEFAULT_AGENT_NAME = 'Nano';
@@ -47,7 +47,7 @@ export async function runTelegramChannel(displayName: string): Promise<void> {
// installed, or the bot's web profile if not. tg://resolve?domain= is
// more direct but silently fails when the scheme isn't registered.
const botUrl = `https://t.me/${botUsername}`;
p.note(
note(
[
`Opening @${botUsername} in Telegram so it's ready when the pairing code shows up.`,
'',
@@ -144,7 +144,7 @@ async function collectTelegramToken(): Promise<string> {
}
}
p.note(
note(
[
"Your assistant talks to you through a Telegram bot you create.",
"Here's how:",
@@ -162,6 +162,7 @@ async function collectTelegramToken(): Promise<string> {
const answer = ensureAnswer(
await p.password({
message: 'Paste your bot token',
clearOnError: true,
validate: (v) => {
if (!v || !v.trim()) return "Token is required";
if (!/^[0-9]+:[A-Za-z0-9_-]{35,}$/.test(v.trim())) {
@@ -252,7 +253,7 @@ async function runPairTelegram(): Promise<
} else {
stopSpinner("Old code expired. Here's a fresh one.");
}
p.note(formatCodeCard(block.fields.CODE ?? '????'), 'Secret code');
note(formatCodeCard(block.fields.CODE ?? '????'), 'Secret code');
s.start('Waiting for you to send the code from Telegram…');
spinnerActive = true;
} else if (block.type === 'PAIR_TELEGRAM_ATTEMPT') {

View File

@@ -46,7 +46,7 @@ import {
writeStepEntry,
} from '../lib/runner.js';
import { askOperatorRole } from '../lib/role-prompt.js';
import { brandBold } from '../lib/theme.js';
import { brandBold, note } from '../lib/theme.js';
const DEFAULT_AGENT_NAME = 'Nano';
const AUTH_CREDS_PATH = path.join(process.cwd(), 'store', 'auth', 'creds.json');
@@ -171,7 +171,7 @@ async function askAuthMethod(): Promise<AuthMethod> {
}
async function askPhoneNumber(): Promise<string> {
p.note(
note(
[
"Enter your phone number the way WhatsApp expects it:",
'',
@@ -249,7 +249,7 @@ async function runWhatsAppAuth(
} else if (block.type === 'WHATSAPP_AUTH_PAIRING_CODE') {
const code = block.fields.CODE ?? '????';
stopSpinner('Your pairing code is ready.');
p.note(formatPairingCard(code), 'Pairing code');
note(formatPairingCard(code), 'Pairing code');
s.start('Waiting for you to enter the code…');
spinnerActive = true;
} else if (block.type === 'WHATSAPP_AUTH') {
@@ -395,7 +395,7 @@ async function restartService(): Promise<void> {
}
async function askChatPhone(authedPhone: string): Promise<string> {
p.note(
note(
[
`Authenticated with ${k.cyan('+' + authedPhone)}.`,
'',