clawmoney 0.4.0 → 0.6.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.
@@ -1,10 +1,11 @@
1
1
  interface SubmitOptions {
2
2
  url: string;
3
+ platform?: string;
3
4
  text?: string;
4
5
  }
5
6
  interface VerifyOptions {
6
7
  witness?: boolean;
7
8
  }
8
9
  export declare function hireSubmitCommand(taskId: string, options: SubmitOptions): Promise<void>;
9
- export declare function hireVerifyCommand(taskId: string, options: VerifyOptions): Promise<void>;
10
+ export declare function hireVerifyCommand(submissionId: string, options: VerifyOptions): Promise<void>;
10
11
  export {};
@@ -1,33 +1,44 @@
1
1
  import chalk from 'chalk';
2
2
  import ora from 'ora';
3
- import { apiPost } from '../utils/api.js';
3
+ import { apiGet, apiPost } from '../utils/api.js';
4
4
  import { awalExec } from '../utils/awal.js';
5
5
  import { requireConfig } from '../utils/config.js';
6
- import { prompt, confirm } from '../utils/prompt.js';
7
6
  export async function hireSubmitCommand(taskId, options) {
8
7
  const config = requireConfig();
8
+ // 自动检测平台(从 task 获取)
9
+ let platform = options.platform;
10
+ if (!platform) {
11
+ try {
12
+ const taskResp = await apiGet(`/api/v1/hire/${taskId}`, config.api_key);
13
+ if (taskResp.ok && taskResp.data.platform) {
14
+ platform = taskResp.data.platform;
15
+ }
16
+ }
17
+ catch { /* ignore */ }
18
+ }
19
+ if (!platform)
20
+ platform = 'twitter';
9
21
  console.log('');
10
- const spinner = ora(`Submitting proof for task ${taskId}...`).start();
22
+ const spinner = ora(`Submitting proof for task ${taskId.slice(0, 8)}...`).start();
11
23
  try {
12
24
  const body = {
25
+ platform,
13
26
  proof_url: options.url,
14
27
  };
15
28
  if (options.text) {
16
- body.content = options.text;
29
+ body.content_text = options.text;
17
30
  }
18
31
  const resp = await apiPost(`/api/v1/hire/${taskId}/submit`, body, config.api_key);
19
32
  if (!resp.ok) {
20
33
  spinner.fail('Submission failed');
21
- console.error(chalk.red(JSON.stringify(resp.data)));
34
+ const detail = typeof resp.data === 'object' ? (resp.data.detail || JSON.stringify(resp.data)) : String(resp.data);
35
+ console.error(chalk.red(` ${detail}`));
22
36
  return;
23
37
  }
24
- spinner.succeed('Proof submitted successfully');
38
+ spinner.succeed('Proof submitted');
25
39
  if (resp.data.id) {
26
40
  console.log(chalk.dim(` Submission ID: ${resp.data.id}`));
27
41
  }
28
- if (resp.data.message) {
29
- console.log(chalk.dim(` ${resp.data.message}`));
30
- }
31
42
  }
32
43
  catch (err) {
33
44
  spinner.fail('Submission failed');
@@ -35,69 +46,112 @@ export async function hireSubmitCommand(taskId, options) {
35
46
  }
36
47
  console.log('');
37
48
  }
38
- export async function hireVerifyCommand(taskId, options) {
49
+ export async function hireVerifyCommand(submissionId, options) {
39
50
  const config = requireConfig();
40
51
  console.log('');
41
52
  if (options.witness) {
42
- // Witness verification via x402
43
- const tweetUrl = await prompt(chalk.cyan('? ') + 'Enter the tweet URL to verify: ');
44
- if (!tweetUrl) {
45
- console.log(chalk.red('Tweet URL is required for witness verification.'));
46
- return;
53
+ // Get submission to extract proof_url
54
+ const subSpinner = ora('Fetching submission...').start();
55
+ let proofUrl = '';
56
+ try {
57
+ // Try to get submission details - submissionId might be used directly
58
+ const resp = await apiGet(`/api/v1/hire/submissions/${submissionId}`, config.api_key);
59
+ if (resp.ok && resp.data.proof_url) {
60
+ proofUrl = resp.data.proof_url;
61
+ subSpinner.succeed(`Proof URL: ${proofUrl}`);
62
+ }
63
+ else {
64
+ subSpinner.warn('Could not fetch submission, will need tweet ID');
65
+ }
47
66
  }
48
- // Extract tweet ID from URL
49
- const tweetIdMatch = tweetUrl.match(/status\/(\d+)/);
50
- if (!tweetIdMatch) {
51
- console.log(chalk.red('Could not extract tweet ID from URL.'));
67
+ catch {
68
+ subSpinner.warn('Could not fetch submission');
69
+ }
70
+ // Extract tweet ID
71
+ let tweetId = '';
72
+ if (proofUrl) {
73
+ const match = proofUrl.match(/status\/(\d+)/);
74
+ if (match)
75
+ tweetId = match[1];
76
+ }
77
+ if (!tweetId) {
78
+ console.error(chalk.red(' Could not extract tweet ID from proof URL'));
52
79
  return;
53
80
  }
54
- const tweetId = tweetIdMatch[1];
55
- const witnessSpinner = ora('Paying for witness verification via x402...').start();
81
+ // Fetch witness proof via x402
82
+ const witnessSpinner = ora('Fetching witness proof via x402 ($0.01)...').start();
83
+ let witnessData;
56
84
  try {
57
- const witnessResult = await awalExec([
58
- 'x402',
59
- 'pay',
60
- `https://witness.bnbot.ai/x/${tweetId}`,
85
+ witnessData = await awalExec([
86
+ 'x402', 'pay', `https://witness.bnbot.ai/x/${tweetId}`,
61
87
  ]);
62
- witnessSpinner.succeed('Witness verification paid');
63
- console.log(chalk.dim(` Response: ${JSON.stringify(witnessResult.data)}`));
64
- // Submit the witness proof
65
- const submitSpinner = ora('Submitting witness proof...').start();
66
- const resp = await apiPost(`/api/v1/hire/${taskId}/verify`, {
67
- type: 'witness',
68
- tweet_id: tweetId,
69
- witness_data: witnessResult.data,
88
+ witnessSpinner.succeed('Witness proof obtained');
89
+ }
90
+ catch (err) {
91
+ witnessSpinner.fail('Witness fetch failed');
92
+ console.error(chalk.red(err.message));
93
+ return;
94
+ }
95
+ // Parse witness response
96
+ const proof = witnessData?.data?.proof || witnessData?.proof;
97
+ if (!proof) {
98
+ console.error(chalk.red(' No proof in witness response'));
99
+ console.log(chalk.dim(` Raw: ${JSON.stringify(witnessData).slice(0, 200)}`));
100
+ return;
101
+ }
102
+ // Submit witness verification
103
+ const verifySpinner = ora('Submitting witness verification...').start();
104
+ try {
105
+ const resp = await apiPost(`/api/v1/hire/submissions/${submissionId}/verify`, {
106
+ vote: 'approve',
107
+ relevance_score: 8,
108
+ quality_score: 7,
109
+ tweet_proof: {
110
+ payload: proof.payload,
111
+ signature: proof.signature,
112
+ signer: proof.signer,
113
+ timestamp: proof.timestamp,
114
+ },
70
115
  }, config.api_key);
71
116
  if (!resp.ok) {
72
- submitSpinner.fail('Witness verification submission failed');
73
- console.error(chalk.red(JSON.stringify(resp.data)));
117
+ verifySpinner.fail('Verification failed');
118
+ const detail = typeof resp.data === 'object' ? (resp.data.detail || JSON.stringify(resp.data)) : String(resp.data);
119
+ console.error(chalk.red(` ${detail}`));
74
120
  }
75
121
  else {
76
- submitSpinner.succeed('Witness verification submitted');
122
+ verifySpinner.succeed('Witness verification submitted');
123
+ if (resp.data.id) {
124
+ console.log(chalk.dim(` Verification ID: ${resp.data.id}`));
125
+ }
77
126
  }
78
127
  }
79
128
  catch (err) {
80
- witnessSpinner.fail('Witness verification failed');
129
+ verifySpinner.fail('Verification failed');
81
130
  console.error(chalk.red(err.message));
82
131
  }
83
132
  }
84
133
  else {
85
134
  // Manual verification
86
- console.log(chalk.bold(` Manual verification for task ${taskId}`));
87
- console.log('');
88
- const approved = await confirm('Approve this submission?', false);
89
- const spinner = ora('Submitting verification...').start();
135
+ const spinner = ora('Submitting manual verification...').start();
90
136
  try {
91
- const resp = await apiPost(`/api/v1/hire/${taskId}/verify`, {
92
- type: 'manual',
93
- approved,
137
+ const resp = await apiPost(`/api/v1/hire/submissions/${submissionId}/verify`, {
138
+ vote: 'approve',
139
+ relevance_score: 7,
140
+ quality_score: 7,
141
+ views: 0,
142
+ likes: 0,
143
+ comments: 0,
94
144
  }, config.api_key);
95
145
  if (!resp.ok) {
96
- spinner.fail('Verification submission failed');
97
- console.error(chalk.red(JSON.stringify(resp.data)));
146
+ spinner.fail('Verification failed');
147
+ const detail = typeof resp.data === 'object' ? (resp.data.detail || JSON.stringify(resp.data)) : String(resp.data);
148
+ console.error(chalk.red(` ${detail}`));
98
149
  }
99
150
  else {
100
- spinner.succeed(`Verification submitted: ${approved ? 'APPROVED' : 'REJECTED'}`);
151
+ spinner.succeed('Manual verification submitted');
152
+ if (resp.data.id) {
153
+ console.log(chalk.dim(` Verification ID: ${resp.data.id}`));
154
+ }
101
155
  }
102
156
  }
103
157
  catch (err) {
@@ -3,8 +3,7 @@ interface TweetOptions {
3
3
  draft?: boolean;
4
4
  }
5
5
  /**
6
- * Post a tweet via BNBot Chrome Extension through the Bridge server.
7
- * Requires `clawmoney serve` to be running.
6
+ * Post a tweet by delegating to bnbot-cli.
8
7
  */
9
8
  export declare function tweetCommand(text: string, options: TweetOptions): Promise<void>;
10
9
  export {};
@@ -1,50 +1,29 @@
1
1
  import chalk from 'chalk';
2
- import ora from 'ora';
3
- import { sendAction } from '../utils/bridge.js';
2
+ import { spawn } from 'node:child_process';
4
3
  /**
5
- * Post a tweet via BNBot Chrome Extension through the Bridge server.
6
- * Requires `clawmoney serve` to be running.
4
+ * Post a tweet by delegating to bnbot-cli.
7
5
  */
8
6
  export async function tweetCommand(text, options) {
9
- const isDraft = options.draft || false;
10
- console.log('');
11
- console.log(chalk.dim(` ${isDraft ? 'Drafting' : 'Posting'} tweet: "${text.slice(0, 80)}${text.length > 80 ? '...' : ''}"`));
12
- if (options.media) {
13
- console.log(chalk.dim(` Media: ${options.media}`));
14
- }
15
- console.log('');
16
- const spinner = ora(isDraft ? 'Filling tweet draft...' : 'Posting tweet...').start();
7
+ const args = ['bnbot', 'tweet', text];
8
+ if (options.draft)
9
+ args.push('--draft');
10
+ if (options.media)
11
+ args.push('--media', options.media);
17
12
  try {
18
- const params = {
19
- text,
20
- draftOnly: isDraft,
21
- };
22
- if (options.media) {
23
- params.media = [{ type: 'image', url: options.media }];
24
- }
25
- const result = await sendAction('post_tweet', params);
26
- if (!result.success) {
27
- spinner.fail(result.error || 'Failed');
28
- return;
29
- }
30
- if (isDraft) {
31
- spinner.succeed('Tweet draft ready — review and post manually');
32
- }
33
- else {
34
- spinner.succeed('Tweet posted');
35
- const data = result.data;
36
- if (data?.url || data?.tweet_url) {
37
- console.log(` ${chalk.dim('URL:')} ${chalk.cyan(data.url || data.tweet_url)}`);
38
- }
39
- }
13
+ const code = await new Promise((resolve, reject) => {
14
+ const child = spawn('npx', args, {
15
+ stdio: 'inherit',
16
+ shell: true,
17
+ });
18
+ child.on('close', (code) => resolve(code || 0));
19
+ child.on('error', reject);
20
+ });
21
+ if (code !== 0)
22
+ process.exit(code);
40
23
  }
41
24
  catch (err) {
42
- spinner.fail('Failed');
43
25
  console.error(chalk.red(err.message));
44
- console.log('');
45
- console.log(chalk.dim(' Make sure:'));
46
- console.log(chalk.dim(' 1. clawmoney serve is running'));
47
- console.log(chalk.dim(' 2. BNBot Chrome Extension is open on a Twitter tab'));
26
+ console.log(chalk.dim(' Make sure bnbot-cli is installed: npm install -g bnbot-cli'));
27
+ process.exit(1);
48
28
  }
49
- console.log('');
50
29
  }
package/dist/index.js CHANGED
@@ -5,7 +5,6 @@ import { browseCommand } from './commands/browse.js';
5
5
  import { hireSubmitCommand, hireVerifyCommand } from './commands/hire.js';
6
6
  import { walletStatusCommand, walletBalanceCommand, walletAddressCommand, walletSendCommand, } from './commands/wallet.js';
7
7
  import { tweetCommand } from './commands/tweet.js';
8
- import { serveCommand } from './commands/serve.js';
9
8
  const program = new Command();
10
9
  program
11
10
  .name('clawmoney')
@@ -46,6 +45,7 @@ hire
46
45
  .command('submit <task-id>')
47
46
  .description('Submit a proof for a hire task')
48
47
  .requiredOption('-u, --url <url>', 'Proof URL (tweet, post, etc.)')
48
+ .option('-p, --platform <platform>', 'Platform (auto-detected from task)')
49
49
  .option('--text <content>', 'Optional text content')
50
50
  .action(async (taskId, options) => {
51
51
  try {
@@ -57,9 +57,9 @@ hire
57
57
  }
58
58
  });
59
59
  hire
60
- .command('verify <task-id>')
61
- .description('Verify a hire task submission')
62
- .option('-w, --witness', 'Use x402 witness verification')
60
+ .command('verify <submission-id>')
61
+ .description('Verify a hire submission')
62
+ .option('-w, --witness', 'Use x402 witness verification ($0.01)')
63
63
  .action(async (taskId, options) => {
64
64
  try {
65
65
  await hireVerifyCommand(taskId, options);
@@ -119,20 +119,6 @@ wallet
119
119
  process.exit(1);
120
120
  }
121
121
  });
122
- // serve
123
- program
124
- .command('serve')
125
- .description('Start the bridge server for Chrome Extension communication')
126
- .option('-p, --port <port>', 'WebSocket port', '18900')
127
- .action(async (options) => {
128
- try {
129
- await serveCommand(options);
130
- }
131
- catch (err) {
132
- console.error(err.message);
133
- process.exit(1);
134
- }
135
- });
136
122
  // tweet
137
123
  program
138
124
  .command('tweet <text>')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawmoney",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "ClawMoney CLI -- Earn crypto with your AI agent",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,12 +16,11 @@
16
16
  "prepublishOnly": "npm run build"
17
17
  },
18
18
  "dependencies": {
19
- "@types/ws": "^8.18.1",
20
19
  "awal": "^2.2.0",
20
+ "bnbot-cli": "^2.0.0",
21
21
  "chalk": "^5.3.0",
22
22
  "commander": "^12.0.0",
23
23
  "ora": "^8.0.0",
24
- "ws": "^8.20.0",
25
24
  "yaml": "^2.4.0"
26
25
  },
27
26
  "devDependencies": {
@@ -1,5 +0,0 @@
1
- interface ServeOptions {
2
- port?: string;
3
- }
4
- export declare function serveCommand(options: ServeOptions): Promise<void>;
5
- export {};
@@ -1,38 +0,0 @@
1
- import chalk from 'chalk';
2
- import { BridgeServer } from '../utils/bridge.js';
3
- export async function serveCommand(options) {
4
- const port = parseInt(options.port || '18900', 10);
5
- const server = new BridgeServer(port);
6
- try {
7
- await server.start();
8
- }
9
- catch (err) {
10
- console.error(chalk.red(err.message));
11
- process.exit(1);
12
- }
13
- console.log('');
14
- console.log(chalk.green(' ClawMoney Bridge Server running'));
15
- console.log(chalk.dim(` WebSocket listening on ws://127.0.0.1:${port}`));
16
- console.log('');
17
- console.log(chalk.dim(' Waiting for BNBot Chrome Extension to connect...'));
18
- console.log(chalk.dim(' Press Ctrl+C to stop'));
19
- console.log('');
20
- // Check extension connection periodically
21
- const statusInterval = setInterval(() => {
22
- if (server.isExtensionConnected()) {
23
- const ver = server.getExtensionVersion();
24
- console.log(chalk.green(` Extension connected${ver ? ` (v${ver})` : ''}`));
25
- clearInterval(statusInterval);
26
- }
27
- }, 2000);
28
- // Graceful shutdown
29
- const shutdown = () => {
30
- console.log('');
31
- console.log(chalk.dim(' Shutting down...'));
32
- clearInterval(statusInterval);
33
- server.stop();
34
- process.exit(0);
35
- };
36
- process.on('SIGINT', shutdown);
37
- process.on('SIGTERM', shutdown);
38
- }
@@ -1,37 +0,0 @@
1
- /**
2
- * Bridge — WebSocket server that sits between CLI commands and the Chrome Extension.
3
- *
4
- * Architecture:
5
- * clawmoney serve → starts WS server (port 18900)
6
- * Chrome Extension → connects as client (localRelayManager)
7
- * clawmoney tweet → connects as client, sends action, gets result via server
8
- *
9
- * The server keeps a persistent connection to the extension.
10
- * CLI commands connect briefly, send an action, and disconnect after getting a result.
11
- */
12
- /**
13
- * The Bridge server — start with `clawmoney serve`
14
- */
15
- export declare class BridgeServer {
16
- private wss;
17
- private extensionClient;
18
- private pendingRequests;
19
- private extensionVersion;
20
- private port;
21
- constructor(port?: number);
22
- start(): Promise<void>;
23
- private handleMessage;
24
- stop(): void;
25
- isExtensionConnected(): boolean;
26
- getExtensionVersion(): string | null;
27
- getPort(): number;
28
- }
29
- /**
30
- * Send an action to the Chrome Extension.
31
- * Auto-starts bridge server if none is running, then waits for extension to connect.
32
- */
33
- export declare function sendAction(actionType: string, params: Record<string, unknown>, port?: number): Promise<{
34
- success: boolean;
35
- data?: unknown;
36
- error?: string;
37
- }>;
@@ -1,259 +0,0 @@
1
- /**
2
- * Bridge — WebSocket server that sits between CLI commands and the Chrome Extension.
3
- *
4
- * Architecture:
5
- * clawmoney serve → starts WS server (port 18900)
6
- * Chrome Extension → connects as client (localRelayManager)
7
- * clawmoney tweet → connects as client, sends action, gets result via server
8
- *
9
- * The server keeps a persistent connection to the extension.
10
- * CLI commands connect briefly, send an action, and disconnect after getting a result.
11
- */
12
- import { WebSocketServer, WebSocket } from 'ws';
13
- import { randomUUID } from 'node:crypto';
14
- const DEFAULT_PORT = 18900;
15
- const ACTION_TIMEOUT = 60000;
16
- /**
17
- * The Bridge server — start with `clawmoney serve`
18
- */
19
- export class BridgeServer {
20
- wss = null;
21
- extensionClient = null;
22
- pendingRequests = new Map();
23
- extensionVersion = null;
24
- port;
25
- constructor(port) {
26
- this.port = port || DEFAULT_PORT;
27
- }
28
- start() {
29
- return new Promise((resolve, reject) => {
30
- this.wss = new WebSocketServer({ port: this.port, host: '127.0.0.1' });
31
- this.wss.on('listening', () => {
32
- resolve();
33
- });
34
- this.wss.on('error', (error) => {
35
- if (error.code === 'EADDRINUSE') {
36
- reject(new Error(`Port ${this.port} already in use. Another serve instance may be running.`));
37
- }
38
- else {
39
- reject(error);
40
- }
41
- });
42
- this.wss.on('connection', (ws) => {
43
- // Determine if this is the extension or a CLI client
44
- // Extension sends a 'status' message on connect
45
- // CLI clients send an 'action' message
46
- ws.on('message', (data) => {
47
- try {
48
- const message = JSON.parse(data.toString());
49
- this.handleMessage(ws, message);
50
- }
51
- catch {
52
- // ignore
53
- }
54
- });
55
- ws.on('close', () => {
56
- if (this.extensionClient === ws) {
57
- this.extensionClient = null;
58
- this.extensionVersion = null;
59
- // Reject all pending requests
60
- for (const [id, pending] of this.pendingRequests) {
61
- clearTimeout(pending.timer);
62
- pending.reject(new Error('Extension disconnected'));
63
- this.pendingRequests.delete(id);
64
- }
65
- }
66
- });
67
- });
68
- });
69
- }
70
- handleMessage(ws, message) {
71
- switch (message.type) {
72
- case 'status':
73
- // Extension connected
74
- if (this.extensionClient && this.extensionClient !== ws && this.extensionClient.readyState === WebSocket.OPEN) {
75
- this.extensionClient.close(1000, 'Replaced by new connection');
76
- }
77
- this.extensionClient = ws;
78
- this.extensionVersion = message.version;
79
- break;
80
- case 'action':
81
- // CLI client sending an action — forward to extension
82
- if (!this.extensionClient || this.extensionClient.readyState !== WebSocket.OPEN) {
83
- ws.send(JSON.stringify({
84
- type: 'action_result',
85
- requestId: message.requestId,
86
- success: false,
87
- error: 'Extension not connected. Make sure BNBot Chrome Extension is running on a Twitter tab.',
88
- }));
89
- return;
90
- }
91
- // Store pending request keyed to the CLI client WebSocket
92
- const requestId = message.requestId;
93
- const timer = setTimeout(() => {
94
- this.pendingRequests.delete(requestId);
95
- if (ws.readyState === WebSocket.OPEN) {
96
- ws.send(JSON.stringify({
97
- type: 'action_result',
98
- requestId,
99
- success: false,
100
- error: `Action '${message.actionType}' timed out`,
101
- }));
102
- }
103
- }, ACTION_TIMEOUT);
104
- this.pendingRequests.set(requestId, {
105
- resolve: (result) => {
106
- if (ws.readyState === WebSocket.OPEN) {
107
- ws.send(JSON.stringify(result));
108
- }
109
- },
110
- reject: (error) => {
111
- if (ws.readyState === WebSocket.OPEN) {
112
- ws.send(JSON.stringify({
113
- type: 'action_result',
114
- requestId,
115
- success: false,
116
- error: error.message,
117
- }));
118
- }
119
- },
120
- timer,
121
- });
122
- // Forward to extension
123
- this.extensionClient.send(JSON.stringify(message));
124
- break;
125
- case 'action_result':
126
- // Extension sending back a result — forward to CLI client
127
- const pending = this.pendingRequests.get(message.requestId);
128
- if (pending) {
129
- clearTimeout(pending.timer);
130
- this.pendingRequests.delete(message.requestId);
131
- pending.resolve(message);
132
- }
133
- break;
134
- case 'heartbeat':
135
- // Respond to heartbeat from extension
136
- break;
137
- }
138
- }
139
- stop() {
140
- if (this.extensionClient) {
141
- this.extensionClient.close(1000, 'Server shutting down');
142
- this.extensionClient = null;
143
- }
144
- if (this.wss) {
145
- this.wss.close();
146
- this.wss = null;
147
- }
148
- for (const [id, pending] of this.pendingRequests) {
149
- clearTimeout(pending.timer);
150
- pending.reject(new Error('Server shutting down'));
151
- }
152
- this.pendingRequests.clear();
153
- }
154
- isExtensionConnected() {
155
- return this.extensionClient !== null && this.extensionClient.readyState === WebSocket.OPEN;
156
- }
157
- getExtensionVersion() {
158
- return this.extensionVersion;
159
- }
160
- getPort() {
161
- return this.port;
162
- }
163
- }
164
- // Singleton bridge server — auto-started on first sendAction call
165
- let _server = null;
166
- /**
167
- * Ensure a bridge server is running. Starts one if needed.
168
- */
169
- async function ensureBridge(port) {
170
- if (_server)
171
- return _server;
172
- // Try connecting to an existing server first
173
- const existing = await new Promise((resolve) => {
174
- const ws = new WebSocket(`ws://127.0.0.1:${port}`);
175
- const t = setTimeout(() => { ws.close(); resolve(false); }, 1000);
176
- ws.on('open', () => { clearTimeout(t); ws.close(); resolve(true); });
177
- ws.on('error', () => { clearTimeout(t); resolve(false); });
178
- });
179
- if (existing) {
180
- // Another bridge (or `clawmoney serve`) is already running — use it as client
181
- return null; // signal to use client mode
182
- }
183
- // Start our own bridge in-process
184
- const server = new BridgeServer(port);
185
- await server.start();
186
- _server = server;
187
- return server;
188
- }
189
- /**
190
- * Send an action to the Chrome Extension.
191
- * Auto-starts bridge server if none is running, then waits for extension to connect.
192
- */
193
- export async function sendAction(actionType, params, port) {
194
- const wsPort = port || DEFAULT_PORT;
195
- // Ensure bridge is running
196
- const server = await ensureBridge(wsPort).catch(() => null);
197
- // If we own the server, wait for extension to connect (up to 30s)
198
- if (server && _server === server) {
199
- const waitStart = Date.now();
200
- while (!server.isExtensionConnected() && Date.now() - waitStart < 30000) {
201
- await new Promise(r => setTimeout(r, 500));
202
- }
203
- if (!server.isExtensionConnected()) {
204
- return {
205
- success: false,
206
- error: 'Chrome Extension not connected. Make sure BNBot extension is running on a Twitter tab with OpenClaw integration enabled.',
207
- };
208
- }
209
- }
210
- // Send action via WebSocket client
211
- return new Promise((resolve, reject) => {
212
- const ws = new WebSocket(`ws://127.0.0.1:${wsPort}`);
213
- const requestId = randomUUID();
214
- let settled = false;
215
- const timer = setTimeout(() => {
216
- if (!settled) {
217
- settled = true;
218
- ws.close();
219
- reject(new Error(`Action '${actionType}' timed out after ${ACTION_TIMEOUT / 1000}s`));
220
- }
221
- }, ACTION_TIMEOUT);
222
- ws.on('open', () => {
223
- ws.send(JSON.stringify({
224
- type: 'action',
225
- requestId,
226
- actionType,
227
- actionPayload: params,
228
- }));
229
- });
230
- ws.on('message', (data) => {
231
- try {
232
- const msg = JSON.parse(data.toString());
233
- if (msg.type === 'action_result' && msg.requestId === requestId) {
234
- settled = true;
235
- clearTimeout(timer);
236
- ws.close();
237
- resolve({ success: msg.success, data: msg.data, error: msg.error });
238
- }
239
- }
240
- catch {
241
- // ignore
242
- }
243
- });
244
- ws.on('error', () => {
245
- if (!settled) {
246
- settled = true;
247
- clearTimeout(timer);
248
- reject(new Error('Cannot connect to bridge. Make sure BNBot Chrome Extension is running.'));
249
- }
250
- });
251
- ws.on('close', () => {
252
- if (!settled) {
253
- settled = true;
254
- clearTimeout(timer);
255
- reject(new Error('Connection closed before response'));
256
- }
257
- });
258
- });
259
- }