@xquik/tweetclaw 1.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.
- package/LICENSE +21 -0
- package/README.md +121 -0
- package/openclaw.plugin.json +24 -0
- package/package.json +64 -0
- package/src/api-spec.ts +1024 -0
- package/src/commands/xstatus.ts +76 -0
- package/src/commands/xtrends.ts +59 -0
- package/src/index.ts +153 -0
- package/src/request.ts +62 -0
- package/src/services/event-poller.ts +97 -0
- package/src/tools/explore.ts +59 -0
- package/src/tools/sandbox.ts +58 -0
- package/src/tools/tweetclaw.ts +213 -0
- package/src/truncate.ts +28 -0
- package/src/types.ts +56 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { createProxiedRequest } from '../request.js';
|
|
2
|
+
import { AsyncFunction, errorResult, specEndpoints, successResult } from './sandbox.js';
|
|
3
|
+
import type { FetchFunction, RequestFunction, ToolResult } from '../types.js';
|
|
4
|
+
|
|
5
|
+
const EXECUTE_DESCRIPTION = `Execute X (Twitter) API calls: search tweets, look up users, download media, compose tweets, run giveaways, monitor accounts, and more. Write an async arrow function.
|
|
6
|
+
|
|
7
|
+
The sandbox provides:
|
|
8
|
+
\`\`\`typescript
|
|
9
|
+
// xquik.request(path, options?) - auth is injected automatically
|
|
10
|
+
declare const xquik: {
|
|
11
|
+
request(path: string, options?: {
|
|
12
|
+
method?: string; // default: 'GET'
|
|
13
|
+
body?: unknown;
|
|
14
|
+
query?: Record<string, string>;
|
|
15
|
+
}): Promise<unknown>;
|
|
16
|
+
};
|
|
17
|
+
declare const spec: { endpoints: EndpointInfo[] };
|
|
18
|
+
\`\`\`
|
|
19
|
+
|
|
20
|
+
Auth is injected automatically - never pass API keys.
|
|
21
|
+
First use "explore" to find endpoints, then write code here to call them.
|
|
22
|
+
|
|
23
|
+
## Workflows
|
|
24
|
+
|
|
25
|
+
### 1. Send a tweet (Subscription required)
|
|
26
|
+
\`\`\`javascript
|
|
27
|
+
async () => {
|
|
28
|
+
// First, find connected accounts
|
|
29
|
+
const { accounts } = await xquik.request('/api/v1/x/accounts');
|
|
30
|
+
// Post the tweet directly
|
|
31
|
+
return xquik.request('/api/v1/x/tweets', {
|
|
32
|
+
method: 'POST',
|
|
33
|
+
body: { account: accounts[0].xUsername, text: 'Hello world!' }
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
\`\`\`
|
|
37
|
+
|
|
38
|
+
### 2. Reply to a tweet
|
|
39
|
+
\`\`\`javascript
|
|
40
|
+
async () => {
|
|
41
|
+
return xquik.request('/api/v1/x/tweets', {
|
|
42
|
+
method: 'POST',
|
|
43
|
+
body: { account: '@myaccount', text: 'Great point!', reply_to_tweet_id: '1234567890' }
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
\`\`\`
|
|
47
|
+
|
|
48
|
+
### 3. Like, retweet, follow (follow requires user ID lookup)
|
|
49
|
+
\`\`\`javascript
|
|
50
|
+
async () => {
|
|
51
|
+
// Like a tweet (tweet ID in path)
|
|
52
|
+
await xquik.request('/api/v1/x/tweets/1234567890/like', {
|
|
53
|
+
method: 'POST', body: { account: '@myaccount' }
|
|
54
|
+
});
|
|
55
|
+
// Retweet
|
|
56
|
+
await xquik.request('/api/v1/x/tweets/1234567890/retweet', {
|
|
57
|
+
method: 'POST', body: { account: '@myaccount' }
|
|
58
|
+
});
|
|
59
|
+
// Follow - requires numeric user ID, look up first
|
|
60
|
+
const user = await xquik.request('/api/v1/x/users/elonmusk');
|
|
61
|
+
await xquik.request(\`/api/v1/x/users/\${user.id}/follow\`, {
|
|
62
|
+
method: 'POST', body: { account: '@myaccount' }
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
\`\`\`
|
|
66
|
+
|
|
67
|
+
### 4. Undo actions (unlike, unfollow, delete tweet)
|
|
68
|
+
\`\`\`javascript
|
|
69
|
+
async () => {
|
|
70
|
+
await xquik.request('/api/v1/x/tweets/1234567890/like', {
|
|
71
|
+
method: 'DELETE', body: { account: '@myaccount' }
|
|
72
|
+
});
|
|
73
|
+
await xquik.request('/api/v1/x/users/44196397/follow', {
|
|
74
|
+
method: 'DELETE', body: { account: '@myaccount' }
|
|
75
|
+
});
|
|
76
|
+
await xquik.request('/api/v1/x/tweets/1234567890', {
|
|
77
|
+
method: 'DELETE', body: { account: '@myaccount' }
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
\`\`\`
|
|
81
|
+
|
|
82
|
+
### 5. Send DM (uses recipient user ID in path)
|
|
83
|
+
\`\`\`javascript
|
|
84
|
+
async () => {
|
|
85
|
+
return xquik.request('/api/v1/x/dm/44196397', {
|
|
86
|
+
method: 'POST',
|
|
87
|
+
body: { account: '@myaccount', text: 'Hey, check this out!' }
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
\`\`\`
|
|
91
|
+
|
|
92
|
+
### 6. Upload media via URL and tweet with image
|
|
93
|
+
\`\`\`javascript
|
|
94
|
+
async () => {
|
|
95
|
+
const media = await xquik.request('/api/v1/x/media', {
|
|
96
|
+
method: 'POST',
|
|
97
|
+
body: { account: '@myaccount', url: 'https://example.com/photo.jpg' }
|
|
98
|
+
});
|
|
99
|
+
return xquik.request('/api/v1/x/tweets', {
|
|
100
|
+
method: 'POST',
|
|
101
|
+
body: { account: '@myaccount', text: 'Check this out!', media_ids: [media.mediaId] }
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
\`\`\`
|
|
105
|
+
|
|
106
|
+
### 7. Search tweets with pagination (Subscription required)
|
|
107
|
+
\`\`\`javascript
|
|
108
|
+
async () => {
|
|
109
|
+
// Use limit param for more than 20 results (max 200)
|
|
110
|
+
return xquik.request('/api/v1/x/tweets/search', {
|
|
111
|
+
query: { q: 'from:elonmusk', limit: '50' }
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
\`\`\`
|
|
115
|
+
|
|
116
|
+
### 8. Browse trending topics (FREE)
|
|
117
|
+
\`\`\`javascript
|
|
118
|
+
async () => {
|
|
119
|
+
return xquik.request('/api/v1/radar');
|
|
120
|
+
}
|
|
121
|
+
\`\`\`
|
|
122
|
+
|
|
123
|
+
### 9. Analyze a user's writing style
|
|
124
|
+
\`\`\`javascript
|
|
125
|
+
async () => {
|
|
126
|
+
// Returns cached style if available (free for all users)
|
|
127
|
+
// Auto-refreshes from X if cache is older than 7 days (subscription required)
|
|
128
|
+
return xquik.request('/api/v1/styles', {
|
|
129
|
+
method: 'POST',
|
|
130
|
+
body: { username: 'dbdevletbahceli' }
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
\`\`\`
|
|
134
|
+
|
|
135
|
+
### 10. Download media and get gallery link (Subscription required)
|
|
136
|
+
\`\`\`javascript
|
|
137
|
+
async () => {
|
|
138
|
+
// Returns galleryUrl only (shareable gallery page with all media)
|
|
139
|
+
return xquik.request('/api/v1/x/media/download', {
|
|
140
|
+
method: 'POST',
|
|
141
|
+
body: { tweetInput: '1234567890' } // tweet ID or full URL
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
\`\`\`
|
|
145
|
+
|
|
146
|
+
### 11. Subscribe (FREE - returns Stripe checkout URL)
|
|
147
|
+
\`\`\`javascript
|
|
148
|
+
async () => {
|
|
149
|
+
return xquik.request('/api/v1/subscribe', { method: 'POST' });
|
|
150
|
+
}
|
|
151
|
+
\`\`\`
|
|
152
|
+
|
|
153
|
+
### 12. Draft & optimize tweet text (3-step compose flow, FREE)
|
|
154
|
+
\`\`\`javascript
|
|
155
|
+
async () => {
|
|
156
|
+
// Use this ONLY when the user wants help WRITING tweet text.
|
|
157
|
+
// To SEND a tweet, use POST /api/v1/x/tweets instead.
|
|
158
|
+
// Step 1: Get algorithm data
|
|
159
|
+
const compose = await xquik.request('/api/v1/compose', {
|
|
160
|
+
method: 'POST',
|
|
161
|
+
body: { step: 'compose', topic: 'AI agents' }
|
|
162
|
+
});
|
|
163
|
+
return compose; // Returns contentRules, followUpQuestions, scorerWeights
|
|
164
|
+
// After user answers: call with body { step: 'refine', goal, tone, topic }
|
|
165
|
+
// After drafting: call with body { step: 'score', draft }
|
|
166
|
+
}
|
|
167
|
+
\`\`\`
|
|
168
|
+
|
|
169
|
+
## Cost
|
|
170
|
+
- Free: /api/v1/compose, /api/v1/styles (cached lookup/save/delete/compare), /api/v1/drafts, /api/v1/radar, /api/v1/subscribe, /api/v1/account, /api/v1/api-keys, /api/v1/bot/*, /api/v1/integrations/*, /api/v1/x/accounts
|
|
171
|
+
- Subscription required: /api/v1/styles (X API refresh when cache >7 days), /api/v1/x/tweets, /api/v1/x/users, /api/v1/x/followers, /api/v1/x/media, /api/v1/x/profile, /api/v1/x/communities, /api/v1/x/dm, /api/v1/extractions, /api/v1/draws, /api/v1/monitors, /api/v1/events, /api/v1/webhooks, /api/v1/styles/:username/performance, /api/v1/trends, /api/v1/trending/:source
|
|
172
|
+
- Write actions (subscription required): POST /api/v1/x/tweets, DELETE /api/v1/x/tweets/:id, POST|DELETE /api/v1/x/tweets/:id/like, POST /api/v1/x/tweets/:id/retweet, POST|DELETE /api/v1/x/users/:id/follow, POST /api/v1/x/dm/:userId, POST /api/v1/x/media, PATCH /api/v1/x/profile, PATCH /api/v1/x/profile/avatar, PATCH /api/v1/x/profile/banner, POST|DELETE /api/v1/x/communities, POST|DELETE /api/v1/x/communities/:id/join
|
|
173
|
+
- IMPORTANT: Always attempt the request. Never assume subscription status. The API returns a clear error if subscription is missing.
|
|
174
|
+
|
|
175
|
+
## Error handling
|
|
176
|
+
- If response contains "subscription is inactive" or status 402, call POST /api/v1/subscribe to get checkout URL
|
|
177
|
+
- NEVER combine free and paid calls in Promise.all - a 402 on one call kills all results. Call free endpoints first, then paid ones separately
|
|
178
|
+
- If a paid call fails, still use free data already fetched (radar, styles, compose). Never discard free data or fall back to web search
|
|
179
|
+
- API errors include status code and message`;
|
|
180
|
+
|
|
181
|
+
const EXECUTION_TIMEOUT_MS = 30_000;
|
|
182
|
+
const MS_PER_SECOND = 1000;
|
|
183
|
+
|
|
184
|
+
interface TweetclawOptions {
|
|
185
|
+
readonly apiKey: string;
|
|
186
|
+
readonly baseUrl: string;
|
|
187
|
+
readonly code: string;
|
|
188
|
+
readonly fetchFunction?: FetchFunction | undefined;
|
|
189
|
+
readonly timeoutMs?: number | undefined;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function handleTweetclaw(options: Readonly<TweetclawOptions>): Promise<ToolResult> {
|
|
193
|
+
const { apiKey, baseUrl, code, fetchFunction, timeoutMs = EXECUTION_TIMEOUT_MS } = options;
|
|
194
|
+
try {
|
|
195
|
+
const request: RequestFunction = createProxiedRequest(baseUrl, apiKey, fetchFunction);
|
|
196
|
+
const executor = new AsyncFunction('xquik', 'spec', `return (${code})()`);
|
|
197
|
+
|
|
198
|
+
const result: unknown = await Promise.race([
|
|
199
|
+
executor({ request }, { endpoints: specEndpoints }),
|
|
200
|
+
new Promise<never>((_resolve, reject) => {
|
|
201
|
+
setTimeout(() => {
|
|
202
|
+
reject(new Error(`Execution timed out after ${String(timeoutMs / MS_PER_SECOND)}s`));
|
|
203
|
+
}, timeoutMs);
|
|
204
|
+
}),
|
|
205
|
+
]);
|
|
206
|
+
|
|
207
|
+
return successResult(result);
|
|
208
|
+
} catch (error: unknown) {
|
|
209
|
+
return errorResult(error);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export { EXECUTE_DESCRIPTION, handleTweetclaw };
|
package/src/truncate.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const MAX_RESPONSE_CHARS = 24_000;
|
|
2
|
+
const CHARS_PER_TOKEN = 4;
|
|
3
|
+
const MAX_TOKENS = 6000;
|
|
4
|
+
|
|
5
|
+
function truncateText(text: string): string {
|
|
6
|
+
if (text.length <= MAX_RESPONSE_CHARS) {
|
|
7
|
+
return text;
|
|
8
|
+
}
|
|
9
|
+
const approximateTokens = Math.ceil(text.length / CHARS_PER_TOKEN);
|
|
10
|
+
const formatted = approximateTokens.toLocaleString('en-US');
|
|
11
|
+
return `${text.slice(0, MAX_RESPONSE_CHARS)}\n\n--- TRUNCATED ---\nResponse was ~${formatted} tokens (limit: ${MAX_TOKENS.toLocaleString('en-US')}). Use more specific queries or filters to reduce response size.`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function stringifyContent(content: unknown): string {
|
|
15
|
+
if (typeof content === 'string') {
|
|
16
|
+
return content;
|
|
17
|
+
}
|
|
18
|
+
if (content === undefined) {
|
|
19
|
+
return 'undefined';
|
|
20
|
+
}
|
|
21
|
+
return JSON.stringify(content, undefined, 2);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function truncateResponse(content: unknown): string {
|
|
25
|
+
return truncateText(stringifyContent(content));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export { truncateResponse, truncateText };
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
interface EndpointParameter {
|
|
2
|
+
readonly description: string;
|
|
3
|
+
readonly in: 'body' | 'path' | 'query';
|
|
4
|
+
readonly name: string;
|
|
5
|
+
readonly required: boolean;
|
|
6
|
+
readonly type: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface EndpointInfo {
|
|
10
|
+
readonly category: string;
|
|
11
|
+
readonly free: boolean;
|
|
12
|
+
readonly method: string;
|
|
13
|
+
readonly parameters?: readonly EndpointParameter[];
|
|
14
|
+
readonly path: string;
|
|
15
|
+
readonly responseShape?: string;
|
|
16
|
+
readonly summary: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface RequestOptions {
|
|
20
|
+
readonly body?: unknown;
|
|
21
|
+
readonly method?: string;
|
|
22
|
+
readonly query?: Readonly<Record<string, string>>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type RequestFunction = (path: string, options?: Readonly<RequestOptions>) => Promise<unknown>;
|
|
26
|
+
|
|
27
|
+
type FetchFunction = typeof fetch;
|
|
28
|
+
|
|
29
|
+
interface ToolResult {
|
|
30
|
+
readonly content: ReadonlyArray<{ readonly text: string; readonly type: 'text' }>;
|
|
31
|
+
readonly isError?: true;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface PluginConfig {
|
|
35
|
+
readonly apiKey: string;
|
|
36
|
+
readonly baseUrl?: string;
|
|
37
|
+
readonly pollingEnabled?: boolean;
|
|
38
|
+
readonly pollingInterval?: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface EventPollerOptions {
|
|
42
|
+
readonly intervalSeconds: number;
|
|
43
|
+
readonly onEvents: (events: ReadonlyArray<Readonly<Record<string, unknown>>>) => void;
|
|
44
|
+
readonly request: RequestFunction;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type {
|
|
48
|
+
EndpointInfo,
|
|
49
|
+
EndpointParameter,
|
|
50
|
+
EventPollerOptions,
|
|
51
|
+
FetchFunction,
|
|
52
|
+
PluginConfig,
|
|
53
|
+
RequestFunction,
|
|
54
|
+
RequestOptions,
|
|
55
|
+
ToolResult,
|
|
56
|
+
};
|