lumabrowser 1.0.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/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ PROPRIETARY SOFTWARE LICENSE
2
+ Copyright (c) 2026 LumaBrowser. All rights reserved.
3
+
4
+ This software and associated documentation files (the "Software") are the
5
+ proprietary property of the copyright holder. The Software is provided solely
6
+ for testing and evaluation purposes.
7
+
8
+ RESTRICTIONS:
9
+ - You may NOT distribute, sublicense, or sell copies of the Software.
10
+ - You may NOT modify, merge, or create derivative works based on the Software.
11
+ - You may NOT use the Software for commercial purposes without a separate
12
+ commercial license agreement.
13
+ - You may NOT reverse engineer, decompile, or disassemble the Software.
14
+
15
+ The Software is provided "AS IS", without warranty of any kind, express or
16
+ implied. In no event shall the copyright holder be liable for any claim,
17
+ damages, or other liability arising from the use of the Software.
18
+
19
+ For commercial licensing inquiries, please contact the copyright holder.
package/README.md ADDED
@@ -0,0 +1,60 @@
1
+ # LumaBrowser
2
+
3
+ > A multi-tab browser with built-in AI automation, web notification interception, and a plugin architecture. Controllable locally or remotely via REST API and MCP (Model Context Protocol).
4
+
5
+ ## Install & Run
6
+
7
+ ```bash
8
+ npx lumabrowser start
9
+ ```
10
+
11
+ That's it. The first run downloads the right build for your OS from the [official GitHub Releases](https://github.com/amurgola/notification2webhookbrowser/releases), caches it under `~/.lumabrowser/`, and launches it.
12
+
13
+ Subsequent runs launch from cache — no re-download unless you ask for one.
14
+
15
+ ## Commands
16
+
17
+ | Command | What it does |
18
+ |---|---|
19
+ | `npx lumabrowser start` | Launch LumaBrowser (installs on first run). |
20
+ | `npx lumabrowser update` | Force re-download of the latest release. |
21
+ | `npx lumabrowser version` | Print the installed LumaBrowser version. |
22
+ | `npx lumabrowser uninstall` | Remove the cached installation. |
23
+ | `npx lumabrowser help` | Show all commands. |
24
+
25
+ ## Global install
26
+
27
+ If you'd rather not type `npx` every time:
28
+
29
+ ```bash
30
+ npm install -g lumabrowser
31
+ lumabrowser start
32
+ ```
33
+
34
+ ## Platform support
35
+
36
+ | OS | Asset type |
37
+ |---|---|
38
+ | Windows (x64) | Portable `.exe` |
39
+ | macOS (Intel / Apple Silicon) | `.app` bundle extracted from `.zip` |
40
+ | Linux (x64) | `.AppImage` |
41
+
42
+ ## What you get
43
+
44
+ - **Multi-tab Electron browser** with system tray integration.
45
+ - **REST API** (`http://localhost:3000`) for programmatic control.
46
+ - **MCP server** so Claude Desktop and other AI tools can drive the browser.
47
+ - **Web notification interception** — any page's `Notification(...)` calls get forwarded to a webhook.
48
+ - **Network watcher** — URL patterns trigger webhooks when matching responses fire.
49
+ - **AI chat sidebar** that can navigate, click, fill forms, and take screenshots.
50
+ - **Local WebGPU LLMs** (Qwen 2.5) for private inference — no API keys required.
51
+ - **Extension system** with runtime enable/disable and zip-based installation.
52
+
53
+ ## Learn more
54
+
55
+ - Docs & downloads: [lumabyte.com](https://lumabyte.com)
56
+ - Source & issues: [github.com/amurgola/notification2webhookbrowser](https://github.com/amurgola/notification2webhookbrowser)
57
+
58
+ ## License
59
+
60
+ Proprietary — see [LICENSE](./LICENSE).
@@ -0,0 +1,329 @@
1
+ #!/usr/bin/env node
2
+ /* eslint-disable no-console */
3
+
4
+ const https = require('https');
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const os = require('os');
8
+ const { spawn, execSync } = require('child_process');
9
+
10
+ const REPO = 'amurgola/notification2webhookbrowser';
11
+ const CACHE_DIR = path.join(os.homedir(), '.lumabrowser');
12
+ const STATE_FILE = path.join(CACHE_DIR, 'install.json');
13
+ const USER_AGENT = 'lumabrowser-cli';
14
+
15
+ const RED = '\x1b[31m';
16
+ const GREEN = '\x1b[32m';
17
+ const YELLOW = '\x1b[33m';
18
+ const CYAN = '\x1b[36m';
19
+ const DIM = '\x1b[2m';
20
+ const RESET = '\x1b[0m';
21
+
22
+ const log = (m) => process.stdout.write(m + '\n');
23
+ const info = (m) => process.stdout.write(`${CYAN}${m}${RESET}\n`);
24
+ const ok = (m) => process.stdout.write(`${GREEN}${m}${RESET}\n`);
25
+ const warn = (m) => process.stdout.write(`${YELLOW}${m}${RESET}\n`);
26
+ const err = (m) => process.stderr.write(`${RED}${m}${RESET}\n`);
27
+
28
+ function printHelp() {
29
+ log(`
30
+ ${CYAN}LumaBrowser${RESET} — browser with notification interception and AI automation
31
+
32
+ ${YELLOW}Usage:${RESET}
33
+ npx lumabrowser ${DIM}[command]${RESET}
34
+
35
+ ${YELLOW}Commands:${RESET}
36
+ start Launch LumaBrowser (downloads on first run) ${DIM}[default]${RESET}
37
+ update Force re-download of the latest version
38
+ uninstall Remove the cached LumaBrowser installation
39
+ version Print the installed LumaBrowser version
40
+ help Show this message
41
+
42
+ ${YELLOW}Examples:${RESET}
43
+ ${DIM}# First run — downloads and launches${RESET}
44
+ npx lumabrowser start
45
+
46
+ ${DIM}# Pull the newest release${RESET}
47
+ npx lumabrowser update
48
+
49
+ ${YELLOW}Cache directory:${RESET} ${DIM}${CACHE_DIR}${RESET}
50
+ ${YELLOW}Docs:${RESET} ${DIM}https://lumabyte.com${RESET}
51
+ `);
52
+ }
53
+
54
+ function readState() {
55
+ try { return JSON.parse(fs.readFileSync(STATE_FILE, 'utf8')); }
56
+ catch { return null; }
57
+ }
58
+
59
+ function writeState(data) {
60
+ fs.mkdirSync(CACHE_DIR, { recursive: true });
61
+ fs.writeFileSync(STATE_FILE, JSON.stringify(data, null, 2));
62
+ }
63
+
64
+ function httpRequest(url, { json = false } = {}) {
65
+ return new Promise((resolve, reject) => {
66
+ const req = https.get(url, {
67
+ headers: {
68
+ 'User-Agent': USER_AGENT,
69
+ 'Accept': json ? 'application/vnd.github+json' : '*/*'
70
+ }
71
+ }, (res) => {
72
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
73
+ res.resume();
74
+ return httpRequest(res.headers.location, { json }).then(resolve, reject);
75
+ }
76
+ if (res.statusCode !== 200) {
77
+ res.resume();
78
+ return reject(new Error(`HTTP ${res.statusCode} for ${url}`));
79
+ }
80
+ if (json) {
81
+ let body = '';
82
+ res.setEncoding('utf8');
83
+ res.on('data', (c) => body += c);
84
+ res.on('end', () => {
85
+ try { resolve(JSON.parse(body)); }
86
+ catch (e) { reject(e); }
87
+ });
88
+ } else {
89
+ resolve(res);
90
+ }
91
+ });
92
+ req.on('error', reject);
93
+ req.setTimeout(30_000, () => req.destroy(new Error('Request timed out')));
94
+ });
95
+ }
96
+
97
+ function downloadFile(url, destPath, label) {
98
+ return new Promise((resolve, reject) => {
99
+ const tmpPath = destPath + '.downloading';
100
+ const file = fs.createWriteStream(tmpPath);
101
+ let downloaded = 0;
102
+ let total = 0;
103
+ let lastPrint = 0;
104
+
105
+ httpRequest(url)
106
+ .then((res) => {
107
+ total = parseInt(res.headers['content-length'] || '0', 10);
108
+ res.on('data', (chunk) => {
109
+ downloaded += chunk.length;
110
+ const now = Date.now();
111
+ if (now - lastPrint > 250) {
112
+ lastPrint = now;
113
+ const mb = (downloaded / 1024 / 1024).toFixed(1);
114
+ const totalMb = (total / 1024 / 1024).toFixed(1);
115
+ const pct = total ? Math.floor((downloaded / total) * 100) : 0;
116
+ const bar = renderBar(pct);
117
+ process.stdout.write(`\r${CYAN}${label}${RESET} ${bar} ${mb}/${totalMb} MB (${pct}%) `);
118
+ }
119
+ });
120
+ res.pipe(file);
121
+ file.on('finish', () => {
122
+ file.close(() => {
123
+ process.stdout.write('\r' + ' '.repeat(80) + '\r');
124
+ fs.renameSync(tmpPath, destPath);
125
+ resolve();
126
+ });
127
+ });
128
+ file.on('error', reject);
129
+ })
130
+ .catch((e) => {
131
+ file.close();
132
+ try { fs.unlinkSync(tmpPath); } catch { /* ignore */ }
133
+ reject(e);
134
+ });
135
+ });
136
+ }
137
+
138
+ function renderBar(pct) {
139
+ const width = 20;
140
+ const filled = Math.floor((pct / 100) * width);
141
+ return '[' + '#'.repeat(filled) + '-'.repeat(width - filled) + ']';
142
+ }
143
+
144
+ function pickAsset(assets) {
145
+ const platform = process.platform;
146
+ const arch = process.arch;
147
+
148
+ const patterns = {
149
+ win32: [
150
+ /portable.*\.exe$/i,
151
+ /win.*portable.*\.exe$/i,
152
+ /lumabrowser[^/]*\.exe$/i
153
+ ],
154
+ darwin: [
155
+ arch === 'arm64'
156
+ ? /(mac|osx|darwin).*arm64.*\.zip$/i
157
+ : /(mac|osx|darwin).*(x64|x86_64).*\.zip$/i,
158
+ /(mac|osx|darwin).*\.zip$/i
159
+ ],
160
+ linux: [
161
+ /\.AppImage$/i
162
+ ]
163
+ }[platform] || [];
164
+
165
+ for (const pattern of patterns) {
166
+ const match = assets.find((a) => pattern.test(a.name));
167
+ if (match) return match;
168
+ }
169
+ return null;
170
+ }
171
+
172
+ async function fetchLatestRelease() {
173
+ return httpRequest(`https://api.github.com/repos/${REPO}/releases/latest`, { json: true });
174
+ }
175
+
176
+ async function install({ force = false } = {}) {
177
+ info('Checking GitHub for the latest LumaBrowser release…');
178
+ const release = await fetchLatestRelease();
179
+ const asset = pickAsset(release.assets || []);
180
+ if (!asset) {
181
+ throw new Error(
182
+ `No compatible LumaBrowser asset found for ${process.platform}/${process.arch} in release ${release.tag_name}.\n` +
183
+ `Available assets: ${(release.assets || []).map((a) => a.name).join(', ') || '(none)'}`
184
+ );
185
+ }
186
+
187
+ const current = readState();
188
+ if (!force && current && current.version === release.tag_name && current.executable && fs.existsSync(current.executable)) {
189
+ return current;
190
+ }
191
+
192
+ fs.mkdirSync(CACHE_DIR, { recursive: true });
193
+ const downloadPath = path.join(CACHE_DIR, asset.name);
194
+ ok(`Downloading LumaBrowser ${release.tag_name} — ${asset.name}`);
195
+ await downloadFile(asset.browser_download_url, downloadPath, 'Downloading');
196
+
197
+ const executable = prepareExecutable(downloadPath);
198
+ const record = {
199
+ version: release.tag_name,
200
+ asset: asset.name,
201
+ executable,
202
+ installedAt: new Date().toISOString()
203
+ };
204
+ writeState(record);
205
+ ok(`Installed LumaBrowser ${release.tag_name}`);
206
+ return record;
207
+ }
208
+
209
+ function prepareExecutable(archivePath) {
210
+ const p = process.platform;
211
+
212
+ if (p === 'win32') {
213
+ return archivePath;
214
+ }
215
+
216
+ if (p === 'linux') {
217
+ try { fs.chmodSync(archivePath, 0o755); } catch { /* ignore */ }
218
+ return archivePath;
219
+ }
220
+
221
+ if (p === 'darwin') {
222
+ const extractDir = path.join(CACHE_DIR, 'app');
223
+ fs.rmSync(extractDir, { recursive: true, force: true });
224
+ fs.mkdirSync(extractDir, { recursive: true });
225
+ try {
226
+ execSync(`ditto -x -k "${archivePath}" "${extractDir}"`, { stdio: 'inherit' });
227
+ } catch (e) {
228
+ throw new Error(`Failed to extract ${archivePath}: ${e.message}`);
229
+ }
230
+ const appBundle = fs.readdirSync(extractDir).find((n) => n.endsWith('.app'));
231
+ if (!appBundle) {
232
+ throw new Error('No .app bundle found in downloaded archive.');
233
+ }
234
+ const appPath = path.join(extractDir, appBundle);
235
+ try { execSync(`xattr -dr com.apple.quarantine "${appPath}"`); } catch { /* best effort */ }
236
+ return appPath;
237
+ }
238
+
239
+ throw new Error(`Unsupported platform: ${p}`);
240
+ }
241
+
242
+ function launchApp(executable, extraArgs) {
243
+ const p = process.platform;
244
+ if (p === 'darwin') {
245
+ const child = spawn('open', ['-a', executable, '--args', ...extraArgs], { stdio: 'ignore', detached: true });
246
+ child.unref();
247
+ return;
248
+ }
249
+ const child = spawn(executable, extraArgs, { stdio: 'ignore', detached: true });
250
+ child.unref();
251
+ }
252
+
253
+ async function cmdStart(extraArgs) {
254
+ const current = readState();
255
+ let record;
256
+ if (current && current.executable && fs.existsSync(current.executable)) {
257
+ record = current;
258
+ } else {
259
+ record = await install({ force: false });
260
+ }
261
+ ok(`Launching LumaBrowser ${record.version}…`);
262
+ launchApp(record.executable, extraArgs);
263
+ log(`${DIM}Tip: run \`npx lumabrowser update\` to pull the newest release.${RESET}`);
264
+ }
265
+
266
+ function cmdUninstall() {
267
+ if (!fs.existsSync(CACHE_DIR)) {
268
+ log('Nothing to uninstall.');
269
+ return;
270
+ }
271
+ fs.rmSync(CACHE_DIR, { recursive: true, force: true });
272
+ ok('Removed cached LumaBrowser installation.');
273
+ }
274
+
275
+ function cmdVersion() {
276
+ const current = readState();
277
+ if (current) {
278
+ log(`LumaBrowser ${current.version}`);
279
+ log(`${DIM} asset: ${current.asset}${RESET}`);
280
+ log(`${DIM} executable: ${current.executable}${RESET}`);
281
+ log(`${DIM} installed: ${current.installedAt}${RESET}`);
282
+ } else {
283
+ log('LumaBrowser is not yet installed.');
284
+ log(`${DIM}Run \`npx lumabrowser start\` to install and launch.${RESET}`);
285
+ }
286
+ }
287
+
288
+ async function main() {
289
+ const [, , rawCmd, ...extraArgs] = process.argv;
290
+ const cmd = (rawCmd || 'start').toLowerCase();
291
+
292
+ try {
293
+ switch (cmd) {
294
+ case 'start':
295
+ await cmdStart(extraArgs);
296
+ break;
297
+ case 'install':
298
+ await install({ force: false });
299
+ break;
300
+ case 'update':
301
+ case 'upgrade':
302
+ await install({ force: true });
303
+ break;
304
+ case 'uninstall':
305
+ case 'remove':
306
+ cmdUninstall();
307
+ break;
308
+ case 'version':
309
+ case '--version':
310
+ case '-v':
311
+ cmdVersion();
312
+ break;
313
+ case 'help':
314
+ case '--help':
315
+ case '-h':
316
+ default:
317
+ if (rawCmd && !['help', '--help', '-h'].includes(cmd)) {
318
+ warn(`Unknown command: ${rawCmd}`);
319
+ }
320
+ printHelp();
321
+ break;
322
+ }
323
+ } catch (e) {
324
+ err(`lumabrowser: ${e.message}`);
325
+ process.exit(1);
326
+ }
327
+ }
328
+
329
+ main();
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "lumabrowser",
3
+ "version": "1.0.0",
4
+ "description": "CLI launcher for LumaBrowser — a multi-tab browser with notification interception and AI-driven automation.",
5
+ "bin": {
6
+ "lumabrowser": "bin/lumabrowser.js"
7
+ },
8
+ "files": [
9
+ "bin/",
10
+ "README.md",
11
+ "LICENSE"
12
+ ],
13
+ "keywords": [
14
+ "lumabrowser",
15
+ "browser",
16
+ "electron",
17
+ "automation",
18
+ "notifications",
19
+ "webhook",
20
+ "mcp",
21
+ "ai"
22
+ ],
23
+ "homepage": "https://lumabyte.com",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/amurgola/notification2webhookbrowser.git"
27
+ },
28
+ "bugs": {
29
+ "url": "https://github.com/amurgola/notification2webhookbrowser/issues"
30
+ },
31
+ "author": "Andrew Murgola",
32
+ "license": "SEE LICENSE IN LICENSE",
33
+ "engines": {
34
+ "node": ">=18"
35
+ }
36
+ }