wispy-ai 1.7.0 → 1.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,16 +1,15 @@
1
1
  /**
2
- * Interactive setup wizard — first-run onboarding experience.
2
+ * Interactive setup wizard — comprehensive first-run onboarding.
3
3
  *
4
4
  * Steps:
5
- * 1. Welcome screen with ASCII art
6
- * 2. API key configuration (+ Telegram, Autonomous Mode)
7
- * 3. Theme selection
8
- * 4. Agent selection
9
- * 5. Integration selection
10
- * 6. MCP Servers
11
- * 7. x402 Agentic Commerce
12
- * 8. Voice/TTS
13
- * 9. Summary
5
+ * 1. Welcome
6
+ * 2. AI Engine + API Keys
7
+ * 3. Channels (Telegram, Discord, Slack, WhatsApp, Matrix, Signal, Email, Phone)
8
+ * 4. Integrations (Google, Productivity, Social, Smart Home, Commerce, Security)
9
+ * 5. Wallet & Commerce (x402)
10
+ * 6. Security & Mode
11
+ * 7. Extras (Voice, MCP, Theme, Agents, Memory)
12
+ * 8. Summary
14
13
  */
15
14
  import * as readline from "readline";
16
15
  import * as fs from "fs";
@@ -18,14 +17,15 @@ import * as path from "path";
18
17
  import chalk from "chalk";
19
18
  import gradient from "gradient-string";
20
19
  import { writeYAML } from "../../utils/file.js";
21
- // ── Brand color ──────────────────────────────────────────
20
+ // ── Brand ────────────────────────────────────────────────
22
21
  const sky = chalk.rgb(49, 204, 255);
23
22
  const skyBold = chalk.rgb(49, 204, 255).bold;
24
23
  const dim = chalk.dim;
25
24
  const bold = chalk.bold;
26
25
  const green = chalk.green;
27
- const yellowBright = chalk.yellowBright;
28
- // ── ASCII Art (gradient block letters) ───────────────────
26
+ const yellow = chalk.yellowBright;
27
+ const accent = chalk.hex("#00FFA3");
28
+ const wizardGradient = gradient(["#31CCFF", "#7B61FF", "#31CCFF"]);
29
29
  const WISPY_ASCII_RAW = [
30
30
  "\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557",
31
31
  "\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557 \u255A\u2588\u2588\u2557 \u2588\u2588\u2554\u255D",
@@ -34,48 +34,9 @@ const WISPY_ASCII_RAW = [
34
34
  "\u255A\u2588\u2588\u2588\u2554\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 ",
35
35
  " \u255A\u2550\u2550\u255D\u255A\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D ",
36
36
  ];
37
- const wizardGradient = gradient(["#31CCFF", "#7B61FF", "#31CCFF"]);
38
- const WISPY_ASCII = "\n" + WISPY_ASCII_RAW.map(line => ` ${wizardGradient(line)}`).join("\n") + "\n";
39
- // ── Theme definitions ────────────────────────────────────
40
- const THEMES = [
41
- { key: "dawn", label: `${chalk.yellow("◉")} Dawn — Warm sunrise tones` },
42
- { key: "day", label: `${chalk.rgb(49, 204, 255)("◉")} Day — Clean sky blue (default)` },
43
- { key: "dusk", label: `${chalk.magenta("◉")} Dusk — Twilight purple` },
44
- { key: "night", label: `${chalk.blue("◉")} Night — Deep dark mode` },
45
- ];
46
- const THEME_ICON = {
47
- dawn: chalk.yellow("◉"),
48
- day: chalk.rgb(49, 204, 255)("◉"),
49
- dusk: chalk.magenta("◉"),
50
- night: chalk.blue("◉"),
51
- };
52
- // ── Agent definitions ────────────────────────────────────
53
- const AGENTS = [
54
- { id: "coder", label: "Coder — Code generation, debugging, refactoring", defaultOn: true },
55
- { id: "researcher", label: "Researcher — Web search, analysis, summarization", defaultOn: true },
56
- { id: "writer", label: "Writer — Content creation, copywriting, docs", defaultOn: false },
57
- { id: "devops", label: "DevOps — CI/CD, deployment, infrastructure", defaultOn: false },
58
- { id: "designer", label: "Designer — UI/UX design, accessibility", defaultOn: false },
59
- { id: "data", label: "Data — SQL, data analysis, visualization", defaultOn: false },
60
- { id: "security", label: "Security — Audit, vulnerability scanning", defaultOn: false },
61
- { id: "planner", label: "Planner — Task breakdown, project planning", defaultOn: false },
62
- ];
63
- // ── Integration definitions ──────────────────────────────
64
- const INTEGRATIONS = [
65
- { id: "google-calendar", label: "Google Calendar" },
66
- { id: "gmail", label: "Gmail" },
67
- { id: "google-drive", label: "Google Drive" },
68
- { id: "discord", label: "Discord" },
69
- { id: "github", label: "GitHub" },
70
- { id: "notion", label: "Notion" },
71
- { id: "slack", label: "Slack" },
72
- { id: "spotify", label: "Spotify" },
73
- { id: "zoho-email", label: "Zoho Mail" },
74
- { id: "anthropic", label: "Anthropic (Claude)" },
75
- { id: "groq", label: "Groq" },
76
- { id: "openrouter", label: "OpenRouter" },
77
- { id: "kimi", label: "Kimi (Moonshot)" },
78
- ];
37
+ const WISPY_ASCII = "\n" +
38
+ WISPY_ASCII_RAW.map((line) => ` ${wizardGradient(line)}`).join("\n") +
39
+ "\n";
79
40
  // ── Helpers ──────────────────────────────────────────────
80
41
  function ask(rl, question) {
81
42
  return new Promise((resolve, reject) => {
@@ -93,637 +54,891 @@ function parseNumberList(input, max) {
93
54
  function capitalize(s) {
94
55
  return s.charAt(0).toUpperCase() + s.slice(1);
95
56
  }
57
+ function masked(key, prefixLen = 8, suffixLen = 4) {
58
+ if (key.length < prefixLen + suffixLen + 4)
59
+ return key.slice(0, 4) + "...";
60
+ return key.slice(0, prefixLen) + "..." + key.slice(-suffixLen);
61
+ }
62
+ function stepHeader(step, total, title, icon) {
63
+ console.clear();
64
+ console.log(WISPY_ASCII);
65
+ const progress = dim(`[${step}/${total}]`);
66
+ console.log(skyBold(` ${icon} Step ${step} of ${total}: ${title} ${progress}\n`));
67
+ }
68
+ function envOrEmpty(name) {
69
+ return process.env[name] || "";
70
+ }
71
+ // ── Definitions ──────────────────────────────────────────
72
+ const THEMES = [
73
+ { key: "dawn", label: `${chalk.yellow("\u25C9")} Dawn -- Warm sunrise tones` },
74
+ { key: "day", label: `${sky("\u25C9")} Day -- Clean sky blue (default)` },
75
+ { key: "dusk", label: `${chalk.magenta("\u25C9")} Dusk -- Twilight purple` },
76
+ { key: "night", label: `${chalk.blue("\u25C9")} Night -- Deep dark mode` },
77
+ ];
78
+ const THEME_ICON = {
79
+ dawn: chalk.yellow("\u25C9"),
80
+ day: sky("\u25C9"),
81
+ dusk: chalk.magenta("\u25C9"),
82
+ night: chalk.blue("\u25C9"),
83
+ };
84
+ const AGENTS = [
85
+ { id: "coder", label: "Coder -- Code generation, debugging, refactoring", defaultOn: true },
86
+ { id: "researcher", label: "Researcher -- Web search, analysis, summarization", defaultOn: true },
87
+ { id: "writer", label: "Writer -- Content creation, copywriting, docs", defaultOn: false },
88
+ { id: "devops", label: "DevOps -- CI/CD, deployment, infrastructure", defaultOn: false },
89
+ { id: "designer", label: "Designer -- UI/UX design, accessibility", defaultOn: false },
90
+ { id: "data", label: "Data -- SQL, data analysis, visualization", defaultOn: false },
91
+ { id: "security", label: "Security -- Audit, vulnerability scanning", defaultOn: false },
92
+ { id: "planner", label: "Planner -- Task breakdown, project planning", defaultOn: false },
93
+ ];
94
+ const CHANNELS = [
95
+ {
96
+ id: "telegram",
97
+ label: "Telegram",
98
+ configKey: "telegram",
99
+ envVars: [{ name: "TELEGRAM_BOT_TOKEN", prompt: "Bot token", hint: "123456:ABC-DEF..." }],
100
+ instructions: [
101
+ `Open Telegram, search ${bold("@BotFather")}`,
102
+ `Send ${sky("/newbot")}, pick a name and username`,
103
+ "Copy the token BotFather gives you",
104
+ ],
105
+ },
106
+ {
107
+ id: "discord",
108
+ label: "Discord",
109
+ configKey: "discord",
110
+ envVars: [{ name: "DISCORD_BOT_TOKEN", prompt: "Bot token", hint: "MTIz..." }],
111
+ instructions: [
112
+ `Go to ${sky("https://discord.com/developers/applications")}`,
113
+ "Create app, go to Bot tab, click Reset Token",
114
+ "Enable Message Content Intent under Privileged Gateway Intents",
115
+ "Invite bot to your server with bot + applications.commands scope",
116
+ ],
117
+ },
118
+ {
119
+ id: "slack",
120
+ label: "Slack",
121
+ configKey: "slack",
122
+ envVars: [
123
+ { name: "SLACK_BOT_TOKEN", prompt: "Bot token", hint: "xoxb-..." },
124
+ { name: "SLACK_SIGNING_SECRET", prompt: "Signing secret", hint: "abc123..." },
125
+ { name: "SLACK_APP_TOKEN", prompt: "App-level token", hint: "xapp-..." },
126
+ ],
127
+ instructions: [
128
+ `Go to ${sky("https://api.slack.com/apps")} and create a new app`,
129
+ "Enable Socket Mode, add chat:write + app_mentions:read scopes",
130
+ "Install to your workspace and copy the tokens",
131
+ ],
132
+ },
133
+ {
134
+ id: "whatsapp",
135
+ label: "WhatsApp",
136
+ configKey: "whatsapp",
137
+ envVars: [],
138
+ instructions: [
139
+ "WhatsApp uses QR code authentication (Baileys)",
140
+ `Run ${sky("wispy gateway")} and scan the QR code with your phone`,
141
+ "No API key needed -- connects directly to WhatsApp Web",
142
+ ],
143
+ },
144
+ {
145
+ id: "matrix",
146
+ label: "Matrix",
147
+ configKey: "matrix",
148
+ envVars: [
149
+ { name: "MATRIX_HOMESERVER", prompt: "Homeserver URL", hint: "https://matrix.org" },
150
+ { name: "MATRIX_ACCESS_TOKEN", prompt: "Access token", hint: "syt_..." },
151
+ { name: "MATRIX_USER_ID", prompt: "User ID", hint: "@wispy:matrix.org" },
152
+ ],
153
+ instructions: [
154
+ "Create a Matrix account on your homeserver",
155
+ `Get an access token from ${sky("Element > Settings > Help & About")}`,
156
+ ],
157
+ },
158
+ {
159
+ id: "signal",
160
+ label: "Signal",
161
+ configKey: "signal",
162
+ envVars: [
163
+ { name: "SIGNAL_CLI_URL", prompt: "signal-cli REST URL", hint: "http://localhost:8080" },
164
+ { name: "SIGNAL_PHONE_NUMBER", prompt: "Phone number", hint: "+1234567890" },
165
+ ],
166
+ instructions: [
167
+ `Run signal-cli REST API: ${sky("docker run -p 8080:8080 bbernhard/signal-cli-rest-api")}`,
168
+ "Register your phone number with signal-cli first",
169
+ ],
170
+ },
171
+ {
172
+ id: "email",
173
+ label: "Email (AgentMail)",
174
+ configKey: "email",
175
+ envVars: [{ name: "AGENTMAIL_API_KEY", prompt: "AgentMail API key", hint: "am_..." }],
176
+ instructions: [
177
+ `Get your API key at ${sky("https://agentmail.to")}`,
178
+ "Wispy gets its own email address to send and receive",
179
+ ],
180
+ },
181
+ {
182
+ id: "phone",
183
+ label: "Phone / SMS (Telnyx)",
184
+ configKey: "phone",
185
+ envVars: [
186
+ { name: "TELNYX_API_KEY", prompt: "Telnyx API key", hint: "KEY..." },
187
+ { name: "TELNYX_CONNECTION_ID", prompt: "Connection ID", hint: "123456789" },
188
+ ],
189
+ instructions: [
190
+ `Sign up at ${sky("https://telnyx.com")} and buy a phone number`,
191
+ "Create a TeXML application and get credentials",
192
+ ],
193
+ },
194
+ ];
195
+ const INTEGRATION_CATEGORIES = [
196
+ {
197
+ name: "Google Workspace",
198
+ icon: "\uD83D\uDD0D",
199
+ note: "All Google integrations share one OAuth setup",
200
+ items: [
201
+ { id: "google-calendar", label: "Google Calendar" },
202
+ { id: "gmail", label: "Gmail" },
203
+ { id: "google-drive", label: "Google Drive" },
204
+ { id: "google-docs", label: "Google Docs" },
205
+ { id: "google-sheets", label: "Google Sheets" },
206
+ { id: "google-meet", label: "Google Meet" },
207
+ { id: "google-maps", label: "Google Maps" },
208
+ { id: "google-search", label: "Google Search" },
209
+ { id: "youtube", label: "YouTube" },
210
+ ],
211
+ },
212
+ {
213
+ name: "Productivity",
214
+ icon: "\uD83D\uDCCB",
215
+ items: [
216
+ { id: "github", label: "GitHub", envVars: ["GITHUB_TOKEN"], link: "https://github.com/settings/tokens" },
217
+ { id: "notion", label: "Notion", envVars: ["NOTION_TOKEN"], link: "https://www.notion.so/my-integrations" },
218
+ { id: "linear", label: "Linear", envVars: ["LINEAR_API_KEY"], link: "https://linear.app/settings/api" },
219
+ { id: "obsidian", label: "Obsidian (local)" },
220
+ { id: "trello", label: "Trello", envVars: ["TRELLO_API_KEY", "TRELLO_TOKEN"] },
221
+ { id: "asana", label: "Asana", envVars: ["ASANA_ACCESS_TOKEN"] },
222
+ { id: "calendly", label: "Calendly", envVars: ["CALENDLY_API_KEY"] },
223
+ ],
224
+ },
225
+ {
226
+ name: "Social Media",
227
+ icon: "\uD83D\uDCF1",
228
+ items: [
229
+ { id: "twitter", label: "Twitter / X", envVars: ["TWITTER_API_KEY", "TWITTER_API_SECRET", "TWITTER_ACCESS_TOKEN", "TWITTER_ACCESS_SECRET"], link: "https://developer.x.com/portal" },
230
+ { id: "linkedin", label: "LinkedIn", envVars: ["LINKEDIN_ACCESS_TOKEN"] },
231
+ { id: "instagram", label: "Instagram", envVars: ["INSTAGRAM_ACCESS_TOKEN"] },
232
+ { id: "reddit", label: "Reddit", envVars: ["REDDIT_CLIENT_ID", "REDDIT_CLIENT_SECRET"] },
233
+ ],
234
+ },
235
+ {
236
+ name: "Smart Home",
237
+ icon: "\uD83C\uDFE0",
238
+ items: [
239
+ { id: "hue", label: "Philips Hue", envVars: ["HUE_BRIDGE_IP", "HUE_USERNAME"] },
240
+ { id: "home-assistant", label: "Home Assistant", envVars: ["HA_URL", "HA_TOKEN"] },
241
+ { id: "sonos", label: "Sonos", envVars: ["SONOS_API_KEY", "SONOS_HOUSEHOLD_ID"] },
242
+ ],
243
+ },
244
+ {
245
+ name: "Commerce & Security",
246
+ icon: "\uD83D\uDD12",
247
+ items: [
248
+ { id: "stripe", label: "Stripe", envVars: ["STRIPE_SECRET_KEY"], link: "https://dashboard.stripe.com/apikeys" },
249
+ { id: "1password", label: "1Password", envVars: ["OP_SERVICE_ACCOUNT_TOKEN"] },
250
+ ],
251
+ },
252
+ {
253
+ name: "Music & Media",
254
+ icon: "\uD83C\uDFB5",
255
+ items: [
256
+ { id: "spotify", label: "Spotify", envVars: ["SPOTIFY_ACCESS_TOKEN"] },
257
+ { id: "image-generation", label: "Image Generation (DALL-E / Stability)", envVars: ["IMAGE_GEN_API_KEY"] },
258
+ ],
259
+ },
260
+ ];
261
+ const MCP_SERVERS = [
262
+ { id: "filesystem", label: "File System -- Read/write files anywhere", command: "npx", args: ["-y", "@anthropic/mcp-server-filesystem", "--allow-write", "/"], default: true },
263
+ { id: "fetch", label: "Web Fetch -- HTTP requests with auth", command: "npx", args: ["-y", "@anthropic/mcp-server-fetch"], default: true },
264
+ { id: "memory", label: "Memory -- Persistent key-value storage", command: "npx", args: ["-y", "@anthropic/mcp-server-memory"], default: false },
265
+ { id: "brave", label: "Brave Search-- Web search via Brave API", command: "npx", args: ["-y", "@anthropic/mcp-server-brave-search"], default: false, envRequired: "BRAVE_API_KEY" },
266
+ { id: "github", label: "GitHub -- Access repos, issues, PRs", command: "npx", args: ["-y", "@modelcontextprotocol/server-github"], default: false, envRequired: "GITHUB_TOKEN" },
267
+ { id: "postgres", label: "PostgreSQL -- Query databases directly", command: "npx", args: ["-y", "@modelcontextprotocol/server-postgres"], default: false, envRequired: "DATABASE_URL" },
268
+ ];
96
269
  // ── Public API ───────────────────────────────────────────
97
- /**
98
- * Check whether this is the first run (no config.yaml exists).
99
- */
100
270
  export function isFirstRun(runtimeDir) {
101
271
  const configPath = path.resolve(runtimeDir, "config.yaml");
102
272
  return !fs.existsSync(configPath);
103
273
  }
104
- /**
105
- * Run the interactive setup wizard.
106
- */
107
274
  export async function runSetupWizard(opts) {
108
275
  const { rootDir, runtimeDir } = opts;
109
- const rl = readline.createInterface({
110
- input: process.stdin,
111
- output: process.stdout,
112
- });
113
- // Graceful Ctrl+C
276
+ const TOTAL_STEPS = 8;
277
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
114
278
  rl.on("SIGINT", () => {
115
279
  console.log(dim("\n\n Setup cancelled.\n"));
116
280
  rl.close();
117
281
  process.exit(0);
118
282
  });
283
+ // Collected state
284
+ const env = {};
285
+ let engine = "gemini";
286
+ let apiKey = "";
287
+ let useVertexAI = false;
288
+ let anthropicKey = "";
289
+ let openaiKey = "";
290
+ let groqKey = "";
291
+ let openrouterKey = "";
292
+ let kimiKey = "";
293
+ let ollamaUrl = "";
294
+ let elevenlabsKey = "";
295
+ const enabledChannels = {};
296
+ let selectedIntegrations = [];
297
+ let autonomousMode = true;
298
+ let fullFilesystemAccess = true;
299
+ let selectedTheme = "day";
300
+ let selectedAgents = ["coder", "researcher"];
301
+ let commerceEnabled = false;
302
+ let agentPrivateKey = "";
303
+ let cdpKeyName = "";
304
+ let cdpPrivateKey = "";
305
+ let voiceModel = "edge-tts";
119
306
  try {
120
- // ── Step 1: Welcome ──────────────────────────────────
307
+ // ════════════════════════════════════════════════════════
308
+ // STEP 1: Welcome
309
+ // ════════════════════════════════════════════════════════
121
310
  console.clear();
122
311
  console.log(WISPY_ASCII);
123
- console.log(skyBold(` ${chalk.rgb(49, 204, 255)("*")} Welcome to Wispy!\n`));
124
- console.log(dim(" Wispy is your autonomous AI agent powered by Google Gemini."));
125
- console.log(dim(" This wizard will walk you through setup step by step.\n"));
126
- console.log(dim(" You will need:"));
127
- console.log(` ${sky("1.")} A Gemini API key ${dim("(free at aistudio.google.com/apikey)")}`);
128
- console.log(` ${sky("2.")} ${dim("Optional:")} A Telegram bot token ${dim("(from @BotFather on Telegram)")}`);
129
- console.log(` ${sky("3.")} ${dim("Optional:")} An Ethereum wallet key ${dim("(for x402 payments)")}`);
312
+ console.log(skyBold(` * Welcome to Wispy Setup!\n`));
313
+ console.log(dim(" This wizard sets up everything your AI agent needs."));
314
+ console.log(dim(" Every step has smart defaults -- just press Enter to skip.\n"));
315
+ console.log(` ${sky("What we will configure:")}`);
316
+ console.log(` ${dim("1.")} AI engine and API keys`);
317
+ console.log(` ${dim("2.")} Messaging channels (Telegram, Discord, Slack, etc.)`);
318
+ console.log(` ${dim("3.")} App integrations (GitHub, Notion, Google, etc.)`);
319
+ console.log(` ${dim("4.")} Blockchain wallet (optional)`);
320
+ console.log(` ${dim("5.")} Security and permissions`);
321
+ console.log(` ${dim("6.")} Voice, tools, theme, and more`);
130
322
  console.log("");
131
- console.log(dim(" Everything else has smart defaults. You can change settings later.\n"));
323
+ console.log(dim(" Takes about 3 minutes. You can reconfigure anytime with:"));
324
+ console.log(` ${sky("wispy setup")}\n`);
132
325
  await ask(rl, dim(" Press Enter to start..."));
133
- // ── Step 2: API Key ──────────────────────────────────
134
- console.clear();
135
- console.log(WISPY_ASCII);
136
- console.log(skyBold(` ${chalk.yellow("▸")} Step 1 of 4: AI Configuration\n`));
137
- console.log(dim(" Wispy needs a Google Gemini API key to think and reason.\n"));
138
- console.log(` ${sky("1)")} ${bold("Gemini API Key")} ${dim("(Recommended)")}`);
139
- console.log(dim(` Free to start. Go to this link and click "Create API key":`));
140
- console.log(` ${sky("https://aistudio.google.com/apikey")}`);
326
+ // ════════════════════════════════════════════════════════
327
+ // STEP 2: AI Engine + Keys
328
+ // ════════════════════════════════════════════════════════
329
+ stepHeader(2, TOTAL_STEPS, "AI Engine & API Keys", chalk.yellow("\u25B8"));
330
+ console.log(dim(" Choose your primary AI engine:\n"));
331
+ console.log(` ${sky("1)")} ${bold("Gemini")} ${dim("(Google -- recommended, free tier available)")}`);
332
+ console.log(dim(` Get key: ${sky("https://aistudio.google.com/apikey")}`));
333
+ console.log(` ${sky("2)")} ${bold("Claude")} ${dim("(Anthropic -- advanced reasoning)")}`);
334
+ console.log(dim(` Get key: ${sky("https://console.anthropic.com/keys")}`));
335
+ console.log(` ${sky("3)")} ${bold("Vertex AI")} ${dim("(Google Cloud -- production / high quota)")}`);
141
336
  console.log("");
142
- console.log(` ${sky("2)")} ${bold("Vertex AI")} ${dim("(For production / higher quotas)")}`);
143
- console.log(dim(` Needs a Google Cloud project with billing.`));
144
- console.log(dim(` Only choose this if you already have a GCP account.`));
145
- console.log("");
146
- const existingKey = process.env.GEMINI_API_KEY;
147
- const existingVertexProject = process.env.GOOGLE_CLOUD_PROJECT;
148
- let apiKey = "";
149
- let useVertexAI = false;
150
- if (existingVertexProject) {
151
- console.log(green(` ✓ Found Vertex AI project: ${existingVertexProject}`));
152
- console.log(dim(" Using Vertex AI for higher quotas.\n"));
153
- useVertexAI = true;
337
+ const engineChoice = await ask(rl, sky(" Select [1/2/3] (default: 1): "));
338
+ if (engineChoice.trim() === "2") {
339
+ engine = "claude";
340
+ console.log("");
341
+ const existing = envOrEmpty("ANTHROPIC_API_KEY");
342
+ if (existing) {
343
+ console.log(green(` Found existing key: ${masked(existing, 10)}`));
344
+ anthropicKey = existing;
345
+ }
346
+ else {
347
+ anthropicKey = (await ask(rl, sky(" Anthropic API key (sk-ant-...): "))).trim();
348
+ if (anthropicKey) {
349
+ console.log(green(" Configured."));
350
+ env.ANTHROPIC_API_KEY = anthropicKey;
351
+ }
352
+ }
353
+ // Still need Gemini for embeddings
354
+ console.log(dim("\n Note: Gemini is still used for embeddings. Adding Gemini key is recommended.\n"));
355
+ const geminiKey = envOrEmpty("GEMINI_API_KEY");
356
+ if (geminiKey) {
357
+ console.log(green(` Found Gemini key: ${masked(geminiKey)}`));
358
+ apiKey = geminiKey;
359
+ }
360
+ else {
361
+ const gk = (await ask(rl, sky(" Gemini API key (optional, for embeddings): "))).trim();
362
+ if (gk) {
363
+ apiKey = gk;
364
+ env.GEMINI_API_KEY = gk;
365
+ }
366
+ }
154
367
  }
155
- else if (existingKey) {
156
- const masked = existingKey.slice(0, 8) + "..." + existingKey.slice(-4);
157
- console.log(green(` ✓ Found existing API key: ${masked}\n`));
158
- apiKey = existingKey;
368
+ else if (engineChoice.trim() === "3") {
369
+ useVertexAI = true;
370
+ console.log("");
371
+ const existing = envOrEmpty("GOOGLE_CLOUD_PROJECT");
372
+ if (existing) {
373
+ console.log(green(` Found Vertex project: ${existing}`));
374
+ }
375
+ else {
376
+ console.log(dim(" Set these in .env:"));
377
+ console.log(sky(" GOOGLE_CLOUD_PROJECT=your-project-id"));
378
+ console.log(sky(" GOOGLE_APPLICATION_CREDENTIALS=/path/to/key.json"));
379
+ const proj = (await ask(rl, sky("\n Project ID (or Enter to skip): "))).trim();
380
+ if (proj)
381
+ env.GOOGLE_CLOUD_PROJECT = proj;
382
+ }
159
383
  }
160
384
  else {
161
- const choice = await ask(rl, sky(" Select [1/2] (default: 1): "));
162
- if (choice.trim() === "2") {
163
- console.log("");
164
- console.log(dim(" To use Vertex AI, you need:"));
165
- console.log(dim(" 1. A Google Cloud project with Vertex AI API enabled"));
166
- console.log(dim(" 2. A service account key JSON file"));
167
- console.log(dim(" 3. Set these environment variables in .env:"));
168
- console.log("");
169
- console.log(sky(" GOOGLE_CLOUD_PROJECT=your-project-id"));
170
- console.log(sky(" GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account.json"));
171
- console.log("");
172
- console.log(dim(" Quick setup guide:"));
173
- console.log(dim(` ${sky("https://cloud.google.com/vertex-ai/docs/authentication")}`));
174
- console.log("");
175
- const vertexProject = await ask(rl, sky(" Enter your project ID (or press Enter to skip): "));
176
- if (vertexProject.trim()) {
177
- useVertexAI = true;
178
- // Will save to .env later
179
- }
180
- else {
181
- console.log(dim(" Skipping Vertex AI. You can set it up later in .env\n"));
182
- }
385
+ // Gemini (default)
386
+ engine = "gemini";
387
+ console.log("");
388
+ const existing = envOrEmpty("GEMINI_API_KEY");
389
+ if (existing) {
390
+ console.log(green(` Found existing key: ${masked(existing)}`));
391
+ apiKey = existing;
183
392
  }
184
393
  else {
185
- console.log("");
186
394
  console.log(dim(" How to get your key:"));
187
395
  console.log(` ${sky("1.")} Open ${sky("https://aistudio.google.com/apikey")}`);
188
- console.log(` ${sky("2.")} Sign in with your Google account`);
189
- console.log(` ${sky("3.")} Click ${bold("\"Create API key\"")}`);
190
- console.log(` ${sky("4.")} Copy the key (starts with ${bold("AIza")}...)`);
191
- console.log("");
192
- apiKey = await ask(rl, sky(" Paste your Gemini API key here: "));
193
- apiKey = apiKey.trim();
194
- if (apiKey && !apiKey.startsWith("AIza")) {
195
- console.log(yellowBright("\n ⚠ Key doesn't start with 'AIza'. Double-check it's correct."));
196
- const proceed = await ask(rl, dim(" Continue anyway? [y/N]: "));
197
- if (!proceed.trim().toLowerCase().startsWith("y")) {
198
- apiKey = "";
199
- console.log(dim(" Skipped. Set GEMINI_API_KEY in .env later.\n"));
396
+ console.log(` ${sky("2.")} Sign in and click ${bold('"Create API key"')}`);
397
+ console.log(` ${sky("3.")} Copy the key (starts with ${bold("AIza")}...)\n`);
398
+ apiKey = (await ask(rl, sky(" Paste your Gemini API key: "))).trim();
399
+ if (apiKey) {
400
+ if (!apiKey.startsWith("AIza")) {
401
+ console.log(yellow("\n Key does not start with 'AIza'. Double-check it."));
402
+ const ok = await ask(rl, dim(" Continue anyway? [y/N]: "));
403
+ if (!ok.trim().toLowerCase().startsWith("y"))
404
+ apiKey = "";
405
+ }
406
+ if (apiKey) {
407
+ env.GEMINI_API_KEY = apiKey;
408
+ console.log(green(" Configured."));
200
409
  }
201
- }
202
- else if (!apiKey) {
203
- console.log(dim(" Skipped. Set GEMINI_API_KEY in .env later.\n"));
204
410
  }
205
411
  }
206
412
  }
207
- // ── Step 2a+: Additional AI Providers (optional) ────
208
- console.clear();
209
- console.log(WISPY_ASCII);
210
- console.log(skyBold(` ${chalk.cyan("▸")} Additional AI Providers (Optional)\n`));
211
- console.log(dim(" Wispy's core engine runs on Gemini. You can also configure"));
212
- console.log(dim(" other providers as tools the agent can delegate tasks to.\n"));
213
- console.log(` ${sky("1)")} ${bold("Anthropic")} ${dim("(Claude Opus, Sonnet)")}`);
214
- console.log(` ${sky("2)")} ${bold("OpenAI")} ${dim("(GPT-4o, Codex, o3)")}`);
215
- console.log(` ${sky("3)")} ${bold("Groq")} ${dim("(Ultra-fast inference, free tier)")}`);
216
- console.log(` ${sky("4)")} ${bold("OpenRouter")} ${dim("(200+ models, unified API)")}`);
217
- console.log(` ${sky("5)")} ${bold("Kimi / Moonshot")} ${dim("(128K context, long docs)")}`);
218
- console.log(` ${sky("6)")} ${bold("Ollama")} ${dim("(Local, free, offline)")}`);
413
+ // Additional providers
219
414
  console.log("");
220
- let anthropicKey = process.env.ANTHROPIC_API_KEY || "";
221
- let openaiKey = process.env.OPENAI_API_KEY || "";
222
- let groqKey = process.env.GROQ_API_KEY || "";
223
- let openrouterKey = process.env.OPENROUTER_API_KEY || "";
224
- let kimiKey = process.env.MOONSHOT_API_KEY || "";
225
- let ollamaUrl = "";
226
- const addProviders = await ask(rl, sky(" Add providers? Enter numbers (e.g. 1,3) or press Enter to skip: "));
227
- if (addProviders.trim()) {
228
- const picks = parseNumberList(addProviders, 6);
415
+ console.log(dim(" ── Additional AI Providers (optional) ──\n"));
416
+ console.log(` ${sky("1)")} Anthropic ${dim("(Claude)")} ${sky("4)")} OpenRouter ${dim("(200+ models)")}`);
417
+ console.log(` ${sky("2)")} OpenAI ${dim("(GPT-4o)")} ${sky("5)")} Kimi ${dim("(128K context)")}`);
418
+ console.log(` ${sky("3)")} Groq ${dim("(fast inference)")} ${sky("6)")} Ollama ${dim("(local, free)")}`);
419
+ console.log(` ${sky("7)")} ElevenLabs ${dim("(voice AI)")}`);
420
+ console.log("");
421
+ const providerInput = await ask(rl, sky(" Add providers? Numbers (e.g. 1,3,7) or Enter to skip: "));
422
+ if (providerInput.trim()) {
423
+ const picks = parseNumberList(providerInput, 7);
229
424
  console.log("");
230
425
  for (const idx of picks) {
231
426
  switch (idx) {
232
427
  case 0: { // Anthropic
233
- if (anthropicKey) {
234
- const masked = anthropicKey.slice(0, 10) + "..." + anthropicKey.slice(-4);
235
- console.log(green(` ✓ Anthropic key found: ${masked}`));
428
+ if (!anthropicKey) {
429
+ const existing = envOrEmpty("ANTHROPIC_API_KEY");
430
+ if (existing) {
431
+ anthropicKey = existing;
432
+ console.log(green(` Anthropic: ${masked(existing, 10)}`));
433
+ }
434
+ else {
435
+ console.log(dim(` Get key: ${sky("https://console.anthropic.com/keys")}`));
436
+ anthropicKey = (await ask(rl, sky(" Anthropic key (sk-ant-...): "))).trim();
437
+ if (anthropicKey) {
438
+ env.ANTHROPIC_API_KEY = anthropicKey;
439
+ console.log(green(" Anthropic configured."));
440
+ }
441
+ }
236
442
  }
237
443
  else {
238
- console.log(dim(` Get your key at: ${sky("https://console.anthropic.com/keys")}`));
239
- const key = await ask(rl, sky(" Anthropic API key (sk-ant-...): "));
240
- anthropicKey = key.trim();
241
- if (anthropicKey)
242
- console.log(green(" ✓ Anthropic configured"));
243
- else
244
- console.log(dim(" Skipped."));
444
+ console.log(green(` Anthropic: already set`));
245
445
  }
246
- console.log("");
247
446
  break;
248
447
  }
249
448
  case 1: { // OpenAI
250
- if (openaiKey) {
251
- const masked = openaiKey.slice(0, 8) + "..." + openaiKey.slice(-4);
252
- console.log(green(` ✓ OpenAI key found: ${masked}`));
449
+ const existing = envOrEmpty("OPENAI_API_KEY");
450
+ if (existing) {
451
+ openaiKey = existing;
452
+ console.log(green(` OpenAI: ${masked(existing)}`));
253
453
  }
254
454
  else {
255
- console.log(dim(` Get your key at: ${sky("https://platform.openai.com/api-keys")}`));
256
- const key = await ask(rl, sky(" OpenAI API key (sk-...): "));
257
- openaiKey = key.trim();
258
- if (openaiKey)
259
- console.log(green(" OpenAI configured"));
260
- else
261
- console.log(dim(" Skipped."));
455
+ console.log(dim(` Get key: ${sky("https://platform.openai.com/api-keys")}`));
456
+ openaiKey = (await ask(rl, sky(" OpenAI key (sk-...): "))).trim();
457
+ if (openaiKey) {
458
+ env.OPENAI_API_KEY = openaiKey;
459
+ console.log(green(" OpenAI configured."));
460
+ }
262
461
  }
263
- console.log("");
264
462
  break;
265
463
  }
266
464
  case 2: { // Groq
267
- if (groqKey) {
268
- const masked = groqKey.slice(0, 8) + "..." + groqKey.slice(-4);
269
- console.log(green(` ✓ Groq key found: ${masked}`));
465
+ const existing = envOrEmpty("GROQ_API_KEY");
466
+ if (existing) {
467
+ groqKey = existing;
468
+ console.log(green(` Groq: ${masked(existing)}`));
270
469
  }
271
470
  else {
272
- console.log(dim(` Get your key at: ${sky("https://console.groq.com/keys")}`));
273
- const key = await ask(rl, sky(" Groq API key (gsk_...): "));
274
- groqKey = key.trim();
275
- if (groqKey)
276
- console.log(green(" Groq configured"));
277
- else
278
- console.log(dim(" Skipped."));
471
+ console.log(dim(` Get key: ${sky("https://console.groq.com/keys")}`));
472
+ groqKey = (await ask(rl, sky(" Groq key (gsk_...): "))).trim();
473
+ if (groqKey) {
474
+ env.GROQ_API_KEY = groqKey;
475
+ console.log(green(" Groq configured."));
476
+ }
279
477
  }
280
- console.log("");
281
478
  break;
282
479
  }
283
480
  case 3: { // OpenRouter
284
- if (openrouterKey) {
285
- const masked = openrouterKey.slice(0, 10) + "..." + openrouterKey.slice(-4);
286
- console.log(green(` ✓ OpenRouter key found: ${masked}`));
481
+ const existing = envOrEmpty("OPENROUTER_API_KEY");
482
+ if (existing) {
483
+ openrouterKey = existing;
484
+ console.log(green(` OpenRouter: ${masked(existing, 10)}`));
287
485
  }
288
486
  else {
289
- console.log(dim(` Get your key at: ${sky("https://openrouter.ai/keys")}`));
290
- const key = await ask(rl, sky(" OpenRouter API key (sk-or-...): "));
291
- openrouterKey = key.trim();
292
- if (openrouterKey)
293
- console.log(green(" OpenRouter configured"));
294
- else
295
- console.log(dim(" Skipped."));
487
+ console.log(dim(` Get key: ${sky("https://openrouter.ai/keys")}`));
488
+ openrouterKey = (await ask(rl, sky(" OpenRouter key (sk-or-...): "))).trim();
489
+ if (openrouterKey) {
490
+ env.OPENROUTER_API_KEY = openrouterKey;
491
+ console.log(green(" OpenRouter configured."));
492
+ }
296
493
  }
297
- console.log("");
298
494
  break;
299
495
  }
300
496
  case 4: { // Kimi
301
- if (kimiKey) {
302
- const masked = kimiKey.slice(0, 8) + "..." + kimiKey.slice(-4);
303
- console.log(green(` ✓ Kimi key found: ${masked}`));
497
+ const existing = envOrEmpty("MOONSHOT_API_KEY");
498
+ if (existing) {
499
+ kimiKey = existing;
500
+ console.log(green(` Kimi: ${masked(existing)}`));
304
501
  }
305
502
  else {
306
- console.log(dim(` Get your key at: ${sky("https://platform.moonshot.cn/console/api-keys")}`));
307
- const key = await ask(rl, sky(" Moonshot API key: "));
308
- kimiKey = key.trim();
309
- if (kimiKey)
310
- console.log(green(" Kimi configured"));
311
- else
312
- console.log(dim(" Skipped."));
503
+ console.log(dim(` Get key: ${sky("https://platform.moonshot.cn/console/api-keys")}`));
504
+ kimiKey = (await ask(rl, sky(" Moonshot key: "))).trim();
505
+ if (kimiKey) {
506
+ env.MOONSHOT_API_KEY = kimiKey;
507
+ console.log(green(" Kimi configured."));
508
+ }
313
509
  }
314
- console.log("");
315
510
  break;
316
511
  }
317
512
  case 5: { // Ollama
318
- console.log(dim(" Make sure Ollama is running locally."));
319
- console.log(dim(` Install: ${sky("https://ollama.com/download")}`));
320
- const url = await ask(rl, sky(" Ollama URL (default: http://localhost:11434): "));
321
- ollamaUrl = url.trim() || "http://localhost:11434";
322
- console.log(green(` ✓ Ollama: ${ollamaUrl}`));
323
- console.log("");
513
+ console.log(dim(` Make sure Ollama is running. Install: ${sky("https://ollama.com/download")}`));
514
+ const url = (await ask(rl, sky(" Ollama URL (default: http://localhost:11434): "))).trim();
515
+ ollamaUrl = url || "http://localhost:11434";
516
+ console.log(green(` Ollama: ${ollamaUrl}`));
517
+ break;
518
+ }
519
+ case 6: { // ElevenLabs
520
+ const existing = envOrEmpty("ELEVENLABS_API_KEY");
521
+ if (existing) {
522
+ elevenlabsKey = existing;
523
+ console.log(green(` ElevenLabs: ${masked(existing)}`));
524
+ }
525
+ else {
526
+ console.log(dim(` Get key: ${sky("https://elevenlabs.io/app/settings/api-keys")}`));
527
+ elevenlabsKey = (await ask(rl, sky(" ElevenLabs key: "))).trim();
528
+ if (elevenlabsKey) {
529
+ env.ELEVENLABS_API_KEY = elevenlabsKey;
530
+ console.log(green(" ElevenLabs configured."));
531
+ }
532
+ }
324
533
  break;
325
534
  }
326
535
  }
536
+ console.log("");
327
537
  }
328
538
  }
329
- else {
330
- console.log(dim(" Skipped. You can add providers later in .env or config.yaml.\n"));
539
+ // ════════════════════════════════════════════════════════
540
+ // STEP 3: Channels
541
+ // ════════════════════════════════════════════════════════
542
+ stepHeader(3, TOTAL_STEPS, "Messaging Channels", chalk.blue("\u25B8"));
543
+ console.log(dim(" Where should Wispy be available? Pick one or more.\n"));
544
+ for (let i = 0; i < CHANNELS.length; i++) {
545
+ const ch = CHANNELS[i];
546
+ const hasKey = ch.envVars.length > 0 && ch.envVars.some((v) => !!envOrEmpty(v.name));
547
+ const status = hasKey ? green(" [key found]") : "";
548
+ console.log(` ${sky(`${i + 1})`)} ${bold(ch.label)}${status}`);
331
549
  }
332
- // ── Step 2b: Telegram Bot Token (optional) ──────────
333
- console.clear();
334
- console.log(WISPY_ASCII);
335
- console.log(skyBold(` ${chalk.blue("▸")} Step 2 of 4: Telegram Bot (Optional)\n`));
336
- console.log(dim(" Connect Telegram to chat with Wispy from your phone."));
337
- console.log(dim(" Skip this if you only want to use the terminal.\n"));
338
- console.log(dim(" How to create a Telegram bot (takes 1 minute):"));
339
- console.log(` ${sky("1.")} Open Telegram and search for ${bold("@BotFather")}`);
340
- console.log(` ${sky("2.")} Send the message: ${sky("/newbot")}`);
341
- console.log(` ${sky("3.")} Choose a name for your bot (e.g. "My Wispy")`);
342
- console.log(` ${sky("4.")} Choose a username (e.g. "my_wispy_bot")`);
343
- console.log(` ${sky("5.")} BotFather will reply with a ${bold("token")} like: ${dim("123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11")}`);
344
- console.log(` ${sky("6.")} Copy that token`);
345
550
  console.log("");
346
- const existingTelegramToken = process.env.TELEGRAM_BOT_TOKEN;
347
- let telegramToken = "";
348
- if (existingTelegramToken) {
349
- const masked = existingTelegramToken.slice(0, 10) + "...";
350
- console.log(green(` ✓ Found existing token: ${masked}\n`));
351
- telegramToken = existingTelegramToken;
352
- }
353
- else {
354
- telegramToken = await ask(rl, sky(" Paste your bot token here (or press Enter to skip): "));
355
- telegramToken = telegramToken.trim();
356
- if (!telegramToken) {
357
- console.log(dim(" Skipped. You can add TELEGRAM_BOT_TOKEN to .env later.\n"));
551
+ console.log(dim(" CLI is always available. REST + WebSocket start with the gateway."));
552
+ console.log(dim(" WhatsApp uses QR code scan -- no token needed.\n"));
553
+ const channelInput = await ask(rl, sky(" Enter numbers (e.g. 1,2,4) or Enter to skip: "));
554
+ if (channelInput.trim()) {
555
+ const picks = parseNumberList(channelInput, CHANNELS.length);
556
+ for (const idx of picks) {
557
+ const ch = CHANNELS[idx];
558
+ enabledChannels[ch.configKey] = true;
559
+ console.log("");
560
+ console.log(skyBold(` -- ${ch.label} Setup --\n`));
561
+ // Show instructions
562
+ for (const step of ch.instructions) {
563
+ console.log(` ${dim("\u2022")} ${step}`);
564
+ }
565
+ console.log("");
566
+ // Collect env vars
567
+ for (const v of ch.envVars) {
568
+ const existing = envOrEmpty(v.name);
569
+ if (existing) {
570
+ console.log(green(` ${v.name}: ${masked(existing)}`));
571
+ env[v.name] = existing;
572
+ }
573
+ else {
574
+ const value = (await ask(rl, sky(` ${v.prompt} (${dim(v.hint)}): `))).trim();
575
+ if (value) {
576
+ env[v.name] = value;
577
+ console.log(green(` Set.`));
578
+ }
579
+ else {
580
+ console.log(dim(` Skipped. Add ${v.name} to .env later.`));
581
+ }
582
+ }
583
+ }
358
584
  }
359
585
  }
360
- // ── Step 2c: Autonomous Mode ──────────────────────────
361
- console.clear();
362
- console.log(WISPY_ASCII);
363
- console.log(skyBold(` ${chalk.yellow("")} Step 3 of 4: How Should Wispy Work?\n`));
364
- console.log(dim(" Choose how much freedom Wispy gets:\n"));
365
- console.log(` ${sky("1)")} ${bold("Standard Mode")} ${dim("(Ask before acting)")}`);
366
- console.log(dim(" Wispy will ask your permission before writing files or running code."));
367
- console.log(dim(" Best for: Beginners, shared computers, production servers.\n"));
368
- console.log(` ${sky("2)")} ${bold("Autonomous Mode")} ${dim("(Recommended)")}`);
369
- console.log(dim(" Wispy acts on its own -- writes files, runs code, creates projects."));
370
- console.log(dim(" Best for: Personal computer, development, getting things done fast.\n"));
371
- const modeChoice = await ask(rl, sky(" Select [1/2] (default: 2 for Autonomous): "));
372
- const autonomousMode = modeChoice.trim() !== "1";
373
- // ── Step 2d: File System Access ──────────────────────
374
- let fullFilesystemAccess = false;
375
- if (autonomousMode) {
376
- console.clear();
377
- console.log(WISPY_ASCII);
378
- console.log(skyBold(` ${chalk.yellow("▸")} File System Access\n`));
379
- console.log(dim(" Autonomous mode is enabled. Wispy can already write files in its workspace."));
380
- console.log(dim(" Enable full filesystem access to let Wispy work on projects anywhere.\n"));
381
- console.log(` ${sky("Examples:")}`);
382
- console.log(dim(" - Edit files across your projects directories"));
383
- console.log(dim(" - Read/write anywhere on your drive"));
384
- console.log(dim(" - Manage files in your Downloads folder\n"));
385
- const fsChoice = await ask(rl, sky(" Enable full filesystem access? [Y/n]: "));
386
- fullFilesystemAccess = !fsChoice.trim().toLowerCase().startsWith("n");
387
- }
388
- // ── Step 3: Theme ────────────────────────────────────
389
- console.clear();
390
- console.log(WISPY_ASCII);
391
- console.log(skyBold(` ${chalk.magenta("▸")} Pick a Color Theme\n`));
392
- console.log(dim(" This changes how Wispy looks in your terminal.\n"));
393
- for (let i = 0; i < THEMES.length; i++) {
394
- const num = sky(`${i + 1})`);
395
- console.log(` ${num} ${THEMES[i].label}`);
396
- }
397
- const themeInput = await ask(rl, sky("\n Select [1-4] (default: 2): "));
398
- const themeIdx = parseInt(themeInput.trim(), 10);
399
- const selectedTheme = themeIdx >= 1 && themeIdx <= 4
400
- ? THEMES[themeIdx - 1].key
401
- : "day";
402
- // ── Step 4: Agents ───────────────────────────────────
403
- console.clear();
404
- console.log(WISPY_ASCII);
405
- console.log(skyBold(` ${chalk.cyan("▸")} Choose Agent Roles\n`));
406
- console.log(dim(" These are Wispy's specializations. Coder + Researcher are pre-selected."));
407
- console.log(dim(" Just press Enter to keep the defaults, or pick more.\n"));
408
- for (let i = 0; i < AGENTS.length; i++) {
409
- const mark = AGENTS[i].defaultOn ? green("[x]") : dim("[ ]");
410
- const num = sky(`${i + 1})`);
411
- console.log(` ${mark} ${num} ${AGENTS[i].label}`);
412
- }
413
- const agentInput = await ask(rl, sky("\n Enter numbers separated by commas (default: 1,2): "));
414
- let selectedAgents;
415
- if (!agentInput.trim()) {
416
- selectedAgents = AGENTS.filter((a) => a.defaultOn).map((a) => a.id);
417
- }
418
- else {
419
- const indices = parseNumberList(agentInput, AGENTS.length);
420
- selectedAgents = indices.length > 0
421
- ? indices.map((i) => AGENTS[i].id)
422
- : AGENTS.filter((a) => a.defaultOn).map((a) => a.id);
423
- }
424
- // ── Step 5: Integrations ─────────────────────────────
425
- console.clear();
426
- console.log(WISPY_ASCII);
427
- console.log(skyBold(` ${chalk.blue("▸")} Connect Apps (Optional)\n`));
428
- console.log(dim(" Connect Wispy to services you use. Skip for now if unsure."));
429
- console.log(dim(" You can always enable these later: wispy integrations enable <name>\n"));
430
- // Display in two columns
431
- const half = Math.ceil(INTEGRATIONS.length / 2);
432
- for (let i = 0; i < half; i++) {
433
- const left = `${dim("[ ]")} ${sky(`${i + 1})`)} ${INTEGRATIONS[i].label}`;
434
- const rightIdx = i + half;
435
- const right = rightIdx < INTEGRATIONS.length
436
- ? `${dim("[ ]")} ${sky(`${rightIdx + 1})`)} ${INTEGRATIONS[rightIdx].label}`
437
- : "";
438
- console.log(` ${left.padEnd(38)}${right}`);
439
- }
440
- const intInput = await ask(rl, sky("\n Enter numbers to enable (or press Enter to skip): "));
441
- let selectedIntegrations;
442
- if (!intInput.trim()) {
443
- selectedIntegrations = [];
444
- }
445
- else {
446
- const indices = parseNumberList(intInput, INTEGRATIONS.length);
447
- selectedIntegrations = indices.map((i) => INTEGRATIONS[i].id);
448
- }
449
- // ── Step 6: MCP Servers ─────────────────────────────
450
- console.clear();
451
- console.log(WISPY_ASCII);
452
- console.log(skyBold(` ${chalk.green("▸")} Extra Tools (MCP Servers)\n`));
453
- console.log(dim(" These give Wispy extra abilities like reading files or browsing the web."));
454
- console.log(dim(" The defaults (File System + Web Fetch) are recommended. Just press Enter.\n"));
455
- const MCP_SERVERS = [
456
- {
457
- id: "filesystem",
458
- label: "File System — Read/write files anywhere (Recommended)",
459
- command: "npx",
460
- args: ["-y", "@anthropic/mcp-server-filesystem", "--allow-write", "/"],
461
- default: true,
462
- },
463
- {
464
- id: "fetch",
465
- label: "Web Fetch — Make HTTP requests with headers & auth",
466
- command: "npx",
467
- args: ["-y", "@anthropic/mcp-server-fetch"],
468
- default: true,
469
- },
470
- {
471
- id: "memory",
472
- label: "Memory — Persistent key-value storage",
473
- command: "npx",
474
- args: ["-y", "@anthropic/mcp-server-memory"],
475
- default: false,
476
- },
477
- {
478
- id: "brave",
479
- label: "Brave Search— Web search via Brave API",
480
- command: "npx",
481
- args: ["-y", "@anthropic/mcp-server-brave-search"],
482
- default: false,
483
- envRequired: "BRAVE_API_KEY",
484
- },
485
- {
486
- id: "github",
487
- label: "GitHub — Access repos, issues, PRs",
488
- command: "npx",
489
- args: ["-y", "@modelcontextprotocol/server-github"],
490
- default: false,
491
- envRequired: "GITHUB_TOKEN",
492
- },
493
- {
494
- id: "postgres",
495
- label: "PostgreSQL — Query databases directly",
496
- command: "npx",
497
- args: ["-y", "@modelcontextprotocol/server-postgres"],
498
- default: false,
499
- envRequired: "DATABASE_URL",
500
- },
501
- ];
502
- for (let i = 0; i < MCP_SERVERS.length; i++) {
503
- const mark = MCP_SERVERS[i].default ? green("[x]") : dim("[ ]");
504
- const num = sky(`${i + 1})`);
505
- const envNote = MCP_SERVERS[i].envRequired ? dim(` (needs ${MCP_SERVERS[i].envRequired})`) : "";
506
- console.log(` ${mark} ${num} ${MCP_SERVERS[i].label}${envNote}`);
586
+ // ════════════════════════════════════════════════════════
587
+ // STEP 4: Integrations (by category)
588
+ // ════════════════════════════════════════════════════════
589
+ stepHeader(4, TOTAL_STEPS, "App Integrations", chalk.cyan("\u25B8"));
590
+ console.log(dim(" Connect Wispy to apps and services. Pick categories to configure."));
591
+ console.log(dim(" You can enable individual integrations later: wispy setup <name>\n"));
592
+ for (let i = 0; i < INTEGRATION_CATEGORIES.length; i++) {
593
+ const cat = INTEGRATION_CATEGORIES[i];
594
+ const items = cat.items.map((it) => it.label).join(", ");
595
+ console.log(` ${sky(`${i + 1})`)} ${bold(cat.name)} ${dim(`(${cat.items.length})`)} ${cat.icon}`);
596
+ console.log(dim(` ${items}`));
507
597
  }
508
598
  console.log("");
509
- console.log(dim(" MCP servers run alongside the gateway and provide additional tools."));
510
- console.log(dim(` Learn more: ${sky("https://modelcontextprotocol.io")}\n`));
511
- const mcpInput = await ask(rl, sky(" Enter numbers to enable (default: 1,2): "));
512
- let selectedMCPs;
513
- if (!mcpInput.trim()) {
514
- selectedMCPs = MCP_SERVERS.filter((m) => m.default);
515
- }
516
- else {
517
- const indices = parseNumberList(mcpInput, MCP_SERVERS.length);
518
- selectedMCPs = indices.length > 0
519
- ? indices.map((i) => MCP_SERVERS[i])
520
- : MCP_SERVERS.filter((m) => m.default);
599
+ const catInput = await ask(rl, sky(" Enter category numbers (e.g. 1,2) or Enter to skip: "));
600
+ if (catInput.trim()) {
601
+ const catPicks = parseNumberList(catInput, INTEGRATION_CATEGORIES.length);
602
+ for (const catIdx of catPicks) {
603
+ const cat = INTEGRATION_CATEGORIES[catIdx];
604
+ console.log("");
605
+ console.log(skyBold(` -- ${cat.icon} ${cat.name} --\n`));
606
+ if (cat.note) {
607
+ console.log(dim(` Note: ${cat.note}\n`));
608
+ }
609
+ // If Google category, prompt for OAuth credentials once
610
+ if (cat.name === "Google Workspace") {
611
+ console.log(dim(" Google integrations use OAuth2. You need:"));
612
+ console.log(` ${dim("1.")} Go to ${sky("https://console.cloud.google.com/apis/credentials")}`);
613
+ console.log(` ${dim("2.")} Create OAuth 2.0 Client ID`);
614
+ console.log(` ${dim("3.")} Enable the APIs you want (Calendar, Gmail, Drive, etc.)\n`);
615
+ const googleClientId = envOrEmpty("GOOGLE_CLIENT_ID");
616
+ const googleClientSecret = envOrEmpty("GOOGLE_CLIENT_SECRET");
617
+ if (googleClientId) {
618
+ console.log(green(` Google OAuth: configured`));
619
+ }
620
+ else {
621
+ const clientId = (await ask(rl, sky(" Google Client ID (or Enter to skip): "))).trim();
622
+ if (clientId) {
623
+ env.GOOGLE_CLIENT_ID = clientId;
624
+ const secret = (await ask(rl, sky(" Google Client Secret: "))).trim();
625
+ if (secret)
626
+ env.GOOGLE_CLIENT_SECRET = secret;
627
+ const redirect = (await ask(rl, sky(" Redirect URI (default: http://localhost:3000/auth/callback): "))).trim();
628
+ env.GOOGLE_REDIRECT_URI = redirect || "http://localhost:3000/auth/callback";
629
+ console.log(green(" Google OAuth configured."));
630
+ }
631
+ }
632
+ console.log("");
633
+ }
634
+ // Show items and let user pick
635
+ for (let i = 0; i < cat.items.length; i++) {
636
+ const item = cat.items[i];
637
+ const hasEnv = item.envVars?.some((v) => !!envOrEmpty(v)) || false;
638
+ const status = hasEnv ? green(" [configured]") : "";
639
+ console.log(` ${sky(`${i + 1})`)} ${item.label}${status}`);
640
+ }
641
+ console.log("");
642
+ const itemInput = await ask(rl, sky(` Enable which? Numbers (e.g. 1,2,3) or Enter for all: `));
643
+ let picked;
644
+ if (!itemInput.trim()) {
645
+ picked = cat.items.map((_, i) => i);
646
+ }
647
+ else {
648
+ picked = parseNumberList(itemInput, cat.items.length);
649
+ }
650
+ for (const itemIdx of picked) {
651
+ const item = cat.items[itemIdx];
652
+ selectedIntegrations.push(item.id);
653
+ // Prompt for env vars if needed
654
+ if (item.envVars && item.envVars.length > 0) {
655
+ const allSet = item.envVars.every((v) => !!envOrEmpty(v));
656
+ if (!allSet) {
657
+ if (item.link)
658
+ console.log(dim(`\n ${item.label}: ${sky(item.link)}`));
659
+ for (const varName of item.envVars) {
660
+ const existing = envOrEmpty(varName);
661
+ if (existing) {
662
+ env[varName] = existing;
663
+ }
664
+ else {
665
+ const val = (await ask(rl, sky(` ${varName}: `))).trim();
666
+ if (val)
667
+ env[varName] = val;
668
+ }
669
+ }
670
+ }
671
+ }
672
+ }
673
+ if (picked.length > 0) {
674
+ const names = picked.map((i) => cat.items[i].label).join(", ");
675
+ console.log(green(`\n Enabled: ${names}`));
676
+ }
677
+ }
521
678
  }
522
- // ── Step 7: x402 Agentic Commerce ──────────────────
523
- console.clear();
524
- console.log(WISPY_ASCII);
525
- console.log(skyBold(` ${chalk.hex('#00FFA3')("▸")} Step 4 of 4: Blockchain Wallet (Optional)\n`));
526
- console.log(dim(" Give Wispy a wallet so it can pay for services automatically."));
527
- console.log(dim(" Uses USDC on SKALE (gasless -- no transaction fees).\n"));
528
- console.log(dim(" What this enables:"));
529
- console.log(` ${chalk.hex('#00FFA3')("")} Agent pays for APIs on its own (micro-payments)`);
530
- console.log(` ${chalk.hex('#00FFA3')("")} DeFi trading with built-in risk controls`);
531
- console.log(` ${chalk.hex('#00FFA3')("•")} Encrypted conditional payments (BITE v2)`);
532
- console.log("");
533
- console.log(dim(" Skip this if you just want to use Wispy for chat and coding.\n"));
534
- const existingAgentKey = process.env.AGENT_PRIVATE_KEY;
535
- let agentPrivateKey = "";
536
- let commerceEnabled = false;
679
+ // ════════════════════════════════════════════════════════
680
+ // STEP 5: Wallet & Commerce
681
+ // ════════════════════════════════════════════════════════
682
+ stepHeader(5, TOTAL_STEPS, "Blockchain Wallet", accent("\u25B8"));
683
+ console.log(dim(" Give Wispy a wallet for autonomous payments (x402 protocol)."));
684
+ console.log(dim(" Uses USDC on SKALE -- gasless, no transaction fees.\n"));
685
+ console.log(` ${accent("\u2022")} Agent pays for APIs on its own (micro-payments)`);
686
+ console.log(` ${accent("\u2022")} DeFi trading with built-in risk controls`);
687
+ console.log(` ${accent("\u2022")} Encrypted conditional payments (BITE v2)\n`);
688
+ console.log(dim(" Skip this if you only want chat and coding.\n"));
689
+ const existingAgentKey = envOrEmpty("AGENT_PRIVATE_KEY");
537
690
  if (existingAgentKey) {
538
- const masked = existingAgentKey.slice(0, 6) + "..." + existingAgentKey.slice(-4);
539
- console.log(green(` ✓ Found wallet key: ${masked}\n`));
691
+ console.log(green(` Found wallet key: ${masked(existingAgentKey, 6)}`));
540
692
  agentPrivateKey = existingAgentKey;
541
693
  commerceEnabled = true;
542
694
  }
543
695
  else {
544
- const enableChoice = await ask(rl, sky(" Enable x402 commerce? [Y/n]: "));
545
- if (!enableChoice.trim() || enableChoice.trim().toLowerCase().startsWith("y")) {
696
+ const walletChoice = await ask(rl, sky(" Enable x402 commerce? [y/N]: "));
697
+ if (walletChoice.trim().toLowerCase().startsWith("y")) {
546
698
  console.log("");
547
- console.log(` ${sky("1)")} ${bold("Generate a new wallet")} ${dim("(Recommended -- creates one for you)")}`);
548
- console.log(` ${sky("2)")} ${bold("Use an existing wallet")} ${dim("(Paste your private key)")}`);
699
+ console.log(` ${sky("1)")} ${bold("Generate new wallet")} ${dim("(recommended)")}`);
700
+ console.log(` ${sky("2)")} ${bold("Use existing wallet")} ${dim("(paste private key)")}`);
549
701
  console.log("");
550
702
  const keyChoice = await ask(rl, sky(" Select [1/2] (default: 1): "));
551
703
  if (keyChoice.trim() === "2") {
552
- const keyInput = await ask(rl, sky(" Enter private key (0x...): "));
553
- agentPrivateKey = keyInput.trim();
554
- if (agentPrivateKey && !agentPrivateKey.startsWith("0x")) {
704
+ agentPrivateKey = (await ask(rl, sky(" Private key (0x...): "))).trim();
705
+ if (agentPrivateKey && !agentPrivateKey.startsWith("0x"))
555
706
  agentPrivateKey = "0x" + agentPrivateKey;
556
- }
557
707
  }
558
708
  else {
559
- // Generate a new wallet with recovery phrase
560
709
  try {
561
710
  const { ethers } = await import("ethers");
562
711
  const wallet = ethers.Wallet.createRandom();
563
712
  agentPrivateKey = wallet.privateKey;
564
713
  const mnemonic = wallet.mnemonic?.phrase || "";
565
714
  console.log("");
566
- console.log(green(` Generated new wallet!\n`));
715
+ console.log(green(` Generated new wallet!\n`));
567
716
  console.log(` ${dim("Address:")} ${sky(wallet.address)}`);
568
717
  if (mnemonic) {
569
718
  console.log("");
570
- console.log(yellowBright(` ┌─────────────────────────────────────────────────┐`));
571
- console.log(yellowBright(` RECOVERY PHRASE Write this down on paper! │`));
572
- console.log(yellowBright(` └─────────────────────────────────────────────────┘`));
719
+ console.log(yellow(" +-------------------------------------------------+"));
720
+ console.log(yellow(" | RECOVERY PHRASE -- Write this down on paper! |"));
721
+ console.log(yellow(" +-------------------------------------------------+"));
573
722
  console.log("");
574
723
  const words = mnemonic.split(" ");
575
724
  for (let i = 0; i < words.length; i += 3) {
576
- const row = words.slice(i, i + 3).map((w, j) => {
725
+ const row = words
726
+ .slice(i, i + 3)
727
+ .map((w, j) => {
577
728
  const num = String(i + j + 1).padStart(2, " ");
578
729
  return `${dim(num + ".")} ${bold(w.padEnd(10))}`;
579
- }).join(" ");
730
+ })
731
+ .join(" ");
580
732
  console.log(` ${row}`);
581
733
  }
582
734
  console.log("");
583
- console.log(yellowBright(" This is the ONLY time you will see this phrase."));
584
- console.log(dim(" You can recover your wallet with these 12 words."));
585
- console.log(dim(" Never share them. Never type them online.\n"));
586
- await ask(rl, dim(" Press Enter once you have saved your recovery phrase..."));
735
+ console.log(yellow(" This is the ONLY time you will see this phrase."));
736
+ console.log(dim(" Never share it. Never type it online.\n"));
737
+ await ask(rl, dim(" Press Enter once saved..."));
587
738
  }
588
739
  }
589
740
  catch {
590
- console.log(yellowBright(" Could not generate wallet."));
591
- console.log(dim(" Add AGENT_PRIVATE_KEY=0x... to .env manually.\n"));
741
+ console.log(yellow(" Could not generate wallet. Add AGENT_PRIVATE_KEY to .env later."));
592
742
  }
593
743
  }
594
744
  if (agentPrivateKey) {
595
745
  commerceEnabled = true;
746
+ env.AGENT_PRIVATE_KEY = agentPrivateKey;
747
+ }
748
+ // Optional CDP wallet
749
+ if (commerceEnabled) {
750
+ console.log("");
751
+ console.log(dim(" Optional: Coinbase Developer Platform (CDP) custodial wallet"));
752
+ const cdpChoice = await ask(rl, sky(" Configure CDP? [y/N]: "));
753
+ if (cdpChoice.trim().toLowerCase().startsWith("y")) {
754
+ console.log(dim(`\n Get credentials: ${sky("https://portal.cdp.coinbase.com/")}\n`));
755
+ cdpKeyName = (await ask(rl, sky(" CDP API Key Name: "))).trim();
756
+ cdpPrivateKey = (await ask(rl, sky(" CDP Private Key: "))).trim();
757
+ if (cdpKeyName)
758
+ env.CDP_API_KEY_NAME = cdpKeyName;
759
+ if (cdpPrivateKey)
760
+ env.CDP_PRIVATE_KEY = cdpPrivateKey;
761
+ }
596
762
  }
597
- }
598
- else {
599
- console.log(dim("\n Skipped. Enable later by adding AGENT_PRIVATE_KEY to .env\n"));
600
763
  }
601
764
  }
602
- // Optional CDP wallet (advanced)
603
- let cdpKeyName = process.env.CDP_API_KEY_NAME || "";
604
- let cdpPrivateKey = process.env.CDP_PRIVATE_KEY || "";
605
- if (commerceEnabled && !cdpKeyName) {
765
+ // ════════════════════════════════════════════════════════
766
+ // STEP 6: Security & Mode
767
+ // ════════════════════════════════════════════════════════
768
+ stepHeader(6, TOTAL_STEPS, "Security & Permissions", chalk.yellow("\u25B8"));
769
+ console.log(dim(" Choose how much freedom Wispy gets:\n"));
770
+ console.log(` ${sky("1)")} ${bold("Autonomous Mode")} ${dim("(recommended)")}`);
771
+ console.log(dim(" Wispy writes files, runs code, and manages projects on its own."));
772
+ console.log(dim(" Best for: personal computer, development, getting things done.\n"));
773
+ console.log(` ${sky("2)")} ${bold("Standard Mode")} ${dim("(ask before acting)")}`);
774
+ console.log(dim(" Wispy asks permission before writing files or running code."));
775
+ console.log(dim(" Best for: shared computers, production servers.\n"));
776
+ const modeChoice = await ask(rl, sky(" Select [1/2] (default: 1): "));
777
+ autonomousMode = modeChoice.trim() !== "2";
778
+ if (autonomousMode) {
606
779
  console.log("");
607
- console.log(dim(" Optional: Coinbase Developer Platform (CDP) wallet"));
608
- console.log(dim(" Provides custodial wallet management. Skip if unsure.\n"));
609
- const cdpChoice = await ask(rl, sky(" Configure CDP wallet? [y/N]: "));
610
- if (cdpChoice.trim().toLowerCase().startsWith("y")) {
611
- console.log(dim("\n Get credentials at: https://portal.cdp.coinbase.com/\n"));
612
- cdpKeyName = (await ask(rl, sky(" CDP API Key Name: "))).trim();
613
- cdpPrivateKey = (await ask(rl, sky(" CDP Private Key: "))).trim();
614
- }
780
+ console.log(dim(" File system access:\n"));
781
+ console.log(` ${sky("1)")} ${bold("Full access")} ${dim("-- read/write anywhere on your drive (recommended)")}`);
782
+ console.log(` ${sky("2)")} ${bold("Workspace only")} ${dim("-- restricted to Wispy's workspace directory")}\n`);
783
+ const fsChoice = await ask(rl, sky(" Select [1/2] (default: 1): "));
784
+ fullFilesystemAccess = fsChoice.trim() !== "2";
615
785
  }
616
- await ask(rl, dim("\n Press Enter to continue..."));
617
- // ── Step 8: Voice/TTS Setup (Edge TTS) ─────────────
618
- console.clear();
619
- console.log(WISPY_ASCII);
620
- console.log(skyBold(` ${chalk.hex('#FF6B6B')("▸")} Voice (Optional)\n`));
621
- console.log(dim(" Let Wispy speak responses aloud in Telegram voice messages."));
622
- console.log(dim(" Uses Edge TTS -- free, fast, and natural-sounding.\n"));
623
- console.log(` ${chalk.hex('#FF6B6B')("Features:")}`);
624
- console.log(` ${sky("•")} Multiple natural voices (male/female, various accents)`);
625
- console.log(` ${sky("•")} Fast generation (2-3 seconds)`);
626
- console.log(` ${sky("")} No API key required`);
627
- console.log(` ${sky("")} Works on any system (no GPU needed)`);
786
+ else {
787
+ fullFilesystemAccess = false;
788
+ }
789
+ // ════════════════════════════════════════════════════════
790
+ // STEP 7: Extras (Voice, MCP, Theme, Agents, Memory)
791
+ // ════════════════════════════════════════════════════════
792
+ stepHeader(7, TOTAL_STEPS, "Extras", chalk.hex("#FF6B6B")("\u25B8"));
793
+ // ── Voice/TTS ────────────────────────────────────────
794
+ console.log(skyBold(" Voice / Text-to-Speech\n"));
795
+ console.log(dim(" Let Wispy speak aloud in voice messages.\n"));
796
+ console.log(` ${sky("1)")} ${bold("Edge TTS")} ${dim("-- free, fast, no API key (default)")}`);
797
+ console.log(` ${sky("2)")} ${bold("ElevenLabs")} ${dim("-- premium quality, requires API key")}`);
798
+ console.log(` ${sky("3)")} ${bold("Qwen3-TTS")} ${dim("-- multilingual via HuggingFace (free)")}`);
799
+ console.log(` ${sky("4)")} ${dim("Skip voice")}`);
628
800
  console.log("");
629
- // Check if Edge TTS is installed
630
- let voiceInstalled = false;
631
- try {
632
- const { execSync } = await import("child_process");
633
- execSync("python -m edge_tts --version", { stdio: "pipe", timeout: 5000 });
634
- voiceInstalled = true;
801
+ const voiceChoice = await ask(rl, sky(" Select [1/2/3/4] (default: 1): "));
802
+ if (voiceChoice.trim() === "2") {
803
+ voiceModel = "elevenlabs";
804
+ if (!elevenlabsKey) {
805
+ const existing = envOrEmpty("ELEVENLABS_API_KEY");
806
+ if (existing) {
807
+ elevenlabsKey = existing;
808
+ console.log(green(` ElevenLabs: ${masked(existing)}`));
809
+ }
810
+ else {
811
+ console.log(dim(`\n Get key: ${sky("https://elevenlabs.io/app/settings/api-keys")}`));
812
+ elevenlabsKey = (await ask(rl, sky(" ElevenLabs key: "))).trim();
813
+ if (elevenlabsKey)
814
+ env.ELEVENLABS_API_KEY = elevenlabsKey;
815
+ }
816
+ }
635
817
  }
636
- catch {
637
- voiceInstalled = false;
818
+ else if (voiceChoice.trim() === "3") {
819
+ voiceModel = "qwen3-tts";
820
+ // Check if gradio_client is installed
821
+ let qwen3Ready = false;
822
+ try {
823
+ const { execSync } = await import("child_process");
824
+ execSync('python -c "from gradio_client import Client"', { stdio: "pipe", timeout: 10000 });
825
+ qwen3Ready = true;
826
+ }
827
+ catch { /* not installed */ }
828
+ if (!qwen3Ready) {
829
+ console.log(dim("\n Qwen3-TTS needs the gradio_client Python package."));
830
+ const install = await ask(rl, sky(" Install now? [Y/n]: "));
831
+ if (!install.trim() || install.trim().toLowerCase().startsWith("y")) {
832
+ try {
833
+ const { execSync } = await import("child_process");
834
+ execSync("pip install gradio_client -q", { stdio: "inherit", timeout: 120000 });
835
+ console.log(green(" Installed."));
836
+ }
837
+ catch {
838
+ console.log(yellow(" Install failed. Run: pip install gradio_client"));
839
+ voiceModel = "edge-tts";
840
+ }
841
+ }
842
+ else {
843
+ voiceModel = "edge-tts";
844
+ }
845
+ }
638
846
  }
639
- if (voiceInstalled) {
640
- console.log(green(" ✓ Edge TTS is already installed!\n"));
847
+ else if (voiceChoice.trim() === "4") {
848
+ voiceModel = "";
641
849
  }
642
850
  else {
643
- console.log(yellowBright(" ⚠ Edge TTS not installed"));
644
- console.log(dim(" Required for natural voice messages in Telegram.\n"));
645
- const installChoice = await ask(rl, sky(" Install Edge TTS now? [Y/n]: "));
646
- if (!installChoice.trim() || installChoice.trim().toLowerCase().startsWith("y")) {
647
- console.log("");
648
- console.log(dim(" Installing Edge TTS..."));
649
- try {
650
- const { execSync } = await import("child_process");
651
- execSync("pip install edge-tts", {
652
- stdio: "inherit",
653
- timeout: 120000,
654
- });
655
- voiceInstalled = true;
656
- console.log("");
657
- console.log(green(" ✓ Edge TTS installed successfully!"));
658
- }
659
- catch (installErr) {
660
- console.log("");
661
- console.log(yellowBright(" ⚠ Installation failed. You can install manually later:"));
662
- console.log(sky(" pip install edge-tts"));
851
+ voiceModel = "edge-tts";
852
+ // Check if Edge TTS is installed
853
+ let edgeReady = false;
854
+ try {
855
+ const { execSync } = await import("child_process");
856
+ execSync("python -m edge_tts --version", { stdio: "pipe", timeout: 5000 });
857
+ edgeReady = true;
858
+ }
859
+ catch { /* not installed */ }
860
+ if (!edgeReady) {
861
+ console.log(dim("\n Edge TTS needs the edge-tts Python package."));
862
+ const install = await ask(rl, sky(" Install now? [Y/n]: "));
863
+ if (!install.trim() || install.trim().toLowerCase().startsWith("y")) {
864
+ try {
865
+ const { execSync } = await import("child_process");
866
+ execSync("pip install edge-tts", { stdio: "inherit", timeout: 120000 });
867
+ console.log(green(" Installed."));
868
+ }
869
+ catch {
870
+ console.log(yellow(" Install failed. Run: pip install edge-tts"));
871
+ }
663
872
  }
664
873
  }
665
874
  else {
666
- console.log(dim("\n Skipped. Install later with: pip install edge-tts\n"));
875
+ console.log(green(" Edge TTS is ready."));
667
876
  }
668
877
  }
669
- // Keep variable name for backward compatibility
670
- const chatterboxInstalled = voiceInstalled;
671
- // Qwen3-TTS via HuggingFace Spaces (cloud-based, no local GPU needed)
672
- let qwen3Installed = false;
673
- try {
674
- const { execSync } = await import("child_process");
675
- execSync('python -c "from gradio_client import Client"', { stdio: "pipe", timeout: 10000 });
676
- qwen3Installed = true;
878
+ // ── MCP Servers ──────────────────────────────────────
879
+ console.log("");
880
+ console.log(skyBold(" MCP Servers (Extra Tools)\n"));
881
+ console.log(dim(" MCP servers give Wispy additional capabilities."));
882
+ console.log(dim(" Defaults (File System + Web Fetch) are recommended.\n"));
883
+ for (let i = 0; i < MCP_SERVERS.length; i++) {
884
+ const m = MCP_SERVERS[i];
885
+ const mark = m.default ? green("[x]") : dim("[ ]");
886
+ const envNote = m.envRequired ? dim(` (needs ${m.envRequired})`) : "";
887
+ console.log(` ${mark} ${sky(`${i + 1})`)} ${m.label}${envNote}`);
888
+ }
889
+ console.log("");
890
+ const mcpInput = await ask(rl, sky(" Numbers to enable (default: 1,2): "));
891
+ let selectedMCPs;
892
+ if (!mcpInput.trim()) {
893
+ selectedMCPs = MCP_SERVERS.filter((m) => m.default);
677
894
  }
678
- catch {
679
- qwen3Installed = false;
895
+ else {
896
+ const indices = parseNumberList(mcpInput, MCP_SERVERS.length);
897
+ selectedMCPs = indices.length > 0 ? indices.map((i) => MCP_SERVERS[i]) : MCP_SERVERS.filter((m) => m.default);
680
898
  }
899
+ // ── Theme ────────────────────────────────────────────
681
900
  console.log("");
682
- console.log(chalk.hex('#FF6B6B')(" Optional: Qwen3-TTS (Premium Voice)"));
683
- console.log(dim(" Best quality TTS via HuggingFace Spaces (cloud-based)."));
684
- console.log(dim(" No local GPU needed - runs on HuggingFace servers!\n"));
685
- console.log(` ${sky("•")} 10 languages: English, Chinese, Japanese, Korean, etc.`);
686
- console.log(` ${sky("•")} 9 voices: Serena, Ryan, Vivian, Dylan, Aiden, etc.`);
687
- console.log(` ${sky("•")} Emotion & style control via natural language`);
688
- console.log(` ${sky("•")} Free to use (HuggingFace Spaces)`);
901
+ console.log(skyBold(" Color Theme\n"));
902
+ for (let i = 0; i < THEMES.length; i++) {
903
+ console.log(` ${sky(`${i + 1})`)} ${THEMES[i].label}`);
904
+ }
905
+ const themeInput = await ask(rl, sky("\n Select [1-4] (default: 2): "));
906
+ const themeIdx = parseInt(themeInput.trim(), 10);
907
+ selectedTheme = themeIdx >= 1 && themeIdx <= 4 ? THEMES[themeIdx - 1].key : "day";
908
+ // ── Agents ───────────────────────────────────────────
689
909
  console.log("");
690
- if (qwen3Installed) {
691
- console.log(green(" Qwen3-TTS client is ready!\n"));
910
+ console.log(skyBold(" Agent Roles\n"));
911
+ console.log(dim(" Wispy's specializations. Coder + Researcher are pre-selected.\n"));
912
+ for (let i = 0; i < AGENTS.length; i++) {
913
+ const mark = AGENTS[i].defaultOn ? green("[x]") : dim("[ ]");
914
+ console.log(` ${mark} ${sky(`${i + 1})`)} ${AGENTS[i].label}`);
692
915
  }
693
- else {
694
- console.log(yellowBright(" ⚠ gradio_client not installed"));
695
- console.log(dim(" Required for Qwen3-TTS cloud API.\n"));
696
- const installChoice = await ask(rl, sky(" Install gradio_client now? [Y/n]: "));
697
- if (!installChoice.trim() || installChoice.trim().toLowerCase().startsWith("y")) {
698
- console.log("");
699
- console.log(dim(" Installing gradio_client..."));
700
- try {
701
- const { execSync } = await import("child_process");
702
- execSync("pip install gradio_client -q", {
703
- stdio: "inherit",
704
- timeout: 120000,
705
- });
706
- qwen3Installed = true;
707
- console.log("");
708
- console.log(green(" ✓ Qwen3-TTS client installed successfully!"));
709
- }
710
- catch (installErr) {
711
- console.log("");
712
- console.log(yellowBright(" ⚠ Installation failed. You can install manually later:"));
713
- console.log(sky(" pip install gradio_client"));
714
- }
715
- }
716
- else {
717
- console.log(dim("\n Skipped. Edge TTS will be used (still sounds great!).\n"));
718
- }
916
+ const agentInput = await ask(rl, sky("\n Numbers (default: 1,2): "));
917
+ if (agentInput.trim()) {
918
+ const indices = parseNumberList(agentInput, AGENTS.length);
919
+ if (indices.length > 0)
920
+ selectedAgents = indices.map((i) => AGENTS[i].id);
719
921
  }
720
- await ask(rl, dim("\n Press Enter to continue..."));
922
+ // ── Memory Bank ──────────────────────────────────────
923
+ console.log("");
924
+ console.log(skyBold(" Memory Bank\n"));
925
+ console.log(dim(" Wispy can remember things across conversations using a structured memory"));
926
+ console.log(dim(" system with 4 types: episodic, semantic, procedural, and preference.\n"));
927
+ console.log(` ${sky("\u2022")} Memories decay over time if not accessed`);
928
+ console.log(` ${sky("\u2022")} Frequently used memories get reinforced`);
929
+ console.log(` ${sky("\u2022")} AI-powered reflection merges similar memories\n`);
930
+ console.log(green(" Memory Bank is enabled by default. No extra configuration needed."));
931
+ console.log("");
932
+ await ask(rl, dim(" Press Enter to continue..."));
721
933
  rl.close();
722
- // ── Save config ──────────────────────────────────────
723
- // Ensure runtime directory exists
934
+ // ════════════════════════════════════════════════════════
935
+ // SAVE CONFIGURATION
936
+ // ════════════════════════════════════════════════════════
724
937
  fs.mkdirSync(runtimeDir, { recursive: true });
938
+ // Build config object
725
939
  const config = {
726
940
  agent: { name: "wispy", id: "main" },
941
+ engine,
727
942
  gemini: {
728
943
  models: {
729
944
  pro: "gemini-2.5-pro-preview-05-06",
@@ -732,11 +947,39 @@ export async function runSetupWizard(opts) {
732
947
  embedding: "text-embedding-004",
733
948
  },
734
949
  },
950
+ ...(engine === "claude" || anthropicKey
951
+ ? {
952
+ claude: {
953
+ ...(anthropicKey ? { apiKey: anthropicKey } : {}),
954
+ models: { reasoning: "claude-sonnet-4-20250514", fast: "claude-haiku-4-5-20251001" },
955
+ },
956
+ }
957
+ : {}),
735
958
  channels: {
736
959
  web: { enabled: true, port: 4000 },
737
960
  rest: { enabled: true, port: 4001 },
738
- telegram: { enabled: !!telegramToken, token: telegramToken || undefined },
739
- whatsapp: { enabled: false },
961
+ telegram: {
962
+ enabled: !!enabledChannels.telegram && !!(env.TELEGRAM_BOT_TOKEN || envOrEmpty("TELEGRAM_BOT_TOKEN")),
963
+ },
964
+ discord: {
965
+ enabled: !!enabledChannels.discord && !!(env.DISCORD_BOT_TOKEN || envOrEmpty("DISCORD_BOT_TOKEN")),
966
+ },
967
+ slack: {
968
+ enabled: !!enabledChannels.slack && !!(env.SLACK_BOT_TOKEN || envOrEmpty("SLACK_BOT_TOKEN")),
969
+ },
970
+ whatsapp: { enabled: !!enabledChannels.whatsapp },
971
+ matrix: {
972
+ enabled: !!enabledChannels.matrix && !!(env.MATRIX_ACCESS_TOKEN || envOrEmpty("MATRIX_ACCESS_TOKEN")),
973
+ },
974
+ signal: {
975
+ enabled: !!enabledChannels.signal && !!(env.SIGNAL_PHONE_NUMBER || envOrEmpty("SIGNAL_PHONE_NUMBER")),
976
+ },
977
+ email: {
978
+ enabled: !!enabledChannels.email && !!(env.AGENTMAIL_API_KEY || envOrEmpty("AGENTMAIL_API_KEY")),
979
+ },
980
+ phone: {
981
+ enabled: !!enabledChannels.phone && !!(env.TELNYX_API_KEY || envOrEmpty("TELNYX_API_KEY")),
982
+ },
740
983
  },
741
984
  providers: {
742
985
  ...(anthropicKey ? { anthropic: { apiKey: anthropicKey } } : {}),
@@ -749,15 +992,21 @@ export async function runSetupWizard(opts) {
749
992
  browser: { enabled: true },
750
993
  wallet: { enabled: commerceEnabled, chain: "skale-bite-sandbox" },
751
994
  memory: { embeddingDimensions: 768, heartbeatIntervalMinutes: 30, hybridSearch: true },
752
- security: { requireApprovalForExternal: !autonomousMode, allowedGroups: [], autonomousMode, fullFilesystemAccess },
995
+ security: {
996
+ requireApprovalForExternal: !autonomousMode,
997
+ allowedGroups: [],
998
+ autonomousMode,
999
+ fullFilesystemAccess,
1000
+ },
753
1001
  thinking: { defaultLevel: "medium", costAware: true },
1002
+ voice: voiceModel ? { enabled: true, model: voiceModel, replyWithVoice: true } : { enabled: false },
754
1003
  theme: selectedTheme,
755
1004
  agents: selectedAgents,
756
1005
  integrations: selectedIntegrations,
757
1006
  };
758
1007
  const configPath = path.resolve(runtimeDir, "config.yaml");
759
1008
  writeYAML(configPath, config);
760
- // ── Save MCP server configuration ───────────────────
1009
+ // Save MCP config
761
1010
  const mcpDir = path.resolve(runtimeDir, "mcp");
762
1011
  fs.mkdirSync(mcpDir, { recursive: true });
763
1012
  const mcpConfig = {
@@ -769,83 +1018,85 @@ export async function runSetupWizard(opts) {
769
1018
  env: {},
770
1019
  })),
771
1020
  };
772
- const mcpConfigPath = path.resolve(mcpDir, "servers.json");
773
- fs.writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2));
774
- // Save credentials to .env
1021
+ fs.writeFileSync(path.resolve(mcpDir, "servers.json"), JSON.stringify(mcpConfig, null, 2));
1022
+ // Save .env
775
1023
  const envPath = path.resolve(rootDir, ".env");
776
1024
  let envContent = fs.existsSync(envPath) ? fs.readFileSync(envPath, "utf-8") : "";
777
- // Add API key if new
778
- if (apiKey && !existingKey) {
779
- if (!envContent.includes("GEMINI_API_KEY=")) {
780
- envContent += `\nGEMINI_API_KEY=${apiKey}`;
781
- }
782
- }
783
- // Add Telegram token if new
784
- if (telegramToken && !existingTelegramToken) {
785
- if (!envContent.includes("TELEGRAM_BOT_TOKEN=")) {
786
- envContent += `\nTELEGRAM_BOT_TOKEN=${telegramToken}`;
787
- }
788
- }
789
- // Add additional provider keys
790
- if (anthropicKey && !process.env.ANTHROPIC_API_KEY) {
791
- if (!envContent.includes("ANTHROPIC_API_KEY=")) {
792
- envContent += `\nANTHROPIC_API_KEY=${anthropicKey}`;
793
- }
794
- }
795
- if (openaiKey && !process.env.OPENAI_API_KEY) {
796
- if (!envContent.includes("OPENAI_API_KEY=")) {
797
- envContent += `\nOPENAI_API_KEY=${openaiKey}`;
798
- }
799
- }
800
- if (groqKey && !process.env.GROQ_API_KEY) {
801
- if (!envContent.includes("GROQ_API_KEY=")) {
802
- envContent += `\nGROQ_API_KEY=${groqKey}`;
803
- }
804
- }
805
- if (openrouterKey && !process.env.OPENROUTER_API_KEY) {
806
- if (!envContent.includes("OPENROUTER_API_KEY=")) {
807
- envContent += `\nOPENROUTER_API_KEY=${openrouterKey}`;
1025
+ // Helper: add env var if not already present
1026
+ const addEnv = (key, value, section) => {
1027
+ if (!value)
1028
+ return;
1029
+ if (envContent.includes(`${key}=`))
1030
+ return;
1031
+ if (section && !envContent.includes(`# ${section}`)) {
1032
+ envContent += `\n\n# ${section}`;
808
1033
  }
809
- }
810
- if (kimiKey && !process.env.MOONSHOT_API_KEY) {
811
- if (!envContent.includes("MOONSHOT_API_KEY=")) {
812
- envContent += `\nMOONSHOT_API_KEY=${kimiKey}`;
1034
+ envContent += `\n${key}=${value}`;
1035
+ };
1036
+ // Engine
1037
+ addEnv("ENGINE", engine, "AI Engine");
1038
+ // Primary keys
1039
+ if (apiKey && !envOrEmpty("GEMINI_API_KEY"))
1040
+ addEnv("GEMINI_API_KEY", apiKey);
1041
+ if (anthropicKey && !envOrEmpty("ANTHROPIC_API_KEY"))
1042
+ addEnv("ANTHROPIC_API_KEY", anthropicKey);
1043
+ // Additional providers
1044
+ if (openaiKey && !envOrEmpty("OPENAI_API_KEY"))
1045
+ addEnv("OPENAI_API_KEY", openaiKey, "Providers");
1046
+ if (groqKey && !envOrEmpty("GROQ_API_KEY"))
1047
+ addEnv("GROQ_API_KEY", groqKey);
1048
+ if (openrouterKey && !envOrEmpty("OPENROUTER_API_KEY"))
1049
+ addEnv("OPENROUTER_API_KEY", openrouterKey);
1050
+ if (kimiKey && !envOrEmpty("MOONSHOT_API_KEY"))
1051
+ addEnv("MOONSHOT_API_KEY", kimiKey);
1052
+ if (elevenlabsKey && !envOrEmpty("ELEVENLABS_API_KEY"))
1053
+ addEnv("ELEVENLABS_API_KEY", elevenlabsKey);
1054
+ // Channel tokens
1055
+ for (const [key, value] of Object.entries(env)) {
1056
+ if (key.startsWith("TELEGRAM_") || key.startsWith("DISCORD_") || key.startsWith("SLACK_") ||
1057
+ key.startsWith("MATRIX_") || key.startsWith("SIGNAL_") || key.startsWith("AGENTMAIL_") ||
1058
+ key.startsWith("TELNYX_")) {
1059
+ addEnv(key, value, "Channels");
813
1060
  }
814
1061
  }
815
- // Add x402 commerce keys
816
- if (agentPrivateKey && !existingAgentKey) {
817
- if (!envContent.includes("AGENT_PRIVATE_KEY=")) {
818
- envContent += `\n\n# x402 Agentic Commerce\nAGENT_PRIVATE_KEY=${agentPrivateKey}`;
1062
+ // Integration keys
1063
+ for (const [key, value] of Object.entries(env)) {
1064
+ if (key.startsWith("GOOGLE_") || key.startsWith("GITHUB_") || key.startsWith("NOTION_") ||
1065
+ key.startsWith("LINEAR_") || key.startsWith("TWITTER_") || key.startsWith("LINKEDIN_") ||
1066
+ key.startsWith("INSTAGRAM_") || key.startsWith("REDDIT_") || key.startsWith("SPOTIFY_") ||
1067
+ key.startsWith("STRIPE_") || key.startsWith("OP_") || key.startsWith("HUE_") ||
1068
+ key.startsWith("HA_") || key.startsWith("SONOS_") || key.startsWith("TRELLO_") ||
1069
+ key.startsWith("ASANA_") || key.startsWith("CALENDLY_") || key.startsWith("IMAGE_GEN_")) {
1070
+ addEnv(key, value, "Integrations");
819
1071
  }
820
1072
  }
821
- if (cdpKeyName && !envContent.includes("CDP_API_KEY_NAME=")) {
822
- envContent += `\nCDP_API_KEY_NAME=${cdpKeyName}`;
823
- }
824
- if (cdpPrivateKey && !envContent.includes("CDP_PRIVATE_KEY=")) {
825
- envContent += `\nCDP_PRIVATE_KEY=${cdpPrivateKey}`;
826
- }
827
- // Write .env if we have content
1073
+ // Commerce
1074
+ if (agentPrivateKey && !envOrEmpty("AGENT_PRIVATE_KEY"))
1075
+ addEnv("AGENT_PRIVATE_KEY", agentPrivateKey, "x402 Commerce");
1076
+ if (cdpKeyName)
1077
+ addEnv("CDP_API_KEY_NAME", cdpKeyName);
1078
+ if (cdpPrivateKey)
1079
+ addEnv("CDP_PRIVATE_KEY", cdpPrivateKey);
828
1080
  if (envContent.trim()) {
829
1081
  fs.writeFileSync(envPath, envContent.trim() + "\n");
830
1082
  }
831
- // ── Step 9: Summary ──────────────────────────────────
1083
+ // ════════════════════════════════════════════════════════
1084
+ // STEP 8: Summary
1085
+ // ════════════════════════════════════════════════════════
832
1086
  console.clear();
833
1087
  console.log(WISPY_ASCII);
834
- console.log(skyBold(` ${chalk.green("")} Setup complete!\n`));
835
- const themeDisplay = `${capitalize(selectedTheme)} ${THEME_ICON[selectedTheme] || ""}`;
836
- const agentsDisplay = selectedAgents.map(capitalize).join(", ");
837
- const intDisplay = selectedIntegrations.length > 0
838
- ? selectedIntegrations.map((id) => {
839
- const found = INTEGRATIONS.find((i) => i.id === id);
840
- return found ? found.label : id;
841
- }).join(", ")
842
- : "None (enable later with /integrations)";
843
- const mcpDisplay = selectedMCPs.length > 0
844
- ? selectedMCPs.map((m) => capitalize(m.id)).join(", ")
845
- : "None";
846
- const aiDisplay = useVertexAI ? "Vertex AI (high quota)" : apiKey ? "Gemini API" : "Not configured";
1088
+ console.log(skyBold(` ${green("\u2713")} Setup complete!\n`));
1089
+ // Engine
1090
+ const engineDisplay = engine === "claude"
1091
+ ? `Claude ${dim("(Anthropic)")}`
1092
+ : useVertexAI
1093
+ ? `Gemini ${dim("(Vertex AI)")}`
1094
+ : apiKey
1095
+ ? `Gemini ${dim("(API key)")}`
1096
+ : yellow("Not configured");
1097
+ // Providers
847
1098
  const extraProviders = [];
848
- if (anthropicKey)
1099
+ if (engine !== "claude" && anthropicKey)
849
1100
  extraProviders.push("Anthropic");
850
1101
  if (openaiKey)
851
1102
  extraProviders.push("OpenAI");
@@ -857,50 +1108,64 @@ export async function runSetupWizard(opts) {
857
1108
  extraProviders.push("Kimi");
858
1109
  if (ollamaUrl)
859
1110
  extraProviders.push("Ollama");
860
- const providersDisplay = extraProviders.length > 0 ? extraProviders.join(", ") : dim("None");
861
- const telegramDisplay = telegramToken ? green("Enabled") : dim("Not configured");
1111
+ if (elevenlabsKey)
1112
+ extraProviders.push("ElevenLabs");
1113
+ // Channels
1114
+ const activeChannels = Object.entries(enabledChannels)
1115
+ .filter(([, v]) => v)
1116
+ .map(([k]) => capitalize(k));
1117
+ // Display
1118
+ console.log(` ${dim("Engine:")} ${engineDisplay}`);
1119
+ console.log(` ${dim("Providers:")} ${extraProviders.length > 0 ? extraProviders.join(", ") : dim("None")}`);
1120
+ console.log(` ${dim("Channels:")} ${activeChannels.length > 0 ? activeChannels.join(", ") : dim("CLI only")}`);
1121
+ console.log(` ${dim("Integrations:")} ${selectedIntegrations.length > 0 ? `${selectedIntegrations.length} enabled` : dim("None")}`);
1122
+ if (selectedIntegrations.length > 0) {
1123
+ // Show first 6 then "and X more"
1124
+ const show = selectedIntegrations.slice(0, 6);
1125
+ const rest = selectedIntegrations.length - show.length;
1126
+ const names = show.map((id) => {
1127
+ for (const cat of INTEGRATION_CATEGORIES) {
1128
+ const found = cat.items.find((i) => i.id === id);
1129
+ if (found)
1130
+ return found.label;
1131
+ }
1132
+ return id;
1133
+ });
1134
+ console.log(` ${dim(" ")}${dim(names.join(", "))}${rest > 0 ? dim(` +${rest} more`) : ""}`);
1135
+ }
862
1136
  const modeDisplay = autonomousMode
863
- ? chalk.yellow(" Autonomous") + dim(" (auto-approve file/code ops)")
864
- : green("🛡️ Standard") + dim(" (requires approval)");
865
- const voiceDisplay = qwen3Installed
866
- ? green(" Qwen3-TTS") + dim(" (premium, multilingual)")
867
- : (chatterboxInstalled
868
- ? green(" Edge TTS") + dim(" (natural voice)")
869
- : dim("Not installed"));
870
- const commerceDisplay = commerceEnabled
871
- ? green("✓ Enabled") + dim(` (${cdpKeyName ? "CDP wallet" : "raw key"})`)
872
- : dim("Not configured");
873
- const fsDisplay = fullFilesystemAccess
874
- ? chalk.yellow("⚡ Full Access") + dim(" (entire drive)")
875
- : dim("Workspace only");
876
- console.log(` ${dim("AI Provider:")} ${aiDisplay}`);
877
- console.log(` ${dim("Providers:")} ${providersDisplay}`);
878
- console.log(` ${dim("Mode:")} ${modeDisplay}`);
879
- console.log(` ${dim("File System:")} ${fsDisplay}`);
880
- console.log(` ${dim("Theme:")} ${themeDisplay}`);
881
- console.log(` ${dim("Agents:")} ${agentsDisplay}`);
882
- console.log(` ${dim("Telegram:")} ${telegramDisplay}`);
883
- console.log(` ${dim("x402 Commerce:")} ${commerceDisplay}`);
884
- console.log(` ${dim("Voice:")} ${voiceDisplay}`);
885
- console.log(` ${dim("MCP Servers:")} ${mcpDisplay}`);
886
- console.log(` ${dim("Integrations:")} ${intDisplay}`);
1137
+ ? chalk.yellow("\u26A1 Autonomous") + (fullFilesystemAccess ? dim(" (full access)") : dim(" (workspace only)"))
1138
+ : green("Shield Standard") + dim(" (ask before acting)");
1139
+ console.log(` ${dim("Mode:")} ${modeDisplay}`);
1140
+ console.log(` ${dim("Wallet:")} ${commerceEnabled ? green("\u2713 Enabled") + dim(` (${cdpKeyName ? "CDP" : "raw key"})`) : dim("Not configured")}`);
1141
+ console.log(` ${dim("Voice:")} ${voiceModel ? green(`\u2713 ${voiceModel}`) : dim("Disabled")}`);
1142
+ console.log(` ${dim("Memory Bank:")} ${green("\u2713 Enabled")} ${dim("(episodic, semantic, procedural, preference)")}`);
1143
+ console.log(` ${dim("MCP Servers:")} ${selectedMCPs.map((m) => capitalize(m.id)).join(", ") || dim("None")}`);
1144
+ console.log(` ${dim("Theme:")} ${capitalize(selectedTheme)} ${THEME_ICON[selectedTheme] || ""}`);
1145
+ console.log(` ${dim("Agents:")} ${selectedAgents.map(capitalize).join(", ")}`);
887
1146
  console.log("");
888
- if (!apiKey && !useVertexAI) {
889
- console.log(yellowBright(" No AI credentials configured!"));
890
- console.log(dim(" To fix: add GEMINI_API_KEY=your-key-here to the .env file"));
891
- console.log(dim(` Get a free key: ${sky("https://aistudio.google.com/apikey")}`));
1147
+ if (!apiKey && !useVertexAI && engine === "gemini") {
1148
+ console.log(yellow(" No Gemini API key configured!"));
1149
+ console.log(dim(` Add GEMINI_API_KEY to .env. Free: ${sky("https://aistudio.google.com/apikey")}`));
1150
+ console.log("");
1151
+ }
1152
+ if (engine === "claude" && !anthropicKey) {
1153
+ console.log(yellow(" No Anthropic API key configured!"));
1154
+ console.log(dim(` Add ANTHROPIC_API_KEY to .env: ${sky("https://console.anthropic.com/keys")}`));
892
1155
  console.log("");
893
1156
  }
894
1157
  console.log(skyBold(" What to do next:\n"));
895
- console.log(` ${sky("wispy chat")} Start chatting with Wispy`);
896
- console.log(` ${sky("wispy gateway")} Start the server (Telegram + REST + Web)`);
1158
+ console.log(` ${sky("wispy chat")} Chat with Wispy in the terminal`);
1159
+ console.log(` ${sky("wispy gateway")} Start all channels (Telegram, Discord, etc.)`);
897
1160
  console.log(` ${sky("wispy doctor")} Check if everything is working`);
898
- console.log(` ${sky("wispy onboard")} Re-run this wizard anytime`);
1161
+ console.log(` ${sky("wispy setup")} Re-run this wizard anytime`);
1162
+ console.log(` ${sky("wispy setup github")} Configure a specific integration`);
899
1163
  console.log("");
900
- console.log(dim(" Try saying: \"Build me a todo app\" or \"Research AI trends in 2026\""));
1164
+ console.log(dim(` Config saved to: ${sky(path.resolve(runtimeDir, "config.yaml"))}`));
1165
+ console.log(dim(` Env saved to: ${sky(path.resolve(rootDir, ".env"))}`));
901
1166
  console.log("");
902
- console.log(dim(" Docs: https://docs.wispy.cc"));
903
- console.log(dim(" Help: https://github.com/hausorlabs/wispy/issues"));
1167
+ console.log(dim(" Docs: https://docs.wispy.cc"));
1168
+ console.log(dim(" Help: https://github.com/hausorlabs/wispy/issues"));
904
1169
  console.log("");
905
1170
  }
906
1171
  catch (err) {