lazy-gravity 0.0.2 → 0.0.3

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 (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +224 -0
  3. package/dist/bin/cli.js +79 -0
  4. package/dist/bin/commands/doctor.js +156 -0
  5. package/dist/bin/commands/open.js +145 -0
  6. package/dist/bin/commands/setup.js +366 -0
  7. package/dist/bin/commands/start.js +15 -0
  8. package/dist/bot/index.js +914 -0
  9. package/dist/commands/chatCommandHandler.js +145 -0
  10. package/dist/commands/cleanupCommandHandler.js +396 -0
  11. package/dist/commands/messageParser.js +28 -0
  12. package/dist/commands/registerSlashCommands.js +149 -0
  13. package/dist/commands/slashCommandHandler.js +104 -0
  14. package/dist/commands/workspaceCommandHandler.js +230 -0
  15. package/dist/database/chatSessionRepository.js +88 -0
  16. package/dist/database/scheduleRepository.js +119 -0
  17. package/dist/database/templateRepository.js +103 -0
  18. package/dist/database/workspaceBindingRepository.js +109 -0
  19. package/dist/events/interactionCreateHandler.js +286 -0
  20. package/dist/events/messageCreateHandler.js +154 -0
  21. package/dist/index.js +10 -0
  22. package/dist/middleware/auth.js +10 -0
  23. package/dist/middleware/sanitize.js +20 -0
  24. package/dist/services/antigravityLauncher.js +89 -0
  25. package/dist/services/approvalDetector.js +384 -0
  26. package/dist/services/autoAcceptService.js +80 -0
  27. package/dist/services/cdpBridgeManager.js +204 -0
  28. package/dist/services/cdpConnectionPool.js +157 -0
  29. package/dist/services/cdpService.js +1311 -0
  30. package/dist/services/channelManager.js +118 -0
  31. package/dist/services/chatSessionService.js +516 -0
  32. package/dist/services/modeService.js +73 -0
  33. package/dist/services/modelService.js +63 -0
  34. package/dist/services/processManager.js +61 -0
  35. package/dist/services/progressSender.js +61 -0
  36. package/dist/services/promptDispatcher.js +17 -0
  37. package/dist/services/quotaService.js +185 -0
  38. package/dist/services/responseMonitor.js +645 -0
  39. package/dist/services/scheduleService.js +134 -0
  40. package/dist/services/screenshotService.js +85 -0
  41. package/dist/services/titleGeneratorService.js +113 -0
  42. package/dist/services/workspaceService.js +64 -0
  43. package/dist/ui/autoAcceptUi.js +34 -0
  44. package/dist/ui/modeUi.js +34 -0
  45. package/dist/ui/modelsUi.js +97 -0
  46. package/dist/ui/screenshotUi.js +51 -0
  47. package/dist/ui/templateUi.js +67 -0
  48. package/dist/utils/cdpPorts.js +5 -0
  49. package/dist/utils/config.js +20 -0
  50. package/dist/utils/configLoader.js +160 -0
  51. package/dist/utils/discordFormatter.js +167 -0
  52. package/dist/utils/i18n.js +77 -0
  53. package/dist/utils/imageHandler.js +154 -0
  54. package/dist/utils/lockfile.js +113 -0
  55. package/dist/utils/logger.js +32 -0
  56. package/dist/utils/logo.js +13 -0
  57. package/dist/utils/metadataExtractor.js +15 -0
  58. package/dist/utils/processLogBuffer.js +98 -0
  59. package/dist/utils/streamMessageFormatter.js +90 -0
  60. package/package.json +73 -5
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.acquireLock = acquireLock;
7
+ const logger_1 = require("./logger");
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const LOCK_FILE = path_1.default.resolve(process.cwd(), '.bot.lock');
11
+ /**
12
+ * Check if a process with the given PID is running
13
+ */
14
+ function isProcessRunning(pid) {
15
+ try {
16
+ process.kill(pid, 0);
17
+ return true;
18
+ }
19
+ catch {
20
+ return false;
21
+ }
22
+ }
23
+ /**
24
+ * Stop an existing process and wait for it to exit
25
+ */
26
+ function killExistingProcess(pid) {
27
+ logger_1.logger.error(`🔄 Stopping existing Bot process (PID: ${pid})...`);
28
+ try {
29
+ process.kill(pid, 'SIGTERM');
30
+ }
31
+ catch {
32
+ // Ignore if already terminated
33
+ return;
34
+ }
35
+ // Wait up to 5 seconds for process to exit
36
+ const deadline = Date.now() + 5000;
37
+ while (Date.now() < deadline) {
38
+ if (!isProcessRunning(pid)) {
39
+ logger_1.logger.error(`✅ Existing process (PID: ${pid}) stopped`);
40
+ return;
41
+ }
42
+ // Wait 50ms (busy wait)
43
+ const waitUntil = Date.now() + 50;
44
+ while (Date.now() < waitUntil) { /* spin */ }
45
+ }
46
+ // Timeout: force kill with SIGKILL
47
+ logger_1.logger.error(`⚠️ Process did not exit with SIGTERM, force killing (SIGKILL)`);
48
+ try {
49
+ process.kill(pid, 'SIGKILL');
50
+ }
51
+ catch {
52
+ // ignore
53
+ }
54
+ }
55
+ /**
56
+ * Acquire a lockfile to prevent duplicate bot instances.
57
+ * If another process is already running, stop it before starting.
58
+ *
59
+ * @returns A function to release the lock
60
+ */
61
+ function acquireLock() {
62
+ // Check existing lock file
63
+ if (fs_1.default.existsSync(LOCK_FILE)) {
64
+ const content = fs_1.default.readFileSync(LOCK_FILE, 'utf-8').trim();
65
+ const existingPid = parseInt(content, 10);
66
+ if (!isNaN(existingPid) && existingPid !== process.pid && isProcessRunning(existingPid)) {
67
+ // Stop existing process and restart
68
+ killExistingProcess(existingPid);
69
+ }
70
+ else if (!isNaN(existingPid) && !isProcessRunning(existingPid)) {
71
+ logger_1.logger.warn(`⚠️ Stale lock file detected (PID: ${existingPid} has exited). Cleaning up.`);
72
+ }
73
+ // Remove stale lock file
74
+ try {
75
+ fs_1.default.unlinkSync(LOCK_FILE);
76
+ }
77
+ catch { /* ignore */ }
78
+ }
79
+ // Create new lock file
80
+ fs_1.default.writeFileSync(LOCK_FILE, String(process.pid), 'utf-8');
81
+ logger_1.logger.error(`🔒 Lock acquired (PID: ${process.pid})`);
82
+ // Cleanup function
83
+ const releaseLock = () => {
84
+ try {
85
+ if (fs_1.default.existsSync(LOCK_FILE)) {
86
+ const content = fs_1.default.readFileSync(LOCK_FILE, 'utf-8').trim();
87
+ if (parseInt(content, 10) === process.pid) {
88
+ fs_1.default.unlinkSync(LOCK_FILE);
89
+ logger_1.logger.error(`🔓 Lock released (PID: ${process.pid})`);
90
+ }
91
+ }
92
+ }
93
+ catch {
94
+ // Ignore errors during cleanup
95
+ }
96
+ };
97
+ // Auto cleanup on process exit
98
+ process.on('exit', releaseLock);
99
+ process.on('SIGINT', () => {
100
+ releaseLock();
101
+ process.exit(0);
102
+ });
103
+ process.on('SIGTERM', () => {
104
+ releaseLock();
105
+ process.exit(0);
106
+ });
107
+ process.on('uncaughtException', (err) => {
108
+ logger_1.logger.error('Uncaught exception:', err);
109
+ releaseLock();
110
+ process.exit(1);
111
+ });
112
+ return releaseLock;
113
+ }
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.logger = void 0;
4
+ const COLORS = {
5
+ red: '\x1b[31m',
6
+ yellow: '\x1b[33m',
7
+ cyan: '\x1b[36m',
8
+ green: '\x1b[32m',
9
+ magenta: '\x1b[35m',
10
+ dim: '\x1b[2m',
11
+ reset: '\x1b[0m',
12
+ };
13
+ exports.logger = {
14
+ info: (...args) => console.info(`${COLORS.cyan}[INFO]${COLORS.reset}`, ...args),
15
+ warn: (...args) => console.warn(`${COLORS.yellow}[WARN]${COLORS.reset}`, ...args),
16
+ error: (...args) => console.error(`${COLORS.red}[ERROR]${COLORS.reset}`, ...args),
17
+ debug: (...args) => console.debug(`${COLORS.dim}[DEBUG]${COLORS.reset}`, ...args),
18
+ /** Important state transitions — stands out in logs */
19
+ phase: (...args) => console.info(`${COLORS.magenta}[PHASE]${COLORS.reset}`, ...args),
20
+ /** Completion-related events — green for success */
21
+ done: (...args) => console.info(`${COLORS.green}[DONE]${COLORS.reset}`, ...args),
22
+ /** Section divider with optional label for structured output */
23
+ divider: (label) => {
24
+ if (label) {
25
+ const pad = Math.max(4, 50 - label.length - 4);
26
+ console.info(`${COLORS.green}[DONE]${COLORS.reset} ${COLORS.dim}── ${label} ${'─'.repeat(pad)}${COLORS.reset}`);
27
+ }
28
+ else {
29
+ console.info(`${COLORS.green}[DONE]${COLORS.reset} ${COLORS.dim}${'─'.repeat(50)}${COLORS.reset}`);
30
+ }
31
+ },
32
+ };
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LOGO = void 0;
4
+ exports.LOGO = `
5
+ . * .
6
+ /\\___/\\ z Z
7
+ * ( - . - )____________z *
8
+ \\_ __)
9
+ \\_ \\________/ / .
10
+ \\__) \\__)
11
+
12
+ ~ Booting... LazyGravity ~
13
+ `;
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.extractMetadataFromFooter = extractMetadataFromFooter;
4
+ function extractMetadataFromFooter(footerText) {
5
+ const result = {};
6
+ const taskIdMatch = footerText.match(/TaskID:\s*([^\s|]+)/i);
7
+ if (taskIdMatch && taskIdMatch[1]) {
8
+ result.taskId = taskIdMatch[1];
9
+ }
10
+ const dirMatch = footerText.match(/Dir:\s*([^\s|]+)/i);
11
+ if (dirMatch && dirMatch[1]) {
12
+ result.directory = dirMatch[1];
13
+ }
14
+ return result;
15
+ }
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ProcessLogBuffer = void 0;
4
+ const DEFAULT_MAX_CHARS = 3500;
5
+ const DEFAULT_MAX_ENTRIES = 120;
6
+ const DEFAULT_MAX_ENTRY_LENGTH = 260;
7
+ function collapseWhitespace(text) {
8
+ return (text || '').replace(/\r/g, '').replace(/\s+/g, ' ').trim();
9
+ }
10
+ function parseBlocks(raw) {
11
+ const normalized = (raw || '').replace(/\r/g, '').trim();
12
+ if (!normalized)
13
+ return [];
14
+ const blocks = normalized
15
+ .split(/\n{2,}/)
16
+ .map((chunk) => collapseWhitespace(chunk))
17
+ .filter((chunk) => chunk.length > 0);
18
+ if (blocks.length > 0)
19
+ return blocks;
20
+ return normalized
21
+ .split('\n')
22
+ .map((line) => collapseWhitespace(line))
23
+ .filter((line) => line.length > 0);
24
+ }
25
+ function pickEmoji(entry) {
26
+ const lower = entry.toLowerCase();
27
+ if (/^thought for\b/.test(lower) || /^thinking\b/.test(lower))
28
+ return '🧠';
29
+ if (/^initiating\b/.test(lower) || /^starting\b/.test(lower))
30
+ return '🚀';
31
+ if (/^[a-z0-9._-]+\s*\/\s*[a-z0-9._-]+$/i.test(entry))
32
+ return '🛠️';
33
+ if (/^title:\s/.test(lower) && /\surl:\s/.test(lower))
34
+ return '🔎';
35
+ if (/^(json|javascript|typescript|python|bash|sh|html|css|xml|yaml|yml|toml|sql|graphql|markdown|text|plaintext|log)$/i.test(entry))
36
+ return '📦';
37
+ return '•';
38
+ }
39
+ function toDisplayEntry(rawEntry, maxEntryLength) {
40
+ const trimmed = collapseWhitespace(rawEntry);
41
+ if (!trimmed)
42
+ return '';
43
+ const clipped = trimmed.length > maxEntryLength
44
+ ? `${trimmed.slice(0, Math.max(0, maxEntryLength - 3))}...`
45
+ : trimmed;
46
+ return `${pickEmoji(clipped)} ${clipped}`;
47
+ }
48
+ class ProcessLogBuffer {
49
+ maxChars;
50
+ maxEntries;
51
+ maxEntryLength;
52
+ entries = [];
53
+ seen = new Set();
54
+ constructor(options = {}) {
55
+ this.maxChars = options.maxChars ?? DEFAULT_MAX_CHARS;
56
+ this.maxEntries = options.maxEntries ?? DEFAULT_MAX_ENTRIES;
57
+ this.maxEntryLength = options.maxEntryLength ?? DEFAULT_MAX_ENTRY_LENGTH;
58
+ }
59
+ append(raw) {
60
+ const blocks = parseBlocks(raw);
61
+ for (const block of blocks) {
62
+ const display = toDisplayEntry(block, this.maxEntryLength);
63
+ if (!display)
64
+ continue;
65
+ const key = display.toLowerCase();
66
+ if (this.seen.has(key))
67
+ continue;
68
+ this.entries.push(display);
69
+ this.seen.add(key);
70
+ }
71
+ this.trim();
72
+ return this.snapshot();
73
+ }
74
+ snapshot() {
75
+ return this.entries.join('\n');
76
+ }
77
+ trim() {
78
+ while (this.entries.length > this.maxEntries) {
79
+ this.dropOldest();
80
+ }
81
+ while (this.entries.length > 1 && this.snapshot().length > this.maxChars) {
82
+ this.dropOldest();
83
+ }
84
+ if (this.entries.length === 1 && this.entries[0].length > this.maxChars) {
85
+ const only = this.entries[0];
86
+ this.entries[0] = `${only.slice(0, Math.max(0, this.maxChars - 3))}...`;
87
+ this.seen.clear();
88
+ this.seen.add(this.entries[0].toLowerCase());
89
+ }
90
+ }
91
+ dropOldest() {
92
+ const removed = this.entries.shift();
93
+ if (!removed)
94
+ return;
95
+ this.seen.delete(removed.toLowerCase());
96
+ }
97
+ }
98
+ exports.ProcessLogBuffer = ProcessLogBuffer;
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildModeModelLines = buildModeModelLines;
4
+ exports.shouldSkipActivityLog = shouldSkipActivityLog;
5
+ exports.splitForEmbedDescription = splitForEmbedDescription;
6
+ exports.fitForSingleEmbedDescription = fitForSingleEmbedDescription;
7
+ /**
8
+ * Generate mode/model lines for initial status display.
9
+ * Consolidates into a single line if Fast and Plan models are the same.
10
+ */
11
+ function buildModeModelLines(modeName, fastModel, planModel) {
12
+ const lines = [`Current Mode: ${modeName}`];
13
+ if (fastModel.trim().toLowerCase() === planModel.trim().toLowerCase()) {
14
+ lines.push(`Model: ${fastModel}`);
15
+ return lines;
16
+ }
17
+ lines.push(`Fast Model: ${fastModel}`);
18
+ lines.push(`Plan Model: ${planModel}`);
19
+ return lines;
20
+ }
21
+ /**
22
+ * Filter out activity logs that tend to be noise in Discord display.
23
+ */
24
+ function shouldSkipActivityLog(activity, modeName, modelName) {
25
+ const normalized = activity.trim().toLowerCase();
26
+ if (!normalized)
27
+ return true;
28
+ const modeLower = modeName.trim().toLowerCase();
29
+ const modelLower = modelName.trim().toLowerCase();
30
+ if (normalized === modeLower || normalized === modelLower)
31
+ return true;
32
+ if (/^(?:fast|planning|plan|generating\.*|thinking\.*|processing\.*|working\.*)$/.test(normalized)) {
33
+ return true;
34
+ }
35
+ // Single-word logs that tend to be noise (create / ready / pull. etc.)
36
+ if (/^[a-z][a-z0-9_-]{1,24}[.!…]?$/.test(normalized)) {
37
+ return true;
38
+ }
39
+ // Detailed trace for file reading operations (Analyzed....)
40
+ if (/^analyzed/.test(normalized)) {
41
+ return true;
42
+ }
43
+ return false;
44
+ }
45
+ /**
46
+ * Split text into multiple chunks for Embed description.
47
+ */
48
+ function splitForEmbedDescription(text, maxLength = 3500) {
49
+ if (text.length <= maxLength)
50
+ return [text];
51
+ const lines = text.split('\n');
52
+ const chunks = [];
53
+ let current = '';
54
+ const flush = () => {
55
+ if (!current)
56
+ return;
57
+ chunks.push(current);
58
+ current = '';
59
+ };
60
+ for (const line of lines) {
61
+ const candidate = current ? `${current}\n${line}` : line;
62
+ if (candidate.length <= maxLength) {
63
+ current = candidate;
64
+ continue;
65
+ }
66
+ flush();
67
+ if (line.length <= maxLength) {
68
+ current = line;
69
+ continue;
70
+ }
71
+ let cursor = 0;
72
+ while (cursor < line.length) {
73
+ chunks.push(line.slice(cursor, cursor + maxLength));
74
+ cursor += maxLength;
75
+ }
76
+ }
77
+ flush();
78
+ return chunks.length > 0 ? chunks : [''];
79
+ }
80
+ /**
81
+ * Fit text within the limit for a single Embed description.
82
+ * When exceeding the limit, truncate the beginning and prioritize displaying the tail (most recent part).
83
+ */
84
+ function fitForSingleEmbedDescription(text, maxLength = 3500) {
85
+ if (text.length <= maxLength)
86
+ return text;
87
+ const prefix = '... (beginning truncated)\n';
88
+ const tailLength = Math.max(0, maxLength - prefix.length);
89
+ return `${prefix}${text.slice(-tailLength)}`;
90
+ }
package/package.json CHANGED
@@ -1,6 +1,74 @@
1
1
  {
2
- "name": "lazy-gravity",
3
- "version": "0.0.2",
4
- "description": "Reserved package name",
5
- "main": "index.js"
6
- }
2
+ "name": "lazy-gravity",
3
+ "version": "0.0.3",
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.",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "lazy-gravity": "dist/bin/cli.js"
8
+ },
9
+ "files": [
10
+ "dist/",
11
+ "README.md",
12
+ "LICENSE"
13
+ ],
14
+ "engines": {
15
+ "node": ">=18.0.0"
16
+ },
17
+ "directories": {
18
+ "doc": "docs"
19
+ },
20
+ "scripts": {
21
+ "test": "npm run test:unit",
22
+ "test:unit": "jest",
23
+ "test:integration": "jest tests/e2e.bot.test.ts",
24
+ "test:watch": "jest --watch",
25
+ "build": "tsc",
26
+ "start": "ts-node src/index.ts",
27
+ "start:built": "node dist/bin/cli.js",
28
+ "dev": "ts-node-dev --respawn src/index.ts",
29
+ "docs:diagram": "mmdc -i docs/diagrams/architecture.mmd -o docs/images/architecture.svg -b transparent",
30
+ "prepublishOnly": "npm run build && npm run test"
31
+ },
32
+ "keywords": [
33
+ "discord",
34
+ "bot",
35
+ "ai",
36
+ "antigravity",
37
+ "remote-control"
38
+ ],
39
+ "author": "LazyGravity Contributors",
40
+ "license": "MIT",
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "git+https://github.com/tokyoweb3/LazyGravity.git"
44
+ },
45
+ "bugs": {
46
+ "url": "https://github.com/tokyoweb3/LazyGravity/issues"
47
+ },
48
+ "homepage": "https://github.com/tokyoweb3/LazyGravity#readme",
49
+ "dependencies": {
50
+ "better-sqlite3": "^12.6.2",
51
+ "commander": "^14.0.3",
52
+ "discord.js": "^14.25.1",
53
+ "dotenv": "^17.3.1",
54
+ "node-cron": "^4.2.1",
55
+ "ws": "^8.19.0"
56
+ },
57
+ "devDependencies": {
58
+ "@mermaid-js/mermaid-cli": "^11.12.0",
59
+ "@types/better-sqlite3": "^7.6.13",
60
+ "@types/jest": "^30.0.0",
61
+ "@types/node": "^25.3.0",
62
+ "@types/node-cron": "^3.0.11",
63
+ "@types/ws": "^8.18.1",
64
+ "jest": "^30.2.0",
65
+ "jest-environment-jsdom": "^30.2.0",
66
+ "jsdom": "^28.1.0",
67
+ "minimatch": "^10.2.1",
68
+ "ts-jest": "^29.4.6",
69
+ "ts-morph": "^27.0.2",
70
+ "ts-node": "^10.9.2",
71
+ "typescript": "^5.9.3",
72
+ "undici": "^6.23.0"
73
+ }
74
+ }