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.
- package/README.md +76 -15
- package/dist/bin/commands/doctor.js +19 -2
- package/dist/bin/commands/setup.js +286 -70
- package/dist/bot/eventRouter.js +70 -0
- package/dist/bot/index.js +353 -147
- package/dist/bot/telegramCommands.js +428 -0
- package/dist/bot/telegramMessageHandler.js +304 -0
- package/dist/bot/telegramProjectCommand.js +137 -0
- package/dist/bot/workspaceQueue.js +61 -0
- package/dist/commands/joinCommandHandler.js +4 -1
- package/dist/database/telegramBindingRepository.js +97 -0
- package/dist/database/userPreferenceRepository.js +46 -1
- package/dist/events/interactionCreateHandler.js +36 -0
- package/dist/events/messageCreateHandler.js +11 -7
- package/dist/handlers/approvalButtonAction.js +99 -0
- package/dist/handlers/autoAcceptButtonAction.js +43 -0
- package/dist/handlers/buttonHandler.js +55 -0
- package/dist/handlers/commandHandler.js +44 -0
- package/dist/handlers/errorPopupButtonAction.js +137 -0
- package/dist/handlers/messageHandler.js +70 -0
- package/dist/handlers/modeSelectAction.js +63 -0
- package/dist/handlers/modelButtonAction.js +102 -0
- package/dist/handlers/planningButtonAction.js +118 -0
- package/dist/handlers/selectHandler.js +41 -0
- package/dist/handlers/templateButtonAction.js +54 -0
- package/dist/platform/adapter.js +8 -0
- package/dist/platform/discord/discordAdapter.js +99 -0
- package/dist/platform/discord/index.js +15 -0
- package/dist/platform/discord/wrappers.js +331 -0
- package/dist/platform/index.js +18 -0
- package/dist/platform/richContentBuilder.js +76 -0
- package/dist/platform/telegram/index.js +16 -0
- package/dist/platform/telegram/telegramAdapter.js +195 -0
- package/dist/platform/telegram/telegramFormatter.js +134 -0
- package/dist/platform/telegram/wrappers.js +329 -0
- package/dist/platform/types.js +28 -0
- package/dist/services/approvalDetector.js +15 -2
- package/dist/services/cdpBridgeManager.js +91 -146
- package/dist/services/defaultModelApplicator.js +54 -0
- package/dist/services/modeService.js +16 -1
- package/dist/services/modelService.js +57 -16
- package/dist/services/notificationSender.js +149 -0
- package/dist/services/responseMonitor.js +1 -2
- package/dist/ui/autoAcceptUi.js +37 -0
- package/dist/ui/modeUi.js +38 -1
- package/dist/ui/modelsUi.js +96 -0
- package/dist/ui/outputUi.js +32 -0
- package/dist/ui/projectListUi.js +55 -0
- package/dist/ui/screenshotUi.js +26 -0
- package/dist/ui/sessionPickerUi.js +35 -1
- package/dist/ui/templateUi.js +41 -0
- package/dist/utils/configLoader.js +63 -12
- package/dist/utils/lockfile.js +5 -5
- package/dist/utils/logger.js +7 -0
- package/dist/utils/telegramImageHandler.js +127 -0
- 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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
|
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
|
package/dist/utils/lockfile.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
89
|
+
logger_1.logger.info(`🔓 Lock released (PID: ${process.pid})`);
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
}
|
package/dist/utils/logger.js
CHANGED
|
@@ -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.
|
|
4
|
-
"description": "Control Antigravity from anywhere — a local, secure Discord
|
|
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
|
},
|