openzoo 0.51.15 → 0.51.17
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/launcher/chat-visibility.cjs +37 -0
- package/launcher/package.json +3 -2
- package/launcher/savings.cjs +24 -8
- package/lib/chatgpt-install.js +1 -1
- package/lib/chatgpt.js +7 -3
- package/lib/funding-status.js +5 -2
- package/lib/info.js +2 -2
- package/lib/proxy.js +34 -8
- package/lib/wallet-reload.js +23 -0
- package/lib/wallet.js +6 -2
- package/lib/windows-chatgpt.js +62 -0
- package/package.json +3 -2
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const { execFile } = require('node:child_process');
|
|
2
|
+
const fs = require('node:fs/promises');
|
|
3
|
+
function run(file, args) {
|
|
4
|
+
return new Promise((resolve, reject) => execFile(file, args, { timeout: 1800, windowsHide: true, maxBuffer: 8192 }, (error, stdout) => error ? reject(error) : resolve(stdout.trim())));
|
|
5
|
+
}
|
|
6
|
+
async function foregroundApp(platform = process.platform) {
|
|
7
|
+
if (platform === 'darwin') {
|
|
8
|
+
return JSON.parse(await run('/usr/bin/osascript', ['-l', 'JavaScript', '-e', 'ObjC.import("AppKit"); var a=$.NSWorkspace.sharedWorkspace.frontmostApplication; JSON.stringify({pid:Number(a.processIdentifier),name:ObjC.unwrap(a.bundleIdentifier)||""});']));
|
|
9
|
+
}
|
|
10
|
+
if (platform === 'win32') {
|
|
11
|
+
const script = 'Add-Type -TypeDefinition \'using System; using System.Runtime.InteropServices; public class OZFocus { [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow(); [DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr h, out uint p); }\'; [uint32]$foregroundPid=0; [void][OZFocus]::GetWindowThreadProcessId([OZFocus]::GetForegroundWindow(),[ref]$foregroundPid); $p=Get-Process -Id $foregroundPid -ErrorAction Stop; @{pid=$foregroundPid;name=$p.ProcessName}|ConvertTo-Json -Compress';
|
|
12
|
+
return JSON.parse(await run('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', script]));
|
|
13
|
+
}
|
|
14
|
+
const pid = Number(await run('xdotool', ['getactivewindow', 'getwindowpid']));
|
|
15
|
+
return { pid, name: (await fs.readFile(`/proc/${pid}/comm`, 'utf8')).trim() };
|
|
16
|
+
}
|
|
17
|
+
function isChatApp(app) {
|
|
18
|
+
return /^(com\.openai\.(chat|chatgpt|codex)|chatgpt|codex)$/i.test(app?.name || '');
|
|
19
|
+
}
|
|
20
|
+
function watchChatVisibility(onChange, { read = foregroundApp, ownPid = process.pid, intervalMs = 750 } = {}) {
|
|
21
|
+
let active = false, busy = false, stopped = false;
|
|
22
|
+
async function check() {
|
|
23
|
+
if (busy || stopped) return;
|
|
24
|
+
busy = true;
|
|
25
|
+
try {
|
|
26
|
+
const app = await read();
|
|
27
|
+
// Clicking our controls keeps the last ChatGPT visibility state.
|
|
28
|
+
const next = app.pid === ownPid ? active : isChatApp(app);
|
|
29
|
+
if (!stopped) { active = next; onChange(active); }
|
|
30
|
+
} catch { if (!stopped) { active = false; onChange(false); } }
|
|
31
|
+
finally { busy = false; }
|
|
32
|
+
}
|
|
33
|
+
check();
|
|
34
|
+
const timer = setInterval(check, intervalMs);
|
|
35
|
+
return () => { stopped = true; clearInterval(timer); };
|
|
36
|
+
}
|
|
37
|
+
module.exports = { foregroundApp, isChatApp, watchChatVisibility };
|
package/launcher/package.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"scripts": {
|
|
7
7
|
"prepare:bundle": "node prepare.mjs",
|
|
8
8
|
"dist": "electron-builder",
|
|
9
|
-
"test": "node --test test.cjs"
|
|
9
|
+
"test": "node --test test.cjs chat-visibility.test.cjs"
|
|
10
10
|
},
|
|
11
11
|
"devDependencies": {
|
|
12
12
|
"electron": "44.3.0",
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
"savings.html",
|
|
26
26
|
"savings-renderer.js",
|
|
27
27
|
"savings-toggle.html",
|
|
28
|
-
"savings-toggle.js"
|
|
28
|
+
"savings-toggle.js",
|
|
29
|
+
"chat-visibility.cjs"
|
|
29
30
|
],
|
|
30
31
|
"extraResources": [
|
|
31
32
|
{
|
package/launcher/savings.cjs
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
const { BrowserWindow, screen, ipcMain, clipboard, shell } = require('electron');
|
|
2
2
|
const path = require('node:path');
|
|
3
|
-
|
|
3
|
+
const { watchChatVisibility } = require('./chat-visibility.cjs');
|
|
4
|
+
let pill, revealPill;
|
|
4
5
|
function showSavings(port = Number(process.env.OPENZOO_SAVINGS_PORT) || 8402) {
|
|
5
|
-
if (pill && !pill.isDestroyed()) {
|
|
6
|
+
if (pill && !pill.isDestroyed()) { revealPill?.(); return pill; }
|
|
6
7
|
const { x, y, width, height } = screen.getPrimaryDisplay().workArea;
|
|
7
8
|
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
|
+
show: false, frame: false, transparent: true, alwaysOnTop: true, skipTaskbar: true, resizable: false,
|
|
9
10
|
webPreferences: { preload: path.join(__dirname, 'savings-preload.cjs'), contextIsolation: true, sandbox: true, nodeIntegration: false } });
|
|
10
11
|
pill.setAlwaysOnTop(true, 'floating');
|
|
11
12
|
pill.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
|
|
@@ -15,25 +16,40 @@ function showSavings(port = Number(process.env.OPENZOO_SAVINGS_PORT) || 8402) {
|
|
|
15
16
|
const window = pill;
|
|
16
17
|
const visibilityControl = new BrowserWindow({ width: 136, height: 40,
|
|
17
18
|
x: x + 16, y: y + height - 100,
|
|
18
|
-
frame: false, transparent: true, alwaysOnTop: true, skipTaskbar: true, resizable: false,
|
|
19
|
+
show: false, frame: false, transparent: true, alwaysOnTop: true, skipTaskbar: true, resizable: false,
|
|
19
20
|
webPreferences: { preload: path.join(__dirname, 'savings-preload.cjs'), contextIsolation: true, sandbox: true, nodeIntegration: false } });
|
|
20
21
|
visibilityControl.setAlwaysOnTop(true, 'floating');
|
|
21
22
|
visibilityControl.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
|
|
22
23
|
visibilityControl.webContents.setWindowOpenHandler(() => ({ action: 'deny' }));
|
|
23
24
|
visibilityControl.webContents.on('will-navigate', e => e.preventDefault());
|
|
24
25
|
visibilityControl.loadFile(path.join(__dirname, 'savings-toggle.html'));
|
|
26
|
+
let chatActive = false, userVisible = true;
|
|
27
|
+
const applyVisibility = () => {
|
|
28
|
+
if (window.isDestroyed() || visibilityControl.isDestroyed()) return;
|
|
29
|
+
if (chatActive) {
|
|
30
|
+
if (!visibilityControl.isVisible()) visibilityControl.showInactive();
|
|
31
|
+
if (userVisible && !window.isVisible()) window.showInactive();
|
|
32
|
+
if (!userVisible && window.isVisible()) window.hide();
|
|
33
|
+
} else {
|
|
34
|
+
window.hide();
|
|
35
|
+
visibilityControl.hide();
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
revealPill = () => { userVisible = true; applyVisibility(); syncVisibility(); };
|
|
25
39
|
const syncVisibility = () => {
|
|
26
|
-
if (!visibilityControl.isDestroyed()) visibilityControl.webContents.send('pill-visibility',
|
|
40
|
+
if (!visibilityControl.isDestroyed()) visibilityControl.webContents.send('pill-visibility', userVisible);
|
|
27
41
|
};
|
|
28
42
|
const toggleVisibility = event => {
|
|
29
43
|
if (event.sender !== visibilityControl.webContents || window.isDestroyed()) return;
|
|
30
|
-
|
|
44
|
+
userVisible = !userVisible;
|
|
45
|
+
applyVisibility();
|
|
31
46
|
syncVisibility();
|
|
32
47
|
};
|
|
33
48
|
ipcMain.on('toggle-savings-visibility', toggleVisibility);
|
|
34
49
|
window.on('show', syncVisibility);
|
|
35
50
|
window.on('hide', syncVisibility);
|
|
36
51
|
visibilityControl.webContents.on('did-finish-load', syncVisibility);
|
|
52
|
+
const stopVisibility = watchChatVisibility(active => { chatActive = active; applyVisibility(); });
|
|
37
53
|
let busy = false, last = null, wallet = null, expandedHeight = 270;
|
|
38
54
|
const resize = height => {
|
|
39
55
|
const b = window.getBounds(), area = screen.getDisplayMatching(b).workArea;
|
|
@@ -84,9 +100,9 @@ function showSavings(port = Number(process.env.OPENZOO_SAVINGS_PORT) || 8402) {
|
|
|
84
100
|
}
|
|
85
101
|
});
|
|
86
102
|
const timer = setInterval(poll, 3000);
|
|
87
|
-
const close = event => { if (event.sender === window.webContents)
|
|
103
|
+
const close = event => { if (event.sender === window.webContents) { userVisible = false; applyVisibility(); syncVisibility(); } };
|
|
88
104
|
ipcMain.on('close-savings', close);
|
|
89
|
-
window.on('closed', () => { ipcMain.removeListener('toggle-savings-visibility', toggleVisibility); if (!visibilityControl.isDestroyed()) visibilityControl.destroy(); clearInterval(timer); for (const [name, fn] of [["collapse-savings",collapse],["open-deposit",deposit],["copy-deposit",copy],["deposit-card",card],["close-deposit",back]]) ipcMain.removeListener(name,fn); ipcMain.removeListener('close-savings', close); if (pill === window) pill = null; });
|
|
105
|
+
window.on('closed', () => { stopVisibility(); revealPill = null; ipcMain.removeListener('toggle-savings-visibility', toggleVisibility); if (!visibilityControl.isDestroyed()) visibilityControl.destroy(); clearInterval(timer); for (const [name, fn] of [["collapse-savings",collapse],["open-deposit",deposit],["copy-deposit",copy],["deposit-card",card],["close-deposit",back]]) ipcMain.removeListener(name,fn); ipcMain.removeListener('close-savings', close); if (pill === window) pill = null; });
|
|
90
106
|
return pill;
|
|
91
107
|
}
|
|
92
108
|
module.exports = { showSavings };
|
package/lib/chatgpt-install.js
CHANGED
|
@@ -20,7 +20,7 @@ export function installerAsset(platform = process.platform, arch = process.arch)
|
|
|
20
20
|
|
|
21
21
|
export function runCommand(command, args, options = {}) {
|
|
22
22
|
return new Promise((resolve, reject) => {
|
|
23
|
-
const child = spawn(command, args, { stdio: ['ignore', 'pipe', 'pipe'], ...options });
|
|
23
|
+
const child = spawn(command, args, { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true, ...options });
|
|
24
24
|
let stdout = '', stderr = '';
|
|
25
25
|
child.stdout?.on('data', b => { stdout = (stdout + b).slice(-1024 * 1024); });
|
|
26
26
|
child.stderr?.on('data', b => { stderr = (stderr + b).slice(-8192); });
|
package/lib/chatgpt.js
CHANGED
|
@@ -11,6 +11,7 @@ import fs from 'node:fs';
|
|
|
11
11
|
import os from 'node:os';
|
|
12
12
|
import path from 'node:path';
|
|
13
13
|
import { config } from './config.js';
|
|
14
|
+
import { launchWindowsPackage } from './windows-chatgpt.js';
|
|
14
15
|
import { startChatGptSpend } from './chatgpt-spend.js';
|
|
15
16
|
import { installChatGpt, windowsChatGptApp, runCommand } from './chatgpt-install.js';
|
|
16
17
|
|
|
@@ -222,7 +223,7 @@ async function ensureProxy({ tunnel }) {
|
|
|
222
223
|
const fd = fs.openSync(logPath, 'a');
|
|
223
224
|
const env = { ...process.env };
|
|
224
225
|
if (!tunnel && !env.OPENZOO_NO_TUNNEL) env.OPENZOO_NO_TUNNEL = '1';
|
|
225
|
-
const child = spawn(process.execPath, [process.argv[1], 'proxy'], { detached: true, stdio: ['ignore', fd, fd], env });
|
|
226
|
+
const child = spawn(process.execPath, [process.argv[1], 'proxy'], { detached: true, windowsHide: true, stdio: ['ignore', fd, fd], env });
|
|
226
227
|
child.unref();
|
|
227
228
|
fs.closeSync(fd);
|
|
228
229
|
process.stderr.write(`openzoo: starting the proxy (v${packageVersion()}) in the background, log ${logPath}`);
|
|
@@ -231,8 +232,7 @@ async function ensureProxy({ tunnel }) {
|
|
|
231
232
|
await new Promise((r) => setTimeout(r, 1000));
|
|
232
233
|
process.stderr.write('.');
|
|
233
234
|
try {
|
|
234
|
-
|
|
235
|
-
if (r.ok) { process.stderr.write('\n'); return { started: true, logPath }; }
|
|
235
|
+
if (await oursOn(config.port)) { process.stderr.write('\n'); return { started: true, logPath }; }
|
|
236
236
|
} catch { /* not yet */ }
|
|
237
237
|
}
|
|
238
238
|
process.stderr.write('\n');
|
|
@@ -334,6 +334,10 @@ export async function setupChatGpt(argv = [], dependencies = {}) {
|
|
|
334
334
|
|
|
335
335
|
/** LaunchServices handles app bundles on macOS; detect early failures elsewhere. */
|
|
336
336
|
export async function launchChatGpt(app, args, { env, platform = process.platform, run = runCommand, spawnImpl = spawn } = {}) {
|
|
337
|
+
if (platform === 'win32' && /[\\/]WindowsApps[\\/]/i.test(app)) {
|
|
338
|
+
await launchWindowsPackage(app, args, { env, run });
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
337
341
|
if (platform === 'darwin' && app.endsWith('/Contents/MacOS/ChatGPT')) {
|
|
338
342
|
await run('open', ['-n', '--env', `CODEX_HOME=${env.CODEX_HOME}`,
|
|
339
343
|
'--env', `CODEX_ELECTRON_USER_DATA_PATH=${env.CODEX_ELECTRON_USER_DATA_PATH}`,
|
package/lib/funding-status.js
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
/** Coalesce live reads, retain unknowns, and never cache an empty balance for a minute. */
|
|
2
2
|
export function liveBalance(read, { ttlMs = 3000, now = Date.now } = {}) {
|
|
3
|
-
let value = null, at = 0, inflight;
|
|
3
|
+
let value = null, at = 0, inflight, generation = 0;
|
|
4
4
|
return {
|
|
5
|
+
reset() { generation++; value = null; at = 0; inflight = undefined; },
|
|
5
6
|
get value() { return value; },
|
|
6
7
|
get checkedAt() { return at || null; },
|
|
7
8
|
async refresh(force = false) {
|
|
8
9
|
if (inflight) return inflight;
|
|
9
10
|
if (!force && at && now() - at < ttlMs) return value;
|
|
11
|
+
const readGeneration = generation;
|
|
10
12
|
inflight = Promise.resolve().then(read).then(next => {
|
|
11
13
|
if (typeof next !== 'number' || !Number.isFinite(next) || next < 0) throw new Error('Balance unavailable');
|
|
14
|
+
if (readGeneration !== generation) return value;
|
|
12
15
|
value = next; at = now(); return value;
|
|
13
|
-
}).catch(() => value).finally(() => { inflight = null; });
|
|
16
|
+
}).catch(() => value).finally(() => { if (readGeneration === generation) inflight = null; });
|
|
14
17
|
return inflight;
|
|
15
18
|
},
|
|
16
19
|
};
|
package/lib/info.js
CHANGED
|
@@ -257,10 +257,10 @@ export { priceHoldings, formatHoldingMoney } from './livestatus.js';
|
|
|
257
257
|
/** Current prepaid credit for this wallet's namespace. */
|
|
258
258
|
export { CREDIT_TIMEOUT_MS };
|
|
259
259
|
|
|
260
|
-
export async function creditBalance() {
|
|
260
|
+
export async function creditBalance(wallet) {
|
|
261
261
|
const { withNamespace } = await import('./namespace.js');
|
|
262
262
|
const r = await fetch(`${config.apiBase}/v1/credits`, {
|
|
263
|
-
headers: withNamespace({}), cache: 'no-store',
|
|
263
|
+
headers: withNamespace({}, wallet), cache: 'no-store',
|
|
264
264
|
signal: AbortSignal.timeout(CREDIT_TIMEOUT_MS),
|
|
265
265
|
});
|
|
266
266
|
if (!r.ok) throw new Error('Credit balance is temporarily unavailable');
|
package/lib/proxy.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { receiptLedger } from './receipt-ledger.js';
|
|
2
2
|
import { billingObserver, finalBilling } from './billing.js';
|
|
3
3
|
import { liveBalance } from './funding-status.js';
|
|
4
|
+
import { walletReloader } from './wallet-reload.js';
|
|
5
|
+
import { loadOrCreateWallet } from './wallet.js';
|
|
4
6
|
import { paymentError } from './payment-error.js';
|
|
5
7
|
import { readFileSync, appendFileSync, mkdirSync } from 'node:fs';
|
|
6
8
|
import os from 'node:os';
|
|
@@ -59,7 +61,7 @@ export function killListen(port, run = execSync, platform = process.platform) {
|
|
|
59
61
|
};
|
|
60
62
|
const tryRun = (cmd, opts = {}) => {
|
|
61
63
|
try {
|
|
62
|
-
return run(cmd, { encoding: 'utf8', timeout: 2000, stdio: ['ignore', 'pipe', 'ignore'], ...opts });
|
|
64
|
+
return run(cmd, { encoding: 'utf8', timeout: 2000, windowsHide: true, stdio: ['ignore', 'pipe', 'ignore'], ...opts });
|
|
63
65
|
} catch { return ''; }
|
|
64
66
|
};
|
|
65
67
|
if (platform === 'win32') {
|
|
@@ -72,7 +74,7 @@ export function killListen(port, run = execSync, platform = process.platform) {
|
|
|
72
74
|
if (Number.isInteger(pid) && pid > 0 && pid !== process.pid) seen.add(pid);
|
|
73
75
|
}
|
|
74
76
|
for (const pid of seen) {
|
|
75
|
-
try { run(`taskkill /PID ${pid} /F`, { stdio: 'ignore', timeout: 2000 }); } catch { /* already gone */ }
|
|
77
|
+
try { run(`taskkill /PID ${pid} /F`, { stdio: 'ignore', timeout: 2000, windowsHide: true }); } catch { /* already gone */ }
|
|
76
78
|
}
|
|
77
79
|
return [...seen];
|
|
78
80
|
}
|
|
@@ -387,7 +389,8 @@ function replayPut(key, data, settle) {
|
|
|
387
389
|
* default, so localhost behaviour is unchanged.
|
|
388
390
|
*/
|
|
389
391
|
export async function startProxy({ silent = false, requireToken = null, sessionMaxUsd = null, autoTunnel = false } = {}) {
|
|
390
|
-
|
|
392
|
+
let client = new PayClient();
|
|
393
|
+
const currentWallet = walletReloader(client, { file: client.walletPath, load: () => new PayClient(loadOrCreateWallet({ create: false })) });
|
|
391
394
|
// HOUSE OUTAGE GATE. Measured 2026-09-01: TOKEN payments settled on chain
|
|
392
395
|
// (tx 3rwy5z…) and the gateway still answered 402 because ITS upstream
|
|
393
396
|
// (OpenRouter) was out of credits. Paying again buys nothing, and the
|
|
@@ -496,7 +499,9 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
496
499
|
let walletUsd = null;
|
|
497
500
|
let walletAt = 0;
|
|
498
501
|
const walletStatus = liveBalance(async () => {
|
|
499
|
-
const
|
|
502
|
+
const readingClient = client;
|
|
503
|
+
const snap = await snapshotBalances(readingClient);
|
|
504
|
+
if (readingClient !== client) throw new Error("Wallet changed during balance read");
|
|
500
505
|
if (!snap.length) throw new Error('Balances unavailable');
|
|
501
506
|
const previous = new Map((lastSnap || []).map(b => [`${b.chain}:${b.symbol}`, b]));
|
|
502
507
|
for (const b of snap) {
|
|
@@ -507,14 +512,20 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
507
512
|
lastSnap = [...previous.values()];
|
|
508
513
|
return priceHoldings(lastSnap, lastPrices).chainUsd;
|
|
509
514
|
});
|
|
510
|
-
const creditStatus = liveBalance(creditBalance);
|
|
515
|
+
const creditStatus = liveBalance(() => creditBalance(client));
|
|
511
516
|
const refreshWallet = async (force = false) => {
|
|
512
|
-
|
|
517
|
+
const readingClient = client;
|
|
518
|
+
const value = await walletStatus.refresh(force);
|
|
519
|
+
if (readingClient !== client) return walletUsd;
|
|
520
|
+
walletUsd = value;
|
|
513
521
|
walletAt = walletStatus.checkedAt;
|
|
514
522
|
return walletUsd;
|
|
515
523
|
};
|
|
516
524
|
const refreshCredit = async (force = false) => {
|
|
517
|
-
|
|
525
|
+
const readingClient = client;
|
|
526
|
+
const value = await creditStatus.refresh(force);
|
|
527
|
+
if (readingClient !== client) return creditUsd;
|
|
528
|
+
creditUsd = value;
|
|
518
529
|
creditAt = creditStatus.checkedAt;
|
|
519
530
|
return creditUsd;
|
|
520
531
|
};
|
|
@@ -532,7 +543,22 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
532
543
|
const walletMoney = () => priceHoldings(lastSnap || [], lastPrices);
|
|
533
544
|
let tunnelError = null;
|
|
534
545
|
|
|
546
|
+
const reloadWallet = () => {
|
|
547
|
+
const next = currentWallet();
|
|
548
|
+
if (next !== client) {
|
|
549
|
+
client = next;
|
|
550
|
+
resetBalanceCache(); resetRailMemory();
|
|
551
|
+
walletStatus.reset(); creditStatus.reset();
|
|
552
|
+
lastSnap = null; walletUsd = creditUsd = null; walletAt = creditAt = 0;
|
|
553
|
+
say(`wallet replaced · now using Solana ${client.address} · Base ${client.evmAddress}`);
|
|
554
|
+
refreshWallet(true); refreshCredit(true);
|
|
555
|
+
}
|
|
556
|
+
return client;
|
|
557
|
+
};
|
|
535
558
|
const server = http.createServer(async (req, res) => {
|
|
559
|
+
// Every request keeps one client even if the file changes mid-response.
|
|
560
|
+
let client;
|
|
561
|
+
try { client = reloadWallet(); } catch (error) { jsonErr(res, 503, error.message); return; }
|
|
536
562
|
// MCP on the SAME port as the proxy. One `npx openzoo` gives a harness
|
|
537
563
|
// both surfaces: point base_url at /v1, or add /mcp for tools.
|
|
538
564
|
if (req.method === 'GET' && (req.url || '').split('?')[0] === '/v1/session') {
|
|
@@ -1137,7 +1163,7 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
1137
1163
|
// Funding checks run for desktop/background launches as well as terminal sessions.
|
|
1138
1164
|
refreshBalances = () => refreshWallet(true);
|
|
1139
1165
|
refreshWallet(); refreshCredit(); refreshPrices();
|
|
1140
|
-
const fundingTimer = setInterval(() => { refreshCredit(); refreshWallet(); refreshPrices(); }, 3000);
|
|
1166
|
+
const fundingTimer = setInterval(() => { try { reloadWallet(); refreshCredit(); refreshWallet(); refreshPrices(); } catch { /* request path reports an incomplete wallet replacement */ } }, 3000);
|
|
1141
1167
|
fundingTimer.unref?.();
|
|
1142
1168
|
server.on('close', () => clearInterval(fundingTimer));
|
|
1143
1169
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import crypto from 'node:crypto';
|
|
3
|
+
/** Swap whole clients, never mutate the signer of an in-flight request. */
|
|
4
|
+
export function walletReloader(initial, { file, load }) {
|
|
5
|
+
const fingerprint = () => crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
|
6
|
+
let current = initial, stamp = fingerprint();
|
|
7
|
+
return () => {
|
|
8
|
+
try {
|
|
9
|
+
const next = fingerprint();
|
|
10
|
+
if (next !== stamp) {
|
|
11
|
+
const replacement = load();
|
|
12
|
+
// load may migrate the legacy array format after validating it.
|
|
13
|
+
stamp = fingerprint();
|
|
14
|
+
current = replacement;
|
|
15
|
+
}
|
|
16
|
+
return current;
|
|
17
|
+
} catch {
|
|
18
|
+
// Do not print parser errors containing secret-key bytes, or keep paying
|
|
19
|
+
// from the old wallet while a replacement is missing/partially copied.
|
|
20
|
+
throw new Error('Could not load wallet.json. Finish copying a valid OpenZoo wallet file; payments are paused until it is ready.');
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
}
|
package/lib/wallet.js
CHANGED
|
@@ -10,7 +10,7 @@ import { config } from './config.js';
|
|
|
10
10
|
* Legacy format (bare 64-byte array, solana-keygen style) is migrated in place.
|
|
11
11
|
* Your keys never leave this machine — the zoo only ever sees signed transfers.
|
|
12
12
|
*/
|
|
13
|
-
export function loadOrCreateWallet() {
|
|
13
|
+
export function loadOrCreateWallet({ create = true } = {}) {
|
|
14
14
|
const p = config.walletPath;
|
|
15
15
|
let solanaBytes;
|
|
16
16
|
let evmKey;
|
|
@@ -25,16 +25,20 @@ export function loadOrCreateWallet() {
|
|
|
25
25
|
evmKey = raw.evm;
|
|
26
26
|
}
|
|
27
27
|
} else {
|
|
28
|
+
if (!create) throw new Error("wallet.json is missing. Finish replacing the wallet file and try again.");
|
|
28
29
|
solanaBytes = Keypair.generate().secretKey;
|
|
29
30
|
created = true;
|
|
30
31
|
}
|
|
32
|
+
// Validate before migrating, so a partial replacement is never overwritten.
|
|
33
|
+
const keypair = Keypair.fromSecretKey(solanaBytes);
|
|
34
|
+
if (evmKey && !/^0x[0-9a-f]{64}$/i.test(evmKey)) throw new Error("Invalid EVM wallet key");
|
|
31
35
|
if (!evmKey) {
|
|
32
36
|
evmKey = `0x${crypto.randomBytes(32).toString('hex')}`;
|
|
33
37
|
fs.mkdirSync(path.dirname(p), { recursive: true, mode: 0o700 });
|
|
34
38
|
fs.writeFileSync(p, JSON.stringify({ solana: Array.from(solanaBytes), evm: evmKey }), { mode: 0o600 });
|
|
35
39
|
}
|
|
36
40
|
return {
|
|
37
|
-
keypair
|
|
41
|
+
keypair,
|
|
38
42
|
evmPrivateKey: evmKey,
|
|
39
43
|
created,
|
|
40
44
|
path: p,
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
// CommandLineToArgvW quoting, without invoking cmd.exe or interpreting shell syntax.
|
|
6
|
+
export function windowsArgument(value) {
|
|
7
|
+
return '"' + String(value).replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/g, '$1$1') + '"';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function packagedLaunchScript({ app, args, env, statusFile }) {
|
|
11
|
+
const data = Buffer.from(JSON.stringify({ app, args, statusFile,
|
|
12
|
+
env: { CODEX_HOME: env.CODEX_HOME, CODEX_ELECTRON_USER_DATA_PATH: env.CODEX_ELECTRON_USER_DATA_PATH,
|
|
13
|
+
OPENZOO_API_KEY: env.OPENZOO_API_KEY } }), 'utf8').toString('base64');
|
|
14
|
+
// This initializer runs INSIDE the package identity. Explicitly set the
|
|
15
|
+
// isolated profile there instead of relying on activation-broker inheritance.
|
|
16
|
+
const child = `$ErrorActionPreference = 'Stop'
|
|
17
|
+
$d = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${data}')) | ConvertFrom-Json
|
|
18
|
+
try {
|
|
19
|
+
foreach ($entry in $d.env.PSObject.Properties) { [Environment]::SetEnvironmentVariable($entry.Name, [string]$entry.Value, 'Process') }
|
|
20
|
+
$p = Start-Process -FilePath $d.app -ArgumentList $d.args -WorkingDirectory (Split-Path -LiteralPath $d.app) -PassThru
|
|
21
|
+
Start-Sleep -Milliseconds 1500
|
|
22
|
+
if ($p.HasExited -and $p.ExitCode -ne 0) { throw "ChatGPT exited during launch ($($p.ExitCode))" }
|
|
23
|
+
@{ok=$true;pid=$p.Id} | ConvertTo-Json -Compress | Set-Content -LiteralPath $d.statusFile -Encoding UTF8
|
|
24
|
+
} catch { @{ok=$false;error=$_.Exception.Message} | ConvertTo-Json -Compress | Set-Content -LiteralPath $d.statusFile -Encoding UTF8; exit 1 }
|
|
25
|
+
`;
|
|
26
|
+
const encoded = Buffer.from(child, 'utf16le').toString('base64');
|
|
27
|
+
return `$ErrorActionPreference = 'Stop'
|
|
28
|
+
$target = $env:OPENZOO_PACKAGED_APP
|
|
29
|
+
$match = $null
|
|
30
|
+
foreach ($pkg in (Get-AppxPackage | Where-Object { $_.Name -match 'OpenAI|ChatGPT|Codex' })) {
|
|
31
|
+
$manifest = Get-AppxPackageManifest -Package $pkg.PackageFullName
|
|
32
|
+
foreach ($entry in $manifest.Package.Applications.Application) {
|
|
33
|
+
if ($entry.Executable -and ((Join-Path $pkg.InstallLocation $entry.Executable) -eq $target)) {
|
|
34
|
+
$match = @{family=$pkg.PackageFamilyName;id=[string]$entry.Id}
|
|
35
|
+
break
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if ($match) { break }
|
|
39
|
+
}
|
|
40
|
+
if (!$match) { throw 'The ChatGPT package registration could not be found. Repair ChatGPT in Windows Settings and try again.' }
|
|
41
|
+
Invoke-CommandInDesktopPackage -PackageFamilyName $match.family -AppId $match.id -Command "$PSHOME\\powershell.exe" -Args '-NoProfile -NonInteractive -WindowStyle Hidden -EncodedCommand ${encoded}' -PreventBreakaway | Out-Null
|
|
42
|
+
$deadline = [DateTime]::UtcNow.AddSeconds(25)
|
|
43
|
+
while (!(Test-Path -LiteralPath $env:OPENZOO_LAUNCH_STATUS)) {
|
|
44
|
+
if ([DateTime]::UtcNow -gt $deadline) { throw 'ChatGPT did not confirm startup. Close ChatGPT and try Launch again.' }
|
|
45
|
+
Start-Sleep -Milliseconds 200
|
|
46
|
+
}
|
|
47
|
+
$result = Get-Content -LiteralPath $env:OPENZOO_LAUNCH_STATUS -Raw | ConvertFrom-Json
|
|
48
|
+
if (!$result.ok) { throw $result.error }
|
|
49
|
+
Write-Output $result.pid
|
|
50
|
+
`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function launchWindowsPackage(app, args, { env, run }) {
|
|
54
|
+
const scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'openzoo-launch-'));
|
|
55
|
+
const statusFile = path.join(scratch, 'status.json');
|
|
56
|
+
try {
|
|
57
|
+
const commandLine = [`--user-data-dir=${env.CODEX_ELECTRON_USER_DATA_PATH}`, ...args].map(windowsArgument).join(' ');
|
|
58
|
+
await run('powershell.exe', ['-NoProfile', '-NonInteractive', '-WindowStyle', 'Hidden', '-Command',
|
|
59
|
+
packagedLaunchScript({ app, args: commandLine, env, statusFile })],
|
|
60
|
+
{ windowsHide: true, env: { ...env, OPENZOO_PACKAGED_APP: app, OPENZOO_LAUNCH_STATUS: statusFile } });
|
|
61
|
+
} finally { fs.rmSync(scratch, { recursive: true, force: true }); }
|
|
62
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.51.
|
|
3
|
+
"version": "0.51.17",
|
|
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",
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"launcher/savings-preload.cjs",
|
|
20
20
|
"launcher/savings-renderer.js",
|
|
21
21
|
"launcher/savings-toggle.html",
|
|
22
|
-
"launcher/savings-toggle.js"
|
|
22
|
+
"launcher/savings-toggle.js",
|
|
23
|
+
"launcher/chat-visibility.cjs"
|
|
23
24
|
],
|
|
24
25
|
"engines": {
|
|
25
26
|
"node": ">=18"
|