openzoo 0.51.8 → 0.51.9

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.
@@ -0,0 +1,7 @@
1
+ # OpenZoo Launcher
2
+
3
+ One installer for ChatGPT. Setup starts automatically when you open the launcher, with a floating savings window showing wallet balance, spend, and savings. Node and npm are bundled; no global runtime or terminal is required. The launcher checks npm's stable `openzoo` release on each launch, installs newer versions into its per-user application data directory, and falls back to the bundled or last successfully installed CLI when offline. It never downgrades. The selected CLI downloads the current chat app if missing and preserves existing installations.
4
+
5
+ Build natively with `npm ci`, `npm run prepare:bundle`, then `npm run dist`. Preparation resolves current Node LTS from nodejs.org, checks the archive SHA-256, and bundles this checkout's OpenZoo package. Push a `launcher-v1.0.0` style tag to publish six native installers through Actions; manual dispatch builds downloadable workflow artifacts without publishing a release. macOS uses existing Apple signing/notarization secrets. Windows currently ships unsigned.
6
+
7
+ Downloaded CLI updates execute as the current user, just like installing OpenZoo from npm. Updates are staged and promoted after a CLI smoke check. Launcher/Electron updates require downloading a newer installer. Configuration, wallets, downloaded apps and background proxy processes belong to OpenZoo and survive closing/uninstalling the launcher.
@@ -0,0 +1,34 @@
1
+ const { app, BrowserWindow, ipcMain } = require('electron');
2
+ const path = require('node:path');
3
+ const { resolveCLI, run } = require('./runtime.cjs');
4
+ const { showSavings } = require('./savings.cjs');
5
+ let window, busy = false;
6
+ if (!app.requestSingleInstanceLock({ savingsReady: process.env.OPENZOO_SAVINGS_READY })) app.quit();
7
+ else {
8
+ app.on('second-instance', (_event, argv, _cwd, data) => { if (argv.includes('--savings-only')) {
9
+ const pill = showSavings();
10
+ const ready = () => { if (data?.savingsReady) { try { require('node:fs').writeFileSync(data.savingsReady, 'ready'); } catch {} } };
11
+ if (pill.webContents.isLoading()) pill.webContents.once('did-finish-load', ready); else ready();
12
+ } else { window?.show(); window?.focus(); } });
13
+ app.whenReady().then(() => {
14
+ if (process.argv.includes('--savings-only')) { showSavings(); return; }
15
+ window = new BrowserWindow({ width: 740, height: 660, backgroundColor: '#101110', webPreferences: { preload: path.join(__dirname, 'preload.cjs'), contextIsolation: true, nodeIntegration: false, sandbox: true } });
16
+ window.webContents.setWindowOpenHandler(() => ({ action: 'deny' }));
17
+ window.webContents.on('will-navigate', event => event.preventDefault());
18
+ window.loadFile(path.join(__dirname, 'index.html'));
19
+ ipcMain.handle('launch', async (event, command) => {
20
+ if (event.sender !== window.webContents || command !== 'chatgpt' || busy) throw new Error('Launch unavailable');
21
+ busy = true;
22
+ const log = message => { if (!window.isDestroyed()) window.webContents.send('log', message); };
23
+ try {
24
+ const bundle = app.isPackaged ? path.join(process.resourcesPath, 'bundle') : path.join(__dirname, 'bundle');
25
+ const { node, cli } = await resolveCLI(bundle, app.getPath('userData'), log);
26
+ showSavings();
27
+ await run(node, [cli, 'chatgpt', '--no-pill'], log);
28
+ log('\nLaunched. You can close this window.\n');
29
+ } catch (e) { log(`\n${e.message}\n`); throw e; }
30
+ finally { busy = false; }
31
+ });
32
+ });
33
+ app.on('window-all-closed', () => app.quit());
34
+ }
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "openzoo-launcher",
3
+ "version": "1.1.0",
4
+ "private": true,
5
+ "main": "main.cjs",
6
+ "scripts": {
7
+ "prepare:bundle": "node prepare.mjs",
8
+ "dist": "electron-builder",
9
+ "test": "node --test test.cjs"
10
+ },
11
+ "devDependencies": {
12
+ "electron": "44.3.0",
13
+ "electron-builder": "26.15.3"
14
+ },
15
+ "build": {
16
+ "appId": "fun.openzoo.launcher",
17
+ "productName": "OpenZoo Launcher",
18
+ "files": [
19
+ "main.cjs",
20
+ "runtime.cjs",
21
+ "preload.cjs",
22
+ "index.html",
23
+ "savings.cjs",
24
+ "savings-preload.cjs",
25
+ "savings.html",
26
+ "savings-renderer.js"
27
+ ],
28
+ "extraResources": [
29
+ {
30
+ "from": "bundle",
31
+ "to": "bundle"
32
+ },
33
+ {
34
+ "from": "openzoo-overlay.json",
35
+ "to": "openzoo-overlay.json"
36
+ }
37
+ ],
38
+ "artifactName": "OpenZoo-Launcher-${version}-${os}-${arch}.${ext}",
39
+ "mac": {
40
+ "target": "dmg",
41
+ "hardenedRuntime": true
42
+ },
43
+ "win": {
44
+ "target": "nsis"
45
+ },
46
+ "nsis": {
47
+ "oneClick": false,
48
+ "perMachine": false,
49
+ "allowToChangeInstallationDirectory": true
50
+ },
51
+ "linux": {
52
+ "target": "AppImage",
53
+ "category": "Network"
54
+ }
55
+ }
56
+ }
@@ -0,0 +1,59 @@
1
+ const path = require('node:path');
2
+ const fs = require('node:fs/promises');
3
+ const { spawn } = require('node:child_process');
4
+ const stable = v => /^\d+\.\d+\.\d+$/.test(v);
5
+ const newer = (a, b) => {
6
+ if (!stable(a) || !stable(b)) return false;
7
+ const aa = a.split('.').map(Number), bb = b.split('.').map(Number);
8
+ for (let i = 0; i < 3; i++) if (aa[i] !== bb[i]) return aa[i] > bb[i];
9
+ return false;
10
+ };
11
+ function environment(node) {
12
+ const env = { ...process.env, PATH: `${path.dirname(node)}${path.delimiter}${process.env.PATH || ''}` };
13
+ delete env.ELECTRON_RUN_AS_NODE;
14
+ delete env.NODE_OPTIONS;
15
+ return env;
16
+ }
17
+ function run(node, args, log, timeout = 600000) {
18
+ return new Promise((resolve, reject) => {
19
+ const child = spawn(node, args, { env: environment(node), windowsHide: true });
20
+ const timer = setTimeout(() => { child.kill(); reject(new Error('Operation timed out. Please try again.')); }, timeout);
21
+ child.stdout.on('data', b => log(b.toString()));
22
+ child.stderr.on('data', b => log(b.toString()));
23
+ child.on('error', e => { clearTimeout(timer); reject(e); });
24
+ child.on('exit', code => { clearTimeout(timer); code === 0 ? resolve() : reject(new Error(`OpenZoo exited with code ${code}`)); });
25
+ });
26
+ }
27
+ async function resolveCLI(bundle, userData, log) {
28
+ const node = path.join(bundle, 'runtime', process.platform === 'win32' ? 'node.exe' : 'bin/node');
29
+ const npm = path.join(bundle, 'runtime', process.platform === 'win32' ? 'node_modules/npm/bin/npm-cli.js' : 'lib/node_modules/npm/bin/npm-cli.js');
30
+ const pkg = dir => path.join(dir, 'node_modules/openzoo');
31
+ let selected = path.join(bundle, 'sidecar');
32
+ let version = JSON.parse(await fs.readFile(path.join(pkg(selected), 'package.json'), 'utf8')).version;
33
+ const updates = path.join(userData, 'versions');
34
+ await fs.mkdir(updates, { recursive: true });
35
+ for (const v of await fs.readdir(updates)) {
36
+ if (newer(v, version)) {
37
+ try { if (JSON.parse(await fs.readFile(path.join(pkg(path.join(updates, v)), 'package.json'), 'utf8')).version === v) { selected = path.join(updates, v); version = v; } } catch {}
38
+ }
39
+ }
40
+ try {
41
+ log('Checking for the latest OpenZoo…\n');
42
+ const response = await fetch('https://registry.npmjs.org/openzoo/latest', { signal: AbortSignal.timeout(10000) });
43
+ if (!response.ok) throw new Error(`Registry returned ${response.status}`);
44
+ const latest = (await response.json()).version;
45
+ if (newer(latest, version)) {
46
+ const stage = await fs.mkdtemp(path.join(updates, '.install-'));
47
+ try {
48
+ await run(node, [npm, 'install', '--prefix', stage, '--registry=https://registry.npmjs.org', '--omit=dev', '--no-audit', '--no-fund', `openzoo@${latest}`], log);
49
+ await run(node, [path.join(stage, 'node_modules/dugite/script/download-git.js')], log);
50
+ await run(node, [path.join(pkg(stage), 'bin/openzoo.js'), '--help'], () => {}, 30000);
51
+ await fs.rename(stage, path.join(updates, latest));
52
+ selected = path.join(updates, latest); version = latest;
53
+ } finally { await fs.rm(stage, { recursive: true, force: true }); }
54
+ }
55
+ } catch (e) { log(`Update unavailable (${e.message}). Using installed OpenZoo ${version}.\n`); }
56
+ log(`Starting OpenZoo ${version}…\n`);
57
+ return { node, cli: path.join(pkg(selected), 'bin/openzoo.js') };
58
+ }
59
+ module.exports = { newer, environment, resolveCLI, run };
@@ -0,0 +1,5 @@
1
+ const { contextBridge, ipcRenderer } = require('electron');
2
+ contextBridge.exposeInMainWorld('savings', {
3
+ listen: fn => ipcRenderer.on('savings', (_event, value) => fn(value)),
4
+ close: () => ipcRenderer.send('close-savings'),
5
+ });
@@ -0,0 +1,10 @@
1
+ const usd = n => typeof n === 'number' && Number.isFinite(n) ? '$' + Math.abs(n).toFixed(Math.abs(n) < .01 && n !== 0 ? 4 : 2) : '—';
2
+ window.savings.listen(d => {
3
+ for (const [id, value] of [['wallet',d.walletUsd],['credit',d.creditUsd],['spent',d.spendUsd],['direct',d.directUsd]]) document.getElementById(id).textContent=usd(value);
4
+ const comparable=typeof d.directUsd==='number' && typeof d.spendUsd==='number' && (d.directUsd>0 || d.spendUsd===0);
5
+ const saved=comparable ? d.directUsd-d.spendUsd : null;
6
+ document.getElementById('saved').textContent=usd(saved);
7
+ document.getElementById('saving-label').textContent=saved!==null && saved<0 ? 'Extra cost' : 'Saved';
8
+ document.getElementById('state').textContent=d.online ? 'Balance updates automatically' : 'Reconnecting · showing last known amounts';
9
+ });
10
+ document.getElementById('close').addEventListener('click',()=>window.savings.close());
@@ -0,0 +1,41 @@
1
+ const { BrowserWindow, screen, ipcMain } = require('electron');
2
+ const path = require('node:path');
3
+ let pill;
4
+ function showSavings(port = Number(process.env.OPENZOO_SAVINGS_PORT) || 8402) {
5
+ if (pill && !pill.isDestroyed()) { pill.show(); return pill; }
6
+ const { x, y, width, height } = screen.getPrimaryDisplay().workArea;
7
+ pill = new BrowserWindow({ width: 420, height: 270, x: x + width - 440, y: y + height - 290,
8
+ frame: false, transparent: true, alwaysOnTop: true, skipTaskbar: true, resizable: false,
9
+ webPreferences: { preload: path.join(__dirname, 'savings-preload.cjs'), contextIsolation: true, sandbox: true, nodeIntegration: false } });
10
+ pill.setAlwaysOnTop(true, 'floating');
11
+ pill.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
12
+ pill.webContents.setWindowOpenHandler(() => ({ action: 'deny' }));
13
+ pill.webContents.on('will-navigate', e => e.preventDefault());
14
+ pill.loadFile(path.join(__dirname, 'savings.html'));
15
+ const window = pill;
16
+ let busy = false, last = null;
17
+ const poll = async () => {
18
+ if (busy || window.isDestroyed()) return;
19
+ busy = true;
20
+ try {
21
+ const r = await fetch(`http://127.0.0.1:${port}/v1/info`, { signal: AbortSignal.timeout(2500) });
22
+ const d = await r.json();
23
+ if (!r.ok || d.youAreTalkingTo !== 'openzoo proxy') throw Error('Unavailable');
24
+ last = d;
25
+ if (!window.isDestroyed()) window.webContents.send('savings', { ...d, online: true });
26
+ } catch { if (!window.isDestroyed()) window.webContents.send('savings', { ...last, online: false }); }
27
+ finally { busy = false; }
28
+ };
29
+ window.webContents.on('did-finish-load', () => {
30
+ poll();
31
+ if (process.env.OPENZOO_SAVINGS_READY) {
32
+ try { require('node:fs').writeFileSync(process.env.OPENZOO_SAVINGS_READY, 'ready'); } catch {}
33
+ }
34
+ });
35
+ const timer = setInterval(poll, 3000);
36
+ const close = event => { if (event.sender === window.webContents) window.close(); };
37
+ ipcMain.on('close-savings', close);
38
+ window.on('closed', () => { clearInterval(timer); ipcMain.removeListener('close-savings', close); if (pill === window) pill = null; });
39
+ return pill;
40
+ }
41
+ module.exports = { showSavings };
@@ -0,0 +1,3 @@
1
+ <!doctype html><html><head><meta charset="utf-8"><meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; script-src 'self'"><title>OpenZoo savings</title><style>
2
+ *{box-sizing:border-box}body{margin:0;padding:8px;background:transparent;font:13px system-ui;color:#fff}.pill{border-radius:20px;background:#18181bf5;border:1px solid #ffffff25;overflow:hidden;box-shadow:0 4px 14px #0004}header{background:#e85d1c;padding:12px 16px;font-weight:650;-webkit-app-region:drag;display:flex;justify-content:space-between;align-items:center}button{-webkit-app-region:no-drag;border:0;background:none;color:inherit;font:18px system-ui;cursor:pointer;padding:0 5px}main{padding:12px 16px}dl{display:grid;grid-template-columns:1fr auto;margin:0;gap:7px}dd{margin:0;font-variant-numeric:tabular-nums}small{color:#bbb;display:block;margin-top:12px;font-size:11px}#state{color:#d8ff76}
3
+ </style></head><body><div class="pill"><header><span>OpenZoo · costs & savings</span><button id="close" aria-label="Close savings pill">×</button></header><main><dl><dt>Wallet balance</dt><dd id="wallet">—</dd><dt>Prepaid credit</dt><dd id="credit">—</dd><dt>Spent</dt><dd id="spent">—</dd><dt>Would’ve cost · estimate</dt><dd id="direct">—</dd><dt id="saving-label">Saved</dt><dd id="saved">—</dd></dl><small id="state" role="status">Checking your balance…</small><small>Shared OpenZoo totals · drag the orange bar to move</small></main></div><script src="savings-renderer.js"></script></body></html>
@@ -163,21 +163,41 @@ export function nativeSavingsLauncher({ platform = process.platform, home = os.h
163
163
  return null;
164
164
  }
165
165
 
166
- export async function startChatGptSpend({ profile, proxyPort, spawnImpl = spawn } = {}) {
167
- const native = nativeSavingsLauncher();
168
- if (native && proxyPort === 8402) {
169
- const child = spawnImpl(native, ['--savings-only'], { detached: true, stdio: 'ignore' });
170
- await new Promise((resolve, reject) => { child.once('spawn', resolve); child.once('error', reject); });
171
- child.unref();
172
- return;
173
- }
174
- fs.mkdirSync(profile, { recursive: true });
175
- const fd = fs.openSync(path.join(profile, 'openzoo-savings.log'), 'a', 0o600);
166
+ export async function startChatGptSpend({ profile, proxyPort = 8402, spawnImpl = spawn } = {}) {
167
+ const app = fileURLToPath(new URL('../launcher/', import.meta.url));
168
+ const electron = path.join(app, 'node_modules/electron/dist', process.platform === 'darwin'
169
+ ? 'Electron.app/Contents/MacOS/Electron' : process.platform === 'win32' ? 'electron.exe' : 'electron');
170
+ const env = { ...process.env, OPENZOO_SAVINGS_PORT: String(proxyPort) };
171
+ delete env.ELECTRON_RUN_AS_NODE;
172
+ const readyDir = fs.mkdtempSync(path.join(os.tmpdir(), 'openzoo-pill-'));
173
+ env.OPENZOO_SAVINGS_READY = path.join(readyDir, 'ready');
174
+ const logDir = profile || path.join(os.homedir(), '.openzoo');
175
+ fs.mkdirSync(logDir, { recursive: true });
176
+ const logPath = path.join(logDir, 'openzoo-savings.log');
177
+ const fd = fs.openSync(logPath, 'a', 0o600);
178
+ let child, failure;
176
179
  try {
177
- const child = spawnImpl(process.execPath, [fileURLToPath(import.meta.url), '--watch', profile, String(proxyPort)], { detached: true, stdio: ['ignore', fd, fd] });
178
- await new Promise((resolve, reject) => { child.once('spawn', resolve); child.once('error', reject); });
180
+ if (fs.existsSync(electron)) {
181
+ child = spawnImpl(electron, [app, '--savings-only'], { detached: true, stdio: ['ignore', fd, fd], env });
182
+ } else {
183
+ // npm caches the standalone display runtime. The CLI ships the display itself.
184
+ // No ChatGPT debugging port or separate OpenZoo installer is required.
185
+ const args = ['exec', '--yes', '--package=electron@44.3.0', '--', 'electron', app, '--savings-only'];
186
+ const npmScript = process.env.npm_execpath;
187
+ child = npmScript && fs.existsSync(npmScript)
188
+ ? spawnImpl(process.execPath, [npmScript, ...args], { detached: true, stdio: ['ignore', fd, fd], env })
189
+ : spawnImpl(process.platform === 'win32' ? 'npm.cmd' : 'npm', args, { detached: true, stdio: ['ignore', fd, fd], env, shell: process.platform === 'win32' });
190
+ }
191
+ child.once('error', error => { failure = error; });
192
+ child.once('exit', code => { if (code) failure = new Error(`Display exited (${code})`); });
179
193
  child.unref();
180
- } finally { fs.closeSync(fd); }
194
+ const deadline = Date.now() + 120000;
195
+ while (!fs.existsSync(env.OPENZOO_SAVINGS_READY)) {
196
+ if (failure) throw failure;
197
+ if (Date.now() > deadline) throw new Error(`Display did not open. See ${logPath}`);
198
+ await new Promise(resolve => setTimeout(resolve, 200));
199
+ }
200
+ } finally { fs.closeSync(fd); fs.rmSync(readyDir, { recursive: true, force: true }); }
181
201
  }
182
202
 
183
203
  /** One watcher per profile, including concurrent launcher invocations. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openzoo",
3
- "version": "0.51.8",
3
+ "version": "0.51.9",
4
4
  "description": "Local x402-paying proxy + MCP server for openzoo.fun — point any OpenAI-compatible harness (Cursor, Claude Code, aider, SDKs) at localhost and it pays per call from a local burner wallet. Solana and Base rails live; Robinhood experimental.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -10,7 +10,14 @@
10
10
  "files": [
11
11
  "bin",
12
12
  "lib",
13
- "README.md"
13
+ "README.md",
14
+ "launcher/main.cjs",
15
+ "launcher/runtime.cjs",
16
+ "launcher/package.json",
17
+ "launcher/savings.cjs",
18
+ "launcher/savings.html",
19
+ "launcher/savings-preload.cjs",
20
+ "launcher/savings-renderer.js"
14
21
  ],
15
22
  "engines": {
16
23
  "node": ">=18"