clawmoney 0.17.46 → 0.18.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/companion/icon.icns +0 -0
- package/companion/icon.png +0 -0
- package/companion/main.js +474 -0
- package/companion/package.json +10 -0
- package/companion/preload.js +35 -0
- package/companion/tray-icon.png +0 -0
- package/companion/ui/assets/index-BNdx-pQ9.js +70 -0
- package/companion/ui/assets/index-JsJRIRy7.css +1 -0
- package/companion/ui/brand/logo-icon.png +0 -0
- package/companion/ui/index.html +16 -0
- package/dist/commands/relay.js +8 -6
- package/dist/index.js +41 -1
- package/dist/relay/pricing.js +7 -0
- package/dist/relay/provider.js +35 -5
- package/dist/relay/upstream/chatgpt-web.d.ts +51 -0
- package/dist/relay/upstream/chatgpt-web.js +114 -0
- package/dist/task/daemon.js +55 -5
- package/dist/task/preflight.d.ts +47 -0
- package/dist/task/preflight.js +515 -0
- package/dist/task/skills/codex/_bnbot.js +3 -0
- package/dist/task/skills/index.d.ts +16 -0
- package/dist/task/skills/index.js +59 -1
- package/dist/task/skills/opencli/_bnbot.d.ts +5 -0
- package/dist/task/skills/opencli/_bnbot.js +21 -0
- package/dist/task/skills/opencli/linkedin-salesnav.d.ts +7 -0
- package/dist/task/skills/opencli/linkedin-salesnav.js +36 -0
- package/dist/task/skills/opencli/platforms.d.ts +4 -0
- package/dist/task/skills/opencli/platforms.js +5 -0
- package/dist/task/skills/opencli/scrapers.d.ts +6 -0
- package/dist/task/skills/opencli/scrapers.js +37 -0
- package/dist/task/skills/x/search.js +1 -1
- package/dist/task/skills/x/trends.js +1 -1
- package/dist/task/skills/x/tweet-article.js +1 -1
- package/dist/task/skills/x/tweet-likers.js +1 -1
- package/dist/task/skills/x/tweet-retweeters.js +1 -1
- package/dist/task/skills/x/tweet.js +1 -1
- package/dist/task/skills/x/user-by-screen-name.js +1 -1
- package/dist/task/skills/x/user-followers.js +1 -1
- package/dist/task/skills/x/user-following.js +1 -1
- package/dist/task/skills/x/user-tweets.js +1 -1
- package/dist/task/skills/x-search.js +1 -1
- package/dist/ui/companion.d.ts +4 -0
- package/dist/ui/companion.js +195 -0
- package/dist/ui/ipc-client.d.ts +4 -0
- package/dist/ui/ipc-client.js +73 -0
- package/package.json +3 -2
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
// ClawMoney companion — Electron main process.
|
|
2
|
+
//
|
|
3
|
+
// A tiny menu-bar (tray) app that gives CLI-only users a persistent icon +
|
|
4
|
+
// a dashboard window, WITHOUT installing the full Tauri desktop app.
|
|
5
|
+
// The CLI (client) talks to this process over a filesystem IPC bridge
|
|
6
|
+
// (see ../src/ui/ipc-client.ts), the same pattern Coinbase's `awal` uses.
|
|
7
|
+
//
|
|
8
|
+
// Plain CommonJS on purpose (Electron main is happiest that way).
|
|
9
|
+
|
|
10
|
+
const { app, BrowserWindow, Tray, Menu, nativeImage, shell, ipcMain } = require('electron');
|
|
11
|
+
const fs = require('node:fs');
|
|
12
|
+
const path = require('node:path');
|
|
13
|
+
const os = require('node:os');
|
|
14
|
+
const http = require('node:http');
|
|
15
|
+
const { spawn } = require('node:child_process');
|
|
16
|
+
|
|
17
|
+
// Rebrand from "Electron" — sets the macOS menu-bar app name (and userData path).
|
|
18
|
+
app.setName('ClawMoney');
|
|
19
|
+
|
|
20
|
+
// Single shared bridge dir, must match src/ui/ipc-client.ts.
|
|
21
|
+
const BRIDGE = path.join(os.tmpdir(), 'clawmoney-ui-bridge');
|
|
22
|
+
const REQ_DIR = path.join(BRIDGE, 'requests');
|
|
23
|
+
const RES_DIR = path.join(BRIDGE, 'responses');
|
|
24
|
+
const PID_FILE = path.join(BRIDGE, 'companion.pid');
|
|
25
|
+
|
|
26
|
+
// The CLI passes the resolved dashboard URL (with auth token) via env.
|
|
27
|
+
const DASHBOARD_URL = process.env.CLAWMONEY_DASHBOARD_URL || 'https://clawmoney.ai/dashboard';
|
|
28
|
+
|
|
29
|
+
// Verbose log so the CLI side can diagnose render failures (white screen etc).
|
|
30
|
+
const LOG_FILE = path.join(__dirname, 'companion.log');
|
|
31
|
+
function log(msg) {
|
|
32
|
+
const line = `[${new Date().toISOString()}] ${msg}`;
|
|
33
|
+
try { fs.appendFileSync(LOG_FILE, line + '\n'); } catch {}
|
|
34
|
+
console.error(line);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Local static server for the bundled desktop UI. We serve over HTTP (not
|
|
38
|
+
// file://) because the UI references public assets by absolute path, e.g.
|
|
39
|
+
// "/brand/logo-icon.png" — file:// would resolve those against the filesystem
|
|
40
|
+
// root and 404. A server root makes them resolve correctly (like Tauri does).
|
|
41
|
+
const MIME = {
|
|
42
|
+
'.html': 'text/html; charset=utf-8',
|
|
43
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
44
|
+
'.css': 'text/css; charset=utf-8',
|
|
45
|
+
'.json': 'application/json; charset=utf-8',
|
|
46
|
+
'.svg': 'image/svg+xml',
|
|
47
|
+
'.png': 'image/png',
|
|
48
|
+
'.jpg': 'image/jpeg',
|
|
49
|
+
'.jpeg': 'image/jpeg',
|
|
50
|
+
'.gif': 'image/gif',
|
|
51
|
+
'.webp': 'image/webp',
|
|
52
|
+
'.ico': 'image/x-icon',
|
|
53
|
+
'.woff': 'font/woff',
|
|
54
|
+
'.woff2': 'font/woff2',
|
|
55
|
+
'.ttf': 'font/ttf',
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
let serverPort = null;
|
|
59
|
+
|
|
60
|
+
function startUIServer() {
|
|
61
|
+
const root = path.join(__dirname, 'ui');
|
|
62
|
+
if (!fs.existsSync(path.join(root, 'index.html'))) {
|
|
63
|
+
log('ui/index.html missing — UI server not started');
|
|
64
|
+
return Promise.resolve(null);
|
|
65
|
+
}
|
|
66
|
+
return new Promise((resolve) => {
|
|
67
|
+
const server = http.createServer((req, res) => {
|
|
68
|
+
let urlPath = decodeURIComponent((req.url || '/').split('?')[0]);
|
|
69
|
+
if (urlPath === '/') urlPath = '/index.html';
|
|
70
|
+
const safe = path.normalize(urlPath).replace(/^(\.\.[/\\])+/, '');
|
|
71
|
+
let filePath = path.join(root, safe);
|
|
72
|
+
if (!filePath.startsWith(root) || !fs.existsSync(filePath) || fs.statSync(filePath).isDirectory()) {
|
|
73
|
+
filePath = path.join(root, 'index.html'); // SPA fallback
|
|
74
|
+
}
|
|
75
|
+
res.writeHead(200, { 'Content-Type': MIME[path.extname(filePath).toLowerCase()] || 'application/octet-stream' });
|
|
76
|
+
fs.createReadStream(filePath).pipe(res);
|
|
77
|
+
});
|
|
78
|
+
server.on('error', (e) => { log(`UI server error: ${e.message}`); resolve(null); });
|
|
79
|
+
server.listen(0, '127.0.0.1', () => {
|
|
80
|
+
serverPort = server.address().port;
|
|
81
|
+
log(`UI server on http://127.0.0.1:${serverPort}`);
|
|
82
|
+
resolve(serverPort);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── Tauri invoke bridge ─────────────────────────────────────────────────────
|
|
88
|
+
// preload.js injects window.__TAURI_INTERNALS__.invoke -> ipcRenderer -> here.
|
|
89
|
+
// We implement the desktop UI's commands against clawmoney config + backend so
|
|
90
|
+
// the UI runs in real (isTauri) mode. Mirrors the Rust commands in
|
|
91
|
+
// clawmoney-desktop/src-tauri/src/main.rs (which mostly call the same backend
|
|
92
|
+
// or `clawmoney` CLI).
|
|
93
|
+
const API_BASE = process.env.CLAWMONEY_API_BASE || 'https://api.bnbot.ai';
|
|
94
|
+
const CONFIG_PATH = path.join(os.homedir(), '.clawmoney', 'config.yaml');
|
|
95
|
+
const EXTENSION_URL = 'https://clawmoney.ai/extension';
|
|
96
|
+
|
|
97
|
+
// Electron GUI apps inherit a minimal PATH; extend it so `bnbot` resolves.
|
|
98
|
+
const CLI_PATH = [
|
|
99
|
+
'/opt/homebrew/bin', '/usr/local/bin', '/usr/bin', '/bin',
|
|
100
|
+
path.join(os.homedir(), '.npm-global', 'bin'),
|
|
101
|
+
process.env.PATH || '',
|
|
102
|
+
].join(':');
|
|
103
|
+
|
|
104
|
+
// Mirror Rust service_status: read the daemon pid file, check the process is alive.
|
|
105
|
+
function readPidStatus(name, pidFile) {
|
|
106
|
+
const pidPath = path.join(os.homedir(), '.clawmoney', pidFile);
|
|
107
|
+
try {
|
|
108
|
+
const pid = parseInt(fs.readFileSync(pidPath, 'utf8').trim(), 10) || null;
|
|
109
|
+
if (pid) process.kill(pid, 0); // throws if the process isn't alive
|
|
110
|
+
return { name, running: !!pid, pid, pidFile: pidPath };
|
|
111
|
+
} catch {
|
|
112
|
+
return { name, running: false, pid: null, pidFile: pidPath };
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Mirror Rust get_extension_status: `bnbot status`; output with "extension" +
|
|
117
|
+
// "connected" => connected; command missing/failed => not installed.
|
|
118
|
+
function getExtensionStatus() {
|
|
119
|
+
return new Promise((resolve) => {
|
|
120
|
+
let done = false;
|
|
121
|
+
let out = '';
|
|
122
|
+
const finish = (installed, connected, status) => {
|
|
123
|
+
if (done) return;
|
|
124
|
+
done = true;
|
|
125
|
+
resolve({ installed, connected, status, installUrl: EXTENSION_URL, detail: out.trim() });
|
|
126
|
+
};
|
|
127
|
+
try {
|
|
128
|
+
const p = spawn('bnbot', ['status'], { env: { ...process.env, PATH: CLI_PATH } });
|
|
129
|
+
const timer = setTimeout(() => { try { p.kill(); } catch { /* ignore */ } finish(false, false, 'missing'); }, 12000);
|
|
130
|
+
p.stdout.on('data', (d) => { out += d; });
|
|
131
|
+
p.stderr.on('data', (d) => { out += d; });
|
|
132
|
+
p.on('error', () => { clearTimeout(timer); finish(false, false, 'missing'); });
|
|
133
|
+
p.on('close', (code) => {
|
|
134
|
+
clearTimeout(timer);
|
|
135
|
+
if (code !== 0) return finish(false, false, 'missing');
|
|
136
|
+
const o = out.toLowerCase();
|
|
137
|
+
const connected = o.includes('extension') && o.includes('connected');
|
|
138
|
+
finish(true, connected, connected ? 'connected' : 'not_connected');
|
|
139
|
+
});
|
|
140
|
+
} catch {
|
|
141
|
+
finish(false, false, 'missing');
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function readClawConfig() {
|
|
147
|
+
try {
|
|
148
|
+
const txt = fs.readFileSync(CONFIG_PATH, 'utf8');
|
|
149
|
+
const get = (k) => {
|
|
150
|
+
const m = txt.match(new RegExp('^' + k + ':\\s*(.+)$', 'm'));
|
|
151
|
+
return m ? m[1].trim().replace(/^["']|["']$/g, '') : undefined;
|
|
152
|
+
};
|
|
153
|
+
return {
|
|
154
|
+
api_key: get('api_key'),
|
|
155
|
+
agent_id: get('agent_id'),
|
|
156
|
+
agent_slug: get('agent_slug'),
|
|
157
|
+
email: get('email'),
|
|
158
|
+
wallet_address: get('wallet_address'),
|
|
159
|
+
};
|
|
160
|
+
} catch {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function apiGet(p, apiKey) {
|
|
166
|
+
try {
|
|
167
|
+
const r = await fetch(API_BASE + p, {
|
|
168
|
+
headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : {},
|
|
169
|
+
});
|
|
170
|
+
const data = await r.json().catch(() => null);
|
|
171
|
+
return { ok: r.ok, status: r.status, data };
|
|
172
|
+
} catch (e) {
|
|
173
|
+
return { ok: false, status: 0, data: null, error: String(e) };
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Mirror desktop's ensure_provider_running: when the dashboard loads and the
|
|
178
|
+
// user is logged in but the Market provider isn't running, start it — so the
|
|
179
|
+
// companion auto-takes orders on open, just like the desktop app does.
|
|
180
|
+
// Uses ELECTRON_RUN_AS_NODE so the companion's own node runs the CLI entry
|
|
181
|
+
// (no dependency on a global `clawmoney` or system node on PATH).
|
|
182
|
+
function ensureMarketRunning() {
|
|
183
|
+
const entry = process.env.CLAWMONEY_CLI_ENTRY;
|
|
184
|
+
if (!entry || !fs.existsSync(entry)) return;
|
|
185
|
+
const cfg = readClawConfig();
|
|
186
|
+
if (!cfg || !cfg.api_key) return; // not logged in → don't take orders
|
|
187
|
+
if (readPidStatus('market', 'provider.pid').running) return; // already up
|
|
188
|
+
try {
|
|
189
|
+
const child = spawn(process.execPath, [entry, 'market', 'start'], {
|
|
190
|
+
env: { ...process.env, ELECTRON_RUN_AS_NODE: '1', PATH: CLI_PATH },
|
|
191
|
+
detached: true,
|
|
192
|
+
stdio: 'ignore',
|
|
193
|
+
});
|
|
194
|
+
child.unref();
|
|
195
|
+
log('ensureMarketRunning: started `clawmoney market start`');
|
|
196
|
+
} catch (e) {
|
|
197
|
+
log(`ensureMarketRunning failed: ${e.message}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function loadDashboard() {
|
|
202
|
+
ensureMarketRunning();
|
|
203
|
+
const cfg = readClawConfig();
|
|
204
|
+
const configured = !!(cfg && cfg.api_key);
|
|
205
|
+
const dash = {
|
|
206
|
+
ok: true,
|
|
207
|
+
apiBase: API_BASE,
|
|
208
|
+
configPath: CONFIG_PATH,
|
|
209
|
+
configured,
|
|
210
|
+
config: configured
|
|
211
|
+
? { agent_slug: cfg.agent_slug, email: cfg.email, wallet_address: cfg.wallet_address }
|
|
212
|
+
: null,
|
|
213
|
+
account: { ok: false },
|
|
214
|
+
wallet: { ok: false },
|
|
215
|
+
extension: { installed: false, connected: false },
|
|
216
|
+
services: { market: readPidStatus('market', 'provider.pid'), surf: readPidStatus('surf', 'relay.pid') },
|
|
217
|
+
history: { ok: true, orders: [], escrow: [], orderCount: 0, escrowCount: 0 },
|
|
218
|
+
orderEarnings: { ok: false },
|
|
219
|
+
skills: [],
|
|
220
|
+
providerLog: [],
|
|
221
|
+
connections: { platforms: [] },
|
|
222
|
+
cliAvailable: true,
|
|
223
|
+
};
|
|
224
|
+
dash.extension = await getExtensionStatus(); // local check, independent of config
|
|
225
|
+
if (!configured) return dash;
|
|
226
|
+
const key = cfg.api_key;
|
|
227
|
+
const [account, wBase, wBsc, skills, orders, escrow, earnings] = await Promise.all([
|
|
228
|
+
apiGet('/api/v1/claw-agents/me', key),
|
|
229
|
+
apiGet('/api/v1/claw-agents/me/wallet/balance?asset=usdc&network=base', key),
|
|
230
|
+
apiGet('/api/v1/claw-agents/me/wallet/balance?asset=usdc&network=bsc', key),
|
|
231
|
+
apiGet('/api/v1/market/skills/mine?active_only=false', key),
|
|
232
|
+
apiGet('/api/v1/market/orders/mine?role=provider&limit=50', key),
|
|
233
|
+
apiGet('/api/v1/market/escrow/assigned?limit=12', key),
|
|
234
|
+
apiGet('/api/v1/market/orders/mine/earnings', key),
|
|
235
|
+
]);
|
|
236
|
+
|
|
237
|
+
if (account.ok) dash.account = { ok: true, data: account.data };
|
|
238
|
+
|
|
239
|
+
const baseAmt = parseFloat((wBase.data && wBase.data.amount) || '0') || 0;
|
|
240
|
+
const bscAmt = parseFloat((wBsc.data && wBsc.data.amount) || '0') || 0;
|
|
241
|
+
dash.wallet = {
|
|
242
|
+
ok: true,
|
|
243
|
+
address: cfg.wallet_address || (wBase.data && wBase.data.address) || null,
|
|
244
|
+
balance: baseAmt + bscAmt,
|
|
245
|
+
base: baseAmt,
|
|
246
|
+
bsc: bscAmt,
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
if (skills.ok) dash.skills = (skills.data && skills.data.data) || [];
|
|
250
|
+
|
|
251
|
+
const orderList = (orders.data && orders.data.data) || [];
|
|
252
|
+
const escrowList = (escrow.data && escrow.data.data) || [];
|
|
253
|
+
dash.history = {
|
|
254
|
+
ok: true,
|
|
255
|
+
orders: orderList,
|
|
256
|
+
escrow: escrowList,
|
|
257
|
+
orderCount: orderList.length,
|
|
258
|
+
escrowCount: escrowList.length,
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
if (earnings.ok) dash.orderEarnings = { ok: true, ...(earnings.data || {}) };
|
|
262
|
+
|
|
263
|
+
return dash;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async function handleTauriCommand(cmd, args) {
|
|
267
|
+
log(`invoke: ${cmd}`);
|
|
268
|
+
try {
|
|
269
|
+
switch (cmd) {
|
|
270
|
+
case 'load_dashboard':
|
|
271
|
+
return await loadDashboard();
|
|
272
|
+
case 'load_provider_log':
|
|
273
|
+
return { ok: true, providerLog: [] };
|
|
274
|
+
default:
|
|
275
|
+
// window/event plugin calls (e.g. plugin:window|start_dragging) — no-op for now.
|
|
276
|
+
if (typeof cmd === 'string' && cmd.startsWith('plugin:')) return {};
|
|
277
|
+
return { ok: true };
|
|
278
|
+
}
|
|
279
|
+
} catch (e) {
|
|
280
|
+
log(`invoke ${cmd} failed: ${e.message}`);
|
|
281
|
+
return { ok: false, error: String(e) };
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
ipcMain.handle('tauri:invoke', (_e, cmd, args) => handleTauriCommand(cmd, args));
|
|
286
|
+
|
|
287
|
+
let win = null;
|
|
288
|
+
let tray = null;
|
|
289
|
+
|
|
290
|
+
// --- single instance: never two icons / two windows --------------------------
|
|
291
|
+
if (!app.requestSingleInstanceLock()) {
|
|
292
|
+
app.quit();
|
|
293
|
+
process.exit(0);
|
|
294
|
+
}
|
|
295
|
+
app.on('second-instance', () => showWindow());
|
|
296
|
+
|
|
297
|
+
function ensureDirs() {
|
|
298
|
+
for (const d of [BRIDGE, REQ_DIR, RES_DIR]) {
|
|
299
|
+
if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true, mode: 0o700 });
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function loadUI() {
|
|
304
|
+
if (serverPort) {
|
|
305
|
+
const url = `http://127.0.0.1:${serverPort}/`;
|
|
306
|
+
log(`loading desktop UI: ${url}`);
|
|
307
|
+
win.loadURL(url);
|
|
308
|
+
} else {
|
|
309
|
+
log(`UI server unavailable, loading ${DASHBOARD_URL}`);
|
|
310
|
+
win.loadURL(DASHBOARD_URL);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function createWindow() {
|
|
315
|
+
win = new BrowserWindow({
|
|
316
|
+
width: 1080,
|
|
317
|
+
height: 720,
|
|
318
|
+
minWidth: 920,
|
|
319
|
+
minHeight: 640,
|
|
320
|
+
show: false,
|
|
321
|
+
// Match the Tauri desktop window: transparent + frameless. The desktop UI
|
|
322
|
+
// draws its own rounded "floating card" + traffic lights, so a native frame
|
|
323
|
+
// or opaque background would double the chrome and leak a black border
|
|
324
|
+
// around the card (exactly what the user saw).
|
|
325
|
+
transparent: true,
|
|
326
|
+
frame: false,
|
|
327
|
+
hasShadow: false,
|
|
328
|
+
backgroundColor: '#00000000',
|
|
329
|
+
webPreferences: {
|
|
330
|
+
preload: path.join(__dirname, 'preload.js'),
|
|
331
|
+
contextIsolation: false,
|
|
332
|
+
nodeIntegration: false,
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
// Diagnostics: surface render failures / renderer console into companion.log.
|
|
336
|
+
win.webContents.on('did-fail-load', (_e, code, desc, url) => log(`did-fail-load ${code} ${desc} ${url}`));
|
|
337
|
+
win.webContents.on('console-message', (e) => log(`[renderer] ${e.message} (${e.sourceId}:${e.lineNumber})`));
|
|
338
|
+
win.webContents.on('did-finish-load', () => {
|
|
339
|
+
log('did-finish-load OK');
|
|
340
|
+
// The desktop UI only goes transparent / fills edge-to-edge when isTauri() is
|
|
341
|
+
// true. We keep isTauri() false (so data stays on mock for this step), and
|
|
342
|
+
// instead force the Tauri look via CSS: kill the fake macOS desktop bg, the
|
|
343
|
+
// body color, the .stage padding, and the letterbox scale transform.
|
|
344
|
+
win.webContents.insertCSS(
|
|
345
|
+
'html,body{background:transparent !important;}' +
|
|
346
|
+
'#desktop{display:none !important;}' +
|
|
347
|
+
'.stage{padding:0 !important;}' +
|
|
348
|
+
'#app{transform:none !important;border-radius:22px !important;}'
|
|
349
|
+
).catch((e) => log(`insertCSS failed: ${e.message}`));
|
|
350
|
+
win.show();
|
|
351
|
+
});
|
|
352
|
+
loadUI();
|
|
353
|
+
// Open external links in the real browser, not inside the app.
|
|
354
|
+
win.webContents.setWindowOpenHandler(({ url }) => {
|
|
355
|
+
shell.openExternal(url);
|
|
356
|
+
return { action: 'deny' };
|
|
357
|
+
});
|
|
358
|
+
// Closing the window just hides it — the tray icon stays put (use Quit to exit).
|
|
359
|
+
win.on('close', (e) => {
|
|
360
|
+
if (!app.isQuitting) {
|
|
361
|
+
e.preventDefault();
|
|
362
|
+
win.hide();
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function showWindow() {
|
|
368
|
+
if (!win) createWindow();
|
|
369
|
+
win.show();
|
|
370
|
+
win.focus();
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function createTray() {
|
|
374
|
+
let icon = nativeImage.createFromPath(path.join(__dirname, 'tray-icon.png'));
|
|
375
|
+
if (process.platform === 'darwin') icon = icon.resize({ width: 18, height: 18 });
|
|
376
|
+
tray = new Tray(icon);
|
|
377
|
+
tray.setToolTip('ClawMoney');
|
|
378
|
+
tray.setContextMenu(
|
|
379
|
+
Menu.buildFromTemplate([
|
|
380
|
+
{ label: 'Open ClawMoney', click: showWindow },
|
|
381
|
+
{ type: 'separator' },
|
|
382
|
+
{
|
|
383
|
+
label: 'Quit',
|
|
384
|
+
click: () => {
|
|
385
|
+
app.isQuitting = true;
|
|
386
|
+
cleanup();
|
|
387
|
+
app.exit(0);
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
])
|
|
391
|
+
);
|
|
392
|
+
tray.on('click', showWindow);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// --- filesystem IPC server (CLI -> companion) --------------------------------
|
|
396
|
+
// CLI writes requests/<id>.json; we handle and write responses/<id>.json.
|
|
397
|
+
function handleRequestFile(file) {
|
|
398
|
+
const reqPath = path.join(REQ_DIR, file);
|
|
399
|
+
let req;
|
|
400
|
+
try {
|
|
401
|
+
req = JSON.parse(fs.readFileSync(reqPath, 'utf8'));
|
|
402
|
+
} catch {
|
|
403
|
+
return; // half-written file; the next watch event will catch it
|
|
404
|
+
}
|
|
405
|
+
try { fs.unlinkSync(reqPath); } catch {}
|
|
406
|
+
|
|
407
|
+
let result;
|
|
408
|
+
switch (req.channel) {
|
|
409
|
+
case 'ping':
|
|
410
|
+
result = { ok: true, pid: process.pid };
|
|
411
|
+
break;
|
|
412
|
+
case 'show':
|
|
413
|
+
showWindow();
|
|
414
|
+
result = { ok: true };
|
|
415
|
+
break;
|
|
416
|
+
case 'navigate':
|
|
417
|
+
if (win && req.data && req.data.url) win.loadURL(req.data.url);
|
|
418
|
+
showWindow();
|
|
419
|
+
result = { ok: true };
|
|
420
|
+
break;
|
|
421
|
+
case 'quit':
|
|
422
|
+
result = { ok: true };
|
|
423
|
+
setTimeout(() => { app.isQuitting = true; cleanup(); app.exit(0); }, 50);
|
|
424
|
+
break;
|
|
425
|
+
default:
|
|
426
|
+
result = { error: `unknown channel: ${req.channel}` };
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
try {
|
|
430
|
+
fs.writeFileSync(
|
|
431
|
+
path.join(RES_DIR, `${req.id}.json`),
|
|
432
|
+
JSON.stringify({ id: req.id, result }),
|
|
433
|
+
{ mode: 0o600 }
|
|
434
|
+
);
|
|
435
|
+
} catch {}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function watchRequests() {
|
|
439
|
+
// fs.watch misses files written before the watcher attaches — drain any
|
|
440
|
+
// requests that arrived during startup first (fixes the IPC "show" timeout).
|
|
441
|
+
try {
|
|
442
|
+
for (const f of fs.readdirSync(REQ_DIR)) {
|
|
443
|
+
if (f.endsWith('.json')) handleRequestFile(f);
|
|
444
|
+
}
|
|
445
|
+
} catch {}
|
|
446
|
+
fs.watch(REQ_DIR, (_event, file) => {
|
|
447
|
+
if (file && file.endsWith('.json')) handleRequestFile(file);
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function cleanup() {
|
|
452
|
+
try { fs.unlinkSync(PID_FILE); } catch {}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
app.whenReady().then(async () => {
|
|
456
|
+
ensureDirs();
|
|
457
|
+
fs.writeFileSync(PID_FILE, String(process.pid), { mode: 0o600 });
|
|
458
|
+
// Dock icon — dev Electron otherwise shows the default atom icon.
|
|
459
|
+
if (process.platform === 'darwin' && app.dock) {
|
|
460
|
+
try {
|
|
461
|
+
app.dock.setIcon(nativeImage.createFromPath(path.join(__dirname, 'icon.png')));
|
|
462
|
+
} catch (e) {
|
|
463
|
+
log(`dock.setIcon failed: ${e.message}`);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
await startUIServer();
|
|
467
|
+
createTray();
|
|
468
|
+
createWindow();
|
|
469
|
+
watchRequests();
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
// Tray app: don't quit when the window closes.
|
|
473
|
+
app.on('window-all-closed', () => {});
|
|
474
|
+
app.on('before-quit', () => { app.isQuitting = true; cleanup(); });
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "clawmoney-companion",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"description": "ClawMoney CLI companion — menu-bar tray icon + dashboard window (Electron). Auto-installed on first `clawmoney ui` / `market start --ui`.",
|
|
6
|
+
"main": "main.js",
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"electron": "^37.2.1"
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Companion preload — injects a Tauri __TAURI_INTERNALS__ bridge so the desktop
|
|
2
|
+
// UI runs in REAL mode (isTauri() === true) instead of mock. @tauri-apps/api's
|
|
3
|
+
// invoke() calls window.__TAURI_INTERNALS__.invoke(); we forward that to the
|
|
4
|
+
// Electron main process (see main.js ipcMain 'tauri:invoke'), which implements
|
|
5
|
+
// the commands against clawmoney config + the backend.
|
|
6
|
+
//
|
|
7
|
+
// contextIsolation:false so we can define window globals directly. The desktop
|
|
8
|
+
// UI doesn't use Tauri events, so callbacks are a minimal local stub.
|
|
9
|
+
const { ipcRenderer } = require('electron');
|
|
10
|
+
|
|
11
|
+
const callbacks = new Map();
|
|
12
|
+
let nextCallbackId = 0;
|
|
13
|
+
|
|
14
|
+
window.__TAURI_INTERNALS__ = {
|
|
15
|
+
invoke: (cmd, args) => ipcRenderer.invoke('tauri:invoke', cmd, args ?? {}),
|
|
16
|
+
transformCallback: (callback, once) => {
|
|
17
|
+
const id = ++nextCallbackId;
|
|
18
|
+
callbacks.set(id, { callback, once: !!once });
|
|
19
|
+
return id;
|
|
20
|
+
},
|
|
21
|
+
unregisterCallback: (id) => callbacks.delete(id),
|
|
22
|
+
runCallback: (id, payload) => {
|
|
23
|
+
const entry = callbacks.get(id);
|
|
24
|
+
if (!entry) return;
|
|
25
|
+
if (entry.once) callbacks.delete(id);
|
|
26
|
+
try { entry.callback(payload); } catch { /* ignore */ }
|
|
27
|
+
},
|
|
28
|
+
callbacks,
|
|
29
|
+
convertFileSrc: (filePath) => filePath,
|
|
30
|
+
metadata: {
|
|
31
|
+
currentWindow: { label: 'main' },
|
|
32
|
+
currentWebview: { windowLabel: 'main', label: 'main' },
|
|
33
|
+
},
|
|
34
|
+
plugins: {},
|
|
35
|
+
};
|
|
Binary file
|