mongotokyo 1.0.8
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 +65 -0
- package/bin/stealth-solver.js +22 -0
- package/package.json +34 -0
- package/src/configManager.js +111 -0
- package/src/main.js +247 -0
- package/src/modelRotator.js +94 -0
- package/src/panicManager.js +46 -0
- package/src/preload.js +20 -0
- package/src/renderer/overlay/index.html +28 -0
- package/src/renderer/overlay/overlay.css +147 -0
- package/src/renderer/overlay/overlay.js +180 -0
- package/src/renderer/selector/selector.css +54 -0
- package/src/renderer/selector/selector.html +15 -0
- package/src/renderer/selector/selector.js +74 -0
- package/src/renderer/setup/setup.css +236 -0
- package/src/renderer/setup/setup.html +97 -0
- package/src/renderer/setup/setup.js +46 -0
- package/src/secrets.js +7 -0
- package/src/solver.js +153 -0
package/README.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# ⚡ mongotokyo
|
|
2
|
+
|
|
3
|
+
> AI-powered floating overlay that solves coding questions & MCQs on any app or browser — no tab switching, no detection.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🎯 **Works everywhere** — floats on top of any browser, IDE, or app
|
|
8
|
+
- 🔄 **Smart model rotation** — auto-switches AI models on rate limits
|
|
9
|
+
- 🫥 **Panic hide** — press `` ` `` to instantly hide everything
|
|
10
|
+
- 🆓 **Free-tier first** — Gemini 2.0 Flash, Gemini 1.5 Flash, Groq (all free)
|
|
11
|
+
- 💡 **Solves both** — coding questions (with full code) and MCQs (with explanations)
|
|
12
|
+
- 🔒 **Screen capture protection** — overlay is invisible to screen recording tools
|
|
13
|
+
|
|
14
|
+
## Quick Start
|
|
15
|
+
|
|
16
|
+
The fastest way to use it is via `npx`:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npx mongotokyo
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Or install it globally:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install -g mongotokyo
|
|
26
|
+
mongotokyo
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
On first launch, a setup window appears — paste your API key(s) and click **Launch**.
|
|
30
|
+
|
|
31
|
+
## Model Priority (auto-rotates)
|
|
32
|
+
|
|
33
|
+
| Priority | Model | Free Limit |
|
|
34
|
+
|---|---|---|
|
|
35
|
+
| 1 | Gemini 2.0 Flash | 1500 req/day free |
|
|
36
|
+
| 2 | Gemini 1.5 Flash | 1500 req/day free |
|
|
37
|
+
| 3 | Groq Llama Vision | 30 RPM free |
|
|
38
|
+
| 4 | OpenRouter Gemma | Free tier |
|
|
39
|
+
| 5 | GPT-4o Mini | Pay-as-you-go |
|
|
40
|
+
|
|
41
|
+
## Hotkeys
|
|
42
|
+
|
|
43
|
+
| Key | Action |
|
|
44
|
+
|---|---|
|
|
45
|
+
| `Ctrl+Shift+S` | Capture a region & solve |
|
|
46
|
+
| `` ` `` (Backtick) | **PANIC HIDE** — instantly hides all windows |
|
|
47
|
+
| `` ` `` again | Restore all windows |
|
|
48
|
+
| `Ctrl+Shift+H` | Soft toggle panel |
|
|
49
|
+
| `Ctrl+Shift+C` | Clear current answer |
|
|
50
|
+
| `Ctrl+Shift+X` | **SELF DESTRUCT** — instantly quits the app |
|
|
51
|
+
| `Escape` | Cancel region selection |
|
|
52
|
+
|
|
53
|
+
## Getting Free API Keys
|
|
54
|
+
|
|
55
|
+
- **Gemini** (recommended): https://aistudio.google.com/app/apikey
|
|
56
|
+
- **Groq**: https://console.groq.com/keys
|
|
57
|
+
- **OpenRouter**: https://openrouter.ai/keys
|
|
58
|
+
|
|
59
|
+
## Config Location
|
|
60
|
+
|
|
61
|
+
Keys are stored in `~/.mongotokyo/config.json` — fully local, never sent to any third-party server except the AI API you choose.
|
|
62
|
+
|
|
63
|
+
## Reset Setup
|
|
64
|
+
|
|
65
|
+
Delete `~/.mongotokyo/config.json` and re-launch to redo setup.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const electron = require('electron');
|
|
4
|
+
const { spawn } = require('child_process');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const appPath = path.join(__dirname, '..');
|
|
8
|
+
const args = process.argv.slice(2);
|
|
9
|
+
|
|
10
|
+
const proc = spawn(electron, [appPath, ...args], {
|
|
11
|
+
stdio: 'inherit',
|
|
12
|
+
windowsHide: false
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
proc.on('close', (code) => {
|
|
16
|
+
process.exit(code ?? 0);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
proc.on('error', (err) => {
|
|
20
|
+
console.error('Failed to launch stealth-solver:', err.message);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mongotokyo",
|
|
3
|
+
"version": "1.0.8",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "src/main.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"mongotokyo": "bin/stealth-solver.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "electron .",
|
|
11
|
+
"dev": "electron . --dev"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"ai",
|
|
15
|
+
"overlay",
|
|
16
|
+
"coding",
|
|
17
|
+
"mcq",
|
|
18
|
+
"solver",
|
|
19
|
+
"electron"
|
|
20
|
+
],
|
|
21
|
+
"author": "Kashish",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"files": [
|
|
24
|
+
"src",
|
|
25
|
+
"bin",
|
|
26
|
+
"README.md"
|
|
27
|
+
],
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@google/generative-ai": "^0.21.0",
|
|
30
|
+
"dotenv": "^17.4.2",
|
|
31
|
+
"electron": "^31.3.0",
|
|
32
|
+
"openai": "^4.52.0"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
|
|
5
|
+
const CONFIG_DIR = path.join(os.homedir(), '.mongotokyo');
|
|
6
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
7
|
+
const STATE_FILE = path.join(CONFIG_DIR, 'state.json');
|
|
8
|
+
|
|
9
|
+
const DEFAULT_CONFIG = {
|
|
10
|
+
hasSetup: false,
|
|
11
|
+
keys: {
|
|
12
|
+
gemini: '',
|
|
13
|
+
groq: '',
|
|
14
|
+
openrouter: '',
|
|
15
|
+
openai: ''
|
|
16
|
+
},
|
|
17
|
+
panicKey: '`',
|
|
18
|
+
captureKey: 'CommandOrControl+Shift+S'
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// Load secrets if they exist (for npm users)
|
|
22
|
+
let secrets = {};
|
|
23
|
+
try {
|
|
24
|
+
secrets = require('./secrets');
|
|
25
|
+
} catch (e) {
|
|
26
|
+
// Not found
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function ensureDir() {
|
|
30
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
31
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function load() {
|
|
36
|
+
ensureDir();
|
|
37
|
+
let config = { ...DEFAULT_CONFIG };
|
|
38
|
+
|
|
39
|
+
// Fallback to secrets if available
|
|
40
|
+
if (secrets.gemini) config.keys.gemini = secrets.gemini;
|
|
41
|
+
if (secrets.groq) config.keys.groq = secrets.groq;
|
|
42
|
+
if (secrets.gemini || secrets.groq) config.hasSetup = true;
|
|
43
|
+
|
|
44
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
45
|
+
try {
|
|
46
|
+
const saved = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
|
47
|
+
// Only merge if keys are actually set (not placeholders)
|
|
48
|
+
if (saved.keys) {
|
|
49
|
+
for (const [p, k] of Object.entries(saved.keys)) {
|
|
50
|
+
if (k && k !== 'USE_ENV_VARIABLE' && !k.startsWith('env:')) {
|
|
51
|
+
config.keys[p] = k;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (saved.panicKey) config.panicKey = saved.panicKey;
|
|
56
|
+
if (saved.captureKey) config.captureKey = saved.captureKey;
|
|
57
|
+
if (saved.hasSetup !== undefined) config.hasSetup = saved.hasSetup || config.hasSetup;
|
|
58
|
+
} catch {
|
|
59
|
+
// Use defaults on error
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ─── Override with Environment Variables ──────────────────────────────────────
|
|
64
|
+
// This allows the user to skip setup if keys are in .env
|
|
65
|
+
const envKeys = {
|
|
66
|
+
gemini: process.env.GEMINI_KEY || process.env.GEMINI_API_KEY,
|
|
67
|
+
groq: process.env.GROQ_KEY || process.env.GROQ_API_KEY,
|
|
68
|
+
openrouter: process.env.OPENROUTER_KEY || process.env.OPENROUTER_API_KEY,
|
|
69
|
+
openai: process.env.OPENAI_KEY || process.env.OPENAI_API_KEY
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// If any key is found in env, merge it and consider setup complete
|
|
73
|
+
let hasEnvKey = false;
|
|
74
|
+
for (const [provider, val] of Object.entries(envKeys)) {
|
|
75
|
+
if (val) {
|
|
76
|
+
config.keys[provider] = val;
|
|
77
|
+
hasEnvKey = true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (hasEnvKey) {
|
|
82
|
+
config.hasSetup = true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return config;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function save(config) {
|
|
89
|
+
ensureDir();
|
|
90
|
+
const current = load();
|
|
91
|
+
const merged = { ...current, ...config, hasSetup: true };
|
|
92
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2));
|
|
93
|
+
return merged;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function loadState() {
|
|
97
|
+
ensureDir();
|
|
98
|
+
if (!fs.existsSync(STATE_FILE)) return {};
|
|
99
|
+
try {
|
|
100
|
+
return JSON.parse(fs.readFileSync(STATE_FILE, 'utf8'));
|
|
101
|
+
} catch {
|
|
102
|
+
return {};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function saveState(state) {
|
|
107
|
+
ensureDir();
|
|
108
|
+
fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
module.exports = { load, save, loadState, saveState, CONFIG_DIR };
|
package/src/main.js
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
require('dotenv').config({ path: path.join(__dirname, '..', '.env') });
|
|
3
|
+
const {
|
|
4
|
+
app,
|
|
5
|
+
BrowserWindow,
|
|
6
|
+
globalShortcut,
|
|
7
|
+
ipcMain,
|
|
8
|
+
screen,
|
|
9
|
+
desktopCapturer
|
|
10
|
+
} = require('electron');
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
|
|
13
|
+
const configManager = require('./configManager');
|
|
14
|
+
const solver = require('./solver');
|
|
15
|
+
const panicManager = require('./panicManager');
|
|
16
|
+
|
|
17
|
+
let overlayWindow = null;
|
|
18
|
+
let selectorWindow = null;
|
|
19
|
+
let setupWindow = null;
|
|
20
|
+
|
|
21
|
+
// ─── Window Factories ──────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
function createOverlayWindow() {
|
|
24
|
+
const { width, height } = screen.getPrimaryDisplay().bounds;
|
|
25
|
+
|
|
26
|
+
overlayWindow = new BrowserWindow({
|
|
27
|
+
width,
|
|
28
|
+
height,
|
|
29
|
+
x: 0,
|
|
30
|
+
y: 0,
|
|
31
|
+
transparent: true,
|
|
32
|
+
frame: false,
|
|
33
|
+
alwaysOnTop: true,
|
|
34
|
+
skipTaskbar: true,
|
|
35
|
+
resizable: false,
|
|
36
|
+
hasShadow: false,
|
|
37
|
+
webPreferences: {
|
|
38
|
+
preload: path.join(__dirname, 'preload.js'),
|
|
39
|
+
contextIsolation: true,
|
|
40
|
+
nodeIntegration: false
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
overlayWindow.setIgnoreMouseEvents(true, { forward: true });
|
|
45
|
+
overlayWindow.setContentProtection(true);
|
|
46
|
+
overlayWindow.loadFile(path.join(__dirname, 'renderer', 'overlay', 'index.html'));
|
|
47
|
+
panicManager.register(overlayWindow);
|
|
48
|
+
return overlayWindow;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function createSetupWindow() {
|
|
52
|
+
setupWindow = new BrowserWindow({
|
|
53
|
+
width: 580,
|
|
54
|
+
height: 680,
|
|
55
|
+
frame: false,
|
|
56
|
+
transparent: true,
|
|
57
|
+
resizable: false,
|
|
58
|
+
center: true,
|
|
59
|
+
webPreferences: {
|
|
60
|
+
preload: path.join(__dirname, 'preload.js'),
|
|
61
|
+
contextIsolation: true,
|
|
62
|
+
nodeIntegration: false
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
setupWindow.loadFile(path.join(__dirname, 'renderer', 'setup', 'setup.html'));
|
|
67
|
+
return setupWindow;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Selector functionality removed for stealth mode
|
|
71
|
+
|
|
72
|
+
// ─── App Lifecycle ─────────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
app.whenReady().then(() => {
|
|
75
|
+
const config = configManager.load();
|
|
76
|
+
|
|
77
|
+
if (!config.hasSetup) {
|
|
78
|
+
createSetupWindow();
|
|
79
|
+
} else {
|
|
80
|
+
createOverlayWindow();
|
|
81
|
+
registerHotkeys();
|
|
82
|
+
console.log('\x1b[32m%s\x1b[0m', '⚡ mongotokyo Active');
|
|
83
|
+
const config = configManager.load();
|
|
84
|
+
const hasEnv = !!(process.env.GEMINI_KEY || process.env.GROQ_KEY);
|
|
85
|
+
if (hasEnv) {
|
|
86
|
+
console.log('\x1b[35m%s\x1b[0m', '🔑 Keys loaded from .env');
|
|
87
|
+
}
|
|
88
|
+
console.log('\x1b[36m%s\x1b[0m', 'Hotkeys:');
|
|
89
|
+
console.log(' - Ctrl+Shift+S: Capture & Solve');
|
|
90
|
+
console.log(' - Ctrl+Shift+M: Toggle Mouse (for scroll/copy)');
|
|
91
|
+
console.log(' - ` (Backtick): Panic Hide/Show');
|
|
92
|
+
console.log(' - Ctrl+Shift+X: Exit');
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
app.on('will-quit', () => {
|
|
97
|
+
globalShortcut.unregisterAll();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
app.on('window-all-closed', () => {
|
|
101
|
+
app.quit();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// ─── Hotkeys ──────────────────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
function registerHotkeys() {
|
|
107
|
+
// Capture + solve (Ctrl+Shift+S)
|
|
108
|
+
globalShortcut.register('CommandOrControl+Shift+S', async () => {
|
|
109
|
+
console.log('\x1b[33m%s\x1b[0m', '📸 Capturing screen...');
|
|
110
|
+
overlayWindow.webContents.send('status', { type: 'loading', message: 'Solving...' });
|
|
111
|
+
await captureAndSolve();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Panic hide/show (backtick ` — fastest single key)
|
|
115
|
+
// Hides the ENTIRE overlay window from screen share / prying eyes
|
|
116
|
+
let panicState = false;
|
|
117
|
+
globalShortcut.register('`', () => {
|
|
118
|
+
panicState = !panicState;
|
|
119
|
+
if (panicState) {
|
|
120
|
+
if (overlayWindow && overlayWindow.isVisible()) {
|
|
121
|
+
overlayWindow.webContents.send('panic', 'hide');
|
|
122
|
+
// Small delay so the renderer can process, then hide the window
|
|
123
|
+
setTimeout(() => { if (overlayWindow) overlayWindow.hide(); }, 80);
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
if (overlayWindow) {
|
|
127
|
+
overlayWindow.show();
|
|
128
|
+
overlayWindow.webContents.send('panic', 'show');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Toggle just the answer text visibility (Ctrl+Shift+H)
|
|
134
|
+
globalShortcut.register('CommandOrControl+Shift+H', () => {
|
|
135
|
+
if (!overlayWindow) return;
|
|
136
|
+
overlayWindow.webContents.send('toggle-text');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Clear answer (Ctrl+Shift+C)
|
|
140
|
+
globalShortcut.register('CommandOrControl+Shift+C', () => {
|
|
141
|
+
if (overlayWindow) overlayWindow.webContents.send('clear');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Toggle Mouse Interaction (Ctrl+Shift+M)
|
|
145
|
+
let ignoreMouse = true;
|
|
146
|
+
globalShortcut.register('CommandOrControl+Shift+M', () => {
|
|
147
|
+
ignoreMouse = !ignoreMouse;
|
|
148
|
+
if (overlayWindow) {
|
|
149
|
+
overlayWindow.setIgnoreMouseEvents(ignoreMouse, { forward: true });
|
|
150
|
+
overlayWindow.webContents.send('status', {
|
|
151
|
+
type: 'info',
|
|
152
|
+
message: ignoreMouse ? 'Mouse: OFF' : 'Mouse: ON'
|
|
153
|
+
});
|
|
154
|
+
console.log(`[Main] Mouse Ignore: ${ignoreMouse}`);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// SELF DESTRUCT / Finish Test (Ctrl+Shift+X)
|
|
159
|
+
// Instantly quits the app and unregisters all keys
|
|
160
|
+
globalShortcut.register('CommandOrControl+Shift+X', () => {
|
|
161
|
+
app.quit();
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ─── IPC Handlers ─────────────────────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
// Setup complete
|
|
168
|
+
ipcMain.on('setup-complete', async (_, config) => {
|
|
169
|
+
configManager.save(config);
|
|
170
|
+
if (setupWindow) { setupWindow.close(); setupWindow = null; }
|
|
171
|
+
createOverlayWindow();
|
|
172
|
+
registerHotkeys();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// Get config
|
|
176
|
+
ipcMain.handle('get-config', () => configManager.load());
|
|
177
|
+
|
|
178
|
+
// Save config
|
|
179
|
+
ipcMain.on('save-config', (_, config) => configManager.save(config));
|
|
180
|
+
|
|
181
|
+
async function captureAndSolve() {
|
|
182
|
+
try {
|
|
183
|
+
const sources = await desktopCapturer.getSources({
|
|
184
|
+
types: ['screen'],
|
|
185
|
+
thumbnailSize: screen.getPrimaryDisplay().bounds
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const source = sources[0];
|
|
189
|
+
const base64 = source.thumbnail.toDataURL();
|
|
190
|
+
|
|
191
|
+
if (overlayWindow) {
|
|
192
|
+
overlayWindow.show();
|
|
193
|
+
overlayWindow.setContentProtection(true);
|
|
194
|
+
overlayWindow.webContents.send('status', { type: 'loading', message: 'Thinking...' });
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const config = configManager.load();
|
|
198
|
+
console.log('\x1b[33m%s\x1b[0m', '🧠 Thinking...');
|
|
199
|
+
const result = await solver.solve(base64, config);
|
|
200
|
+
console.log('\x1b[32m%s\x1b[0m', '✅ Result received!');
|
|
201
|
+
|
|
202
|
+
// ── Route based on question category ──────────────────────────────────────
|
|
203
|
+
if (result.type === 'web') {
|
|
204
|
+
// MERN / Web project → create folder on Desktop with all files
|
|
205
|
+
if (result.files && result.files.length > 0) {
|
|
206
|
+
const desktopPath = app.getPath('desktop');
|
|
207
|
+
const rawName = result.questionName || `WebProject_${Date.now()}`;
|
|
208
|
+
const folderName = rawName.replace(/[\\/:*?"<>|]/g, '_').trim() || `WebProject_${Date.now()}`;
|
|
209
|
+
const folderPath = path.join(desktopPath, folderName);
|
|
210
|
+
|
|
211
|
+
if (!fs.existsSync(folderPath)) {
|
|
212
|
+
fs.mkdirSync(folderPath, { recursive: true });
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
for (const file of result.files) {
|
|
216
|
+
if (file.name && file.content) {
|
|
217
|
+
const safeName = file.name.replace(/[\\/:*?"<>|]/g, '_');
|
|
218
|
+
// Support nested paths like "src/App.jsx"
|
|
219
|
+
const fullPath = path.join(folderPath, safeName);
|
|
220
|
+
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
221
|
+
fs.writeFileSync(fullPath, file.content, 'utf8');
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
// DSA / MCQ / mixed → show on overlay screen (no folder needed)
|
|
227
|
+
|
|
228
|
+
if (overlayWindow) {
|
|
229
|
+
overlayWindow.webContents.send('answer', result);
|
|
230
|
+
}
|
|
231
|
+
} catch (err) {
|
|
232
|
+
if (overlayWindow) {
|
|
233
|
+
console.error('\x1b[31m%s\x1b[0m', `❌ Error: ${err.message}`);
|
|
234
|
+
overlayWindow.show();
|
|
235
|
+
overlayWindow.setContentProtection(true);
|
|
236
|
+
overlayWindow.webContents.send('status', { type: 'error', message: err.message });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Window controls from renderer
|
|
242
|
+
ipcMain.on('set-ignore-mouse', (event, ignore) => {
|
|
243
|
+
const win = BrowserWindow.fromWebContents(event.sender);
|
|
244
|
+
if (win) win.setIgnoreMouseEvents(ignore, { forward: true });
|
|
245
|
+
});
|
|
246
|
+
ipcMain.on('hide-window', () => { if (overlayWindow) overlayWindow.hide(); });
|
|
247
|
+
ipcMain.on('quit-app', () => app.quit());
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smart model rotation pool.
|
|
3
|
+
* Models are tried in priority order.
|
|
4
|
+
* When a model hits a rate limit, it enters a cooldown period
|
|
5
|
+
* and the next available model is used automatically.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const MODEL_POOL = [
|
|
9
|
+
{
|
|
10
|
+
id: 'gemini-2.0-flash',
|
|
11
|
+
provider: 'gemini',
|
|
12
|
+
model: 'gemini-2.0-flash',
|
|
13
|
+
name: 'Gemini 2.0 Flash',
|
|
14
|
+
requiresKey: 'gemini'
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
id: 'gemini-1.5-flash',
|
|
18
|
+
provider: 'gemini',
|
|
19
|
+
model: 'gemini-1.5-flash',
|
|
20
|
+
name: 'Gemini 1.5 Flash',
|
|
21
|
+
requiresKey: 'gemini'
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: 'gemini-1.5-pro',
|
|
25
|
+
provider: 'gemini',
|
|
26
|
+
model: 'gemini-1.5-pro',
|
|
27
|
+
name: 'Gemini 1.5 Pro',
|
|
28
|
+
requiresKey: 'gemini'
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: 'groq-llama-4-scout',
|
|
32
|
+
provider: 'groq',
|
|
33
|
+
model: 'meta-llama/llama-4-scout-17b-16e-instruct',
|
|
34
|
+
name: 'Groq Llama 4 Scout',
|
|
35
|
+
requiresKey: 'groq'
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: 'gpt-4o-mini',
|
|
39
|
+
provider: 'openai',
|
|
40
|
+
model: 'gpt-4o-mini',
|
|
41
|
+
name: 'GPT-4o Mini',
|
|
42
|
+
requiresKey: 'openai'
|
|
43
|
+
}
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
// modelId -> timestamp until which it's cooling down
|
|
47
|
+
const coolingModels = new Map();
|
|
48
|
+
|
|
49
|
+
function getAvailableModels(keys) {
|
|
50
|
+
const now = Date.now();
|
|
51
|
+
return MODEL_POOL.filter((m) => {
|
|
52
|
+
if (!keys[m.requiresKey]) return false; // no API key configured
|
|
53
|
+
const coolUntil = coolingModels.get(m.id) || 0;
|
|
54
|
+
return now > coolUntil; // not cooling
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function cooldown(modelId, seconds = 60) {
|
|
59
|
+
coolingModels.set(modelId, Date.now() + seconds * 1000);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function getNextModel(keys) {
|
|
63
|
+
const available = getAvailableModels(keys);
|
|
64
|
+
return available[0] || null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function isRateLimitError(err) {
|
|
68
|
+
const msg = (err?.message || '').toLowerCase();
|
|
69
|
+
const status = err?.status || err?.statusCode || 0;
|
|
70
|
+
return (
|
|
71
|
+
status === 429 ||
|
|
72
|
+
msg.includes('rate limit') ||
|
|
73
|
+
msg.includes('quota') ||
|
|
74
|
+
msg.includes('resource exhausted') ||
|
|
75
|
+
msg.includes('too many requests')
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function getModelStatus(keys) {
|
|
80
|
+
const now = Date.now();
|
|
81
|
+
return MODEL_POOL.map((m) => {
|
|
82
|
+
const hasKey = !!keys[m.requiresKey];
|
|
83
|
+
const coolUntil = coolingModels.get(m.id) || 0;
|
|
84
|
+
const cooling = now < coolUntil;
|
|
85
|
+
return {
|
|
86
|
+
...m,
|
|
87
|
+
hasKey,
|
|
88
|
+
cooling,
|
|
89
|
+
coolRemaining: cooling ? Math.ceil((coolUntil - now) / 1000) : 0
|
|
90
|
+
};
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports = { MODEL_POOL, getNextModel, cooldown, isRateLimitError, getModelStatus };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const { BrowserWindow } = require('electron');
|
|
2
|
+
|
|
3
|
+
let isPanicked = false;
|
|
4
|
+
const hiddenState = new Map(); // windowId -> wasVisible
|
|
5
|
+
|
|
6
|
+
function register(win) {
|
|
7
|
+
win.on('closed', () => hiddenState.delete(win.id));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function toggle() {
|
|
11
|
+
if (isPanicked) {
|
|
12
|
+
restore();
|
|
13
|
+
} else {
|
|
14
|
+
hide();
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function hide() {
|
|
19
|
+
isPanicked = true;
|
|
20
|
+
hiddenState.clear();
|
|
21
|
+
|
|
22
|
+
BrowserWindow.getAllWindows().forEach((win) => {
|
|
23
|
+
hiddenState.set(win.id, win.isVisible());
|
|
24
|
+
if (win.isVisible()) {
|
|
25
|
+
win.hide();
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function restore() {
|
|
31
|
+
isPanicked = false;
|
|
32
|
+
|
|
33
|
+
BrowserWindow.getAllWindows().forEach((win) => {
|
|
34
|
+
if (hiddenState.get(win.id)) {
|
|
35
|
+
win.show();
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
hiddenState.clear();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isPanic() {
|
|
43
|
+
return isPanicked;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = { register, toggle, hide, restore, isPanic };
|
package/src/preload.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const { contextBridge, ipcRenderer } = require('electron');
|
|
2
|
+
|
|
3
|
+
contextBridge.exposeInMainWorld('electronAPI', {
|
|
4
|
+
// Overlay receives
|
|
5
|
+
onAnswer: (cb) => ipcRenderer.on('answer', (_, data) => cb(data)),
|
|
6
|
+
onStatus: (cb) => ipcRenderer.on('status', (_, data) => cb(data)),
|
|
7
|
+
onClear: (cb) => ipcRenderer.on('clear', () => cb()),
|
|
8
|
+
onPanic: (cb) => ipcRenderer.on('panic', (_, state) => cb(state)),
|
|
9
|
+
onToggleText: (cb) => ipcRenderer.on('toggle-text', () => cb()),
|
|
10
|
+
|
|
11
|
+
// Setup
|
|
12
|
+
completeSetup: (config) => ipcRenderer.send('setup-complete', config),
|
|
13
|
+
getConfig: () => ipcRenderer.invoke('get-config'),
|
|
14
|
+
saveConfig: (config) => ipcRenderer.send('save-config', config),
|
|
15
|
+
|
|
16
|
+
// Window controls
|
|
17
|
+
setIgnoreMouse: (ignore) => ipcRenderer.send('set-ignore-mouse', ignore),
|
|
18
|
+
hideWindow: () => ipcRenderer.send('hide-window'),
|
|
19
|
+
quitApp: () => ipcRenderer.send('quit-app')
|
|
20
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Stealth Solver</title>
|
|
7
|
+
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
|
8
|
+
<link rel="stylesheet" href="overlay.css" />
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
|
|
12
|
+
<!-- Tiny pulsing dot while loading (bottom-right) -->
|
|
13
|
+
<div id="loadingDot"></div>
|
|
14
|
+
|
|
15
|
+
<!-- Error dot (replaces loading dot on failure) -->
|
|
16
|
+
<div id="errorDot"></div>
|
|
17
|
+
|
|
18
|
+
<!-- Answer panel — MCQ text OR DSA code block, faint bottom-right -->
|
|
19
|
+
<div id="answerPanel">
|
|
20
|
+
<div id="answerLines"></div>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<!-- Short-lived saved notification for web/MERN questions -->
|
|
24
|
+
<div id="savedToast"></div>
|
|
25
|
+
|
|
26
|
+
<script src="overlay.js"></script>
|
|
27
|
+
</body>
|
|
28
|
+
</html>
|