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,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CDP_PORTS = void 0;
4
+ /** CDP port list scanned for Antigravity connections */
5
+ exports.CDP_PORTS = [9222, 9223, 9333, 9444, 9555, 9666];
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveResponseDeliveryMode = resolveResponseDeliveryMode;
4
+ exports.loadConfig = loadConfig;
5
+ const configLoader_1 = require("./configLoader");
6
+ /**
7
+ * Response delivery is fixed to 'stream'.
8
+ * Env vars are read for backward compatibility but the value is always 'stream'.
9
+ */
10
+ function resolveResponseDeliveryMode() {
11
+ return 'stream';
12
+ }
13
+ /**
14
+ * Load application config.
15
+ * Delegates to ConfigLoader which resolves:
16
+ * env vars > ~/.lazy-gravity/config.json > .env > defaults
17
+ */
18
+ function loadConfig() {
19
+ return configLoader_1.ConfigLoader.load();
20
+ }
@@ -0,0 +1,160 @@
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.ConfigLoader = void 0;
37
+ const fs = __importStar(require("fs"));
38
+ const os = __importStar(require("os"));
39
+ const path = __importStar(require("path"));
40
+ const dotenv = __importStar(require("dotenv"));
41
+ // Load .env at module init time (same as the original config.ts behavior).
42
+ // dotenv will NOT override already-set env vars by default.
43
+ dotenv.config();
44
+ const CONFIG_DIR_NAME = '.lazy-gravity';
45
+ const CONFIG_FILE_NAME = 'config.json';
46
+ const DEFAULT_DB_NAME = 'antigravity.db';
47
+ // ---------------------------------------------------------------------------
48
+ // Pure helpers (no side-effects)
49
+ // ---------------------------------------------------------------------------
50
+ function getConfigDir() {
51
+ return path.join(os.homedir(), CONFIG_DIR_NAME);
52
+ }
53
+ function getConfigFilePath() {
54
+ return path.join(getConfigDir(), CONFIG_FILE_NAME);
55
+ }
56
+ function getDefaultDbPath() {
57
+ return path.join(getConfigDir(), DEFAULT_DB_NAME);
58
+ }
59
+ /** Expand leading `~` or `~/` to the user's home directory. */
60
+ function expandTilde(raw) {
61
+ if (raw === '~')
62
+ return os.homedir();
63
+ if (raw.startsWith('~/'))
64
+ return path.join(os.homedir(), raw.slice(2));
65
+ return raw;
66
+ }
67
+ function readPersistedConfig(filePath) {
68
+ if (!fs.existsSync(filePath))
69
+ return {};
70
+ const raw = fs.readFileSync(filePath, 'utf-8');
71
+ return JSON.parse(raw);
72
+ }
73
+ /**
74
+ * Merge layers with priority: env vars > persisted config > defaults.
75
+ * Returns a fresh AppConfig object (immutable pattern).
76
+ */
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');
89
+ }
90
+ const defaultDir = path.join(os.homedir(), 'Code');
91
+ const rawDir = process.env.WORKSPACE_BASE_DIR ?? persisted.workspaceBaseDir ?? defaultDir;
92
+ const workspaceBaseDir = expandTilde(rawDir);
93
+ const guildId = process.env.GUILD_ID ?? persisted.guildId ?? undefined;
94
+ const autoApproveFileEdits = resolveBoolean(process.env.AUTO_APPROVE_FILE_EDITS, persisted.autoApproveFileEdits, false);
95
+ return {
96
+ discordToken: token,
97
+ clientId,
98
+ guildId,
99
+ allowedUserIds,
100
+ workspaceBaseDir,
101
+ autoApproveFileEdits,
102
+ };
103
+ }
104
+ function resolveAllowedUserIds(persisted) {
105
+ const envValue = process.env.ALLOWED_USER_IDS;
106
+ if (envValue) {
107
+ return envValue
108
+ .split(',')
109
+ .map((id) => id.trim())
110
+ .filter((id) => id.length > 0);
111
+ }
112
+ if (persisted.allowedUserIds && persisted.allowedUserIds.length > 0) {
113
+ return [...persisted.allowedUserIds];
114
+ }
115
+ return [];
116
+ }
117
+ function resolveBoolean(envValue, persistedValue, defaultValue) {
118
+ if (envValue !== undefined)
119
+ return envValue.toLowerCase() === 'true';
120
+ if (persistedValue !== undefined)
121
+ return persistedValue;
122
+ return defaultValue;
123
+ }
124
+ // ---------------------------------------------------------------------------
125
+ // Public API (ConfigLoader namespace)
126
+ // ---------------------------------------------------------------------------
127
+ exports.ConfigLoader = {
128
+ /** Return the config directory path (~/.lazy-gravity/). */
129
+ getConfigDir,
130
+ /** Return the full path to config.json. */
131
+ getConfigFilePath,
132
+ /** Return the default database file path (~/.lazy-gravity/antigravity.db). */
133
+ getDefaultDbPath,
134
+ /** Check whether ~/.lazy-gravity/config.json exists on disk. */
135
+ configExists() {
136
+ return fs.existsSync(getConfigFilePath());
137
+ },
138
+ /**
139
+ * Load config using resolution order:
140
+ * env vars > ~/.lazy-gravity/config.json > .env > defaults
141
+ */
142
+ load(persistedOverride) {
143
+ const persisted = persistedOverride ?? readPersistedConfig(getConfigFilePath());
144
+ return mergeConfig(persisted);
145
+ },
146
+ /**
147
+ * Persist the given config to ~/.lazy-gravity/config.json.
148
+ * Creates the directory if it doesn't exist.
149
+ */
150
+ save(config) {
151
+ const dir = getConfigDir();
152
+ if (!fs.existsSync(dir)) {
153
+ fs.mkdirSync(dir, { recursive: true });
154
+ }
155
+ // Merge with existing persisted config so partial saves are additive
156
+ const existing = readPersistedConfig(getConfigFilePath());
157
+ const merged = { ...existing, ...config };
158
+ fs.writeFileSync(getConfigFilePath(), JSON.stringify(merged, null, 2) + '\n', 'utf-8');
159
+ },
160
+ };
@@ -0,0 +1,167 @@
1
+ "use strict";
2
+ /**
3
+ * Lean discordFormatter using Set-based UI chrome literal matching + small regex patterns.
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.formatForDiscord = formatForDiscord;
7
+ exports.splitOutputAndLogs = splitOutputAndLogs;
8
+ exports.sanitizeActivityLines = sanitizeActivityLines;
9
+ /** Known UI chrome literal strings (exact match after trim + lowercase) */
10
+ const UI_CHROME_LITERALS = new Set([
11
+ 'analyzed',
12
+ 'analyzing',
13
+ 'reading',
14
+ 'writing',
15
+ 'running',
16
+ 'searching',
17
+ 'planning',
18
+ 'thinking',
19
+ 'thinking...',
20
+ 'processing',
21
+ 'loading',
22
+ 'executing',
23
+ 'testing',
24
+ 'debugging',
25
+ 'read',
26
+ 'wrote',
27
+ 'ran',
28
+ 'good',
29
+ 'bad',
30
+ 'good bad',
31
+ 'show details',
32
+ 'json',
33
+ 'css',
34
+ 'html',
35
+ 'xml',
36
+ 'yaml',
37
+ 'toml',
38
+ 'sql',
39
+ 'graphql',
40
+ ]);
41
+ /** Small regex patterns for UI chrome lines */
42
+ const UI_CHROME_REGEXES = [
43
+ /^[+-]\d+$/, // line number prefixes: +5, -3
44
+ /^\d+\s*chars?$/i, // char counts: "120 chars"
45
+ /^line\s+\d+/i, // editor positions: "line 5"
46
+ /^col\s+\d+/i, // editor positions: "col 10"
47
+ /^tool call:/i, // tool call traces
48
+ /^tool result:/i, // tool result traces
49
+ /^calling tool\b/i, // tool invocations
50
+ /^tool response\b/i, // tool responses
51
+ /^mcp\b/i, // MCP traces
52
+ /^thought for\s*<?\d+/i, // thought traces
53
+ /^show details$/i, // UI button text
54
+ /^[a-z0-9._-]+\s*\/\s*[a-z0-9._-]+$/i, // MCP server/tool format: "jina-mcp-server / search_web"
55
+ /^full output written to\b/i, // tool result redirect: "Full output written to ..."
56
+ /^output\.[a-z0-9._-]+(?:#l\d+(?:-\d+)?)?$/i, // output file ref: "output.txt#L1-131"
57
+ ];
58
+ /**
59
+ * Check if a line is UI chrome (not real assistant output).
60
+ */
61
+ function isUiChromeLine(line) {
62
+ const trimmed = (line || '').trim();
63
+ if (!trimmed)
64
+ return false;
65
+ const lower = trimmed.toLowerCase();
66
+ if (UI_CHROME_LITERALS.has(lower))
67
+ return true;
68
+ for (const re of UI_CHROME_REGEXES) {
69
+ if (re.test(trimmed))
70
+ return true;
71
+ }
72
+ return false;
73
+ }
74
+ /**
75
+ * Format text for Discord Embed display.
76
+ * Wraps table lines and tree lines in code blocks.
77
+ */
78
+ function formatForDiscord(text) {
79
+ const lines = text.split('\n');
80
+ const result = [];
81
+ let inSpecialBlock = false;
82
+ for (let i = 0; i < lines.length; i++) {
83
+ const line = lines[i];
84
+ const trimmed = line.trim();
85
+ const isTableLine = (trimmed.startsWith('|') && trimmed.endsWith('|') && trimmed.length > 2) ||
86
+ /^\|[\s\-:]+\|/.test(trimmed);
87
+ const isTreeLine = /[├└│┌┐┘┤┬┴┼]/.test(line) ||
88
+ /^\s*[│├└]\s*──/.test(line) ||
89
+ /^\s*\|.*──/.test(line);
90
+ const isSpecialLine = isTableLine || isTreeLine;
91
+ if (isSpecialLine && !inSpecialBlock) {
92
+ result.push('```');
93
+ inSpecialBlock = true;
94
+ result.push(line);
95
+ }
96
+ else if (isSpecialLine && inSpecialBlock) {
97
+ result.push(line);
98
+ }
99
+ else if (!isSpecialLine && inSpecialBlock) {
100
+ result.push('```');
101
+ inSpecialBlock = false;
102
+ result.push(line);
103
+ }
104
+ else {
105
+ result.push(line);
106
+ }
107
+ }
108
+ if (inSpecialBlock) {
109
+ result.push('```');
110
+ }
111
+ return result.join('\n');
112
+ }
113
+ /**
114
+ * Split raw text into output (real content) and logs (UI chrome).
115
+ * Code blocks are always preserved as output.
116
+ */
117
+ function splitOutputAndLogs(rawText) {
118
+ const normalized = (rawText || '').replace(/\r/g, '');
119
+ if (!normalized.trim()) {
120
+ return { output: '', logs: '' };
121
+ }
122
+ const outputLines = [];
123
+ const logLines = [];
124
+ let inCodeBlock = false;
125
+ const lines = normalized.split('\n');
126
+ for (const line of lines) {
127
+ const trimmed = (line || '').trim();
128
+ if (trimmed.startsWith('```')) {
129
+ inCodeBlock = !inCodeBlock;
130
+ outputLines.push(line);
131
+ continue;
132
+ }
133
+ if (inCodeBlock) {
134
+ outputLines.push(line);
135
+ continue;
136
+ }
137
+ if (!trimmed) {
138
+ outputLines.push(line);
139
+ continue;
140
+ }
141
+ if (isUiChromeLine(trimmed)) {
142
+ logLines.push(trimmed);
143
+ }
144
+ else {
145
+ outputLines.push(line);
146
+ }
147
+ }
148
+ const normalizeText = (text) => text
149
+ .replace(/\n{3,}/g, '\n\n')
150
+ .trim();
151
+ return {
152
+ output: normalizeText(outputLines.join('\n')),
153
+ logs: normalizeText(logLines.join('\n')),
154
+ };
155
+ }
156
+ /**
157
+ * Remove UI chrome lines from activity text.
158
+ */
159
+ function sanitizeActivityLines(raw) {
160
+ const lines = (raw || '')
161
+ .replace(/\r/g, '')
162
+ .split('\n')
163
+ .map((line) => line.trim())
164
+ .filter((line) => line.length > 0);
165
+ const kept = lines.filter((line) => !isUiChromeLine(line));
166
+ return Array.from(new Set(kept)).join('\n');
167
+ }
@@ -0,0 +1,77 @@
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.initI18n = initI18n;
37
+ exports.t = t;
38
+ const logger_1 = require("./logger");
39
+ const fs = __importStar(require("fs"));
40
+ const path = __importStar(require("path"));
41
+ let currentLanguage = 'en';
42
+ let translations = {};
43
+ function initI18n(lang = 'en') {
44
+ currentLanguage = lang;
45
+ loadTranslations();
46
+ }
47
+ function loadTranslations() {
48
+ try {
49
+ const enPath = path.join(__dirname, '../../locales/en.json');
50
+ const jaPath = path.join(__dirname, '../../locales/ja.json');
51
+ if (fs.existsSync(enPath)) {
52
+ translations['en'] = JSON.parse(fs.readFileSync(enPath, 'utf8'));
53
+ }
54
+ else {
55
+ translations['en'] = {};
56
+ }
57
+ if (fs.existsSync(jaPath)) {
58
+ translations['ja'] = JSON.parse(fs.readFileSync(jaPath, 'utf8'));
59
+ }
60
+ else {
61
+ translations['ja'] = {};
62
+ }
63
+ }
64
+ catch (error) {
65
+ logger_1.logger.error('Failed to load translations:', error);
66
+ }
67
+ }
68
+ function t(key, variables) {
69
+ const langDict = translations[currentLanguage] || translations['en'];
70
+ let text = langDict?.[key] || translations['en']?.[key] || key;
71
+ if (variables) {
72
+ for (const [vKey, vValue] of Object.entries(variables)) {
73
+ text = text.replace(new RegExp(`{{${vKey}}}`, 'g'), String(vValue));
74
+ }
75
+ }
76
+ return text;
77
+ }
@@ -0,0 +1,154 @@
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.isImageAttachment = isImageAttachment;
37
+ exports.mimeTypeToExtension = mimeTypeToExtension;
38
+ exports.sanitizeFileName = sanitizeFileName;
39
+ exports.buildPromptWithAttachmentUrls = buildPromptWithAttachmentUrls;
40
+ exports.downloadInboundImageAttachments = downloadInboundImageAttachments;
41
+ exports.cleanupInboundImageAttachments = cleanupInboundImageAttachments;
42
+ exports.toDiscordAttachment = toDiscordAttachment;
43
+ const discord_js_1 = require("discord.js");
44
+ const fs = __importStar(require("fs/promises"));
45
+ const os = __importStar(require("os"));
46
+ const path = __importStar(require("path"));
47
+ const logger_1 = require("./logger");
48
+ const MAX_INBOUND_IMAGE_ATTACHMENTS = 4;
49
+ const IMAGE_EXT_PATTERN = /\.(png|jpe?g|webp|gif|bmp)$/i;
50
+ const TEMP_IMAGE_DIR = path.join(os.tmpdir(), 'lazy-gravity-images');
51
+ function isImageAttachment(contentType, fileName) {
52
+ if ((contentType || '').toLowerCase().startsWith('image/'))
53
+ return true;
54
+ return IMAGE_EXT_PATTERN.test(fileName || '');
55
+ }
56
+ function mimeTypeToExtension(mimeType) {
57
+ const normalized = (mimeType || '').toLowerCase();
58
+ if (normalized.includes('jpeg') || normalized.includes('jpg'))
59
+ return 'jpg';
60
+ if (normalized.includes('webp'))
61
+ return 'webp';
62
+ if (normalized.includes('gif'))
63
+ return 'gif';
64
+ if (normalized.includes('bmp'))
65
+ return 'bmp';
66
+ return 'png';
67
+ }
68
+ function sanitizeFileName(fileName) {
69
+ const sanitized = fileName.replace(/[^a-zA-Z0-9._-]/g, '-').replace(/-+/g, '-').replace(/^-+|-+$/g, '');
70
+ return sanitized || `image-${Date.now()}.png`;
71
+ }
72
+ function buildPromptWithAttachmentUrls(prompt, attachments) {
73
+ const base = prompt.trim() || 'Please review the attached images and respond accordingly.';
74
+ if (attachments.length === 0)
75
+ return base;
76
+ const lines = attachments.map((image, index) => `${index + 1}. ${image.name}\nURL: ${image.url}`);
77
+ return `${base}\n\n[Discord Attached Images]\n${lines.join('\n\n')}\n\nPlease refer to the attached images above in your response.`;
78
+ }
79
+ async function downloadInboundImageAttachments(message) {
80
+ const allAttachments = Array.from(message.attachments.values());
81
+ const imageAttachments = allAttachments
82
+ .filter((attachment) => isImageAttachment(attachment.contentType, attachment.name))
83
+ .slice(0, MAX_INBOUND_IMAGE_ATTACHMENTS);
84
+ if (imageAttachments.length === 0)
85
+ return [];
86
+ await fs.mkdir(TEMP_IMAGE_DIR, { recursive: true });
87
+ const downloaded = [];
88
+ let index = 0;
89
+ for (const attachment of imageAttachments) {
90
+ try {
91
+ const response = await fetch(attachment.url);
92
+ if (!response.ok) {
93
+ logger_1.logger.warn(`[ImageBridge] Attachment image download failed (id=${attachment.id || 'unknown'}, status=${response.status})`);
94
+ continue;
95
+ }
96
+ const bytes = Buffer.from(await response.arrayBuffer());
97
+ if (bytes.length === 0)
98
+ continue;
99
+ const mimeType = attachment.contentType || 'image/png';
100
+ const hasExt = IMAGE_EXT_PATTERN.test(attachment.name || '');
101
+ const ext = mimeTypeToExtension(mimeType);
102
+ const originalName = sanitizeFileName(attachment.name || `discord-image-${index + 1}.${ext}`);
103
+ const name = hasExt ? originalName : `${originalName}.${ext}`;
104
+ const localPath = path.join(TEMP_IMAGE_DIR, `${Date.now()}-${message.id}-${index}-${name}`);
105
+ await fs.writeFile(localPath, bytes);
106
+ downloaded.push({
107
+ localPath,
108
+ url: attachment.url,
109
+ name,
110
+ mimeType,
111
+ });
112
+ index += 1;
113
+ }
114
+ catch (error) {
115
+ logger_1.logger.warn(`[ImageBridge] Attachment image processing failed (id=${attachment.id || 'unknown'})`, error?.message || error);
116
+ }
117
+ }
118
+ return downloaded;
119
+ }
120
+ async function cleanupInboundImageAttachments(attachments) {
121
+ for (const image of attachments) {
122
+ await fs.unlink(image.localPath).catch(() => { });
123
+ }
124
+ }
125
+ async function toDiscordAttachment(image, index) {
126
+ let buffer = null;
127
+ let mimeType = image.mimeType || 'image/png';
128
+ if (image.base64Data) {
129
+ try {
130
+ buffer = Buffer.from(image.base64Data, 'base64');
131
+ }
132
+ catch {
133
+ buffer = null;
134
+ }
135
+ }
136
+ else if (image.url && /^https?:\/\//i.test(image.url)) {
137
+ try {
138
+ const response = await fetch(image.url);
139
+ if (response.ok) {
140
+ buffer = Buffer.from(await response.arrayBuffer());
141
+ mimeType = response.headers.get('content-type') || mimeType;
142
+ }
143
+ }
144
+ catch {
145
+ buffer = null;
146
+ }
147
+ }
148
+ if (!buffer || buffer.length === 0)
149
+ return null;
150
+ const fallbackExt = mimeTypeToExtension(mimeType);
151
+ const baseName = sanitizeFileName(image.name || `generated-image-${index + 1}.${fallbackExt}`);
152
+ const finalName = IMAGE_EXT_PATTERN.test(baseName) ? baseName : `${baseName}.${fallbackExt}`;
153
+ return new discord_js_1.AttachmentBuilder(buffer, { name: finalName });
154
+ }