clawmoney 0.18.0 → 0.18.1
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/main.js +177 -6
- package/package.json +1 -1
package/companion/main.js
CHANGED
|
@@ -12,7 +12,7 @@ const fs = require('node:fs');
|
|
|
12
12
|
const path = require('node:path');
|
|
13
13
|
const os = require('node:os');
|
|
14
14
|
const http = require('node:http');
|
|
15
|
-
const { spawn } = require('node:child_process');
|
|
15
|
+
const { spawn, spawnSync } = require('node:child_process');
|
|
16
16
|
|
|
17
17
|
// Rebrand from "Electron" — sets the macOS menu-bar app name (and userData path).
|
|
18
18
|
app.setName('ClawMoney');
|
|
@@ -186,13 +186,17 @@ function ensureMarketRunning() {
|
|
|
186
186
|
if (!cfg || !cfg.api_key) return; // not logged in → don't take orders
|
|
187
187
|
if (readPidStatus('market', 'provider.pid').running) return; // already up
|
|
188
188
|
try {
|
|
189
|
-
|
|
190
|
-
|
|
189
|
+
// Use system `node` (not process.execPath). process.execPath is the ClawMoney
|
|
190
|
+
// electron binary; even with ELECTRON_RUN_AS_NODE, the CLI's daemon inherits
|
|
191
|
+
// that execPath and re-launches as a GUI electron window → single-instance
|
|
192
|
+
// clash → the companion flickers/relaunches. `node` keeps the daemon headless.
|
|
193
|
+
const child = spawn('node', [entry, 'market', 'start'], {
|
|
194
|
+
env: { ...process.env, PATH: CLI_PATH },
|
|
191
195
|
detached: true,
|
|
192
196
|
stdio: 'ignore',
|
|
193
197
|
});
|
|
194
198
|
child.unref();
|
|
195
|
-
log('ensureMarketRunning: started `clawmoney market start`');
|
|
199
|
+
log('ensureMarketRunning: started `node clawmoney market start`');
|
|
196
200
|
} catch (e) {
|
|
197
201
|
log(`ensureMarketRunning failed: ${e.message}`);
|
|
198
202
|
}
|
|
@@ -263,6 +267,71 @@ async function loadDashboard() {
|
|
|
263
267
|
return dash;
|
|
264
268
|
}
|
|
265
269
|
|
|
270
|
+
// LLM subscriptions that can be resold via the relay (mirrors desktop LLM_SPECS).
|
|
271
|
+
const LLM_SPECS = [
|
|
272
|
+
{ cli: 'codex', package: '@openai/codex', token: '.codex/auth.json', model: 'gpt-5.5' },
|
|
273
|
+
{ cli: 'chatgpt-web', package: '@jackwener/opencli', token: '', model: 'gpt-5.2' },
|
|
274
|
+
{ cli: 'gemini', package: '@google/gemini-cli', token: '.gemini/oauth_creds.json', model: 'gemini-2.5-flash' },
|
|
275
|
+
];
|
|
276
|
+
const RELAY_RESALE_PATH = path.join(os.homedir(), '.clawmoney', 'relay-resale.json');
|
|
277
|
+
|
|
278
|
+
function commandExists(bin) {
|
|
279
|
+
try {
|
|
280
|
+
return spawnSync('which', [bin], { env: { ...process.env, PATH: CLI_PATH }, encoding: 'utf8' }).status === 0;
|
|
281
|
+
} catch { return false; }
|
|
282
|
+
}
|
|
283
|
+
function llmProbeBinary(cli) { return cli === 'chatgpt-web' ? 'opencli' : cli; }
|
|
284
|
+
function llmReady(cli, token) {
|
|
285
|
+
// chatgpt-web has no token file; treat opencli's presence as the ready signal.
|
|
286
|
+
if (cli === 'chatgpt-web') return commandExists('opencli');
|
|
287
|
+
return token ? fs.existsSync(path.join(os.homedir(), ...token.split('/'))) : false;
|
|
288
|
+
}
|
|
289
|
+
function readRelaySettings() {
|
|
290
|
+
try {
|
|
291
|
+
if (fs.existsSync(RELAY_RESALE_PATH)) return JSON.parse(fs.readFileSync(RELAY_RESALE_PATH, 'utf8'));
|
|
292
|
+
} catch { /* ignore */ }
|
|
293
|
+
return {};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Run a process async (Promise) so the Electron main thread never blocks —
|
|
297
|
+
// mirrors desktop's run_cli on a spawn_blocking thread. The UI stays responsive
|
|
298
|
+
// while market/relay daemons start/stop or npm installs run.
|
|
299
|
+
function runProc(cmd, procArgs, timeoutMs = 25000) {
|
|
300
|
+
return new Promise((resolve) => {
|
|
301
|
+
let child;
|
|
302
|
+
try {
|
|
303
|
+
child = spawn(cmd, procArgs, { env: { ...process.env, PATH: CLI_PATH } });
|
|
304
|
+
} catch (e) {
|
|
305
|
+
resolve({ ok: false, error: String(e) });
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
let out = '';
|
|
309
|
+
let err = '';
|
|
310
|
+
const timer = setTimeout(() => {
|
|
311
|
+
try { child.kill('SIGKILL'); } catch { /* ignore */ }
|
|
312
|
+
resolve({ ok: false, error: 'timeout' });
|
|
313
|
+
}, timeoutMs);
|
|
314
|
+
child.stdout?.on('data', (d) => { out += d; });
|
|
315
|
+
child.stderr?.on('data', (d) => { err += d; });
|
|
316
|
+
child.on('close', (code) => {
|
|
317
|
+
clearTimeout(timer);
|
|
318
|
+
resolve({ ok: code === 0, stdout: out.trim(), stderr: err.trim() });
|
|
319
|
+
});
|
|
320
|
+
child.on('error', (e) => {
|
|
321
|
+
clearTimeout(timer);
|
|
322
|
+
resolve({ ok: false, error: String(e) });
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Run a clawmoney CLI subcommand via system `node` (not process.execPath, which
|
|
328
|
+
// is the electron binary — see ensureMarketRunning). Returns a Promise.
|
|
329
|
+
function runCli(cliArgs, timeoutMs = 25000) {
|
|
330
|
+
const entry = process.env.CLAWMONEY_CLI_ENTRY;
|
|
331
|
+
if (!entry || !fs.existsSync(entry)) return Promise.resolve({ ok: false, error: 'CLI entry not found' });
|
|
332
|
+
return runProc('node', [entry, ...cliArgs], timeoutMs);
|
|
333
|
+
}
|
|
334
|
+
|
|
266
335
|
async function handleTauriCommand(cmd, args) {
|
|
267
336
|
log(`invoke: ${cmd}`);
|
|
268
337
|
try {
|
|
@@ -271,8 +340,105 @@ async function handleTauriCommand(cmd, args) {
|
|
|
271
340
|
return await loadDashboard();
|
|
272
341
|
case 'load_provider_log':
|
|
273
342
|
return { ok: true, providerLog: [] };
|
|
343
|
+
// Local-service controls → drive the clawmoney CLI daemons.
|
|
344
|
+
case 'start_market': return await runCli(['market', 'start']);
|
|
345
|
+
case 'stop_market': return await runCli(['market', 'stop']);
|
|
346
|
+
case 'start_surf': return await runCli(['relay', 'start']);
|
|
347
|
+
case 'stop_surf': return await runCli(['relay', 'stop']);
|
|
348
|
+
case 'launch_chrome':
|
|
349
|
+
try { spawn('open', ['-a', 'Google Chrome'], { detached: true, stdio: 'ignore' }).unref(); } catch { /* ignore */ }
|
|
350
|
+
return { ok: true };
|
|
351
|
+
case 'open_external':
|
|
352
|
+
if (args && args.url) shell.openExternal(String(args.url));
|
|
353
|
+
return { ok: true };
|
|
354
|
+
case 'install_extension':
|
|
355
|
+
shell.openExternal(EXTENSION_URL);
|
|
356
|
+
return { ok: true };
|
|
357
|
+
case 'logout': {
|
|
358
|
+
// Clear api_key so the UI returns to the signed-out state.
|
|
359
|
+
try {
|
|
360
|
+
const txt = fs.readFileSync(CONFIG_PATH, 'utf8');
|
|
361
|
+
fs.writeFileSync(CONFIG_PATH, txt.replace(/^api_key:.*$/m, '').replace(/\n{3,}/g, '\n\n'));
|
|
362
|
+
} catch { /* ignore */ }
|
|
363
|
+
return { ok: true };
|
|
364
|
+
}
|
|
365
|
+
// ── Agent 上架 (relay resale of LLM subscriptions) ──
|
|
366
|
+
case 'llm_detect': {
|
|
367
|
+
const providers = LLM_SPECS.map((s) => ({
|
|
368
|
+
cli: s.cli,
|
|
369
|
+
package: s.package,
|
|
370
|
+
installed: commandExists(llmProbeBinary(s.cli)),
|
|
371
|
+
loggedIn: llmReady(s.cli, s.token),
|
|
372
|
+
defaultModel: s.model,
|
|
373
|
+
}));
|
|
374
|
+
return { ok: true, providers };
|
|
375
|
+
}
|
|
376
|
+
case 'llm_install': {
|
|
377
|
+
const spec = LLM_SPECS.find((s) => s.cli === (args && args.cli));
|
|
378
|
+
if (!spec) return { ok: false, error: `unknown cli ${args && args.cli}` };
|
|
379
|
+
return await runProc('npm', ['install', '-g', spec.package], 180000);
|
|
380
|
+
}
|
|
381
|
+
case 'llm_login': {
|
|
382
|
+
const cli = args && args.cli;
|
|
383
|
+
if (cli === 'gemini') {
|
|
384
|
+
try {
|
|
385
|
+
spawn('osascript', ['-e', 'tell application "Terminal"\nactivate\ndo script "gemini"\nend tell'],
|
|
386
|
+
{ detached: true, stdio: 'ignore' }).unref();
|
|
387
|
+
} catch { /* ignore */ }
|
|
388
|
+
return { ok: true, message: '已打开终端 — 在终端里选 "Login with Google" 登录,完成后回来' };
|
|
389
|
+
}
|
|
390
|
+
const bin = llmProbeBinary(cli);
|
|
391
|
+
if (!commandExists(bin)) return { ok: false, error: `${cli} 未安装` };
|
|
392
|
+
try {
|
|
393
|
+
spawn(bin, ['login'], { env: { ...process.env, PATH: CLI_PATH }, detached: true, stdio: 'ignore' }).unref();
|
|
394
|
+
} catch (e) { return { ok: false, error: String(e) }; }
|
|
395
|
+
return { ok: true };
|
|
396
|
+
}
|
|
397
|
+
case 'llm_set_enabled': {
|
|
398
|
+
const cli = args && args.cli;
|
|
399
|
+
const enabled = !!(args && args.enabled);
|
|
400
|
+
try {
|
|
401
|
+
const s = readRelaySettings();
|
|
402
|
+
const disabled = new Set(Array.isArray(s.disabledClis) ? s.disabledClis : []);
|
|
403
|
+
if (enabled) disabled.delete(cli); else disabled.add(cli);
|
|
404
|
+
s.disabledClis = [...disabled];
|
|
405
|
+
fs.mkdirSync(path.dirname(RELAY_RESALE_PATH), { recursive: true });
|
|
406
|
+
fs.writeFileSync(RELAY_RESALE_PATH, JSON.stringify(s, null, 2));
|
|
407
|
+
if (enabled && s.online) {
|
|
408
|
+
const spec = LLM_SPECS.find((x) => x.cli === cli);
|
|
409
|
+
if (spec && llmReady(spec.cli, spec.token)) {
|
|
410
|
+
await runCli(['relay', 'register', '--cli', spec.cli, '--model', spec.model, '--concurrency', String(s.concurrency || 2)], 30000);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
} catch (e) { return { ok: false, error: String(e) }; }
|
|
414
|
+
return { ok: true };
|
|
415
|
+
}
|
|
416
|
+
case 'relay_settings_get':
|
|
417
|
+
return readRelaySettings();
|
|
418
|
+
case 'relay_settings_set': {
|
|
419
|
+
const settings = (args && args.settings) ? args.settings : (args || {});
|
|
420
|
+
try {
|
|
421
|
+
fs.mkdirSync(path.dirname(RELAY_RESALE_PATH), { recursive: true });
|
|
422
|
+
fs.writeFileSync(RELAY_RESALE_PATH, JSON.stringify(settings, null, 2));
|
|
423
|
+
} catch (e) { log(`relay_settings_set write: ${e.message}`); }
|
|
424
|
+
if (settings.online === true) {
|
|
425
|
+
const conc = String(settings.concurrency || 2);
|
|
426
|
+
const disabled = Array.isArray(settings.disabledClis) ? settings.disabledClis : [];
|
|
427
|
+
let registered = 0;
|
|
428
|
+
for (const spec of LLM_SPECS) {
|
|
429
|
+
if (disabled.includes(spec.cli)) continue;
|
|
430
|
+
if (llmReady(spec.cli, spec.token)) {
|
|
431
|
+
await runCli(['relay', 'register', '--cli', spec.cli, '--model', spec.model, '--concurrency', conc], 30000);
|
|
432
|
+
registered++;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return { ok: true, registered, applied: await runCli(['relay', 'start']) };
|
|
436
|
+
}
|
|
437
|
+
return { ok: true, applied: await runCli(['relay', 'stop']) };
|
|
438
|
+
}
|
|
274
439
|
default:
|
|
275
|
-
// window/event plugin calls (
|
|
440
|
+
// window/event plugin calls (plugin:window|*) + not-yet-wired commands
|
|
441
|
+
// (llm_upload etc) — no-op so the UI doesn't error.
|
|
276
442
|
if (typeof cmd === 'string' && cmd.startsWith('plugin:')) return {};
|
|
277
443
|
return { ok: true };
|
|
278
444
|
}
|
|
@@ -345,7 +511,12 @@ function createWindow() {
|
|
|
345
511
|
'html,body{background:transparent !important;}' +
|
|
346
512
|
'#desktop{display:none !important;}' +
|
|
347
513
|
'.stage{padding:0 !important;}' +
|
|
348
|
-
'#app{transform:none !important;border-radius:22px !important;}'
|
|
514
|
+
'#app{transform:none !important;border-radius:22px !important;}' +
|
|
515
|
+
// Frameless-window dragging: desktop UI uses Tauri JS dragging (no-op in
|
|
516
|
+
// Electron), so make the top bars draggable via CSS app-region; keep all
|
|
517
|
+
// interactive elements clickable.
|
|
518
|
+
'.page-header,.sidebar-header{-webkit-app-region:drag;}' +
|
|
519
|
+
'.page-actions,.page-actions *,button,a,input,select,[role=button]{-webkit-app-region:no-drag;}'
|
|
349
520
|
).catch((e) => log(`insertCSS failed: ${e.message}`));
|
|
350
521
|
win.show();
|
|
351
522
|
});
|