lazy-gravity 0.2.0 → 0.3.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 (56) hide show
  1. package/README.md +76 -15
  2. package/dist/bin/commands/doctor.js +19 -2
  3. package/dist/bin/commands/setup.js +286 -70
  4. package/dist/bot/eventRouter.js +70 -0
  5. package/dist/bot/index.js +353 -147
  6. package/dist/bot/telegramCommands.js +428 -0
  7. package/dist/bot/telegramMessageHandler.js +304 -0
  8. package/dist/bot/telegramProjectCommand.js +137 -0
  9. package/dist/bot/workspaceQueue.js +61 -0
  10. package/dist/commands/joinCommandHandler.js +4 -1
  11. package/dist/database/telegramBindingRepository.js +97 -0
  12. package/dist/database/userPreferenceRepository.js +46 -1
  13. package/dist/events/interactionCreateHandler.js +36 -0
  14. package/dist/events/messageCreateHandler.js +11 -7
  15. package/dist/handlers/approvalButtonAction.js +99 -0
  16. package/dist/handlers/autoAcceptButtonAction.js +43 -0
  17. package/dist/handlers/buttonHandler.js +55 -0
  18. package/dist/handlers/commandHandler.js +44 -0
  19. package/dist/handlers/errorPopupButtonAction.js +137 -0
  20. package/dist/handlers/messageHandler.js +70 -0
  21. package/dist/handlers/modeSelectAction.js +63 -0
  22. package/dist/handlers/modelButtonAction.js +102 -0
  23. package/dist/handlers/planningButtonAction.js +118 -0
  24. package/dist/handlers/selectHandler.js +41 -0
  25. package/dist/handlers/templateButtonAction.js +54 -0
  26. package/dist/platform/adapter.js +8 -0
  27. package/dist/platform/discord/discordAdapter.js +99 -0
  28. package/dist/platform/discord/index.js +15 -0
  29. package/dist/platform/discord/wrappers.js +331 -0
  30. package/dist/platform/index.js +18 -0
  31. package/dist/platform/richContentBuilder.js +76 -0
  32. package/dist/platform/telegram/index.js +16 -0
  33. package/dist/platform/telegram/telegramAdapter.js +195 -0
  34. package/dist/platform/telegram/telegramFormatter.js +134 -0
  35. package/dist/platform/telegram/wrappers.js +329 -0
  36. package/dist/platform/types.js +28 -0
  37. package/dist/services/approvalDetector.js +15 -2
  38. package/dist/services/cdpBridgeManager.js +91 -146
  39. package/dist/services/defaultModelApplicator.js +54 -0
  40. package/dist/services/modeService.js +16 -1
  41. package/dist/services/modelService.js +57 -16
  42. package/dist/services/notificationSender.js +149 -0
  43. package/dist/services/responseMonitor.js +1 -2
  44. package/dist/ui/autoAcceptUi.js +37 -0
  45. package/dist/ui/modeUi.js +38 -1
  46. package/dist/ui/modelsUi.js +96 -0
  47. package/dist/ui/outputUi.js +32 -0
  48. package/dist/ui/projectListUi.js +55 -0
  49. package/dist/ui/screenshotUi.js +26 -0
  50. package/dist/ui/sessionPickerUi.js +35 -1
  51. package/dist/ui/templateUi.js +41 -0
  52. package/dist/utils/configLoader.js +63 -12
  53. package/dist/utils/lockfile.js +5 -5
  54. package/dist/utils/logger.js +7 -0
  55. package/dist/utils/telegramImageHandler.js +127 -0
  56. package/package.json +4 -2
@@ -75,17 +75,25 @@ function readPersistedConfig(filePath) {
75
75
  * Returns a fresh AppConfig object (immutable pattern).
76
76
  */
77
77
  function mergeConfig(persisted) {
78
- const token = process.env.DISCORD_BOT_TOKEN ?? persisted.discordToken;
79
- if (!token) {
80
- throw new Error('Missing required environment variable: DISCORD_BOT_TOKEN');
81
- }
82
- const clientId = process.env.CLIENT_ID ?? persisted.clientId;
83
- if (!clientId) {
84
- throw new Error('Missing required environment variable: CLIENT_ID');
85
- }
86
- const allowedUserIds = resolveAllowedUserIds(persisted);
87
- if (allowedUserIds.length === 0) {
88
- throw new Error('Missing required environment variable: ALLOWED_USER_IDS');
78
+ // Resolve platforms FIRST so we only validate credentials for enabled platforms
79
+ const platforms = resolvePlatforms(process.env.PLATFORMS, persisted.platforms);
80
+ // Discord credentials — only required when Discord is an active platform
81
+ let discordToken;
82
+ let clientId;
83
+ let allowedUserIds = [];
84
+ if (platforms.includes('discord')) {
85
+ discordToken = process.env.DISCORD_BOT_TOKEN ?? persisted.discordToken;
86
+ if (!discordToken) {
87
+ throw new Error('Missing required environment variable: DISCORD_BOT_TOKEN');
88
+ }
89
+ clientId = process.env.CLIENT_ID ?? persisted.clientId;
90
+ if (!clientId) {
91
+ throw new Error('Missing required environment variable: CLIENT_ID');
92
+ }
93
+ allowedUserIds = resolveAllowedUserIds(persisted);
94
+ if (allowedUserIds.length === 0) {
95
+ throw new Error('Missing required environment variable: ALLOWED_USER_IDS');
96
+ }
89
97
  }
90
98
  const defaultDir = path.join(os.homedir(), 'Code');
91
99
  const rawDir = process.env.WORKSPACE_BASE_DIR ?? persisted.workspaceBaseDir ?? defaultDir;
@@ -94,8 +102,14 @@ function mergeConfig(persisted) {
94
102
  const autoApproveFileEdits = resolveBoolean(process.env.AUTO_APPROVE_FILE_EDITS, persisted.autoApproveFileEdits, false);
95
103
  const logLevel = resolveLogLevel(process.env.LOG_LEVEL, persisted.logLevel);
96
104
  const extractionMode = resolveExtractionMode(process.env.EXTRACTION_MODE, persisted.extractionMode);
105
+ // Telegram credentials — only required when Telegram is an active platform
106
+ const telegramToken = process.env.TELEGRAM_BOT_TOKEN ?? persisted.telegramToken ?? undefined;
107
+ const telegramAllowedUserIds = resolveTelegramAllowedUserIds(persisted);
108
+ if (platforms.includes('telegram') && !telegramToken) {
109
+ throw new Error('TELEGRAM_BOT_TOKEN is required when platforms include "telegram"');
110
+ }
97
111
  return {
98
- discordToken: token,
112
+ discordToken,
99
113
  clientId,
100
114
  guildId,
101
115
  allowedUserIds,
@@ -103,6 +117,9 @@ function mergeConfig(persisted) {
103
117
  autoApproveFileEdits,
104
118
  logLevel,
105
119
  extractionMode,
120
+ telegramToken,
121
+ telegramAllowedUserIds,
122
+ platforms,
106
123
  };
107
124
  }
108
125
  function resolveAllowedUserIds(persisted) {
@@ -132,6 +149,36 @@ function resolveExtractionMode(envValue, persistedValue) {
132
149
  return 'legacy';
133
150
  return 'structured';
134
151
  }
152
+ function resolveTelegramAllowedUserIds(persisted) {
153
+ const envValue = process.env.TELEGRAM_ALLOWED_USER_IDS;
154
+ if (envValue) {
155
+ return envValue
156
+ .split(',')
157
+ .map((id) => id.trim())
158
+ .filter((id) => id.length > 0);
159
+ }
160
+ if (persisted.telegramAllowedUserIds && persisted.telegramAllowedUserIds.length > 0) {
161
+ return [...persisted.telegramAllowedUserIds];
162
+ }
163
+ return undefined;
164
+ }
165
+ const VALID_PLATFORMS = ['discord', 'telegram'];
166
+ function resolvePlatforms(envValue, persistedValue) {
167
+ if (envValue) {
168
+ const parsed = envValue
169
+ .split(',')
170
+ .map((p) => p.trim().toLowerCase())
171
+ .filter((p) => VALID_PLATFORMS.includes(p));
172
+ if (parsed.length > 0)
173
+ return parsed;
174
+ }
175
+ if (persistedValue && persistedValue.length > 0) {
176
+ const validated = persistedValue.filter((p) => VALID_PLATFORMS.includes(p));
177
+ if (validated.length > 0)
178
+ return validated;
179
+ }
180
+ return ['discord'];
181
+ }
135
182
  function resolveBoolean(envValue, persistedValue, defaultValue) {
136
183
  if (envValue !== undefined)
137
184
  return envValue.toLowerCase() === 'true';
@@ -153,6 +200,10 @@ exports.ConfigLoader = {
153
200
  configExists() {
154
201
  return fs.existsSync(getConfigFilePath());
155
202
  },
203
+ /** Read persisted config from disk. Returns empty object if file doesn't exist. */
204
+ readPersisted() {
205
+ return readPersistedConfig(getConfigFilePath());
206
+ },
156
207
  /**
157
208
  * Load config using resolution order:
158
209
  * env vars > ~/.lazy-gravity/config.json > .env > defaults
@@ -24,7 +24,7 @@ function isProcessRunning(pid) {
24
24
  * Stop an existing process and wait for it to exit
25
25
  */
26
26
  function killExistingProcess(pid) {
27
- logger_1.logger.error(`🔄 Stopping existing Bot process (PID: ${pid})...`);
27
+ logger_1.logger.warn(`🔄 Stopping existing Bot process (PID: ${pid})...`);
28
28
  try {
29
29
  process.kill(pid, 'SIGTERM');
30
30
  }
@@ -36,7 +36,7 @@ function killExistingProcess(pid) {
36
36
  const deadline = Date.now() + 5000;
37
37
  while (Date.now() < deadline) {
38
38
  if (!isProcessRunning(pid)) {
39
- logger_1.logger.error(`✅ Existing process (PID: ${pid}) stopped`);
39
+ logger_1.logger.info(`✅ Existing process (PID: ${pid}) stopped`);
40
40
  return;
41
41
  }
42
42
  // Wait 50ms (busy wait)
@@ -44,7 +44,7 @@ function killExistingProcess(pid) {
44
44
  while (Date.now() < waitUntil) { /* spin */ }
45
45
  }
46
46
  // Timeout: force kill with SIGKILL
47
- logger_1.logger.error(`⚠️ Process did not exit with SIGTERM, force killing (SIGKILL)`);
47
+ logger_1.logger.warn(`⚠️ Process did not exit with SIGTERM, force killing (SIGKILL)`);
48
48
  try {
49
49
  process.kill(pid, 'SIGKILL');
50
50
  }
@@ -78,7 +78,7 @@ function acquireLock() {
78
78
  }
79
79
  // Create new lock file
80
80
  fs_1.default.writeFileSync(LOCK_FILE, String(process.pid), 'utf-8');
81
- logger_1.logger.error(`🔒 Lock acquired (PID: ${process.pid})`);
81
+ logger_1.logger.info(`🔒 Lock acquired (PID: ${process.pid})`);
82
82
  // Cleanup function
83
83
  const releaseLock = () => {
84
84
  try {
@@ -86,7 +86,7 @@ function acquireLock() {
86
86
  const content = fs_1.default.readFileSync(LOCK_FILE, 'utf-8').trim();
87
87
  if (parseInt(content, 10) === process.pid) {
88
88
  fs_1.default.unlinkSync(LOCK_FILE);
89
- logger_1.logger.error(`🔓 Lock released (PID: ${process.pid})`);
89
+ logger_1.logger.info(`🔓 Lock released (PID: ${process.pid})`);
90
90
  }
91
91
  }
92
92
  }
@@ -9,6 +9,7 @@ exports.COLORS = {
9
9
  cyan: '\x1b[36m',
10
10
  green: '\x1b[32m',
11
11
  magenta: '\x1b[35m',
12
+ boldYellow: '\x1b[1;33m',
12
13
  dim: '\x1b[2m',
13
14
  reset: '\x1b[0m',
14
15
  };
@@ -74,6 +75,12 @@ function createLogger(initialLevel = 'info') {
74
75
  logBuffer_1.logBuffer.append('info', `[DONE] ${args.join(' ')}`);
75
76
  }
76
77
  },
78
+ /** User prompt text - always visible regardless of log level */
79
+ prompt(text) {
80
+ const formatted = `${getTimestamp()} ${exports.COLORS.boldYellow}[PROMPT]${exports.COLORS.reset} ${exports.COLORS.boldYellow}${text}${exports.COLORS.reset}`;
81
+ console.info(formatted);
82
+ logBuffer_1.logBuffer.append('info', `[PROMPT] ${text}`);
83
+ },
77
84
  /** Section divider with optional label for structured output */
78
85
  divider(label) {
79
86
  if (shouldLog('info')) {
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+ /**
3
+ * Telegram image download utility.
4
+ *
5
+ * Downloads photos from Telegram Bot API and saves them to a temp directory,
6
+ * producing InboundImageAttachment[] that can be passed to
7
+ * cdp.injectMessageWithImageFiles().
8
+ *
9
+ * Reuses shared types and helpers from imageHandler.ts.
10
+ */
11
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
12
+ if (k2 === undefined) k2 = k;
13
+ var desc = Object.getOwnPropertyDescriptor(m, k);
14
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
15
+ desc = { enumerable: true, get: function() { return m[k]; } };
16
+ }
17
+ Object.defineProperty(o, k2, desc);
18
+ }) : (function(o, m, k, k2) {
19
+ if (k2 === undefined) k2 = k;
20
+ o[k2] = m[k];
21
+ }));
22
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
23
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
24
+ }) : function(o, v) {
25
+ o["default"] = v;
26
+ });
27
+ var __importStar = (this && this.__importStar) || (function () {
28
+ var ownKeys = function(o) {
29
+ ownKeys = Object.getOwnPropertyNames || function (o) {
30
+ var ar = [];
31
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
32
+ return ar;
33
+ };
34
+ return ownKeys(o);
35
+ };
36
+ return function (mod) {
37
+ if (mod && mod.__esModule) return mod;
38
+ var result = {};
39
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
40
+ __setModuleDefault(result, mod);
41
+ return result;
42
+ };
43
+ })();
44
+ Object.defineProperty(exports, "__esModule", { value: true });
45
+ exports.downloadTelegramPhotos = downloadTelegramPhotos;
46
+ const fs = __importStar(require("fs/promises"));
47
+ const os = __importStar(require("os"));
48
+ const path = __importStar(require("path"));
49
+ const imageHandler_1 = require("./imageHandler");
50
+ const logger_1 = require("./logger");
51
+ const MAX_TELEGRAM_IMAGE_ATTACHMENTS = 4;
52
+ const TEMP_IMAGE_DIR = path.join(os.tmpdir(), 'lazy-gravity-images');
53
+ /**
54
+ * Extract the Telegram file_id from a telegram-file:// URL.
55
+ * Returns null if the URL doesn't match the expected scheme.
56
+ */
57
+ function extractFileId(url) {
58
+ const prefix = 'telegram-file://';
59
+ if (!url.startsWith(prefix))
60
+ return null;
61
+ return url.slice(prefix.length);
62
+ }
63
+ /**
64
+ * Download Telegram photo attachments to local temp files.
65
+ *
66
+ * Uses the Telegram Bot API getFile endpoint to resolve file_id → file_path,
67
+ * then downloads the file via https://api.telegram.org/file/bot<token>/<path>.
68
+ *
69
+ * @param attachments - PlatformAttachment[] from the wrapped Telegram message
70
+ * @param botToken - The bot token for constructing download URLs
71
+ * @param api - The bot API object (needs getFile method)
72
+ * @returns Downloaded images as InboundImageAttachment[]
73
+ */
74
+ async function downloadTelegramPhotos(attachments, botToken, api) {
75
+ if (!api.getFile) {
76
+ logger_1.logger.warn('[TelegramImageHandler] bot.api.getFile is not available');
77
+ return [];
78
+ }
79
+ const imageAttachments = attachments
80
+ .filter((att) => (att.contentType || '').startsWith('image/'))
81
+ .slice(0, MAX_TELEGRAM_IMAGE_ATTACHMENTS);
82
+ if (imageAttachments.length === 0)
83
+ return [];
84
+ await fs.mkdir(TEMP_IMAGE_DIR, { recursive: true });
85
+ const downloaded = [];
86
+ for (let i = 0; i < imageAttachments.length; i++) {
87
+ const attachment = imageAttachments[i];
88
+ const fileId = extractFileId(attachment.url);
89
+ if (!fileId) {
90
+ logger_1.logger.warn(`[TelegramImageHandler] Invalid file URL: ${attachment.url}`);
91
+ continue;
92
+ }
93
+ try {
94
+ // Resolve file_id to file_path via Bot API
95
+ const fileInfo = await api.getFile(fileId);
96
+ if (!fileInfo.file_path) {
97
+ logger_1.logger.warn(`[TelegramImageHandler] No file_path returned for file_id=${fileId}`);
98
+ continue;
99
+ }
100
+ // Download the file
101
+ const downloadUrl = `https://api.telegram.org/file/bot${botToken}/${fileInfo.file_path}`;
102
+ const response = await fetch(downloadUrl);
103
+ if (!response.ok) {
104
+ logger_1.logger.warn(`[TelegramImageHandler] Download failed (file_id=${fileId}, status=${response.status})`);
105
+ continue;
106
+ }
107
+ const bytes = Buffer.from(await response.arrayBuffer());
108
+ if (bytes.length === 0)
109
+ continue;
110
+ const mimeType = attachment.contentType || 'image/jpeg';
111
+ const ext = (0, imageHandler_1.mimeTypeToExtension)(mimeType);
112
+ const name = (0, imageHandler_1.sanitizeFileName)(attachment.name || `telegram-photo-${i + 1}.${ext}`);
113
+ const localPath = path.join(TEMP_IMAGE_DIR, `${Date.now()}-tg-${i}-${name}`);
114
+ await fs.writeFile(localPath, bytes);
115
+ downloaded.push({
116
+ localPath,
117
+ url: downloadUrl,
118
+ name,
119
+ mimeType,
120
+ });
121
+ }
122
+ catch (error) {
123
+ logger_1.logger.warn(`[TelegramImageHandler] Failed to download photo (file_id=${fileId}):`, error?.message || error);
124
+ }
125
+ }
126
+ return downloaded;
127
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "lazy-gravity",
3
- "version": "0.2.0",
4
- "description": "Control Antigravity from anywhere — a local, secure Discord Bot that lets you remotely operate Antigravity on your home PC from your smartphone's Discord app.",
3
+ "version": "0.3.0",
4
+ "description": "Control Antigravity from anywhere — a local, secure bot (Discord + Telegram) that lets you remotely operate Antigravity on your home PC from your smartphone.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
7
7
  "lazy-gravity": "dist/bin/cli.js"
@@ -47,10 +47,12 @@
47
47
  },
48
48
  "homepage": "https://github.com/tokyoweb3/LazyGravity#readme",
49
49
  "dependencies": {
50
+ "@inquirer/select": "^4.4.2",
50
51
  "better-sqlite3": "^12.6.2",
51
52
  "commander": "^14.0.3",
52
53
  "discord.js": "^14.25.1",
53
54
  "dotenv": "^17.3.1",
55
+ "grammy": "^1.41.1",
54
56
  "node-cron": "^4.2.1",
55
57
  "ws": "^8.19.0"
56
58
  },