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.
- package/README.md +22 -7
- package/dist/bin/cli.js +18 -18
- package/dist/bin/commands/doctor.js +25 -19
- package/dist/bin/commands/start.js +25 -2
- package/dist/bot/index.js +445 -126
- package/dist/commands/joinCommandHandler.js +302 -0
- package/dist/commands/joinDetachCommandHandler.js +285 -0
- package/dist/commands/registerSlashCommands.js +40 -0
- package/dist/commands/workspaceCommandHandler.js +17 -28
- package/dist/database/chatSessionRepository.js +10 -0
- package/dist/database/userPreferenceRepository.js +72 -0
- package/dist/events/interactionCreateHandler.js +338 -30
- package/dist/events/messageCreateHandler.js +161 -47
- package/dist/services/antigravityLauncher.js +4 -3
- package/dist/services/approvalDetector.js +7 -0
- package/dist/services/assistantDomExtractor.js +339 -0
- package/dist/services/cdpBridgeManager.js +323 -39
- package/dist/services/cdpConnectionPool.js +117 -33
- package/dist/services/cdpService.js +149 -53
- package/dist/services/chatSessionService.js +229 -8
- package/dist/services/errorPopupDetector.js +271 -0
- package/dist/services/planningDetector.js +318 -0
- package/dist/services/responseMonitor.js +308 -70
- package/dist/services/retryStore.js +46 -0
- package/dist/services/updateCheckService.js +147 -0
- package/dist/services/userMessageDetector.js +221 -0
- package/dist/ui/buttonUtils.js +33 -0
- package/dist/ui/modeUi.js +11 -1
- package/dist/ui/modelsUi.js +24 -13
- package/dist/ui/outputUi.js +30 -0
- package/dist/ui/projectListUi.js +83 -0
- package/dist/ui/sessionPickerUi.js +48 -0
- package/dist/utils/antigravityPaths.js +94 -0
- package/dist/utils/configLoader.js +18 -0
- package/dist/utils/discordButtonUtils.js +33 -0
- package/dist/utils/discordFormatter.js +149 -16
- package/dist/utils/htmlToDiscordMarkdown.js +184 -0
- package/dist/utils/logBuffer.js +47 -0
- package/dist/utils/logFileTransport.js +147 -0
- package/dist/utils/logger.js +86 -21
- package/dist/utils/pathUtils.js +57 -0
- package/dist/utils/plainTextFormatter.js +70 -0
- package/dist/utils/processLogBuffer.js +4 -0
- package/package.json +4 -4
|
@@ -0,0 +1,94 @@
|
|
|
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.getAntigravityCliPath = getAntigravityCliPath;
|
|
37
|
+
exports.getAntigravityFallback = getAntigravityFallback;
|
|
38
|
+
exports.getAntigravityCdpHint = getAntigravityCdpHint;
|
|
39
|
+
const os = __importStar(require("os"));
|
|
40
|
+
const path = __importStar(require("path"));
|
|
41
|
+
const APP_NAME = 'Antigravity';
|
|
42
|
+
/**
|
|
43
|
+
* Get the Antigravity CLI binary path for the current platform.
|
|
44
|
+
*
|
|
45
|
+
* - macOS: /Applications/Antigravity.app/Contents/Resources/app/bin/antigravity
|
|
46
|
+
* - Windows: %LOCALAPPDATA%\Programs\Antigravity\Antigravity.exe
|
|
47
|
+
* - Linux: antigravity (assumed in PATH)
|
|
48
|
+
*/
|
|
49
|
+
function getAntigravityCliPath() {
|
|
50
|
+
switch (process.platform) {
|
|
51
|
+
case 'darwin':
|
|
52
|
+
return '/Applications/Antigravity.app/Contents/Resources/app/bin/antigravity';
|
|
53
|
+
case 'win32': {
|
|
54
|
+
const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
|
|
55
|
+
return path.join(localAppData, 'Programs', APP_NAME, `${APP_NAME}.exe`);
|
|
56
|
+
}
|
|
57
|
+
default:
|
|
58
|
+
return APP_NAME.toLowerCase();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Get fallback launch command and args for opening a workspace.
|
|
63
|
+
*
|
|
64
|
+
* - macOS: open -a Antigravity <path>
|
|
65
|
+
* - Windows: use full exe path with shell (handles spaces in paths)
|
|
66
|
+
* - Linux: antigravity <path>
|
|
67
|
+
*/
|
|
68
|
+
function getAntigravityFallback(workspacePath) {
|
|
69
|
+
switch (process.platform) {
|
|
70
|
+
case 'darwin':
|
|
71
|
+
return { command: 'open', args: ['-a', APP_NAME, workspacePath] };
|
|
72
|
+
case 'win32': {
|
|
73
|
+
const exePath = getAntigravityCliPath();
|
|
74
|
+
return { command: exePath, args: [workspacePath], options: { shell: true } };
|
|
75
|
+
}
|
|
76
|
+
default:
|
|
77
|
+
return { command: APP_NAME.toLowerCase(), args: [workspacePath] };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Get a platform-appropriate hint for starting Antigravity with CDP.
|
|
82
|
+
*
|
|
83
|
+
* Used in user-facing messages (Discord embeds, CLI doctor, logs).
|
|
84
|
+
*/
|
|
85
|
+
function getAntigravityCdpHint(port = 9222) {
|
|
86
|
+
switch (process.platform) {
|
|
87
|
+
case 'darwin':
|
|
88
|
+
return `open -a ${APP_NAME} --args --remote-debugging-port=${port}`;
|
|
89
|
+
case 'win32':
|
|
90
|
+
return `${APP_NAME}.exe --remote-debugging-port=${port}`;
|
|
91
|
+
default:
|
|
92
|
+
return `${APP_NAME.toLowerCase()} --remote-debugging-port=${port}`;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -92,6 +92,8 @@ function mergeConfig(persisted) {
|
|
|
92
92
|
const workspaceBaseDir = expandTilde(rawDir);
|
|
93
93
|
const guildId = process.env.GUILD_ID ?? persisted.guildId ?? undefined;
|
|
94
94
|
const autoApproveFileEdits = resolveBoolean(process.env.AUTO_APPROVE_FILE_EDITS, persisted.autoApproveFileEdits, false);
|
|
95
|
+
const logLevel = resolveLogLevel(process.env.LOG_LEVEL, persisted.logLevel);
|
|
96
|
+
const extractionMode = resolveExtractionMode(process.env.EXTRACTION_MODE, persisted.extractionMode);
|
|
95
97
|
return {
|
|
96
98
|
discordToken: token,
|
|
97
99
|
clientId,
|
|
@@ -99,6 +101,8 @@ function mergeConfig(persisted) {
|
|
|
99
101
|
allowedUserIds,
|
|
100
102
|
workspaceBaseDir,
|
|
101
103
|
autoApproveFileEdits,
|
|
104
|
+
logLevel,
|
|
105
|
+
extractionMode,
|
|
102
106
|
};
|
|
103
107
|
}
|
|
104
108
|
function resolveAllowedUserIds(persisted) {
|
|
@@ -114,6 +118,20 @@ function resolveAllowedUserIds(persisted) {
|
|
|
114
118
|
}
|
|
115
119
|
return [];
|
|
116
120
|
}
|
|
121
|
+
const VALID_LOG_LEVELS = ['debug', 'info', 'warn', 'error', 'none'];
|
|
122
|
+
function resolveLogLevel(envValue, persistedValue) {
|
|
123
|
+
const raw = envValue?.toLowerCase() ?? persistedValue;
|
|
124
|
+
if (raw && VALID_LOG_LEVELS.includes(raw)) {
|
|
125
|
+
return raw;
|
|
126
|
+
}
|
|
127
|
+
return 'info';
|
|
128
|
+
}
|
|
129
|
+
function resolveExtractionMode(envValue, persistedValue) {
|
|
130
|
+
const raw = envValue ?? persistedValue;
|
|
131
|
+
if (raw === 'legacy')
|
|
132
|
+
return 'legacy';
|
|
133
|
+
return 'structured';
|
|
134
|
+
}
|
|
117
135
|
function resolveBoolean(envValue, persistedValue, defaultValue) {
|
|
118
136
|
if (envValue !== undefined)
|
|
119
137
|
return envValue.toLowerCase() === 'true';
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.disableAllButtons = disableAllButtons;
|
|
4
|
+
const discord_js_1 = require("discord.js");
|
|
5
|
+
/**
|
|
6
|
+
* Disable all buttons in message component rows.
|
|
7
|
+
* Shared utility used by interaction handlers and detector callbacks.
|
|
8
|
+
*/
|
|
9
|
+
function disableAllButtons(components) {
|
|
10
|
+
return components
|
|
11
|
+
.map((row) => {
|
|
12
|
+
const rowAny = row;
|
|
13
|
+
if (!Array.isArray(rowAny.components))
|
|
14
|
+
return null;
|
|
15
|
+
const nextRow = new discord_js_1.ActionRowBuilder();
|
|
16
|
+
const disabledButtons = rowAny.components
|
|
17
|
+
.map((component) => {
|
|
18
|
+
const componentType = component?.type ?? component?.data?.type;
|
|
19
|
+
if (componentType !== 2)
|
|
20
|
+
return null;
|
|
21
|
+
const payload = typeof component?.toJSON === 'function'
|
|
22
|
+
? component.toJSON()
|
|
23
|
+
: component;
|
|
24
|
+
return discord_js_1.ButtonBuilder.from(payload).setDisabled(true);
|
|
25
|
+
})
|
|
26
|
+
.filter((button) => button !== null);
|
|
27
|
+
if (disabledButtons.length === 0)
|
|
28
|
+
return null;
|
|
29
|
+
nextRow.addComponents(...disabledButtons);
|
|
30
|
+
return nextRow;
|
|
31
|
+
})
|
|
32
|
+
.filter((row) => row !== null);
|
|
33
|
+
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.formatForDiscord = formatForDiscord;
|
|
7
7
|
exports.splitOutputAndLogs = splitOutputAndLogs;
|
|
8
|
+
exports.separateOutputForDelivery = separateOutputForDelivery;
|
|
8
9
|
exports.sanitizeActivityLines = sanitizeActivityLines;
|
|
9
10
|
/** Known UI chrome literal strings (exact match after trim + lowercase) */
|
|
10
11
|
const UI_CHROME_LITERALS = new Set([
|
|
@@ -54,6 +55,9 @@ const UI_CHROME_REGEXES = [
|
|
|
54
55
|
/^[a-z0-9._-]+\s*\/\s*[a-z0-9._-]+$/i, // MCP server/tool format: "jina-mcp-server / search_web"
|
|
55
56
|
/^full output written to\b/i, // tool result redirect: "Full output written to ..."
|
|
56
57
|
/^output\.[a-z0-9._-]+(?:#l\d+(?:-\d+)?)?$/i, // output file ref: "output.txt#L1-131"
|
|
58
|
+
/^\s*\{\s*$/, // lone JSON opening brace
|
|
59
|
+
/^\s*\}\s*$/, // lone JSON closing brace
|
|
60
|
+
/^\s*"[^"]*"\s*:\s*/, // JSON key-value line: "query": "..."
|
|
57
61
|
];
|
|
58
62
|
/**
|
|
59
63
|
* Check if a line is UI chrome (not real assistant output).
|
|
@@ -71,17 +75,31 @@ function isUiChromeLine(line) {
|
|
|
71
75
|
}
|
|
72
76
|
return false;
|
|
73
77
|
}
|
|
78
|
+
/** Regex matching file references like src/bot/index.ts:54 or tests/utils/config.ts.
|
|
79
|
+
* Consumes one trailing space so the replacement `\`ref\`` doesn't leave a double space. */
|
|
80
|
+
const FILE_REF_REGEX = /(?<![`/\\])(\b[a-zA-Z][\w.-]*(?:\/[\w.-]+)+(?::\d+(?:-\d+)?)?)\s?(?!`)/g;
|
|
74
81
|
/**
|
|
75
82
|
* Format text for Discord Embed display.
|
|
76
83
|
* Wraps table lines and tree lines in code blocks.
|
|
84
|
+
* Wraps file references (e.g. src/bot/index.ts:54) in inline code backticks.
|
|
77
85
|
*/
|
|
78
86
|
function formatForDiscord(text) {
|
|
79
87
|
const lines = text.split('\n');
|
|
80
88
|
const result = [];
|
|
81
89
|
let inSpecialBlock = false;
|
|
90
|
+
let inCodeBlock = false;
|
|
82
91
|
for (let i = 0; i < lines.length; i++) {
|
|
83
92
|
const line = lines[i];
|
|
84
93
|
const trimmed = line.trim();
|
|
94
|
+
if (trimmed.startsWith('```')) {
|
|
95
|
+
inCodeBlock = !inCodeBlock;
|
|
96
|
+
result.push(line);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (inCodeBlock) {
|
|
100
|
+
result.push(line);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
85
103
|
const isTableLine = (trimmed.startsWith('|') && trimmed.endsWith('|') && trimmed.length > 2) ||
|
|
86
104
|
/^\|[\s\-:]+\|/.test(trimmed);
|
|
87
105
|
const isTreeLine = /[├└│┌┐┘┤┬┴┼]/.test(line) ||
|
|
@@ -99,10 +117,10 @@ function formatForDiscord(text) {
|
|
|
99
117
|
else if (!isSpecialLine && inSpecialBlock) {
|
|
100
118
|
result.push('```');
|
|
101
119
|
inSpecialBlock = false;
|
|
102
|
-
result.push(line);
|
|
120
|
+
result.push(wrapFileReferences(line));
|
|
103
121
|
}
|
|
104
122
|
else {
|
|
105
|
-
result.push(line);
|
|
123
|
+
result.push(wrapFileReferences(line));
|
|
106
124
|
}
|
|
107
125
|
}
|
|
108
126
|
if (inSpecialBlock) {
|
|
@@ -110,47 +128,162 @@ function formatForDiscord(text) {
|
|
|
110
128
|
}
|
|
111
129
|
return result.join('\n');
|
|
112
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* Wrap file references (e.g. src/bot/index.ts:54) in inline code backticks.
|
|
133
|
+
*/
|
|
134
|
+
function wrapFileReferences(line) {
|
|
135
|
+
return line.replace(FILE_REF_REGEX, '`$1`');
|
|
136
|
+
}
|
|
137
|
+
/** Check if a line is an MCP tool call format (server / tool_name) */
|
|
138
|
+
function isMcpFormatLine(line) {
|
|
139
|
+
return /^[a-z0-9._-]+\s*\/\s*[a-z0-9._-]+$/i.test(line);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Check if a line is a multi-word activity log (e.g. "Analyzing current workspace").
|
|
143
|
+
* Used only by splitOutputAndLogs — NOT by sanitizeActivityLines, which needs
|
|
144
|
+
* to keep these lines in the activity log output.
|
|
145
|
+
*/
|
|
146
|
+
function isActivityLogLine(line) {
|
|
147
|
+
const trimmed = (line || '').trim();
|
|
148
|
+
if (!trimmed)
|
|
149
|
+
return false;
|
|
150
|
+
return /^(?:analy[sz]ing|reading|writing|running|searching|planning|thinking|processing|loading|executing|testing|debugging|analyzed|read|wrote|ran)\s+.+/i.test(trimmed)
|
|
151
|
+
&& trimmed.length <= 220;
|
|
152
|
+
}
|
|
113
153
|
/**
|
|
114
154
|
* Split raw text into output (real content) and logs (UI chrome).
|
|
115
155
|
* Code blocks are always preserved as output.
|
|
156
|
+
*
|
|
157
|
+
* When tool call blocks are detected (MCP format lines), applies a
|
|
158
|
+
* "last paragraph wins" heuristic: only the final block of non-chrome
|
|
159
|
+
* text is treated as the assistant's answer; everything between tool
|
|
160
|
+
* call regions is treated as thinking text and moved to logs.
|
|
116
161
|
*/
|
|
117
162
|
function splitOutputAndLogs(rawText) {
|
|
118
163
|
const normalized = (rawText || '').replace(/\r/g, '');
|
|
119
164
|
if (!normalized.trim()) {
|
|
120
165
|
return { output: '', logs: '' };
|
|
121
166
|
}
|
|
122
|
-
const outputLines = [];
|
|
123
|
-
const logLines = [];
|
|
124
|
-
let inCodeBlock = false;
|
|
125
167
|
const lines = normalized.split('\n');
|
|
168
|
+
const classes = [];
|
|
169
|
+
let inCodeBlock = false;
|
|
126
170
|
for (const line of lines) {
|
|
127
171
|
const trimmed = (line || '').trim();
|
|
128
172
|
if (trimmed.startsWith('```')) {
|
|
129
173
|
inCodeBlock = !inCodeBlock;
|
|
130
|
-
|
|
174
|
+
classes.push('code');
|
|
131
175
|
continue;
|
|
132
176
|
}
|
|
133
177
|
if (inCodeBlock) {
|
|
134
|
-
|
|
178
|
+
classes.push('code');
|
|
135
179
|
continue;
|
|
136
180
|
}
|
|
137
181
|
if (!trimmed) {
|
|
138
|
-
|
|
182
|
+
classes.push('blank');
|
|
139
183
|
continue;
|
|
140
184
|
}
|
|
141
|
-
if (isUiChromeLine(trimmed)) {
|
|
142
|
-
|
|
185
|
+
if (isUiChromeLine(trimmed) || isActivityLogLine(trimmed)) {
|
|
186
|
+
classes.push('chrome');
|
|
143
187
|
}
|
|
144
188
|
else {
|
|
145
|
-
|
|
189
|
+
classes.push('output');
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
// Check if tool calls are present — triggers "last paragraph wins" heuristic
|
|
193
|
+
const hasMcpCalls = lines.some((l, i) => classes[i] === 'chrome' && isMcpFormatLine(l.trim()));
|
|
194
|
+
if (!hasMcpCalls) {
|
|
195
|
+
// Simple path: no tool calls, line-by-line separation
|
|
196
|
+
const outputLines = [];
|
|
197
|
+
const logLines = [];
|
|
198
|
+
for (let i = 0; i < lines.length; i++) {
|
|
199
|
+
if (classes[i] === 'chrome') {
|
|
200
|
+
logLines.push(lines[i].trim());
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
outputLines.push(lines[i]);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
output: collapseBlankLines(outputLines.join('\n')),
|
|
208
|
+
logs: collapseBlankLines(logLines.join('\n')),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
// Complex path: tool calls present — only the LAST paragraph is output.
|
|
212
|
+
// Scan bottom-up to find the last non-chrome text line.
|
|
213
|
+
let lastOutputEnd = -1;
|
|
214
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
215
|
+
if (classes[i] === 'output' || classes[i] === 'code') {
|
|
216
|
+
lastOutputEnd = i;
|
|
217
|
+
break;
|
|
146
218
|
}
|
|
147
219
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
.trim();
|
|
220
|
+
if (lastOutputEnd === -1) {
|
|
221
|
+
// No output lines at all
|
|
222
|
+
const logLines = lines.filter((_, i) => classes[i] === 'chrome').map((l) => l.trim());
|
|
223
|
+
return { output: '', logs: collapseBlankLines(logLines.join('\n')) };
|
|
224
|
+
}
|
|
225
|
+
// Walk upward from lastOutputEnd to find the start of the final paragraph.
|
|
226
|
+
// Stop at any blank line or chrome line — in the legacy tool-call path, only
|
|
227
|
+
// the immediate last paragraph is preserved as output. Multi-paragraph final
|
|
228
|
+
// answers are handled correctly by the structured extraction mode (Phase 1).
|
|
229
|
+
let lastOutputStart = lastOutputEnd;
|
|
230
|
+
for (let i = lastOutputEnd - 1; i >= 0; i--) {
|
|
231
|
+
if (classes[i] === 'blank' || classes[i] === 'chrome') {
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
if (classes[i] === 'output' || classes[i] === 'code') {
|
|
235
|
+
lastOutputStart = i;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
const outputLines = [];
|
|
239
|
+
const logLines = [];
|
|
240
|
+
for (let i = 0; i < lines.length; i++) {
|
|
241
|
+
if (i >= lastOutputStart && i <= lastOutputEnd) {
|
|
242
|
+
// Inside the final answer block — keep non-chrome lines as output
|
|
243
|
+
if (classes[i] !== 'chrome') {
|
|
244
|
+
outputLines.push(lines[i]);
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
logLines.push(lines[i].trim());
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
else if (classes[i] === 'chrome' || classes[i] === 'output') {
|
|
251
|
+
// Outside the final block — chrome or thinking text → logs
|
|
252
|
+
logLines.push(lines[i].trim());
|
|
253
|
+
}
|
|
254
|
+
// blank lines outside the final block are dropped
|
|
255
|
+
}
|
|
256
|
+
return {
|
|
257
|
+
output: collapseBlankLines(outputLines.join('\n')),
|
|
258
|
+
logs: collapseBlankLines(logLines.join('\n')),
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
/** Collapse 3+ consecutive newlines to 2, and trim */
|
|
262
|
+
function collapseBlankLines(text) {
|
|
263
|
+
return text.replace(/\n{3,}/g, '\n\n').trim();
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Separate raw text into output and logs, choosing between DOM-structured
|
|
267
|
+
* extraction and legacy string-based separation.
|
|
268
|
+
*
|
|
269
|
+
* When domSource is 'dom-structured', DOM results are used directly.
|
|
270
|
+
* When domSource is 'legacy-fallback', falls back to splitOutputAndLogs().
|
|
271
|
+
*/
|
|
272
|
+
function separateOutputForDelivery(options) {
|
|
273
|
+
const { rawText, domSource, domOutputText, domActivityLines } = options;
|
|
274
|
+
if (domSource === 'dom-structured' && domOutputText !== undefined) {
|
|
275
|
+
return {
|
|
276
|
+
source: 'dom-structured',
|
|
277
|
+
output: domOutputText,
|
|
278
|
+
logs: (domActivityLines ?? []).join('\n'),
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
// Fallback to legacy string-based separation
|
|
282
|
+
const separated = splitOutputAndLogs(rawText);
|
|
151
283
|
return {
|
|
152
|
-
|
|
153
|
-
|
|
284
|
+
source: 'legacy-fallback',
|
|
285
|
+
output: separated.output,
|
|
286
|
+
logs: separated.logs,
|
|
154
287
|
};
|
|
155
288
|
}
|
|
156
289
|
/**
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Lightweight HTML-to-Discord-Markdown converter.
|
|
4
|
+
* Runs on Node.js (no browser DOM required).
|
|
5
|
+
* Converts common HTML tags to Discord-compatible Markdown.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.htmlToDiscordMarkdown = htmlToDiscordMarkdown;
|
|
9
|
+
/**
|
|
10
|
+
* Convert an HTML string to Discord-compatible Markdown.
|
|
11
|
+
*
|
|
12
|
+
* Supported conversions:
|
|
13
|
+
* - <h1>-<h3> → # - ###
|
|
14
|
+
* - <strong>/<b> → **...**
|
|
15
|
+
* - <em>/<i> → *...*
|
|
16
|
+
* - <code> → `...`
|
|
17
|
+
* - <pre><code> → ```\n...\n```
|
|
18
|
+
* - <ol><li> → 1. item
|
|
19
|
+
* - <ul><li> → - item
|
|
20
|
+
* - <p> → \n\n
|
|
21
|
+
* - <br> → \n
|
|
22
|
+
* - <span class="context-scope-mention"> → `text`
|
|
23
|
+
* - Elements with file-path title attribute → title + text
|
|
24
|
+
* - <style> → removed entirely
|
|
25
|
+
* - All other tags → stripped, text preserved
|
|
26
|
+
*/
|
|
27
|
+
function htmlToDiscordMarkdown(html) {
|
|
28
|
+
if (!html)
|
|
29
|
+
return '';
|
|
30
|
+
let result = html;
|
|
31
|
+
// Remove <style> tags and their content
|
|
32
|
+
result = result.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '');
|
|
33
|
+
// Handle <br> and <br/>
|
|
34
|
+
result = result.replace(/<br\s*\/?>/gi, '\n');
|
|
35
|
+
// Handle <hr>
|
|
36
|
+
result = result.replace(/<hr\s*\/?>/gi, '\n---\n');
|
|
37
|
+
// Handle headings (h1-h3)
|
|
38
|
+
result = result.replace(/<h1[^>]*>([\s\S]*?)<\/h1>/gi, (_m, content) => `\n# ${stripTags(content).trim()}\n`);
|
|
39
|
+
result = result.replace(/<h2[^>]*>([\s\S]*?)<\/h2>/gi, (_m, content) => `\n## ${stripTags(content).trim()}\n`);
|
|
40
|
+
result = result.replace(/<h3[^>]*>([\s\S]*?)<\/h3>/gi, (_m, content) => `\n### ${stripTags(content).trim()}\n`);
|
|
41
|
+
// Handle <pre><code> blocks (must come before inline <code>)
|
|
42
|
+
// Extract language from class="language-xxx" if present.
|
|
43
|
+
// Do NOT decode entities here — let the final decodeEntities() handle them
|
|
44
|
+
// after stripTags() has run, to avoid decoded < > being stripped as tags.
|
|
45
|
+
result = result.replace(/<pre[^>]*>\s*<code(?:\s+class="language-([^"]*)")?[^>]*>([\s\S]*?)<\/code>\s*<\/pre>/gi, (_m, lang, content) => `\n\`\`\`${lang || ''}\n${content}\n\`\`\`\n`);
|
|
46
|
+
// Handle inline <code>
|
|
47
|
+
result = result.replace(/<code[^>]*>([\s\S]*?)<\/code>/gi, '`$1`');
|
|
48
|
+
// Handle <strong> and <b>
|
|
49
|
+
result = result.replace(/<(?:strong|b)(?:\s[^>]*)?>((?: |\s|[^<]|<(?!\/(?:strong|b)>))*)<\/(?:strong|b)>/gi, '**$1**');
|
|
50
|
+
// Handle <em> and <i>
|
|
51
|
+
result = result.replace(/<(?:em|i)(?:\s[^>]*)?>((?: |\s|[^<]|<(?!\/(?:em|i)>))*)<\/(?:em|i)>/gi, '*$1*');
|
|
52
|
+
// Handle <span class="context-scope-mention"> → `text`
|
|
53
|
+
result = result.replace(/<span[^>]*class="[^"]*context-scope-mention[^"]*"[^>]*>([\s\S]*?)<\/span>/gi, (_m, text) => `\`${stripTags(text).trim()}\``);
|
|
54
|
+
// Handle elements with title attribute containing file paths
|
|
55
|
+
// e.g. <div title="src/bot/index.ts">:54</div> → src/bot/index.ts:54
|
|
56
|
+
result = result.replace(/<(?:div|span|a)[^>]*\btitle="([^"]*)"[^>]*>([\s\S]*?)<\/(?:div|span|a)>/gi, (_m, title, text) => {
|
|
57
|
+
if (looksLikeFilePath(title)) {
|
|
58
|
+
return `${title}${stripTags(text).trim()}`;
|
|
59
|
+
}
|
|
60
|
+
return stripTags(text);
|
|
61
|
+
});
|
|
62
|
+
// Handle <p> and <div> BEFORE list processing so <li> content is clean text
|
|
63
|
+
result = result.replace(/<p[^>]*>([\s\S]*?)<\/p>/gi, '$1\n\n');
|
|
64
|
+
result = result.replace(/<div[^>]*>([\s\S]*?)<\/div>/gi, '$1\n');
|
|
65
|
+
// Handle lists — process innermost first to support nesting.
|
|
66
|
+
// Loop up to 5 times to peel nested lists from inside out.
|
|
67
|
+
for (let iteration = 0; iteration < 5; iteration++) {
|
|
68
|
+
if (!/<(?:ul|ol)\b/i.test(result))
|
|
69
|
+
break;
|
|
70
|
+
// Process innermost <ul> (no nested <ul>/<ol> inside)
|
|
71
|
+
result = result.replace(/<ul[^>]*>((?:(?!<\/?(?:ul|ol)\b)[\s\S])*?)<\/ul>/gi, (_m, content) => {
|
|
72
|
+
const items = content.replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, (_lm, text) => formatListItem('- ', text));
|
|
73
|
+
return `\n${items}`;
|
|
74
|
+
});
|
|
75
|
+
// Process innermost <ol> (no nested <ul>/<ol> inside)
|
|
76
|
+
result = result.replace(/<ol[^>]*>((?:(?!<\/?(?:ul|ol)\b)[\s\S])*?)<\/ol>/gi, (_m, content) => {
|
|
77
|
+
let counter = 0;
|
|
78
|
+
const items = content.replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, (_lm, text) => {
|
|
79
|
+
counter++;
|
|
80
|
+
return formatListItem(`${counter}. `, text);
|
|
81
|
+
});
|
|
82
|
+
return `\n${items}`;
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
// Strip remaining HTML tags
|
|
86
|
+
result = stripTags(result);
|
|
87
|
+
// Decode HTML entities
|
|
88
|
+
result = decodeEntities(result);
|
|
89
|
+
// Escape double underscores outside code blocks/inline code to prevent
|
|
90
|
+
// Discord from interpreting __dirname, __proto__ etc. as underline markup.
|
|
91
|
+
result = escapeDoubleUnderscores(result);
|
|
92
|
+
// Clean up excessive whitespace
|
|
93
|
+
result = result.replace(/\n{3,}/g, '\n\n');
|
|
94
|
+
result = result.trim();
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Format a list item: strip tags, then indent any lines that are
|
|
99
|
+
* already-processed nested list items (starting with - or 1.).
|
|
100
|
+
*/
|
|
101
|
+
function formatListItem(prefix, rawContent) {
|
|
102
|
+
const cleaned = stripTags(rawContent).trim();
|
|
103
|
+
if (!cleaned)
|
|
104
|
+
return '';
|
|
105
|
+
const lines = cleaned.split('\n').filter((l) => l.trim());
|
|
106
|
+
if (lines.length === 0)
|
|
107
|
+
return '';
|
|
108
|
+
// First line gets the bullet prefix
|
|
109
|
+
const result = [prefix + lines[0]];
|
|
110
|
+
// Subsequent lines: indent by 2 spaces (nested content)
|
|
111
|
+
for (let i = 1; i < lines.length; i++) {
|
|
112
|
+
const trimmed = lines[i].trimStart();
|
|
113
|
+
// Already a list marker from inner processing — indent it
|
|
114
|
+
if (/^[-•]/.test(trimmed) || /^\d+\.\s/.test(trimmed)) {
|
|
115
|
+
result.push(' ' + trimmed);
|
|
116
|
+
}
|
|
117
|
+
else if (trimmed.startsWith('```')) {
|
|
118
|
+
// Code block fence — indent
|
|
119
|
+
result.push(' ' + trimmed);
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
// Continuation text — indent
|
|
123
|
+
result.push(' ' + trimmed);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return result.join('\n') + '\n';
|
|
127
|
+
}
|
|
128
|
+
/** Check if a string looks like a file path */
|
|
129
|
+
function looksLikeFilePath(value) {
|
|
130
|
+
if (!value)
|
|
131
|
+
return false;
|
|
132
|
+
// Must contain at least one / and end with an extension-like pattern
|
|
133
|
+
return /^[a-zA-Z0-9._\-/]+\.[a-zA-Z0-9]+$/.test(value) && value.includes('/');
|
|
134
|
+
}
|
|
135
|
+
/** Strip all HTML tags from a string */
|
|
136
|
+
function stripTags(html) {
|
|
137
|
+
return html.replace(/<[^>]+>/g, '');
|
|
138
|
+
}
|
|
139
|
+
/** Decode common HTML entities and generic numeric entities */
|
|
140
|
+
function decodeEntities(text) {
|
|
141
|
+
return text
|
|
142
|
+
.replace(/&/g, '&')
|
|
143
|
+
.replace(/</g, '<')
|
|
144
|
+
.replace(/>/g, '>')
|
|
145
|
+
.replace(/"/g, '"')
|
|
146
|
+
.replace(/'/g, "'")
|
|
147
|
+
.replace(/'/g, "'")
|
|
148
|
+
.replace(/ /g, ' ')
|
|
149
|
+
.replace(/&#x([0-9a-fA-F]+);/g, (_m, hex) => String.fromCodePoint(parseInt(hex, 16)))
|
|
150
|
+
.replace(/&#(\d+);/g, (_m, dec) => String.fromCodePoint(parseInt(dec, 10)));
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Escape double underscores outside code blocks and inline code
|
|
154
|
+
* to prevent Discord from interpreting them as underline markup.
|
|
155
|
+
* e.g. __dirname → \_\_dirname
|
|
156
|
+
*/
|
|
157
|
+
function escapeDoubleUnderscores(text) {
|
|
158
|
+
const lines = text.split('\n');
|
|
159
|
+
const result = [];
|
|
160
|
+
let inCodeBlock = false;
|
|
161
|
+
for (const line of lines) {
|
|
162
|
+
if (line.trimStart().startsWith('```')) {
|
|
163
|
+
inCodeBlock = !inCodeBlock;
|
|
164
|
+
result.push(line);
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (inCodeBlock) {
|
|
168
|
+
result.push(line);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
// Outside code blocks: escape __ that are NOT inside inline backticks
|
|
172
|
+
// Split by backtick-delimited segments, only escape outside backticks
|
|
173
|
+
const parts = line.split(/(`[^`]*`)/g);
|
|
174
|
+
const escaped = parts.map((part, idx) => {
|
|
175
|
+
// Odd indices are inside backticks — leave as-is
|
|
176
|
+
if (idx % 2 === 1)
|
|
177
|
+
return part;
|
|
178
|
+
// Even indices are outside backticks — escape __
|
|
179
|
+
return part.replace(/__/g, '\\_\\_');
|
|
180
|
+
});
|
|
181
|
+
result.push(escaped.join(''));
|
|
182
|
+
}
|
|
183
|
+
return result.join('\n');
|
|
184
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.logBuffer = exports.LogBuffer = void 0;
|
|
4
|
+
const MAX_ENTRIES = 200;
|
|
5
|
+
// Strip ANSI escape codes for clean buffer storage
|
|
6
|
+
const ANSI_REGEX = /\x1b\[[0-9;]*m/g;
|
|
7
|
+
function stripAnsi(text) {
|
|
8
|
+
return text.replace(ANSI_REGEX, '');
|
|
9
|
+
}
|
|
10
|
+
class LogBuffer {
|
|
11
|
+
buffer = [];
|
|
12
|
+
head = 0;
|
|
13
|
+
count = 0;
|
|
14
|
+
append(level, message) {
|
|
15
|
+
const entry = {
|
|
16
|
+
timestamp: new Date().toISOString(),
|
|
17
|
+
level,
|
|
18
|
+
message: stripAnsi(message),
|
|
19
|
+
};
|
|
20
|
+
if (this.count < MAX_ENTRIES) {
|
|
21
|
+
this.buffer.push(entry);
|
|
22
|
+
this.count++;
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
this.buffer[this.head] = entry;
|
|
26
|
+
}
|
|
27
|
+
this.head = (this.head + 1) % MAX_ENTRIES;
|
|
28
|
+
}
|
|
29
|
+
getRecent(count, levelFilter) {
|
|
30
|
+
const all = [];
|
|
31
|
+
for (let i = 0; i < this.count; i++) {
|
|
32
|
+
const idx = (this.head - this.count + i + MAX_ENTRIES * 2) % MAX_ENTRIES;
|
|
33
|
+
all.push(this.buffer[idx]);
|
|
34
|
+
}
|
|
35
|
+
const filtered = levelFilter
|
|
36
|
+
? all.filter((e) => e.level === levelFilter)
|
|
37
|
+
: all;
|
|
38
|
+
return filtered.slice(-count);
|
|
39
|
+
}
|
|
40
|
+
clear() {
|
|
41
|
+
this.buffer.length = 0;
|
|
42
|
+
this.head = 0;
|
|
43
|
+
this.count = 0;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
exports.LogBuffer = LogBuffer;
|
|
47
|
+
exports.logBuffer = new LogBuffer();
|