wolverine-ai 6.6.2 → 7.0.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,198 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Twitter/X Official API v2 — Post tweets via OAuth 1.0a
4
+ *
5
+ * Usage:
6
+ * node tweet.js "Your tweet text here"
7
+ * node tweet.js --reply-to 123456789 "Reply text"
8
+ * node tweet.js --quote 123456789 "Quote tweet text"
9
+ * node tweet.js --thread "First tweet" "Second tweet" "Third tweet"
10
+ *
11
+ * Environment variables (required):
12
+ * TWITTER_CONSUMER_KEY - OAuth 1.0a Consumer Key (API Key)
13
+ * TWITTER_CONSUMER_SECRET - OAuth 1.0a Consumer Secret (API Key Secret)
14
+ * TWITTER_ACCESS_TOKEN - OAuth 1.0a Access Token
15
+ * TWITTER_ACCESS_TOKEN_SECRET - OAuth 1.0a Access Token Secret
16
+ *
17
+ * Optional:
18
+ * HTTPS_PROXY - HTTP proxy (e.g. http://127.0.0.1:7897)
19
+ * TWITTER_DRY_RUN=1 - Print tweet without sending
20
+ *
21
+ * Output (JSON): { ok, id, url, remaining, limit }
22
+ */
23
+ const crypto = require('crypto');
24
+ const https = require('https');
25
+ const http = require('http');
26
+ const tls = require('tls');
27
+ const { URL } = require('url');
28
+
29
+ // --- Config ---
30
+ const CK = process.env.TWITTER_CONSUMER_KEY;
31
+ const CS = process.env.TWITTER_CONSUMER_SECRET;
32
+ const AT = process.env.TWITTER_ACCESS_TOKEN;
33
+ const ATS = process.env.TWITTER_ACCESS_TOKEN_SECRET;
34
+ const PROXY = process.env.HTTPS_PROXY || process.env.https_proxy || '';
35
+ const DRY = process.env.TWITTER_DRY_RUN === '1';
36
+
37
+ function die(msg) { console.error(JSON.stringify({ ok: false, error: msg })); process.exit(1); }
38
+ if (!CK || !CS || !AT || !ATS) die('Missing TWITTER_CONSUMER_KEY / TWITTER_CONSUMER_SECRET / TWITTER_ACCESS_TOKEN / TWITTER_ACCESS_TOKEN_SECRET');
39
+
40
+ // --- OAuth 1.0a ---
41
+ function penc(s) {
42
+ return encodeURIComponent(s).replace(/[!'()*]/g, c => '%' + c.charCodeAt(0).toString(16).toUpperCase());
43
+ }
44
+
45
+ function oauthSign(method, url, params) {
46
+ const paramStr = Object.keys(params).sort().map(k => `${penc(k)}=${penc(params[k])}`).join('&');
47
+ const baseStr = `${method}&${penc(url)}&${penc(paramStr)}`;
48
+ const sigKey = `${penc(CS)}&${penc(ATS)}`;
49
+ return crypto.createHmac('sha1', sigKey).update(baseStr).digest('base64');
50
+ }
51
+
52
+ function authHeader(method, url) {
53
+ const p = {
54
+ oauth_consumer_key: CK,
55
+ oauth_nonce: crypto.randomBytes(32).toString('hex'),
56
+ oauth_signature_method: 'HMAC-SHA1',
57
+ oauth_timestamp: Math.floor(Date.now() / 1000).toString(),
58
+ oauth_token: AT,
59
+ oauth_version: '1.0'
60
+ };
61
+ p.oauth_signature = oauthSign(method, url, p);
62
+ return 'OAuth ' + Object.keys(p).map(k => `${penc(k)}="${penc(p[k])}"`).join(', ');
63
+ }
64
+
65
+ // --- HTTP with optional proxy ---
66
+ function request(method, urlStr, body) {
67
+ return new Promise((resolve, reject) => {
68
+ const u = new URL(urlStr);
69
+ const bodyStr = body ? JSON.stringify(body) : '';
70
+ const headers = { 'Authorization': authHeader(method, urlStr) };
71
+ if (body) {
72
+ headers['Content-Type'] = 'application/json';
73
+ headers['Content-Length'] = Buffer.byteLength(bodyStr);
74
+ }
75
+ const opts = { hostname: u.hostname, path: u.pathname + u.search, method, headers };
76
+
77
+ function doRequest(socket) {
78
+ if (socket) { opts.socket = socket; opts.createConnection = () => socket; }
79
+ const req = https.request(opts, resp => {
80
+ let d = ''; resp.on('data', c => d += c);
81
+ resp.on('end', () => {
82
+ try { resolve({ status: resp.statusCode, data: JSON.parse(d), headers: resp.headers }); }
83
+ catch { resolve({ status: resp.statusCode, data: d, headers: resp.headers }); }
84
+ });
85
+ });
86
+ req.on('error', reject);
87
+ if (bodyStr) req.write(bodyStr);
88
+ req.end();
89
+ }
90
+
91
+ if (PROXY) {
92
+ const pu = new URL(PROXY);
93
+ const proxyReq = http.request({
94
+ hostname: pu.hostname, port: pu.port,
95
+ method: 'CONNECT', path: `${u.hostname}:443`
96
+ });
97
+ proxyReq.on('connect', (res, socket) => {
98
+ if (res.statusCode !== 200) return reject(new Error(`Proxy CONNECT failed: ${res.statusCode}`));
99
+ const tlsSocket = tls.connect({ socket, servername: u.hostname }, () => doRequest(tlsSocket));
100
+ tlsSocket.on('error', reject);
101
+ });
102
+ proxyReq.on('error', reject);
103
+ proxyReq.end();
104
+ } else {
105
+ doRequest(null);
106
+ }
107
+ });
108
+ }
109
+
110
+ // --- Tweet weight (Twitter's weighted char count) ---
111
+ function tweetWeight(text) {
112
+ let w = 0;
113
+ for (const ch of text) {
114
+ w += (ch.codePointAt(0) > 0x1FFF) ? 2 : 1;
115
+ }
116
+ // URLs count as 23 chars
117
+ const urlRegex = /https?:\/\/\S+/g;
118
+ const urls = text.match(urlRegex) || [];
119
+ for (const url of urls) {
120
+ let urlWeight = 0;
121
+ for (const ch of url) urlWeight += (ch.codePointAt(0) > 0x1FFF) ? 2 : 1;
122
+ w = w - urlWeight + 23;
123
+ }
124
+ return w;
125
+ }
126
+
127
+ // --- Post a single tweet ---
128
+ async function postTweet(text, opts = {}) {
129
+ const weight = tweetWeight(text);
130
+ if (weight > 280) die(`Tweet too long: ${weight}/280 weighted chars`);
131
+
132
+ if (DRY) {
133
+ console.error(`[DRY RUN] ${weight}/280: ${text}`);
134
+ return { ok: true, id: 'dry-run', url: '#', remaining: '-', limit: '-' };
135
+ }
136
+
137
+ const body = { text };
138
+ if (opts.replyTo) body.reply = { in_reply_to_tweet_id: opts.replyTo };
139
+ if (opts.quoteTweetId) body.quote_tweet_id = opts.quoteTweetId;
140
+
141
+ const r = await request('POST', 'https://api.twitter.com/2/tweets', body);
142
+ if (r.status === 201) {
143
+ const id = r.data.data.id;
144
+ return {
145
+ ok: true,
146
+ id,
147
+ url: `https://x.com/i/status/${id}`,
148
+ remaining: r.headers['x-rate-limit-remaining'],
149
+ limit: r.headers['x-rate-limit-limit']
150
+ };
151
+ }
152
+ die(`API error ${r.status}: ${JSON.stringify(r.data)}`);
153
+ }
154
+
155
+ // --- CLI ---
156
+ async function main() {
157
+ const args = process.argv.slice(2);
158
+ let replyTo = null, quoteTweetId = null, thread = false;
159
+ const texts = [];
160
+
161
+ for (let i = 0; i < args.length; i++) {
162
+ if (args[i] === '--reply-to' && args[i + 1]) { replyTo = args[++i]; continue; }
163
+ if (args[i] === '--quote' && args[i + 1]) { quoteTweetId = args[++i]; continue; }
164
+ if (args[i] === '--thread') { thread = true; continue; }
165
+ if (args[i] === '--help' || args[i] === '-h') {
166
+ console.log(`Usage: node tweet.js [options] "text" ["text2" ...]
167
+ Options:
168
+ --reply-to <id> Reply to a tweet
169
+ --quote <id> Quote tweet
170
+ --thread Post multiple args as a thread
171
+ --help Show this help
172
+
173
+ Env vars: TWITTER_CONSUMER_KEY, TWITTER_CONSUMER_SECRET, TWITTER_ACCESS_TOKEN, TWITTER_ACCESS_TOKEN_SECRET
174
+ Optional: HTTPS_PROXY, TWITTER_DRY_RUN=1`);
175
+ process.exit(0);
176
+ }
177
+ texts.push(args[i]);
178
+ }
179
+
180
+ if (texts.length === 0) die('No tweet text provided. Use --help for usage.');
181
+
182
+ if (thread && texts.length > 1) {
183
+ // Post thread
184
+ const results = [];
185
+ let prevId = replyTo;
186
+ for (const t of texts) {
187
+ const r = await postTweet(t, { replyTo: prevId, quoteTweetId: results.length === 0 ? quoteTweetId : undefined });
188
+ results.push(r);
189
+ prevId = r.id;
190
+ }
191
+ console.log(JSON.stringify({ ok: true, thread: results }));
192
+ } else {
193
+ const r = await postTweet(texts.join(' '), { replyTo, quoteTweetId });
194
+ console.log(JSON.stringify(r));
195
+ }
196
+ }
197
+
198
+ main().catch(e => die(e.message));