clawmoney 0.2.0 → 0.3.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.
@@ -0,0 +1,5 @@
1
+ interface ServeOptions {
2
+ port?: string;
3
+ }
4
+ export declare function serveCommand(options: ServeOptions): Promise<void>;
5
+ export {};
@@ -0,0 +1,38 @@
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,10 +1,10 @@
1
1
  interface TweetOptions {
2
2
  media?: string;
3
+ draft?: boolean;
3
4
  }
4
5
  /**
5
- * Post a tweet via bnbot-cli.
6
- * bnbot-cli is CJS; we spawn it as a subprocess for simplicity
7
- * since it requires the BNBot Chrome Extension to be running.
6
+ * Post a tweet via BNBot Chrome Extension through the Bridge server.
7
+ * Requires `clawmoney serve` to be running.
8
8
  */
9
9
  export declare function tweetCommand(text: string, options: TweetOptions): Promise<void>;
10
10
  export {};
@@ -1,69 +1,50 @@
1
1
  import chalk from 'chalk';
2
2
  import ora from 'ora';
3
- import { spawn } from 'node:child_process';
3
+ import { sendAction } from '../utils/bridge.js';
4
4
  /**
5
- * Post a tweet via bnbot-cli.
6
- * bnbot-cli is CJS; we spawn it as a subprocess for simplicity
7
- * since it requires the BNBot Chrome Extension to be running.
5
+ * Post a tweet via BNBot Chrome Extension through the Bridge server.
6
+ * Requires `clawmoney serve` to be running.
8
7
  */
9
8
  export async function tweetCommand(text, options) {
9
+ const isDraft = options.draft || false;
10
10
  console.log('');
11
- console.log(chalk.dim(` Posting tweet: "${text.slice(0, 80)}${text.length > 80 ? '...' : ''}"`));
11
+ console.log(chalk.dim(` ${isDraft ? 'Drafting' : 'Posting'} tweet: "${text.slice(0, 80)}${text.length > 80 ? '...' : ''}"`));
12
12
  if (options.media) {
13
13
  console.log(chalk.dim(` Media: ${options.media}`));
14
14
  }
15
15
  console.log('');
16
- const spinner = ora('Posting tweet via BNBot...').start();
16
+ const spinner = ora(isDraft ? 'Filling tweet draft...' : 'Posting tweet...').start();
17
17
  try {
18
- const args = ['bnbot-cli', 'tweet', text];
18
+ const params = {
19
+ text,
20
+ draftOnly: isDraft,
21
+ };
19
22
  if (options.media) {
20
- args.push('--media', options.media);
23
+ params.media = [{ type: 'image', url: options.media }];
21
24
  }
22
- const result = await new Promise((resolve, reject) => {
23
- const child = spawn('npx', args, {
24
- stdio: ['inherit', 'pipe', 'pipe'],
25
- shell: true,
26
- });
27
- let stdout = '';
28
- let stderr = '';
29
- child.stdout.on('data', (chunk) => {
30
- stdout += chunk.toString();
31
- });
32
- child.stderr.on('data', (chunk) => {
33
- stderr += chunk.toString();
34
- });
35
- child.on('close', (code) => {
36
- if (code !== 0) {
37
- reject(new Error(stderr.trim() || `bnbot-cli exited with code ${code}`));
38
- return;
39
- }
40
- resolve(stdout.trim());
41
- });
42
- child.on('error', (err) => {
43
- reject(new Error(`Failed to spawn bnbot-cli: ${err.message}`));
44
- });
45
- });
46
- spinner.succeed('Tweet posted');
47
- // Try to parse and display tweet URL
48
- try {
49
- const data = JSON.parse(result);
50
- if (data.url || data.tweet_url) {
51
- console.log(` ${chalk.dim('URL:')} ${chalk.cyan(data.url || data.tweet_url)}`);
52
- }
25
+ const result = await sendAction('post_tweet', params);
26
+ if (!result.success) {
27
+ spinner.fail(result.error || 'Failed');
28
+ return;
53
29
  }
54
- catch {
55
- if (result) {
56
- console.log(chalk.dim(` ${result}`));
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)}`);
57
38
  }
58
39
  }
59
40
  }
60
41
  catch (err) {
61
- spinner.fail('Failed to post tweet');
42
+ spinner.fail('Failed');
62
43
  console.error(chalk.red(err.message));
63
44
  console.log('');
64
45
  console.log(chalk.dim(' Make sure:'));
65
- console.log(chalk.dim(' 1. BNBot Chrome Extension is running'));
66
- console.log(chalk.dim(' 2. bnbot-cli is installed: npm install -g bnbot-cli'));
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'));
67
48
  }
68
49
  console.log('');
69
50
  }
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ 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';
8
9
  const program = new Command();
9
10
  program
10
11
  .name('clawmoney')
@@ -118,11 +119,26 @@ wallet
118
119
  process.exit(1);
119
120
  }
120
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
+ });
121
136
  // tweet
122
137
  program
123
138
  .command('tweet <text>')
124
139
  .description('Post a tweet via BNBot Chrome Extension')
125
140
  .option('-m, --media <path>', 'Path to media file')
141
+ .option('-d, --draft', 'Draft mode: fill tweet composer without posting')
126
142
  .action(async (text, options) => {
127
143
  try {
128
144
  await tweetCommand(text, options);
@@ -0,0 +1,37 @@
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 extension via the running Bridge server.
31
+ * Connects as a CLI client, sends action, waits for result, disconnects.
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
+ }>;
@@ -0,0 +1,218 @@
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
+ /**
165
+ * Send an action to the extension via the running Bridge server.
166
+ * Connects as a CLI client, sends action, waits for result, disconnects.
167
+ */
168
+ export async function sendAction(actionType, params, port) {
169
+ const wsPort = port || DEFAULT_PORT;
170
+ return new Promise((resolve, reject) => {
171
+ const ws = new WebSocket(`ws://127.0.0.1:${wsPort}`);
172
+ const requestId = randomUUID();
173
+ let settled = false;
174
+ const timer = setTimeout(() => {
175
+ if (!settled) {
176
+ settled = true;
177
+ ws.close();
178
+ reject(new Error(`Action '${actionType}' timed out after ${ACTION_TIMEOUT / 1000}s`));
179
+ }
180
+ }, ACTION_TIMEOUT);
181
+ ws.on('open', () => {
182
+ ws.send(JSON.stringify({
183
+ type: 'action',
184
+ requestId,
185
+ actionType,
186
+ actionPayload: params,
187
+ }));
188
+ });
189
+ ws.on('message', (data) => {
190
+ try {
191
+ const msg = JSON.parse(data.toString());
192
+ if (msg.type === 'action_result' && msg.requestId === requestId) {
193
+ settled = true;
194
+ clearTimeout(timer);
195
+ ws.close();
196
+ resolve({ success: msg.success, data: msg.data, error: msg.error });
197
+ }
198
+ }
199
+ catch {
200
+ // ignore
201
+ }
202
+ });
203
+ ws.on('error', (err) => {
204
+ if (!settled) {
205
+ settled = true;
206
+ clearTimeout(timer);
207
+ reject(new Error(`Cannot connect to bridge server on port ${wsPort}. Run 'clawmoney serve' first.`));
208
+ }
209
+ });
210
+ ws.on('close', () => {
211
+ if (!settled) {
212
+ settled = true;
213
+ clearTimeout(timer);
214
+ reject(new Error('Connection closed before response'));
215
+ }
216
+ });
217
+ });
218
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawmoney",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "ClawMoney CLI -- Earn crypto with your AI agent",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,16 +16,17 @@
16
16
  "prepublishOnly": "npm run build"
17
17
  },
18
18
  "dependencies": {
19
+ "@types/ws": "^8.18.1",
19
20
  "awal": "^2.2.0",
20
- "bnbot-cli": "^1.3.0",
21
- "commander": "^12.0.0",
22
- "yaml": "^2.4.0",
23
21
  "chalk": "^5.3.0",
24
- "ora": "^8.0.0"
22
+ "commander": "^12.0.0",
23
+ "ora": "^8.0.0",
24
+ "ws": "^8.20.0",
25
+ "yaml": "^2.4.0"
25
26
  },
26
27
  "devDependencies": {
27
- "typescript": "^5.4.0",
28
- "@types/node": "^20.0.0"
28
+ "@types/node": "^20.0.0",
29
+ "typescript": "^5.4.0"
29
30
  },
30
31
  "engines": {
31
32
  "node": ">=18"