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.
- package/dist/commands/hire.d.ts +2 -1
- package/dist/commands/hire.js +101 -47
- package/dist/commands/tweet.d.ts +1 -2
- package/dist/commands/tweet.js +19 -40
- package/dist/index.js +4 -18
- package/package.json +2 -3
- package/dist/commands/serve.d.ts +0 -5
- package/dist/commands/serve.js +0 -38
- package/dist/utils/bridge.d.ts +0 -37
- package/dist/utils/bridge.js +0 -259
package/dist/commands/hire.d.ts
CHANGED
|
@@ -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(
|
|
10
|
+
export declare function hireVerifyCommand(submissionId: string, options: VerifyOptions): Promise<void>;
|
|
10
11
|
export {};
|
package/dist/commands/hire.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
|
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(
|
|
49
|
+
export async function hireVerifyCommand(submissionId, options) {
|
|
39
50
|
const config = requireConfig();
|
|
40
51
|
console.log('');
|
|
41
52
|
if (options.witness) {
|
|
42
|
-
//
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
55
|
-
const witnessSpinner = ora('
|
|
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
|
-
|
|
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
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
73
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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/${
|
|
92
|
-
|
|
93
|
-
|
|
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
|
|
97
|
-
|
|
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(
|
|
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) {
|
package/dist/commands/tweet.d.ts
CHANGED
|
@@ -3,8 +3,7 @@ interface TweetOptions {
|
|
|
3
3
|
draft?: boolean;
|
|
4
4
|
}
|
|
5
5
|
/**
|
|
6
|
-
* Post a tweet
|
|
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 {};
|
package/dist/commands/tweet.js
CHANGED
|
@@ -1,50 +1,29 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import
|
|
3
|
-
import { sendAction } from '../utils/bridge.js';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
4
3
|
/**
|
|
5
|
-
* Post a tweet
|
|
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
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
if (options.media)
|
|
13
|
-
|
|
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
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
if (
|
|
27
|
-
|
|
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
|
-
|
|
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 <
|
|
61
|
-
.description('Verify a hire
|
|
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.
|
|
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": {
|
package/dist/commands/serve.d.ts
DELETED
package/dist/commands/serve.js
DELETED
|
@@ -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
|
-
}
|
package/dist/utils/bridge.d.ts
DELETED
|
@@ -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
|
-
}>;
|
package/dist/utils/bridge.js
DELETED
|
@@ -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
|
-
}
|