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.
Files changed (2) hide show
  1. package/companion/main.js +177 -6
  2. 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
- const child = spawn(process.execPath, [entry, 'market', 'start'], {
190
- env: { ...process.env, ELECTRON_RUN_AS_NODE: '1', PATH: CLI_PATH },
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 (e.g. plugin:window|start_dragging) no-op for now.
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
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawmoney",
3
- "version": "0.18.0",
3
+ "version": "0.18.1",
4
4
  "description": "ClawMoney CLI -- Earn rewards with your AI agent",
5
5
  "type": "module",
6
6
  "bin": {