lazy-gravity 0.2.0 → 0.4.0

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.
Files changed (66) hide show
  1. package/README.md +77 -15
  2. package/dist/bin/cli.js +0 -0
  3. package/dist/bin/commands/doctor.js +19 -2
  4. package/dist/bin/commands/open.js +1 -1
  5. package/dist/bin/commands/setup.js +286 -70
  6. package/dist/bot/eventRouter.js +70 -0
  7. package/dist/bot/index.js +355 -147
  8. package/dist/bot/telegramCommands.js +478 -0
  9. package/dist/bot/telegramMessageHandler.js +308 -0
  10. package/dist/bot/telegramProjectCommand.js +137 -0
  11. package/dist/bot/workspaceQueue.js +61 -0
  12. package/dist/commands/joinCommandHandler.js +4 -1
  13. package/dist/database/telegramBindingRepository.js +97 -0
  14. package/dist/database/userPreferenceRepository.js +46 -1
  15. package/dist/events/interactionCreateHandler.js +36 -0
  16. package/dist/events/messageCreateHandler.js +11 -7
  17. package/dist/handlers/approvalButtonAction.js +99 -0
  18. package/dist/handlers/autoAcceptButtonAction.js +43 -0
  19. package/dist/handlers/buttonHandler.js +55 -0
  20. package/dist/handlers/commandHandler.js +44 -0
  21. package/dist/handlers/errorPopupButtonAction.js +137 -0
  22. package/dist/handlers/messageHandler.js +70 -0
  23. package/dist/handlers/modeSelectAction.js +63 -0
  24. package/dist/handlers/modelButtonAction.js +102 -0
  25. package/dist/handlers/planningButtonAction.js +118 -0
  26. package/dist/handlers/selectHandler.js +41 -0
  27. package/dist/handlers/templateButtonAction.js +54 -0
  28. package/dist/platform/adapter.js +8 -0
  29. package/dist/platform/discord/discordAdapter.js +99 -0
  30. package/dist/platform/discord/index.js +15 -0
  31. package/dist/platform/discord/wrappers.js +331 -0
  32. package/dist/platform/index.js +18 -0
  33. package/dist/platform/richContentBuilder.js +76 -0
  34. package/dist/platform/telegram/index.js +16 -0
  35. package/dist/platform/telegram/telegramAdapter.js +195 -0
  36. package/dist/platform/telegram/telegramFormatter.js +134 -0
  37. package/dist/platform/telegram/wrappers.js +333 -0
  38. package/dist/platform/types.js +28 -0
  39. package/dist/services/approvalDetector.js +15 -2
  40. package/dist/services/cdpBridgeManager.js +91 -146
  41. package/dist/services/cdpService.js +88 -2
  42. package/dist/services/chatSessionService.js +50 -10
  43. package/dist/services/defaultModelApplicator.js +54 -0
  44. package/dist/services/modeService.js +16 -1
  45. package/dist/services/modelService.js +57 -16
  46. package/dist/services/notificationSender.js +149 -0
  47. package/dist/services/responseMonitor.js +1 -2
  48. package/dist/services/screenshotService.js +2 -2
  49. package/dist/ui/autoAcceptUi.js +37 -0
  50. package/dist/ui/modeUi.js +38 -1
  51. package/dist/ui/modelsUi.js +96 -0
  52. package/dist/ui/outputUi.js +32 -0
  53. package/dist/ui/projectListUi.js +55 -0
  54. package/dist/ui/screenshotUi.js +26 -0
  55. package/dist/ui/sessionPickerUi.js +35 -1
  56. package/dist/ui/templateUi.js +41 -0
  57. package/dist/utils/configLoader.js +63 -12
  58. package/dist/utils/lockfile.js +5 -5
  59. package/dist/utils/logger.js +7 -0
  60. package/dist/utils/telegramImageHandler.js +127 -0
  61. package/package.json +6 -3
  62. package/dist/commands/joinDetachCommandHandler.js +0 -285
  63. package/dist/services/retryStore.js +0 -46
  64. package/dist/ui/buttonUtils.js +0 -33
  65. package/dist/utils/antigravityPaths.js +0 -94
  66. package/dist/utils/logFileTransport.js +0 -147
package/README.md CHANGED
@@ -3,19 +3,20 @@
3
3
  </p>
4
4
 
5
5
  <p align="center">
6
- <img src="https://img.shields.io/badge/version-0.2.0-blue?style=flat-square" alt="Version" />
6
+ <img src="https://img.shields.io/badge/version-0.3.0-blue?style=flat-square" alt="Version" />
7
7
  <img src="https://img.shields.io/badge/Antigravity-1.19.5-ff6b35?style=flat-square" alt="Antigravity" />
8
8
  <img src="https://img.shields.io/badge/node-18.x+-brightgreen?style=flat-square&logo=node.js" alt="Node.js" />
9
9
  <img src="https://img.shields.io/badge/discord.js-14.x-5865F2?style=flat-square&logo=discord&logoColor=white" alt="discord.js" />
10
+ <img src="https://img.shields.io/badge/telegram-optional-26A5E4?style=flat-square&logo=telegram&logoColor=white" alt="Telegram" />
10
11
  <img src="https://img.shields.io/badge/protocol-CDP%20%2F%20WebSocket-orange?style=flat-square" alt="CDP/WebSocket" />
11
12
  <img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="License" />
12
13
  </p>
13
14
 
14
15
  # LazyGravity
15
16
 
16
- **LazyGravity** is a local, secure Discord Bot that lets you remotely operate [Antigravity](https://antigravity.dev) on your home PC — from your smartphone's Discord app, anywhere.
17
+ **LazyGravity** is a local, secure bot that lets you remotely operate [Antigravity](https://antigravity.dev) on your home PC — from your smartphone, anywhere. Supports **Discord** and **Telegram** (optional).
17
18
 
18
- Send natural language instructions like "fix that bug" or "start designing the new feature" from your phone. Antigravity executes them locally on your home PC using its full resources, and reports results back to Discord.
19
+ Send natural language instructions like "fix that bug" or "start designing the new feature" from your phone. Antigravity executes them locally on your home PC using its full resources, and reports results back to your chat platform.
19
20
 
20
21
  https://github.com/user-attachments/assets/08eac63e-5ede-469b-ac6c-1c40ec77b0c0
21
22
 
@@ -33,7 +34,7 @@ The interactive wizard walks you through Discord bot creation, token setup, and
33
34
 
34
35
  ```bash
35
36
  lazy-gravity open # Launch Antigravity with CDP enabled
36
- lazy-gravity start # Start the Discord bot
37
+ lazy-gravity start # Start the bot (Discord by default, or both platforms)
37
38
  ```
38
39
 
39
40
  Or run directly without installing:
@@ -47,23 +48,30 @@ npx lazy-gravity
47
48
  ## Features
48
49
 
49
50
  1. **Fully Local & Secure**
50
- - **No external server or port exposure** — runs as a local process on your PC, communicating directly with Discord.
51
- - **Whitelist access control**: only authorized Discord user IDs (`allowedUserIds`) can interact with the bot.
51
+ - **No external server or port exposure** — runs as a local process on your PC, communicating directly with Discord/Telegram.
52
+ - **Whitelist access control**: only authorized user IDs can interact with the bot (per-platform allowlists).
52
53
  - **Secure credential management**: Bot tokens and API keys are stored locally (never in source code).
53
54
  - **Path traversal prevention & resource protection**: sandboxed directory access and concurrent task limits prevent abuse.
54
55
 
55
- 2. **Project Management (Channel-Directory Binding)**
56
- - Use `/project` to bind a Discord channel to a local project directory via an interactive select menu with buttons.
57
- - Messages sent in a bound channel are automatically forwarded to Antigravity with the correct project context.
56
+ 2. **Multi-Platform Support**
57
+ - **Discord** (default): Full feature set with slash commands, rich embeds, reactions, and channel management.
58
+ - **Telegram** (optional): Send prompts, receive responses, and use inline keyboard buttons. Requires [grammy](https://grammy.dev/) (`npm install grammy`).
59
+ - Run both platforms simultaneously from a single process, or use either one standalone.
58
60
 
59
- 3. **Context-Aware Embed Replies**
60
- - Results are delivered as rich Discord Embeds. Use Discord's Reply feature on any result to continue the conversation — the bot preserves full context (directory, task history) across reply chains.
61
+ 3. **Project Management (Channel-Directory Binding)**
62
+ - **Discord**: Use `/project` to bind a channel to a local project directory via an interactive select menu.
63
+ - **Telegram**: Use `/project` to bind a chat to a workspace directory.
64
+ - Messages sent in a bound channel/chat are automatically forwarded to Antigravity with the correct project context.
61
65
 
62
- 4. **Real-Time Progress Monitoring**
66
+ 4. **Context-Aware Replies**
67
+ - **Discord**: Results delivered as rich Embeds. Use Reply to continue the conversation with full context preserved.
68
+ - **Telegram**: Results delivered as formatted HTML messages with inline keyboard buttons.
69
+
70
+ 5. **Real-Time Progress Monitoring**
63
71
  - Long-running Antigravity tasks report progress as a series of messages (delivery confirmed / planning / analysis / execution / implementation / final summary).
64
72
 
65
- 5. **File Attachments & Context Parsing**
66
- - Send images (screenshots, mockups) or text files via Discord — they are automatically forwarded to Antigravity as context.
73
+ 6. **File Attachments & Context Parsing**
74
+ - Send images (screenshots, mockups) or text files — they are automatically forwarded to Antigravity as context.
67
75
 
68
76
  ## Usage & Commands
69
77
 
@@ -94,6 +102,26 @@ Just type in any bound channel:
94
102
  - `🧹 /cleanup [days]` — Scan and clean up inactive session channels (default: 7 days)
95
103
  - `❓ /help` — Display list of available commands
96
104
 
105
+ ### Telegram Commands
106
+
107
+ Telegram commands use underscores instead of subcommand syntax (Telegram does not allow hyphens or spaces in command names).
108
+
109
+ - `/project` — Manage workspace bindings (list, select, create)
110
+ - `/project_create <name>` — Create a new workspace directory
111
+ - `/new` — Start a new chat session
112
+ - `/template` — List prompt templates with execute buttons
113
+ - `/template_add <name> <prompt>` — Add a new prompt template
114
+ - `/template_delete <name>` — Delete a prompt template
115
+ - `/mode` — Switch execution mode
116
+ - `/model` — Switch LLM model
117
+ - `/screenshot` — Capture Antigravity screenshot
118
+ - `/autoaccept [on|off]` — Toggle auto-accept mode
119
+ - `/logs [count]` — Show recent log entries
120
+ - `/stop` — Interrupt active LLM generation
121
+ - `/status` — Show bot status and connections
122
+ - `/ping` — Check bot latency
123
+ - `/help` — Show available commands
124
+
97
125
  ### CLI Commands
98
126
 
99
127
  ```bash
@@ -161,6 +189,20 @@ Then start the bot:
161
189
  npm run start
162
190
  ```
163
191
 
192
+ #### Adding Telegram Support (Optional)
193
+
194
+ 1. Install grammy: `npm install grammy`
195
+ 2. Create a bot via [@BotFather](https://t.me/BotFather) on Telegram and copy the token.
196
+ 3. Add the following to your `.env`:
197
+
198
+ ```env
199
+ PLATFORMS=discord,telegram # or just "telegram" for Telegram-only
200
+ TELEGRAM_BOT_TOKEN=your_telegram_bot_token_here
201
+ TELEGRAM_ALLOWED_USER_IDS=123456789 # Your Telegram numeric user ID
202
+ ```
203
+
204
+ For Telegram-only deployments, Discord credentials (`DISCORD_BOT_TOKEN`, `CLIENT_ID`, `ALLOWED_USER_IDS`) are not required.
205
+
164
206
  Alternatively, you can build and use the CLI:
165
207
 
166
208
  ```bash
@@ -232,7 +274,27 @@ Run `lazy-gravity doctor` to diagnose configuration and connectivity issues.
232
274
  2. Connects via WebSocket to CDP (`Runtime.evaluate` for DOM operations)
233
275
  3. Injects messages into the chat input, monitors Antigravity responses, and captures screenshots
234
276
 
235
- **On disconnect**: automatically retries up to 3 times (`maxReconnectAttempts`). If all retries fail, an error notification is sent to Discord.
277
+ **On disconnect**: automatically retries up to 3 times (`maxReconnectAttempts`). If all retries fail, an error notification is sent to the active chat platform.
278
+
279
+ ## Platform Architecture
280
+
281
+ LazyGravity uses a **platform abstraction layer** so the core bot logic is platform-independent:
282
+
283
+ ```
284
+ src/platform/
285
+ ├── types.ts # Shared interfaces (PlatformMessage, PlatformChannel, etc.)
286
+ ├── adapter.ts # PlatformAdapter interface
287
+ ├── richContentBuilder.ts # Immutable builder for rich content (embeds/HTML)
288
+ ├── discord/ # Discord adapter (discord.js wrappers)
289
+ │ ├── discordAdapter.ts
290
+ │ └── wrappers.ts
291
+ └── telegram/ # Telegram adapter (grammy-compatible wrappers)
292
+ ├── telegramAdapter.ts
293
+ ├── telegramFormatter.ts # Markdown → Telegram HTML conversion
294
+ └── wrappers.ts
295
+ ```
296
+
297
+ Both adapters implement the same `PlatformAdapter` interface and emit events through `PlatformAdapterEvents`. The `EventRouter` dispatches events to platform-agnostic handlers, and the `WorkspaceQueue` serializes concurrent requests per workspace across platforms.
236
298
 
237
299
  ## License
238
300
 
package/dist/bin/cli.js CHANGED
File without changes
@@ -71,8 +71,23 @@ function checkEnvFile() {
71
71
  const envPath = path.resolve(process.cwd(), '.env');
72
72
  return { exists: fs.existsSync(envPath), path: envPath };
73
73
  }
74
+ const VALID_PLATFORMS = ['discord', 'telegram'];
75
+ function getActivePlatforms() {
76
+ const raw = process.env.PLATFORMS || 'discord';
77
+ return raw
78
+ .split(',')
79
+ .map((p) => p.trim().toLowerCase())
80
+ .filter((p) => VALID_PLATFORMS.includes(p));
81
+ }
74
82
  function checkRequiredEnvVars() {
75
- const required = ['DISCORD_BOT_TOKEN', 'CLIENT_ID', 'ALLOWED_USER_IDS'];
83
+ const platforms = getActivePlatforms();
84
+ const required = [];
85
+ if (platforms.includes('discord')) {
86
+ required.push('DISCORD_BOT_TOKEN', 'CLIENT_ID', 'ALLOWED_USER_IDS');
87
+ }
88
+ if (platforms.includes('telegram')) {
89
+ required.push('TELEGRAM_BOT_TOKEN', 'TELEGRAM_ALLOWED_USER_IDS');
90
+ }
76
91
  return required.map((name) => ({
77
92
  name,
78
93
  set: Boolean(process.env[name]),
@@ -114,7 +129,9 @@ async function doctorAction() {
114
129
  warn(`.env file not found: ${env.path} (not needed — config.json used)`);
115
130
  }
116
131
  }
117
- // 4. Required environment variables (check both env and config.json sources)
132
+ // 4. Required environment variables (platform-aware)
133
+ const platforms = getActivePlatforms();
134
+ ok(`Active platforms: ${platforms.join(', ')}`);
118
135
  const vars = checkRequiredEnvVars();
119
136
  for (const v of vars) {
120
137
  if (v.set) {
@@ -81,7 +81,7 @@ function openMacOS(port) {
81
81
  }
82
82
  function openWindows(port) {
83
83
  return new Promise((resolve, reject) => {
84
- (0, child_process_1.execFile)(`${APP_NAME}.exe`, [`--remote-debugging-port=${port}`], { shell: true }, (err) => {
84
+ (0, child_process_1.execFile)(APP_NAME, [`--remote-debugging-port=${port}`], { shell: true }, (err) => {
85
85
  if (err) {
86
86
  reject(new Error(`Failed to open ${APP_NAME}: ${err.message}`));
87
87
  return;
@@ -39,6 +39,17 @@ const https = __importStar(require("https"));
39
39
  const fs = __importStar(require("fs"));
40
40
  const os = __importStar(require("os"));
41
41
  const path = __importStar(require("path"));
42
+ // @inquirer/select is ESM-only — use native import() that tsc won't rewrite to require()
43
+ // eslint-disable-next-line @typescript-eslint/no-implied-eval, no-new-func
44
+ const dynamicImport = new Function('specifier', 'return import(specifier)');
45
+ let _select;
46
+ async function getSelect() {
47
+ if (_select === undefined) {
48
+ const mod = await dynamicImport('@inquirer/select');
49
+ _select = mod.default;
50
+ }
51
+ return _select;
52
+ }
42
53
  const configLoader_1 = require("../../utils/configLoader");
43
54
  const cdpPorts_1 = require("../../utils/cdpPorts");
44
55
  // ---------------------------------------------------------------------------
@@ -208,8 +219,8 @@ function askSecret(rl, prompt) {
208
219
  // ---------------------------------------------------------------------------
209
220
  // Output helpers
210
221
  // ---------------------------------------------------------------------------
211
- function stepHeader(step, total, title) {
212
- console.log(` ${C.cyan}[Step ${step}/${total}]${C.reset} ${C.bold}${title}${C.reset}`);
222
+ function sectionHeader(title) {
223
+ console.log(`\n ${C.cyan}—${C.reset} ${C.bold}${title}${C.reset}\n`);
213
224
  }
214
225
  function hint(text) {
215
226
  console.log(` ${C.dim}${text}${C.reset}`);
@@ -225,9 +236,73 @@ function buildInviteUrl(clientId) {
225
236
  return `https://discord.com/api/oauth2/authorize?client_id=${clientId}&permissions=${permissions}&scope=bot%20applications.commands`;
226
237
  }
227
238
  // ---------------------------------------------------------------------------
228
- // Setup steps
239
+ // Status detection (pure functions)
240
+ // ---------------------------------------------------------------------------
241
+ function isDiscordConfigured(p) {
242
+ return !!(p.discordToken && p.clientId && p.allowedUserIds && p.allowedUserIds.length > 0);
243
+ }
244
+ function isTelegramConfigured(p) {
245
+ return !!(p.telegramToken && p.telegramAllowedUserIds && p.telegramAllowedUserIds.length > 0);
246
+ }
247
+ function workspaceLabel(p) {
248
+ return p.workspaceBaseDir ?? (path.join(os.homedir(), 'Code') + ' (default)');
249
+ }
250
+ function isValidTelegramTokenFormat(token) {
251
+ return /^\d+:[A-Za-z0-9_-]+$/.test(token);
252
+ }
253
+ /**
254
+ * Recompute platforms from the current persisted state and save.
255
+ * Called after each individual setup flow so Ctrl+C mid-session
256
+ * still leaves a valid platforms array.
257
+ */
258
+ function savePlatformsFromState() {
259
+ const current = configLoader_1.ConfigLoader.readPersisted();
260
+ const platforms = [];
261
+ if (isDiscordConfigured(current))
262
+ platforms.push('discord');
263
+ if (isTelegramConfigured(current))
264
+ platforms.push('telegram');
265
+ configLoader_1.ConfigLoader.save({ platforms });
266
+ }
267
+ /**
268
+ * Add a platform to the persisted platforms list (idempotent).
269
+ */
270
+ function addPlatform(platform) {
271
+ const current = configLoader_1.ConfigLoader.readPersisted();
272
+ const platforms = current.platforms ?? [];
273
+ if (!platforms.includes(platform)) {
274
+ configLoader_1.ConfigLoader.save({ platforms: [...platforms, platform] });
275
+ }
276
+ }
277
+ /**
278
+ * Remove a platform from the persisted platforms list (idempotent).
279
+ * Credentials are preserved — only the enabled flag changes.
280
+ */
281
+ function removePlatform(platform) {
282
+ const current = configLoader_1.ConfigLoader.readPersisted();
283
+ const platforms = (current.platforms ?? []).filter((p) => p !== platform);
284
+ configLoader_1.ConfigLoader.save({ platforms });
285
+ }
286
+ function platformStatus(hasCredentials, platforms, platform) {
287
+ if (!hasCredentials)
288
+ return 'not_configured';
289
+ if (platforms?.includes(platform))
290
+ return 'enabled';
291
+ return 'disabled';
292
+ }
293
+ function statusBadge(status) {
294
+ switch (status) {
295
+ case 'enabled':
296
+ return `${C.green}[enabled]${C.reset}`;
297
+ case 'disabled':
298
+ return `${C.yellow}[disabled]${C.reset}`;
299
+ case 'not_configured':
300
+ return `${C.dim}[not configured]${C.reset}`;
301
+ }
302
+ }
303
+ // ---------------------------------------------------------------------------
304
+ // Input prompt helpers
229
305
  // ---------------------------------------------------------------------------
230
- const TOTAL_STEPS = 4;
231
306
  async function promptToken(rl) {
232
307
  while (true) {
233
308
  const token = await askSecret(rl, ` ${C.yellow}>${C.reset} `);
@@ -236,20 +311,17 @@ async function promptToken(rl) {
236
311
  continue;
237
312
  }
238
313
  const trimmed = token.trim();
239
- // Extract Client ID from token
240
314
  const clientId = extractBotIdFromToken(trimmed);
241
315
  if (!clientId) {
242
316
  errMsg('Invalid token format. A Discord bot token has 3 dot-separated segments.');
243
317
  continue;
244
318
  }
245
- // Verify token against Discord API
246
319
  process.stdout.write(` ${C.dim}Verifying token...${C.reset}`);
247
320
  const botInfo = await verifyToken(trimmed);
248
321
  if (botInfo) {
249
322
  process.stdout.write(`\r ${C.green}Verified!${C.reset} Bot: ${C.bold}${botInfo.username}${C.reset} (${botInfo.id})\n`);
250
323
  return { token: trimmed, clientId: botInfo.id, botName: botInfo.username };
251
324
  }
252
- // API failed but token format is valid — use extracted ID
253
325
  process.stdout.write(`\r ${C.yellow}Could not verify online${C.reset} — using extracted ID: ${clientId}\n`);
254
326
  return { token: trimmed, clientId, botName: null };
255
327
  }
@@ -291,76 +363,220 @@ async function promptWorkspaceDir(rl) {
291
363
  errMsg('Please enter an existing directory.');
292
364
  }
293
365
  }
294
- async function runSetupWizard() {
295
- const rl = createInterface();
366
+ async function platformSubMenu(rl, platformName, status) {
367
+ const select = await getSelect();
368
+ const choices = status === 'disabled'
369
+ ? [
370
+ { name: 'Enable', value: 'enable' },
371
+ { name: 'Reconfigure', value: 'reconfigure' },
372
+ { name: 'Back', value: 'back' },
373
+ ]
374
+ : [
375
+ { name: 'Reconfigure', value: 'reconfigure' },
376
+ { name: 'Disable', value: 'disable' },
377
+ { name: 'Back', value: 'back' },
378
+ ];
379
+ rl.pause();
296
380
  try {
297
- console.log(SETUP_LOGO);
298
- console.log(` ${C.bold}Interactive setup — ${TOTAL_STEPS} steps${C.reset}\n`);
299
- stepHeader(1, TOTAL_STEPS, 'Discord Bot Token');
300
- hint('1. Go to https://discord.com/developers/applications and log in');
301
- hint('2. Click "New Application" (top-right), enter a name (e.g. LazyGravity), and create it');
302
- hint('3. Go to the "Bot" tab on the left sidebar');
303
- hint('4. Click "Reset Token" to generate and copy the token');
304
- hint(`5. Scroll down to ${C.bold}"Privileged Gateway Intents"${C.dim} and enable ALL of:`);
305
- hint(` ${C.cyan}PRESENCE INTENT${C.dim}`);
306
- hint(` ${C.cyan}SERVER MEMBERS INTENT${C.dim}`);
307
- hint(` ${C.cyan}MESSAGE CONTENT INTENT${C.dim} ${C.yellow}(required — bot cannot read messages without this)${C.dim}`);
308
- hint(`6. Click ${C.bold}"Save Changes"${C.dim} at the bottom (Warning banner)`);
309
- hintBlank();
310
- const { token: discordToken, clientId } = await promptToken(rl);
311
- console.log('');
312
- stepHeader(2, TOTAL_STEPS, 'Guild (Server) ID');
313
- hint('This registers slash commands instantly to your server.');
314
- hint('1. Open Discord Settings > Advanced > enable "Developer Mode"');
315
- hint('2. Right-click your server icon > "Copy Server ID"');
316
- hint(`${C.yellow}Press Enter to skip${C.dim} (commands will register globally, may take ~1 hour)`);
317
- hintBlank();
318
- const guildId = await promptGuildId(rl);
319
- console.log('');
320
- stepHeader(3, TOTAL_STEPS, 'Allowed Discord User IDs');
321
- hint('Only these users can send commands to the bot.');
322
- hint('1. In Discord, right-click your own profile icon');
323
- hint('2. Click "Copy User ID" (requires Developer Mode from step 2)');
324
- hint('Multiple IDs: separate with commas (e.g. 123456,789012)');
325
- hintBlank();
326
- const allowedUserIds = await promptAllowedUserIds(rl);
327
- console.log('');
328
- stepHeader(4, TOTAL_STEPS, 'Workspace Base Directory');
329
- hint('The parent directory where your coding projects live.');
330
- hint('LazyGravity will scan subdirectories as workspaces.');
331
- hintBlank();
332
- const workspaceBaseDir = await promptWorkspaceDir(rl);
333
- console.log('');
334
- return { discordToken, clientId, guildId, allowedUserIds, workspaceBaseDir };
381
+ return await select({
382
+ message: `${platformName}:`,
383
+ choices,
384
+ });
335
385
  }
336
386
  finally {
337
- rl.close();
387
+ rl.resume();
388
+ }
389
+ }
390
+ // ---------------------------------------------------------------------------
391
+ // Individual setup flows (each saves immediately)
392
+ // ---------------------------------------------------------------------------
393
+ async function runDiscordSetup(rl) {
394
+ sectionHeader('Discord Bot Token');
395
+ hint('1. Go to https://discord.com/developers/applications and log in');
396
+ hint('2. Click "New Application" (top-right), enter a name (e.g. LazyGravity), and create it');
397
+ hint('3. Go to the "Bot" tab on the left sidebar');
398
+ hint('4. Click "Reset Token" to generate and copy the token');
399
+ hint(`5. Scroll down to ${C.bold}"Privileged Gateway Intents"${C.dim} and enable ALL of:`);
400
+ hint(` ${C.cyan}PRESENCE INTENT${C.dim}`);
401
+ hint(` ${C.cyan}SERVER MEMBERS INTENT${C.dim}`);
402
+ hint(` ${C.cyan}MESSAGE CONTENT INTENT${C.dim} ${C.yellow}(required — bot cannot read messages without this)${C.dim}`);
403
+ hint(`6. Click ${C.bold}"Save Changes"${C.dim} at the bottom (Warning banner)`);
404
+ hintBlank();
405
+ const { token: discordToken, clientId } = await promptToken(rl);
406
+ console.log('');
407
+ sectionHeader('Guild (Server) ID');
408
+ hint('This registers slash commands instantly to your server.');
409
+ hint('1. Open Discord Settings > Advanced > enable "Developer Mode"');
410
+ hint('2. Right-click your server icon > "Copy Server ID"');
411
+ hint(`${C.yellow}Press Enter to skip${C.dim} (commands will register globally, may take ~1 hour)`);
412
+ hintBlank();
413
+ const guildId = await promptGuildId(rl);
414
+ console.log('');
415
+ sectionHeader('Allowed Discord User IDs');
416
+ hint('Only these users can send commands to the bot.');
417
+ hint('1. In Discord, right-click your own profile icon');
418
+ hint('2. Click "Copy User ID" (requires Developer Mode from step above)');
419
+ hint('Multiple IDs: separate with commas (e.g. 123456,789012)');
420
+ hintBlank();
421
+ const allowedUserIds = await promptAllowedUserIds(rl);
422
+ console.log('');
423
+ configLoader_1.ConfigLoader.save({ discordToken, clientId, guildId, allowedUserIds });
424
+ savePlatformsFromState();
425
+ const inviteUrl = buildInviteUrl(clientId);
426
+ console.log(` ${C.green}Discord saved!${C.reset}`);
427
+ console.log(` ${C.dim}Invite URL:${C.reset} ${inviteUrl}\n`);
428
+ }
429
+ async function runTelegramSetup(rl) {
430
+ sectionHeader('Telegram Bot Token');
431
+ hint('1. Open Telegram and message @BotFather');
432
+ hint('2. Send /newbot and follow the prompts to create a bot');
433
+ hint('3. Copy the token BotFather gives you');
434
+ hintBlank();
435
+ let telegramToken = '';
436
+ while (true) {
437
+ const raw = await askSecret(rl, ` ${C.yellow}>${C.reset} `);
438
+ if (!isNonEmpty(raw)) {
439
+ errMsg('Token cannot be empty. Please try again.');
440
+ continue;
441
+ }
442
+ const trimmed = raw.trim();
443
+ if (!isValidTelegramTokenFormat(trimmed)) {
444
+ errMsg('Invalid token format. Telegram tokens look like: 123456:ABCdef...');
445
+ continue;
446
+ }
447
+ telegramToken = trimmed;
448
+ break;
338
449
  }
450
+ console.log('');
451
+ sectionHeader('Allowed Telegram User IDs');
452
+ hint('Only these users can send messages to the bot.');
453
+ hint('To find your ID: message @userinfobot on Telegram');
454
+ hint('Multiple IDs: separate with commas (e.g. 123456,789012)');
455
+ hintBlank();
456
+ const telegramAllowedUserIds = await promptAllowedUserIds(rl);
457
+ console.log('');
458
+ configLoader_1.ConfigLoader.save({ telegramToken, telegramAllowedUserIds });
459
+ savePlatformsFromState();
460
+ console.log(` ${C.green}Telegram saved!${C.reset}\n`);
461
+ }
462
+ async function runWorkspaceSetup(rl) {
463
+ sectionHeader('Workspace Base Directory');
464
+ hint('The parent directory where your coding projects live.');
465
+ hint('LazyGravity will scan subdirectories as workspaces.');
466
+ hintBlank();
467
+ const workspaceBaseDir = await promptWorkspaceDir(rl);
468
+ console.log('');
469
+ configLoader_1.ConfigLoader.save({ workspaceBaseDir });
470
+ console.log(` ${C.green}Workspace saved!${C.reset}\n`);
339
471
  }
340
472
  // ---------------------------------------------------------------------------
341
473
  // Public action
342
474
  // ---------------------------------------------------------------------------
343
475
  async function setupAction() {
344
- const result = await runSetupWizard();
345
- configLoader_1.ConfigLoader.save({
346
- discordToken: result.discordToken,
347
- clientId: result.clientId,
348
- guildId: result.guildId,
349
- allowedUserIds: result.allowedUserIds,
350
- workspaceBaseDir: result.workspaceBaseDir,
351
- });
352
- const configPath = configLoader_1.ConfigLoader.getConfigFilePath();
353
- const inviteUrl = buildInviteUrl(result.clientId);
354
- console.log(` ${C.green}Setup complete!${C.reset}\n`);
355
- console.log(` ${C.dim}Saved to${C.reset} ${configPath}\n`);
356
- console.log(` ${C.cyan}Next steps:${C.reset}`);
357
- console.log(` ${C.bold}1.${C.reset} ${C.yellow}Verify Privileged Gateway Intents are enabled${C.reset} in the Bot tab:`);
358
- console.log(` ${C.dim}Required: PRESENCE INTENT, SERVER MEMBERS INTENT, MESSAGE CONTENT INTENT${C.reset}`);
359
- console.log(` https://discord.com/developers/applications/${result.clientId}/bot\n`);
360
- console.log(` ${C.bold}2.${C.reset} Add the bot to your server:`);
361
- console.log(` ${inviteUrl}\n`);
362
- console.log(` ${C.bold}3.${C.reset} Open Antigravity with CDP enabled:`);
363
- console.log(` ${C.green}lazy-gravity open${C.reset}`);
364
- console.log(` ${C.dim}(auto-selects an available port from: ${cdpPorts_1.CDP_PORTS.join(', ')})${C.reset}\n`);
365
- console.log(` ${C.bold}4.${C.reset} Run: ${C.green}lazy-gravity start${C.reset}\n`);
476
+ const rl = createInterface();
477
+ try {
478
+ console.log(SETUP_LOGO);
479
+ while (true) {
480
+ const config = configLoader_1.ConfigLoader.readPersisted();
481
+ const discordSt = platformStatus(isDiscordConfigured(config), config.platforms, 'discord');
482
+ const telegramSt = platformStatus(isTelegramConfigured(config), config.platforms, 'telegram');
483
+ const wsLabel = `${C.dim}${workspaceLabel(config)}${C.reset}`;
484
+ const select = await getSelect();
485
+ rl.pause();
486
+ const choice = await select({
487
+ message: 'Configure:',
488
+ choices: [
489
+ { name: `Discord ${statusBadge(discordSt)}`, value: 'discord' },
490
+ { name: `Telegram ${statusBadge(telegramSt)}`, value: 'telegram' },
491
+ { name: `Workspace Directory ${wsLabel}`, value: 'workspace' },
492
+ { name: `Done save & exit`, value: 'done' },
493
+ ],
494
+ });
495
+ rl.resume();
496
+ switch (choice) {
497
+ case 'discord':
498
+ if (discordSt === 'not_configured') {
499
+ await runDiscordSetup(rl);
500
+ }
501
+ else {
502
+ const action = await platformSubMenu(rl, 'Discord', discordSt);
503
+ switch (action) {
504
+ case 'enable':
505
+ addPlatform('discord');
506
+ console.log(` ${C.green}Discord enabled.${C.reset}\n`);
507
+ break;
508
+ case 'reconfigure':
509
+ await runDiscordSetup(rl);
510
+ break;
511
+ case 'disable':
512
+ removePlatform('discord');
513
+ console.log(` ${C.yellow}Discord disabled.${C.reset} Credentials kept.\n`);
514
+ break;
515
+ case 'back':
516
+ break;
517
+ }
518
+ }
519
+ break;
520
+ case 'telegram':
521
+ if (telegramSt === 'not_configured') {
522
+ await runTelegramSetup(rl);
523
+ }
524
+ else {
525
+ const action = await platformSubMenu(rl, 'Telegram', telegramSt);
526
+ switch (action) {
527
+ case 'enable':
528
+ addPlatform('telegram');
529
+ console.log(` ${C.green}Telegram enabled.${C.reset}\n`);
530
+ break;
531
+ case 'reconfigure':
532
+ await runTelegramSetup(rl);
533
+ break;
534
+ case 'disable':
535
+ removePlatform('telegram');
536
+ console.log(` ${C.yellow}Telegram disabled.${C.reset} Credentials kept.\n`);
537
+ break;
538
+ case 'back':
539
+ break;
540
+ }
541
+ }
542
+ break;
543
+ case 'workspace':
544
+ await runWorkspaceSetup(rl);
545
+ break;
546
+ case 'done': {
547
+ const finalConfig = configLoader_1.ConfigLoader.readPersisted();
548
+ const platforms = finalConfig.platforms ?? [];
549
+ if (platforms.length === 0) {
550
+ errMsg('No platforms enabled yet. Please enable at least one platform.');
551
+ break;
552
+ }
553
+ const configPath = configLoader_1.ConfigLoader.getConfigFilePath();
554
+ console.log(`\n ${C.green}Setup complete!${C.reset} Platforms: ${platforms.join(', ')}\n`);
555
+ console.log(` ${C.dim}Saved to${C.reset} ${configPath}\n`);
556
+ if (platforms.includes('discord') && finalConfig.clientId) {
557
+ const inviteUrl = buildInviteUrl(finalConfig.clientId);
558
+ console.log(` ${C.cyan}Discord:${C.reset}`);
559
+ console.log(` ${C.bold}1.${C.reset} ${C.yellow}Verify Privileged Gateway Intents are enabled${C.reset} in the Bot tab:`);
560
+ console.log(` ${C.dim}Required: PRESENCE INTENT, SERVER MEMBERS INTENT, MESSAGE CONTENT INTENT${C.reset}`);
561
+ console.log(` https://discord.com/developers/applications/${finalConfig.clientId}/bot\n`);
562
+ console.log(` ${C.bold}2.${C.reset} Add the bot to your server:`);
563
+ console.log(` ${inviteUrl}\n`);
564
+ }
565
+ if (platforms.includes('telegram')) {
566
+ console.log(` ${C.cyan}Telegram:${C.reset}`);
567
+ console.log(` ${C.dim}Your Telegram bot is ready. Message it on Telegram after starting.${C.reset}\n`);
568
+ }
569
+ console.log(` ${C.cyan}Start:${C.reset}`);
570
+ console.log(` ${C.bold}1.${C.reset} Open Antigravity with CDP enabled:`);
571
+ console.log(` ${C.green}lazy-gravity open${C.reset}`);
572
+ console.log(` ${C.dim}(auto-selects an available port from: ${cdpPorts_1.CDP_PORTS.join(', ')})${C.reset}\n`);
573
+ console.log(` ${C.bold}2.${C.reset} Run: ${C.green}lazy-gravity start${C.reset}\n`);
574
+ return;
575
+ }
576
+ }
577
+ }
578
+ }
579
+ finally {
580
+ rl.close();
581
+ }
366
582
  }