@dynamicu/chromedebug-mcp 2.6.1 → 2.6.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.
- package/chrome-extension/disable-react-devtools.js +19 -0
- package/chrome-extension/manifest.free.json +3 -3
- package/chrome-extension/options.html +2 -1
- package/chrome-extension/popup.html +15 -1
- package/chrome-extension/popup.js +191 -35
- package/package.json +1 -1
- package/scripts/package-webstore-extension.js +1 -0
- package/scripts/webpack.config.free.cjs +1 -0
- package/scripts/webpack.config.pro.cjs +1 -0
- package/src/cli.js +7 -0
- package/src/database.js +82 -37
- package/src/index.js +52 -5
- package/src/logger.js +9 -2
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Disable React DevTools to prevent semver validation error
|
|
3
|
+
*
|
|
4
|
+
* This prevents the React DevTools browser extension from trying to inject
|
|
5
|
+
* into ChromeDebug's extension pages, which causes a console error:
|
|
6
|
+
* "Invalid argument not valid semver ('' received)"
|
|
7
|
+
*
|
|
8
|
+
* IMPORTANT: This only affects ChromeDebug extension pages (popup, options).
|
|
9
|
+
* It does NOT affect developers' React applications in other tabs.
|
|
10
|
+
* Each browser tab/window has its own isolated JavaScript context.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
window.__REACT_DEVTOOLS_GLOBAL_HOOK__ = {
|
|
14
|
+
isDisabled: true,
|
|
15
|
+
supportsFiber: true,
|
|
16
|
+
inject: function() {},
|
|
17
|
+
onCommitFiberRoot: function() {},
|
|
18
|
+
onCommitFiberUnmount: function() {}
|
|
19
|
+
};
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": 3,
|
|
3
|
-
"name": "ChromeDebug MCP Assistant FREE v2.
|
|
4
|
-
"version": "2.
|
|
5
|
-
"description": "ChromeDebug MCP visual element selector [FREE Edition] [Build: 2025-10-
|
|
3
|
+
"name": "ChromeDebug MCP Assistant FREE v2.6.0",
|
|
4
|
+
"version": "2.6.0",
|
|
5
|
+
"description": "ChromeDebug MCP visual element selector [FREE Edition] [Build: 2025-10-24-v2.6.0]",
|
|
6
6
|
"permissions": [
|
|
7
7
|
"activeTab",
|
|
8
8
|
"scripting",
|
|
@@ -164,6 +164,19 @@
|
|
|
164
164
|
</div>
|
|
165
165
|
|
|
166
166
|
<div id="license-message" style="margin-top: 5px; font-size: 12px; text-align: center;"></div>
|
|
167
|
+
|
|
168
|
+
<!-- Inline Activation Manager (shown when activation limit reached) -->
|
|
169
|
+
<div id="inline-activation-manager" style="display: none; margin-top: 15px; padding: 12px; background: #fff3cd; border: 1px solid #ffc107; border-radius: 4px; max-height: 0; overflow: hidden; transition: max-height 0.3s ease-out;">
|
|
170
|
+
<div style="font-size: 13px; font-weight: 500; color: #856404; margin-bottom: 10px;">
|
|
171
|
+
⚠️ Activation Limit Reached
|
|
172
|
+
</div>
|
|
173
|
+
<div style="font-size: 12px; color: #856404; margin-bottom: 12px;">
|
|
174
|
+
Please deactivate an existing activation to continue
|
|
175
|
+
</div>
|
|
176
|
+
<div id="activations-list-inline" style="max-height: 250px; overflow-y: auto; background: white; border-radius: 4px; border: 1px solid #ddd;">
|
|
177
|
+
<!-- Activations will be loaded here dynamically -->
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
167
180
|
</div>
|
|
168
181
|
|
|
169
182
|
<div class="status">
|
|
@@ -526,7 +539,8 @@
|
|
|
526
539
|
color: #c62828;
|
|
527
540
|
}
|
|
528
541
|
</style>
|
|
529
|
-
|
|
542
|
+
|
|
543
|
+
<script src="disable-react-devtools.js"></script>
|
|
530
544
|
<script src="extension-config.js"></script>
|
|
531
545
|
<script type="module" src="popup.js"></script>
|
|
532
546
|
</body>
|
|
@@ -995,6 +995,193 @@ async function initializeLicenseUI() {
|
|
|
995
995
|
}
|
|
996
996
|
}
|
|
997
997
|
|
|
998
|
+
// Inline Activation Manager Functions
|
|
999
|
+
/**
|
|
1000
|
+
* Show the inline activation manager with slide-down animation
|
|
1001
|
+
*/
|
|
1002
|
+
async function showInlineActivationManager(licenseKey) {
|
|
1003
|
+
const inlineManager = document.getElementById('inline-activation-manager');
|
|
1004
|
+
const activationsList = document.getElementById('activations-list-inline');
|
|
1005
|
+
|
|
1006
|
+
// Show the container
|
|
1007
|
+
inlineManager.style.display = 'block';
|
|
1008
|
+
|
|
1009
|
+
// Trigger animation
|
|
1010
|
+
setTimeout(() => {
|
|
1011
|
+
inlineManager.style.maxHeight = '400px';
|
|
1012
|
+
}, 10);
|
|
1013
|
+
|
|
1014
|
+
// Load activations
|
|
1015
|
+
try {
|
|
1016
|
+
activationsList.innerHTML = '<div style="padding: 15px; text-align: center; color: #666;">Loading activations...</div>';
|
|
1017
|
+
|
|
1018
|
+
// Get current instance ID
|
|
1019
|
+
const stored = await chrome.storage.local.get(['chromedebug_instance_id']);
|
|
1020
|
+
const currentInstanceId = stored.chromedebug_instance_id;
|
|
1021
|
+
|
|
1022
|
+
// Fetch activations from Firebase
|
|
1023
|
+
const data = await licenseClient.listActivations(licenseKey);
|
|
1024
|
+
const activations = data.activations || [];
|
|
1025
|
+
|
|
1026
|
+
// Render activations
|
|
1027
|
+
renderInlineActivations(activations, currentInstanceId, licenseKey);
|
|
1028
|
+
|
|
1029
|
+
} catch (error) {
|
|
1030
|
+
console.error('[License] Error loading activations:', error);
|
|
1031
|
+
activationsList.innerHTML = `<div style="padding: 15px; text-align: center; color: #f44336;">Failed to load activations: ${error.message}</div>`;
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
/**
|
|
1036
|
+
* Hide the inline activation manager with slide-up animation
|
|
1037
|
+
*/
|
|
1038
|
+
function hideInlineActivationManager() {
|
|
1039
|
+
const inlineManager = document.getElementById('inline-activation-manager');
|
|
1040
|
+
inlineManager.style.maxHeight = '0';
|
|
1041
|
+
|
|
1042
|
+
setTimeout(() => {
|
|
1043
|
+
inlineManager.style.display = 'none';
|
|
1044
|
+
}, 300);
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
/**
|
|
1048
|
+
* Render activations in the inline manager
|
|
1049
|
+
*/
|
|
1050
|
+
function renderInlineActivations(activations, currentInstanceId, licenseKey) {
|
|
1051
|
+
const container = document.getElementById('activations-list-inline');
|
|
1052
|
+
container.innerHTML = '';
|
|
1053
|
+
|
|
1054
|
+
if (activations.length === 0) {
|
|
1055
|
+
container.innerHTML = '<div style="padding: 15px; text-align: center; color: #666;">No activations found.</div>';
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
activations.forEach((activation, index) => {
|
|
1060
|
+
const item = document.createElement('div');
|
|
1061
|
+
item.style.cssText = 'padding: 12px; border-bottom: 1px solid #e0e0e0; transition: background 0.2s;';
|
|
1062
|
+
item.onmouseenter = () => item.style.background = '#f9f9f9';
|
|
1063
|
+
item.onmouseleave = () => item.style.background = isCurrentDevice ? '#e8f5e9' : 'white';
|
|
1064
|
+
|
|
1065
|
+
// Check if this is the current device
|
|
1066
|
+
const isCurrentDevice = activation.name === currentInstanceId || activation.identifier === currentInstanceId;
|
|
1067
|
+
|
|
1068
|
+
if (isCurrentDevice) {
|
|
1069
|
+
item.style.background = '#e8f5e9';
|
|
1070
|
+
item.style.borderLeft = '3px solid #4caf50';
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// Format date
|
|
1074
|
+
const date = new Date(activation.createdAt);
|
|
1075
|
+
const formattedDate = date.toLocaleString('en-US', {
|
|
1076
|
+
month: 'short',
|
|
1077
|
+
day: 'numeric',
|
|
1078
|
+
year: 'numeric',
|
|
1079
|
+
hour: '2-digit',
|
|
1080
|
+
minute: '2-digit'
|
|
1081
|
+
});
|
|
1082
|
+
|
|
1083
|
+
// Build device info
|
|
1084
|
+
let deviceDisplay = 'Unknown Device';
|
|
1085
|
+
if (activation.deviceInfo) {
|
|
1086
|
+
deviceDisplay = activation.deviceInfo.deviceName ||
|
|
1087
|
+
`${activation.deviceInfo.platform || 'Unknown'} • ${activation.deviceInfo.browser || 'Unknown'}`;
|
|
1088
|
+
} else if (activation.name && activation.name !== currentInstanceId) {
|
|
1089
|
+
deviceDisplay = activation.name;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
item.innerHTML = `
|
|
1093
|
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
|
|
1094
|
+
<span style="font-weight: 500; font-size: 13px; color: #333;">Activation ${index + 1}</span>
|
|
1095
|
+
${isCurrentDevice ? '<span style="background: #4caf50; color: white; padding: 2px 6px; border-radius: 10px; font-size: 10px; font-weight: bold;">CURRENT</span>' : ''}
|
|
1096
|
+
</div>
|
|
1097
|
+
<div style="font-size: 12px; color: #666; margin-bottom: 4px;">
|
|
1098
|
+
<strong style="color: #333;">${deviceDisplay}</strong>
|
|
1099
|
+
</div>
|
|
1100
|
+
<div style="font-size: 11px; color: #999; margin-bottom: 8px;">
|
|
1101
|
+
${formattedDate}
|
|
1102
|
+
</div>
|
|
1103
|
+
<button
|
|
1104
|
+
class="deactivate-btn-inline"
|
|
1105
|
+
data-instance-id="${activation.identifier}"
|
|
1106
|
+
data-activation-number="${index + 1}"
|
|
1107
|
+
data-license-key="${licenseKey}"
|
|
1108
|
+
style="padding: 5px 10px; background: ${isCurrentDevice ? '#ccc' : '#f44336'}; color: white; border: none; border-radius: 3px; cursor: ${isCurrentDevice ? 'not-allowed' : 'pointer'}; font-size: 11px; width: 100%;"
|
|
1109
|
+
${isCurrentDevice ? 'disabled' : ''}
|
|
1110
|
+
>
|
|
1111
|
+
${isCurrentDevice ? '✓ Current Device' : 'Deactivate'}
|
|
1112
|
+
</button>
|
|
1113
|
+
`;
|
|
1114
|
+
|
|
1115
|
+
// Add click handler for deactivate button
|
|
1116
|
+
if (!isCurrentDevice) {
|
|
1117
|
+
const deactivateBtn = item.querySelector('.deactivate-btn-inline');
|
|
1118
|
+
deactivateBtn.addEventListener('click', () => {
|
|
1119
|
+
handleInlineDeactivate(activation.identifier, index + 1, licenseKey);
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
container.appendChild(item);
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
/**
|
|
1128
|
+
* Handle deactivation of an instance (inline)
|
|
1129
|
+
*/
|
|
1130
|
+
async function handleInlineDeactivate(instanceId, activationNumber, licenseKey) {
|
|
1131
|
+
const confirmed = confirm(
|
|
1132
|
+
`Are you sure you want to deactivate Activation ${activationNumber}?\n\n` +
|
|
1133
|
+
`This will free up an activation slot and automatically activate on this device.`
|
|
1134
|
+
);
|
|
1135
|
+
|
|
1136
|
+
if (!confirmed) {
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
const messageDiv = document.getElementById('license-message');
|
|
1141
|
+
|
|
1142
|
+
try {
|
|
1143
|
+
// Disable all deactivate buttons
|
|
1144
|
+
const buttons = document.querySelectorAll('.deactivate-btn-inline');
|
|
1145
|
+
buttons.forEach(btn => btn.disabled = true);
|
|
1146
|
+
|
|
1147
|
+
messageDiv.textContent = 'Deactivating...';
|
|
1148
|
+
messageDiv.style.color = '#2196F3';
|
|
1149
|
+
|
|
1150
|
+
// Deactivate the instance
|
|
1151
|
+
const result = await licenseClient.deactivateInstance(licenseKey, instanceId);
|
|
1152
|
+
console.log('[License] Deactivation result:', result);
|
|
1153
|
+
|
|
1154
|
+
if (result.deactivated) {
|
|
1155
|
+
messageDiv.textContent = 'Deactivated! Activating on this device...';
|
|
1156
|
+
messageDiv.style.color = '#4caf50';
|
|
1157
|
+
|
|
1158
|
+
// Hide inline activation manager
|
|
1159
|
+
hideInlineActivationManager();
|
|
1160
|
+
|
|
1161
|
+
// Wait a moment, then retry activation
|
|
1162
|
+
setTimeout(async () => {
|
|
1163
|
+
// Set the license key in the input
|
|
1164
|
+
document.getElementById('license-key-input').value = licenseKey;
|
|
1165
|
+
|
|
1166
|
+
// Retry activation automatically
|
|
1167
|
+
await handleLicenseActivation();
|
|
1168
|
+
}, 1000);
|
|
1169
|
+
|
|
1170
|
+
} else {
|
|
1171
|
+
throw new Error(result.error || 'Deactivation failed');
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
} catch (error) {
|
|
1175
|
+
console.error('[License] Deactivation error:', error);
|
|
1176
|
+
messageDiv.textContent = `Deactivation failed: ${error.message}`;
|
|
1177
|
+
messageDiv.style.color = '#f44336';
|
|
1178
|
+
|
|
1179
|
+
// Re-enable buttons
|
|
1180
|
+
const buttons = document.querySelectorAll('.deactivate-btn-inline:not([disabled])');
|
|
1181
|
+
buttons.forEach(btn => btn.disabled = false);
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
|
|
998
1185
|
// License activation handler
|
|
999
1186
|
async function handleLicenseActivation() {
|
|
1000
1187
|
const licenseKey = document.getElementById('license-key-input').value.trim();
|
|
@@ -1033,43 +1220,12 @@ async function handleLicenseActivation() {
|
|
|
1033
1220
|
|
|
1034
1221
|
if (result.error === 'ACTIVATION_LIMIT_REACHED' ||
|
|
1035
1222
|
(result.error && result.error.toLowerCase().includes('activation limit'))) {
|
|
1036
|
-
console.log('[License] Activation limit reached,
|
|
1037
|
-
messageDiv.textContent = '
|
|
1223
|
+
console.log('[License] Activation limit reached, showing inline activation manager');
|
|
1224
|
+
messageDiv.textContent = 'Please deactivate an existing activation to continue';
|
|
1038
1225
|
messageDiv.style.color = '#ff9800';
|
|
1039
1226
|
|
|
1040
|
-
//
|
|
1041
|
-
await
|
|
1042
|
-
'ls_license_key': licenseKey.trim(),
|
|
1043
|
-
'activation_manager_open': true
|
|
1044
|
-
});
|
|
1045
|
-
|
|
1046
|
-
// Open activation manager in a new window
|
|
1047
|
-
const width = 500;
|
|
1048
|
-
const height = 600;
|
|
1049
|
-
const left = Math.round((screen.width - width) / 2);
|
|
1050
|
-
const top = Math.round((screen.height - height) / 2);
|
|
1051
|
-
|
|
1052
|
-
const activationManagerUrl = chrome.runtime.getURL('activation-manager.html');
|
|
1053
|
-
console.log('[License] Opening activation manager at URL:', activationManagerUrl);
|
|
1054
|
-
|
|
1055
|
-
chrome.windows.create({
|
|
1056
|
-
url: activationManagerUrl,
|
|
1057
|
-
type: 'popup',
|
|
1058
|
-
width: width,
|
|
1059
|
-
height: height,
|
|
1060
|
-
left: left,
|
|
1061
|
-
top: top
|
|
1062
|
-
}, (window) => {
|
|
1063
|
-
if (chrome.runtime.lastError) {
|
|
1064
|
-
console.error('[License] Failed to open activation manager:', chrome.runtime.lastError);
|
|
1065
|
-
messageDiv.textContent = 'Error: Could not open activation manager. See console.';
|
|
1066
|
-
messageDiv.style.color = '#f44336';
|
|
1067
|
-
} else {
|
|
1068
|
-
console.log('[License] Activation manager window created:', window);
|
|
1069
|
-
messageDiv.textContent = 'Please deactivate an existing activation to continue';
|
|
1070
|
-
messageDiv.style.color = '#ff9800';
|
|
1071
|
-
}
|
|
1072
|
-
});
|
|
1227
|
+
// Show inline activation manager
|
|
1228
|
+
await showInlineActivationManager(licenseKey.trim());
|
|
1073
1229
|
} else {
|
|
1074
1230
|
messageDiv.textContent = result.error || 'Invalid license key';
|
|
1075
1231
|
messageDiv.style.color = '#f44336';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dynamicu/chromedebug-mcp",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.3",
|
|
4
4
|
"description": "ChromeDebug MCP - MCP server that provides full control over a Chrome browser instance for debugging and automation with AI assistants like Claude Code",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -50,6 +50,7 @@ module.exports = {
|
|
|
50
50
|
{ from: 'chrome-extension/web-vitals.iife.js', to: 'web-vitals.iife.js' },
|
|
51
51
|
|
|
52
52
|
// Copy supporting modules (these are imported by bundled scripts)
|
|
53
|
+
{ from: 'chrome-extension/disable-react-devtools.js', to: 'disable-react-devtools.js' },
|
|
53
54
|
{ from: 'chrome-extension/pii-redactor.js', to: 'pii-redactor.js' },
|
|
54
55
|
{ from: 'chrome-extension/data-buffer.js', to: 'data-buffer.js' },
|
|
55
56
|
{ from: 'chrome-extension/performance-monitor.js', to: 'performance-monitor.js' },
|
|
@@ -52,6 +52,7 @@ module.exports = {
|
|
|
52
52
|
{ from: 'chrome-extension/web-vitals.iife.js', to: 'web-vitals.iife.js' },
|
|
53
53
|
|
|
54
54
|
// Copy supporting modules (these are imported by bundled scripts)
|
|
55
|
+
{ from: 'chrome-extension/disable-react-devtools.js', to: 'disable-react-devtools.js' },
|
|
55
56
|
{ from: 'chrome-extension/pii-redactor.js', to: 'pii-redactor.js' },
|
|
56
57
|
{ from: 'chrome-extension/data-buffer.js', to: 'data-buffer.js' },
|
|
57
58
|
{ from: 'chrome-extension/performance-monitor.js', to: 'performance-monitor.js' },
|
package/src/cli.js
CHANGED
|
@@ -38,6 +38,13 @@ async function main() {
|
|
|
38
38
|
// Parse arguments first to get session settings
|
|
39
39
|
const args = app.parseArguments();
|
|
40
40
|
|
|
41
|
+
// Set database path environment variable if provided via CLI
|
|
42
|
+
// This ensures the database singleton uses the correct path BEFORE initialization
|
|
43
|
+
if (args.dbPath) {
|
|
44
|
+
process.env.CHROMEDEBUG_DB_PATH = args.dbPath;
|
|
45
|
+
logger.debug(`[CLI] Database path set: ${args.dbPath}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
41
48
|
// Detect Windows and provide WSL2 guidance
|
|
42
49
|
if (process.platform === 'win32' && !process.env.WSL_DISTRO_NAME && !process.env.CHROMEDEBUG_FORCE_WINDOWS) {
|
|
43
50
|
console.error(`
|
package/src/database.js
CHANGED
|
@@ -3,6 +3,7 @@ import Database from 'better-sqlite3';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
5
|
import fs from 'fs';
|
|
6
|
+
import os from 'os';
|
|
6
7
|
import { ProjectManager } from './services/project-manager.js';
|
|
7
8
|
import logger from './utils/logger.js';
|
|
8
9
|
|
|
@@ -11,49 +12,90 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
11
12
|
// Initialize project manager for path resolution
|
|
12
13
|
const projectManager = new ProjectManager();
|
|
13
14
|
|
|
14
|
-
/**
|
|
15
|
-
* Gets the appropriate database path (always use global chromedebug.db)
|
|
16
|
-
* @returns {string} Database file path
|
|
17
|
-
*/
|
|
18
|
-
function getDatabasePath() {
|
|
19
|
-
// Always use global database for cross-project accessibility
|
|
20
|
-
const globalDbPath = path.join(__dirname, '../data/chromedebug.db');
|
|
21
|
-
|
|
22
|
-
// Ensure data directory exists
|
|
23
|
-
const dbDir = path.dirname(globalDbPath);
|
|
24
|
-
if (!fs.existsSync(dbDir)) {
|
|
25
|
-
fs.mkdirSync(dbDir, { recursive: true });
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
logger.debug(`[Database] Using global database: ${globalDbPath}`);
|
|
29
|
-
return globalDbPath;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Get the database path (project-local or global)
|
|
33
|
-
const DB_PATH = getDatabasePath();
|
|
34
|
-
|
|
35
15
|
class ChromePilotDatabase {
|
|
36
|
-
constructor() {
|
|
16
|
+
constructor(options = {}) {
|
|
37
17
|
this.db = null;
|
|
38
18
|
this.initialized = false;
|
|
19
|
+
|
|
20
|
+
// Priority: options.dbPath > env var > centralized default
|
|
21
|
+
this.dbPath = options.dbPath ||
|
|
22
|
+
process.env.CHROMEDEBUG_DB_PATH ||
|
|
23
|
+
this.getCentralDbPath();
|
|
24
|
+
|
|
25
|
+
logger.debug(`[Database] Using database path: ${this.dbPath}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Gets the centralized database path in user's home directory
|
|
30
|
+
* @returns {string} Centralized database file path
|
|
31
|
+
*/
|
|
32
|
+
getCentralDbPath() {
|
|
33
|
+
return path.join(os.homedir(), '.chromedebug', 'chrome-pilot.db');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Ensures the database directory exists with proper permissions
|
|
38
|
+
*/
|
|
39
|
+
ensureDbDirectory() {
|
|
40
|
+
const dir = path.dirname(this.dbPath);
|
|
41
|
+
if (!fs.existsSync(dir)) {
|
|
42
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
43
|
+
logger.debug(`[Database] Created database directory: ${dir}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Migrates from old project-local database to centralized database
|
|
49
|
+
* Only runs if:
|
|
50
|
+
* 1. We're using the central DB location (not a custom path)
|
|
51
|
+
* 2. Old project DB exists
|
|
52
|
+
* 3. Central DB doesn't exist yet
|
|
53
|
+
*/
|
|
54
|
+
migrateIfNeeded() {
|
|
55
|
+
const projectDb = path.join(__dirname, '../data/chrome-pilot.db');
|
|
56
|
+
const usingCentralDb = this.dbPath === this.getCentralDbPath();
|
|
57
|
+
|
|
58
|
+
// Only migrate if using centralized path, project DB exists, and central doesn't
|
|
59
|
+
if (usingCentralDb && fs.existsSync(projectDb) && !fs.existsSync(this.dbPath)) {
|
|
60
|
+
logger.info(`[Database] Migrating database from ${projectDb} to ${this.dbPath}`);
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
// Ensure central directory exists
|
|
64
|
+
this.ensureDbDirectory();
|
|
65
|
+
|
|
66
|
+
// Copy the database file
|
|
67
|
+
fs.copyFileSync(projectDb, this.dbPath);
|
|
68
|
+
|
|
69
|
+
logger.info('[Database] Migration complete. Old database preserved at original location.');
|
|
70
|
+
logger.info('[Database] All future recordings will be stored in the centralized database.');
|
|
71
|
+
} catch (error) {
|
|
72
|
+
logger.error(`[Database] Migration failed: ${error.message}`);
|
|
73
|
+
logger.error('[Database] Will create new database at centralized location.');
|
|
74
|
+
}
|
|
75
|
+
} else if (usingCentralDb && fs.existsSync(this.dbPath)) {
|
|
76
|
+
logger.debug('[Database] Using existing centralized database');
|
|
77
|
+
} else if (!usingCentralDb) {
|
|
78
|
+
logger.debug(`[Database] Using custom database path: ${this.dbPath}`);
|
|
79
|
+
}
|
|
39
80
|
}
|
|
40
81
|
|
|
41
82
|
init() {
|
|
42
83
|
if (this.initialized) return;
|
|
43
|
-
|
|
44
|
-
// Ensure
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
84
|
+
|
|
85
|
+
// Ensure database directory exists
|
|
86
|
+
this.ensureDbDirectory();
|
|
87
|
+
|
|
88
|
+
// Migrate from old location if needed
|
|
89
|
+
this.migrateIfNeeded();
|
|
90
|
+
|
|
91
|
+
// Open database connection
|
|
92
|
+
this.db = new Database(this.dbPath);
|
|
51
93
|
this.db.pragma('journal_mode = WAL'); // Enable WAL mode for better concurrent access
|
|
52
|
-
|
|
94
|
+
|
|
53
95
|
// Create tables
|
|
54
96
|
this.createTables();
|
|
55
97
|
this.initialized = true;
|
|
56
|
-
logger.debug(`ChromeDebug MCP database initialized at: ${
|
|
98
|
+
logger.debug(`ChromeDebug MCP database initialized at: ${this.dbPath}`);
|
|
57
99
|
}
|
|
58
100
|
|
|
59
101
|
createTables() {
|
|
@@ -1300,16 +1342,16 @@ class ChromePilotDatabase {
|
|
|
1300
1342
|
// Get database stats
|
|
1301
1343
|
getStats() {
|
|
1302
1344
|
this.init();
|
|
1303
|
-
|
|
1345
|
+
|
|
1304
1346
|
const recordingsCount = this.db.prepare(`SELECT COUNT(*) as count FROM recordings`).get().count;
|
|
1305
1347
|
const framesCount = this.db.prepare(`SELECT COUNT(*) as count FROM frames`).get().count;
|
|
1306
1348
|
const logsCount = this.db.prepare(`SELECT COUNT(*) as count FROM console_logs`).get().count;
|
|
1307
|
-
|
|
1349
|
+
|
|
1308
1350
|
return {
|
|
1309
1351
|
recordings: recordingsCount,
|
|
1310
1352
|
frames: framesCount,
|
|
1311
1353
|
logs: logsCount,
|
|
1312
|
-
dbPath:
|
|
1354
|
+
dbPath: this.dbPath
|
|
1313
1355
|
};
|
|
1314
1356
|
}
|
|
1315
1357
|
|
|
@@ -2175,5 +2217,8 @@ class ChromePilotDatabase {
|
|
|
2175
2217
|
}
|
|
2176
2218
|
}
|
|
2177
2219
|
|
|
2178
|
-
// Export singleton instance
|
|
2179
|
-
export const database = new ChromePilotDatabase();
|
|
2220
|
+
// Export singleton instance (uses default centralized path)
|
|
2221
|
+
export const database = new ChromePilotDatabase();
|
|
2222
|
+
|
|
2223
|
+
// Export class for custom instantiation if needed
|
|
2224
|
+
export { ChromePilotDatabase };
|
package/src/index.js
CHANGED
|
@@ -46,6 +46,13 @@ class ChromePilotApp {
|
|
|
46
46
|
const args = this.parseArguments();
|
|
47
47
|
const finalOptions = { ...options, ...args };
|
|
48
48
|
|
|
49
|
+
// Set database path environment variable if provided via CLI
|
|
50
|
+
// This ensures the database singleton uses the correct path
|
|
51
|
+
if (finalOptions.dbPath) {
|
|
52
|
+
process.env.CHROMEDEBUG_DB_PATH = finalOptions.dbPath;
|
|
53
|
+
logger.debug(`[App] Database path set via CLI: ${finalOptions.dbPath}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
49
56
|
// Create Chrome controller instance
|
|
50
57
|
this.chromeController = new ChromeController();
|
|
51
58
|
|
|
@@ -144,7 +151,8 @@ class ChromePilotApp {
|
|
|
144
151
|
debug: false,
|
|
145
152
|
sessionId: null,
|
|
146
153
|
noCleanup: false,
|
|
147
|
-
verbose: false
|
|
154
|
+
verbose: false,
|
|
155
|
+
dbPath: null
|
|
148
156
|
};
|
|
149
157
|
|
|
150
158
|
const argv = process.argv.slice(2);
|
|
@@ -169,6 +177,14 @@ class ChromePilotApp {
|
|
|
169
177
|
process.exit(1);
|
|
170
178
|
}
|
|
171
179
|
break;
|
|
180
|
+
case '--db-path':
|
|
181
|
+
if (i + 1 < argv.length) {
|
|
182
|
+
args.dbPath = argv[++i];
|
|
183
|
+
} else {
|
|
184
|
+
logger.error('--db-path requires a value');
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
187
|
+
break;
|
|
172
188
|
case '--no-cleanup':
|
|
173
189
|
args.noCleanup = true;
|
|
174
190
|
break;
|
|
@@ -182,6 +198,8 @@ class ChromePilotApp {
|
|
|
182
198
|
default:
|
|
183
199
|
if (arg.startsWith('--session-id=')) {
|
|
184
200
|
args.sessionId = arg.split('=')[1];
|
|
201
|
+
} else if (arg.startsWith('--db-path=')) {
|
|
202
|
+
args.dbPath = arg.split('=')[1];
|
|
185
203
|
} else if (arg.startsWith('--')) {
|
|
186
204
|
logger.error(`Unknown argument: ${arg}`);
|
|
187
205
|
this.showHelp();
|
|
@@ -216,6 +234,7 @@ Options:
|
|
|
216
234
|
--watch Watch for file changes (development mode)
|
|
217
235
|
--debug Enable debug logging
|
|
218
236
|
--session-id ID Set custom session ID for resource isolation
|
|
237
|
+
--db-path PATH Set custom database file path (default: ~/.chromedebug/chrome-pilot.db)
|
|
219
238
|
--no-cleanup Skip cleanup of dead processes on startup
|
|
220
239
|
--verbose Enable verbose logging including session info
|
|
221
240
|
--help Show this help message
|
|
@@ -225,13 +244,20 @@ Session Isolation:
|
|
|
225
244
|
This allows multiple Chrome Debug MCP instances to run simultaneously
|
|
226
245
|
without interfering with each other.
|
|
227
246
|
|
|
247
|
+
Database Location:
|
|
248
|
+
By default, ChromeDebug uses a centralized database at ~/.chromedebug/chrome-pilot.db
|
|
249
|
+
This allows recordings from all projects to be accessible in one place.
|
|
250
|
+
Use --db-path to override, or set CHROMEDEBUG_DB_PATH environment variable.
|
|
251
|
+
|
|
228
252
|
Examples:
|
|
229
253
|
node src/index.js --session-id claude-session-1 --verbose
|
|
230
254
|
node src/index.js --no-cleanup --debug
|
|
255
|
+
node src/index.js --db-path /custom/path/to/database.db
|
|
231
256
|
|
|
232
257
|
Environment Variables:
|
|
233
|
-
NODE_ENV
|
|
234
|
-
CHROME_PILOT_PORT
|
|
258
|
+
NODE_ENV Set to 'development' for debug mode
|
|
259
|
+
CHROME_PILOT_PORT Configure HTTP server port
|
|
260
|
+
CHROMEDEBUG_DB_PATH Set custom database file path
|
|
235
261
|
|
|
236
262
|
For more information, see the documentation in CLAUDE.md
|
|
237
263
|
`);
|
|
@@ -260,5 +286,26 @@ For more information, see the documentation in CLAUDE.md
|
|
|
260
286
|
}
|
|
261
287
|
|
|
262
288
|
// Export for library usage and testing
|
|
263
|
-
|
|
264
|
-
|
|
289
|
+
export { ChromePilotApp };
|
|
290
|
+
|
|
291
|
+
// Auto-start when run directly (not imported as a module)
|
|
292
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
293
|
+
const app = new ChromePilotApp();
|
|
294
|
+
|
|
295
|
+
// Handle graceful shutdown
|
|
296
|
+
process.on('SIGINT', async () => {
|
|
297
|
+
await app.shutdown();
|
|
298
|
+
process.exit(0);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
process.on('SIGTERM', async () => {
|
|
302
|
+
await app.shutdown();
|
|
303
|
+
process.exit(0);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// Start the application
|
|
307
|
+
app.start().catch(error => {
|
|
308
|
+
logger.error('Failed to start ChromeDebug:', error);
|
|
309
|
+
process.exit(1);
|
|
310
|
+
});
|
|
311
|
+
}
|
package/src/logger.js
CHANGED
|
@@ -77,15 +77,22 @@ export function logHttpRequest(logger, req, options = {}) {
|
|
|
77
77
|
/**
|
|
78
78
|
* Intercept stdout to prevent MCP JSON-RPC pollution
|
|
79
79
|
* Call this at application startup for MCP servers
|
|
80
|
+
* Allows MCP JSON-RPC messages through while redirecting other output
|
|
80
81
|
*/
|
|
81
82
|
export function interceptStdout() {
|
|
82
83
|
const originalWrite = process.stdout.write;
|
|
83
84
|
|
|
84
85
|
process.stdout.write = function(chunk, encoding, callback) {
|
|
85
|
-
// Redirect all stdout to stderr with a warning prefix
|
|
86
86
|
const message = chunk.toString();
|
|
87
|
-
const redirectedMessage = `[STDOUT-REDIRECT] ${message}`;
|
|
88
87
|
|
|
88
|
+
// Allow MCP JSON-RPC messages through (they start with { and contain "jsonrpc")
|
|
89
|
+
if (message.trim().startsWith('{') && message.includes('"jsonrpc"')) {
|
|
90
|
+
// This is an MCP message - let it through to stdout unchanged
|
|
91
|
+
return originalWrite.call(process.stdout, chunk, encoding, callback);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Redirect all other stdout to stderr with a warning prefix
|
|
95
|
+
const redirectedMessage = `[STDOUT-REDIRECT] ${message}`;
|
|
89
96
|
return process.stderr.write(redirectedMessage, encoding, callback);
|
|
90
97
|
};
|
|
91
98
|
|