lazy-gravity 0.0.4 → 0.2.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 (44) hide show
  1. package/README.md +22 -7
  2. package/dist/bin/cli.js +18 -18
  3. package/dist/bin/commands/doctor.js +25 -19
  4. package/dist/bin/commands/start.js +25 -2
  5. package/dist/bot/index.js +445 -126
  6. package/dist/commands/joinCommandHandler.js +302 -0
  7. package/dist/commands/joinDetachCommandHandler.js +285 -0
  8. package/dist/commands/registerSlashCommands.js +40 -0
  9. package/dist/commands/workspaceCommandHandler.js +17 -28
  10. package/dist/database/chatSessionRepository.js +10 -0
  11. package/dist/database/userPreferenceRepository.js +72 -0
  12. package/dist/events/interactionCreateHandler.js +338 -30
  13. package/dist/events/messageCreateHandler.js +161 -47
  14. package/dist/services/antigravityLauncher.js +4 -3
  15. package/dist/services/approvalDetector.js +7 -0
  16. package/dist/services/assistantDomExtractor.js +339 -0
  17. package/dist/services/cdpBridgeManager.js +323 -39
  18. package/dist/services/cdpConnectionPool.js +117 -33
  19. package/dist/services/cdpService.js +149 -53
  20. package/dist/services/chatSessionService.js +229 -8
  21. package/dist/services/errorPopupDetector.js +271 -0
  22. package/dist/services/planningDetector.js +318 -0
  23. package/dist/services/responseMonitor.js +308 -70
  24. package/dist/services/retryStore.js +46 -0
  25. package/dist/services/updateCheckService.js +147 -0
  26. package/dist/services/userMessageDetector.js +221 -0
  27. package/dist/ui/buttonUtils.js +33 -0
  28. package/dist/ui/modeUi.js +11 -1
  29. package/dist/ui/modelsUi.js +24 -13
  30. package/dist/ui/outputUi.js +30 -0
  31. package/dist/ui/projectListUi.js +83 -0
  32. package/dist/ui/sessionPickerUi.js +48 -0
  33. package/dist/utils/antigravityPaths.js +94 -0
  34. package/dist/utils/configLoader.js +18 -0
  35. package/dist/utils/discordButtonUtils.js +33 -0
  36. package/dist/utils/discordFormatter.js +149 -16
  37. package/dist/utils/htmlToDiscordMarkdown.js +184 -0
  38. package/dist/utils/logBuffer.js +47 -0
  39. package/dist/utils/logFileTransport.js +147 -0
  40. package/dist/utils/logger.js +86 -21
  41. package/dist/utils/pathUtils.js +57 -0
  42. package/dist/utils/plainTextFormatter.js +70 -0
  43. package/dist/utils/processLogBuffer.js +4 -0
  44. package/package.json +4 -4
@@ -0,0 +1,147 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.LogFileTransportImpl = void 0;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const os = __importStar(require("os"));
40
+ const DEFAULT_LOG_DIR = path.join(os.homedir(), '.lazy-gravity', 'logs');
41
+ const LOG_FILE_PREFIX = 'lazy-gravity-';
42
+ const LOG_FILE_EXT = '.log';
43
+ /** Maximum number of log files to keep (default 14 days). */
44
+ const DEFAULT_MAX_FILES = 14;
45
+ /** Maximum size in bytes for a single log file (default 10 MB). */
46
+ const DEFAULT_MAX_SIZE_BYTES = 10 * 1024 * 1024;
47
+ function formatDate(date) {
48
+ const y = date.getFullYear();
49
+ const m = String(date.getMonth() + 1).padStart(2, '0');
50
+ const d = String(date.getDate()).padStart(2, '0');
51
+ return `${y}-${m}-${d}`;
52
+ }
53
+ function buildFileName(dateStr) {
54
+ return `${LOG_FILE_PREFIX}${dateStr}${LOG_FILE_EXT}`;
55
+ }
56
+ class LogFileTransportImpl {
57
+ logDir;
58
+ currentDate;
59
+ currentFilePath;
60
+ constructor(logDir = DEFAULT_LOG_DIR) {
61
+ this.logDir = logDir;
62
+ this.currentDate = formatDate(new Date());
63
+ this.currentFilePath = path.join(this.logDir, buildFileName(this.currentDate));
64
+ this.ensureDir();
65
+ this.scheduleCleanup();
66
+ }
67
+ write(level, timestamp, message) {
68
+ this.rollIfNeeded();
69
+ const line = `${timestamp} [${level}] ${message}\n`;
70
+ try {
71
+ fs.appendFileSync(this.currentFilePath, line, 'utf-8');
72
+ }
73
+ catch {
74
+ // Silently ignore write errors to avoid crashing the bot
75
+ }
76
+ }
77
+ /**
78
+ * Remove old log files that exceed maxFiles count or maxSizeBytes per file.
79
+ * Runs asynchronously to avoid blocking startup.
80
+ */
81
+ cleanup(maxFiles = DEFAULT_MAX_FILES, maxSizeBytes = DEFAULT_MAX_SIZE_BYTES) {
82
+ setImmediate(() => {
83
+ try {
84
+ this.cleanupSync(maxFiles, maxSizeBytes);
85
+ }
86
+ catch {
87
+ // Silently ignore cleanup errors
88
+ }
89
+ });
90
+ }
91
+ /** Synchronous cleanup for testability. */
92
+ cleanupSync(maxFiles = DEFAULT_MAX_FILES, maxSizeBytes = DEFAULT_MAX_SIZE_BYTES) {
93
+ if (!fs.existsSync(this.logDir))
94
+ return;
95
+ const entries = fs
96
+ .readdirSync(this.logDir)
97
+ .filter((f) => f.startsWith(LOG_FILE_PREFIX) && f.endsWith(LOG_FILE_EXT))
98
+ .sort(); // chronological order (YYYY-MM-DD sorts naturally)
99
+ // Remove files exceeding size limit
100
+ for (const entry of entries) {
101
+ const filePath = path.join(this.logDir, entry);
102
+ try {
103
+ const stat = fs.statSync(filePath);
104
+ if (stat.size > maxSizeBytes) {
105
+ fs.unlinkSync(filePath);
106
+ }
107
+ }
108
+ catch {
109
+ // Ignore stat/unlink errors
110
+ }
111
+ }
112
+ // Re-read after size-based cleanup
113
+ const remaining = fs
114
+ .readdirSync(this.logDir)
115
+ .filter((f) => f.startsWith(LOG_FILE_PREFIX) && f.endsWith(LOG_FILE_EXT))
116
+ .sort();
117
+ // Remove oldest files if count exceeds limit
118
+ const excess = remaining.length - maxFiles;
119
+ if (excess > 0) {
120
+ for (let i = 0; i < excess; i++) {
121
+ const filePath = path.join(this.logDir, remaining[i]);
122
+ try {
123
+ fs.unlinkSync(filePath);
124
+ }
125
+ catch {
126
+ // Ignore unlink errors
127
+ }
128
+ }
129
+ }
130
+ }
131
+ ensureDir() {
132
+ if (!fs.existsSync(this.logDir)) {
133
+ fs.mkdirSync(this.logDir, { recursive: true });
134
+ }
135
+ }
136
+ rollIfNeeded() {
137
+ const today = formatDate(new Date());
138
+ if (today !== this.currentDate) {
139
+ this.currentDate = today;
140
+ this.currentFilePath = path.join(this.logDir, buildFileName(today));
141
+ }
142
+ }
143
+ scheduleCleanup() {
144
+ this.cleanup();
145
+ }
146
+ }
147
+ exports.LogFileTransportImpl = LogFileTransportImpl;
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.logger = void 0;
4
- const COLORS = {
3
+ exports.logger = exports.COLORS = void 0;
4
+ exports.createLogger = createLogger;
5
+ const logBuffer_1 = require("./logBuffer");
6
+ exports.COLORS = {
5
7
  red: '\x1b[31m',
6
8
  yellow: '\x1b[33m',
7
9
  cyan: '\x1b[36m',
@@ -10,23 +12,86 @@ const COLORS = {
10
12
  dim: '\x1b[2m',
11
13
  reset: '\x1b[0m',
12
14
  };
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
- },
15
+ const LEVEL_PRIORITY = {
16
+ debug: 0,
17
+ info: 1,
18
+ warn: 2,
19
+ error: 3,
20
+ none: 4,
32
21
  };
22
+ const getTimestamp = () => {
23
+ const now = new Date();
24
+ const timeString = now.toLocaleTimeString('ja-JP', { hour12: false });
25
+ return `${exports.COLORS.dim}[${timeString}]${exports.COLORS.reset}`;
26
+ };
27
+ function createLogger(initialLevel = 'info') {
28
+ let currentLevel = initialLevel;
29
+ function shouldLog(methodLevel) {
30
+ return LEVEL_PRIORITY[methodLevel] >= LEVEL_PRIORITY[currentLevel];
31
+ }
32
+ return {
33
+ info(...args) {
34
+ if (shouldLog('info')) {
35
+ const formatted = `${getTimestamp()} ${exports.COLORS.cyan}[INFO]${exports.COLORS.reset}`;
36
+ console.info(formatted, ...args);
37
+ logBuffer_1.logBuffer.append('info', `[INFO] ${args.join(' ')}`);
38
+ }
39
+ },
40
+ warn(...args) {
41
+ if (shouldLog('warn')) {
42
+ const formatted = `${getTimestamp()} ${exports.COLORS.yellow}[WARN]${exports.COLORS.reset}`;
43
+ console.warn(formatted, ...args);
44
+ logBuffer_1.logBuffer.append('warn', `[WARN] ${args.join(' ')}`);
45
+ }
46
+ },
47
+ error(...args) {
48
+ if (shouldLog('error')) {
49
+ const formatted = `${getTimestamp()} ${exports.COLORS.red}[ERROR]${exports.COLORS.reset}`;
50
+ console.error(formatted, ...args);
51
+ logBuffer_1.logBuffer.append('error', `[ERROR] ${args.join(' ')}`);
52
+ }
53
+ },
54
+ debug(...args) {
55
+ if (shouldLog('debug')) {
56
+ const formatted = `${getTimestamp()} ${exports.COLORS.dim}[DEBUG]${exports.COLORS.reset}`;
57
+ console.debug(formatted, ...args);
58
+ logBuffer_1.logBuffer.append('debug', `[DEBUG] ${args.join(' ')}`);
59
+ }
60
+ },
61
+ /** Important state transitions - stands out in logs */
62
+ phase(...args) {
63
+ if (shouldLog('info')) {
64
+ const formatted = `${getTimestamp()} ${exports.COLORS.magenta}[PHASE]${exports.COLORS.reset}`;
65
+ console.info(formatted, ...args);
66
+ logBuffer_1.logBuffer.append('info', `[PHASE] ${args.join(' ')}`);
67
+ }
68
+ },
69
+ /** Completion-related events - green for success */
70
+ done(...args) {
71
+ if (shouldLog('info')) {
72
+ const formatted = `${getTimestamp()} ${exports.COLORS.green}[DONE]${exports.COLORS.reset}`;
73
+ console.info(formatted, ...args);
74
+ logBuffer_1.logBuffer.append('info', `[DONE] ${args.join(' ')}`);
75
+ }
76
+ },
77
+ /** Section divider with optional label for structured output */
78
+ divider(label) {
79
+ if (shouldLog('info')) {
80
+ if (label) {
81
+ const pad = Math.max(4, 50 - label.length - 4);
82
+ console.info(`${exports.COLORS.green}[DONE]${exports.COLORS.reset} ${exports.COLORS.dim}── ${label} ${'─'.repeat(pad)}${exports.COLORS.reset}`);
83
+ }
84
+ else {
85
+ console.info(`${exports.COLORS.green}[DONE]${exports.COLORS.reset} ${exports.COLORS.dim}${'─'.repeat(50)}${exports.COLORS.reset}`);
86
+ }
87
+ }
88
+ },
89
+ setLogLevel(level) {
90
+ currentLevel = level;
91
+ },
92
+ getLogLevel() {
93
+ return currentLevel;
94
+ },
95
+ };
96
+ }
97
+ exports.logger = createLogger('info');
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getAntigravityCliPath = getAntigravityCliPath;
4
+ exports.extractProjectNameFromPath = extractProjectNameFromPath;
5
+ exports.getAntigravityCdpHint = getAntigravityCdpHint;
6
+ /**
7
+ * Helper to resolve the correct Antigravity CLI executable path based on the operating system
8
+ * and environment variables.
9
+ *
10
+ * Precedence:
11
+ * 1. process.env.ANTIGRAVITY_PATH (Explicit override)
12
+ * 2. OS-specific default paths (Mac: /Applications/..., Windows: %LOCALAPPDATA%\..., Linux: 'antigravity')
13
+ */
14
+ function getAntigravityCliPath() {
15
+ // Allow user to set explicit path via ANTIGRAVITY_PATH (especially useful for Linux AppImages)
16
+ if (process.env.ANTIGRAVITY_PATH) {
17
+ return process.env.ANTIGRAVITY_PATH;
18
+ }
19
+ if (process.platform === 'darwin') {
20
+ return '/Applications/Antigravity.app/Contents/Resources/app/bin/antigravity';
21
+ }
22
+ if (process.platform === 'win32') {
23
+ const localAppData = process.env.LOCALAPPDATA;
24
+ if (localAppData) {
25
+ return `${localAppData}\\Programs\\Antigravity\\Antigravity.exe`;
26
+ }
27
+ return 'Antigravity.exe'; // Fallback if LOCALAPPDATA is undefined
28
+ }
29
+ // Default for Linux or any unknown OS, assuming 'antigravity' is in the system PATH
30
+ return 'antigravity';
31
+ }
32
+ /**
33
+ * Helper to extract the project name from a full workspace path.
34
+ * Handles both Windows (backslash) and POSIX (forward slash) paths.
35
+ *
36
+ * @param workspacePath The full path to the workspace directory
37
+ * @returns The final folder name
38
+ */
39
+ function extractProjectNameFromPath(workspacePath) {
40
+ return workspacePath.split(/[/\\]/).filter(Boolean).pop() || '';
41
+ }
42
+ /**
43
+ * Get a platform-appropriate hint for starting Antigravity with CDP.
44
+ *
45
+ * Used in user-facing messages (Discord embeds, CLI doctor, logs).
46
+ */
47
+ function getAntigravityCdpHint(port = 9222) {
48
+ const APP_NAME = 'Antigravity';
49
+ switch (process.platform) {
50
+ case 'darwin':
51
+ return `open -a ${APP_NAME} --args --remote-debugging-port=${port}`;
52
+ case 'win32':
53
+ return `${APP_NAME}.exe --remote-debugging-port=${port}`;
54
+ default:
55
+ return `${APP_NAME.toLowerCase()} --remote-debugging-port=${port}`;
56
+ }
57
+ }
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ /**
3
+ * Format embed-like data as plain text for Discord messages.
4
+ * Used when user has output format set to 'plain'.
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.splitPlainText = splitPlainText;
8
+ exports.formatAsPlainText = formatAsPlainText;
9
+ /**
10
+ * Split text into chunks at line boundaries, respecting Discord's 2000 char limit.
11
+ * Uses maxLength of 1900 to leave room for formatting overhead.
12
+ */
13
+ function splitPlainText(text, maxLength = 1900) {
14
+ if (text.length <= maxLength)
15
+ return [text];
16
+ const lines = text.split('\n');
17
+ const chunks = [];
18
+ let current = '';
19
+ const flush = () => {
20
+ if (!current)
21
+ return;
22
+ chunks.push(current);
23
+ current = '';
24
+ };
25
+ for (const line of lines) {
26
+ const candidate = current ? `${current}\n${line}` : line;
27
+ if (candidate.length <= maxLength) {
28
+ current = candidate;
29
+ continue;
30
+ }
31
+ flush();
32
+ if (line.length <= maxLength) {
33
+ current = line;
34
+ continue;
35
+ }
36
+ // Line itself exceeds maxLength — hard-split
37
+ let cursor = 0;
38
+ while (cursor < line.length) {
39
+ chunks.push(line.slice(cursor, cursor + maxLength));
40
+ cursor += maxLength;
41
+ }
42
+ }
43
+ flush();
44
+ return chunks.length > 0 ? chunks : [''];
45
+ }
46
+ /**
47
+ * Convert embed-style data to a plain text string array (chunked for Discord).
48
+ * Format: **{title}**\n{description}\n\n{field.name}: {field.value}\n...\n_{footer}_
49
+ */
50
+ function formatAsPlainText(input) {
51
+ const parts = [];
52
+ if (input.title) {
53
+ parts.push(`**${input.title}**`);
54
+ }
55
+ if (input.description) {
56
+ parts.push(input.description);
57
+ }
58
+ if (input.fields && input.fields.length > 0) {
59
+ parts.push(''); // blank line separator
60
+ for (const field of input.fields) {
61
+ parts.push(`**${field.name}:** ${field.value}`);
62
+ }
63
+ }
64
+ if (input.footerText) {
65
+ parts.push(''); // blank line separator
66
+ parts.push(`_${input.footerText}_`);
67
+ }
68
+ const text = parts.join('\n');
69
+ return splitPlainText(text);
70
+ }
@@ -30,6 +30,10 @@ function pickEmoji(entry) {
30
30
  return '🚀';
31
31
  if (/^[a-z0-9._-]+\s*\/\s*[a-z0-9._-]+$/i.test(entry))
32
32
  return '🛠️';
33
+ if (/^(?:analy[sz]ed|read|wrote|created|updated|deleted|built|compiled|installed|resolved|downloaded|connected|fetched)\b/i.test(entry))
34
+ return '📄';
35
+ if (/^(?:analy[sz]ing|reading|writing|running|searching|fetching|checking|scanning|creating|updating|deleting|building|compiling|deploying|parsing|resolving|downloading|uploading|connecting|installing|executing|testing|debugging|processing|loading)\b/i.test(entry))
36
+ return '🔍';
33
37
  if (/^title:\s/.test(lower) && /\surl:\s/.test(lower))
34
38
  return '🔎';
35
39
  if (/^(json|javascript|typescript|python|bash|sh|html|css|xml|yaml|yml|toml|sql|graphql|markdown|text|plaintext|log)$/i.test(entry))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lazy-gravity",
3
- "version": "0.0.4",
3
+ "version": "0.2.0",
4
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
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -23,9 +23,9 @@
23
23
  "test:integration": "jest tests/e2e.bot.test.ts",
24
24
  "test:watch": "jest --watch",
25
25
  "build": "tsc",
26
- "start": "ts-node src/index.ts",
26
+ "start": "ts-node src/bin/cli.ts",
27
27
  "start:built": "node dist/bin/cli.js",
28
- "dev": "ts-node-dev --respawn src/index.ts",
28
+ "dev": "ts-node-dev --respawn src/bin/cli.ts",
29
29
  "docs:diagram": "mmdc -i docs/diagrams/architecture.mmd -o docs/images/architecture.svg -b transparent",
30
30
  "prepublishOnly": "npm run build && npm run test"
31
31
  },
@@ -69,6 +69,6 @@
69
69
  "ts-morph": "^27.0.2",
70
70
  "ts-node": "^10.9.2",
71
71
  "typescript": "^5.9.3",
72
- "undici": "^6.23.0"
72
+ "undici": "^7.22.0"
73
73
  }
74
74
  }