@xquik/tweetclaw 1.1.0 → 1.2.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/README.md CHANGED
@@ -43,21 +43,21 @@ AI uses explore → filters spec by category "composition"
43
43
 
44
44
  ### `tweetclaw` (execute API calls)
45
45
 
46
- Execute authenticated API calls. Auth is injected automatically - the LLM never sees your API key.
46
+ Execute authenticated API calls. Auth is injected automatically - the LLM never sees your API key.
47
47
 
48
48
  ```
49
- You: "Search tweets about AI agents"
49
+ You: "Post a tweet saying 'Hello from TweetClaw!'"
50
50
 
51
- AI uses explore → finds /api/v1/x/tweets/search
52
- AI uses tweetclaw calls the endpoint with auth
53
- → Returns tweet results
51
+ AI uses tweetclaw → finds connected account, posts tweet
52
+ Returns { tweetId, success: true }
54
53
  ```
55
54
 
56
55
  ```
57
- You: "Post a tweet saying 'Hello from TweetClaw!'"
56
+ You: "Search tweets about AI agents"
58
57
 
59
- AI uses tweetclaw → finds connected account, posts tweet
60
- Returns { tweetId, success: true }
58
+ AI uses explore → finds /api/v1/x/tweets/search
59
+ AI uses tweetclaw calls the endpoint with auth
60
+ → Returns tweet results
61
61
  ```
62
62
 
63
63
  ## Commands
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "id": "tweetclaw",
3
3
  "name": "TweetClaw",
4
+ "version": "1.2.0",
4
5
  "description": "Post tweets, reply, like, retweet, follow, DM from your chat - full X/Twitter automation powered by Xquik",
5
6
  "configSchema": {
6
7
  "type": "object",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xquik/tweetclaw",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Post tweets, reply, like, retweet, follow, DM & more from OpenClaw - full X/Twitter automation via Xquik",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -49,7 +49,7 @@
49
49
  },
50
50
  "devDependencies": {
51
51
  "@eslint/js": "^10.0.1",
52
- "@sinclair/typebox": "^0.34.0",
52
+ "@sinclair/typebox": "^0.34.48",
53
53
  "@types/node": "^25.5.0",
54
54
  "@typescript-eslint/eslint-plugin": "^8.57.0",
55
55
  "@typescript-eslint/parser": "^8.57.0",
@@ -50,12 +50,15 @@ async () => spec.endpoints.filter(e => e.category === 'composition')
50
50
 
51
51
  Execute authenticated API calls. Auth is injected automatically.
52
52
 
53
- Example: "Search tweets about AI agents"
53
+ Example: "Post a tweet saying 'Hello from TweetClaw!'"
54
54
 
55
55
  ```javascript
56
56
  async () => {
57
- const results = await xquik.request('/api/v1/x/tweets/search', { query: { q: 'AI agents' } });
58
- return results;
57
+ const { accounts } = await xquik.request('/api/v1/x/accounts');
58
+ return xquik.request('/api/v1/x/tweets', {
59
+ method: 'POST',
60
+ body: { account: accounts[0].xUsername, text: 'Hello from TweetClaw!' }
61
+ });
59
62
  }
60
63
  ```
61
64
 
@@ -80,16 +83,49 @@ When polling is enabled (default), TweetClaw checks for new events every 60 seco
80
83
 
81
84
  ```
82
85
  You: "Post a tweet saying 'Hello from TweetClaw!'"
83
- Agent uses explore -> finds POST /api/v1/x/tweets
84
- Agent uses tweetclaw -> posts the tweet with auth
86
+ Agent uses tweetclaw -> finds connected account, posts tweet
87
+ ```
88
+
89
+ ### Reply to a tweet
90
+
91
+ ```
92
+ You: "Reply 'Great thread!' to this tweet: https://x.com/user/status/123"
93
+ Agent uses tweetclaw -> posts reply with reply_to_tweet_id
94
+ ```
95
+
96
+ ### Like, retweet, follow
97
+
98
+ ```
99
+ You: "Like and retweet this tweet, then follow the author"
100
+ Agent uses tweetclaw -> likes tweet, retweets, looks up user ID, follows
101
+ ```
102
+
103
+ ### Send a DM
104
+
105
+ ```
106
+ You: "DM @username saying 'Hey, let's collaborate!'"
107
+ Agent uses tweetclaw -> looks up user ID, sends DM
108
+ ```
109
+
110
+ ### Update profile
111
+
112
+ ```
113
+ You: "Change my bio to 'Building cool stuff' and update my avatar"
114
+ Agent uses tweetclaw -> PATCH /api/v1/x/profile, PATCH /api/v1/x/profile/avatar
115
+ ```
116
+
117
+ ### Upload media and tweet with image
118
+
119
+ ```
120
+ You: "Tweet 'Check this out!' with this image: https://example.com/photo.jpg"
121
+ Agent uses tweetclaw -> uploads media, posts tweet with media_ids
85
122
  ```
86
123
 
87
124
  ### Search tweets
88
125
 
89
126
  ```
90
127
  You: "Search tweets about AI agents"
91
- Agent uses explore -> finds GET /api/v1/x/tweets/search
92
- Agent uses tweetclaw -> calls the endpoint
128
+ Agent uses tweetclaw -> calls search endpoint with query
93
129
  ```
94
130
 
95
131
  ### Run a giveaway draw
@@ -99,6 +135,13 @@ You: "Pick 3 random winners from replies to this tweet: https://x.com/..."
99
135
  Agent uses tweetclaw -> creates draw with filters
100
136
  ```
101
137
 
138
+ ### Extract bulk data
139
+
140
+ ```
141
+ You: "Extract the last 1000 followers of @elonmusk"
142
+ Agent uses tweetclaw -> estimates cost, creates extraction job
143
+ ```
144
+
102
145
  ### Monitor an account
103
146
 
104
147
  ```
@@ -106,11 +149,32 @@ You: "Monitor @elonmusk for new tweets and follower changes"
106
149
  Agent uses tweetclaw -> creates monitor with event types
107
150
  ```
108
151
 
109
- ### Compose an optimized tweet
152
+ ### Download tweet media
153
+
154
+ ```
155
+ You: "Download all media from this tweet"
156
+ Agent uses tweetclaw -> returns gallery URL with all media
157
+ ```
158
+
159
+ ### Compose an optimized tweet (free)
160
+
161
+ ```
162
+ You: "Help me write a tweet about our product launch"
163
+ Agent uses tweetclaw -> 3-step compose/refine/score workflow
164
+ ```
165
+
166
+ ### Analyze writing style (free)
167
+
168
+ ```
169
+ You: "Analyze @username's tweet style"
170
+ Agent uses tweetclaw -> returns style analysis with tone, patterns, metrics
171
+ ```
172
+
173
+ ### Browse trending topics (free)
110
174
 
111
175
  ```
112
- You: "Compose a tweet about our product launch"
113
- Agent uses tweetclaw -> 3-step compose/refine/score workflow (free)
176
+ You: "What's trending on X right now?"
177
+ Agent uses tweetclaw -> returns curated trending topics from 7 sources
114
178
  ```
115
179
 
116
180
  ## API Categories
package/src/index.ts CHANGED
@@ -15,66 +15,64 @@ function isPollerEvent(value: unknown): value is PollerEvent {
15
15
  return typeof value === 'object' && value !== null;
16
16
  }
17
17
 
18
+ function isPluginConfig(value: unknown): value is PluginConfig {
19
+ return typeof value === 'object' && value !== null && 'apiKey' in value;
20
+ }
21
+
18
22
  const DEFAULT_POLLING_INTERVAL_SECONDS = 60;
19
23
 
24
+ interface ToolResult {
25
+ readonly content: ReadonlyArray<{ readonly text: string; readonly type: string }>;
26
+ readonly isError?: true;
27
+ }
28
+
20
29
  interface CommandContext {
21
30
  readonly args?: string;
31
+ readonly commandBody?: string;
32
+ readonly senderId?: string;
22
33
  }
23
34
 
24
35
  interface OpenClawApi {
25
- readonly config: {
26
- readonly plugins?: {
27
- readonly entries?: {
28
- readonly tweetclaw?: {
29
- readonly config?: PluginConfig;
30
- };
31
- };
32
- };
33
- };
34
36
  readonly logger: {
37
+ readonly debug?: (message: string) => void;
38
+ readonly error: (message: string) => void;
35
39
  readonly info: (message: string) => void;
36
40
  readonly warn: (message: string) => void;
37
41
  };
42
+ readonly pluginConfig?: Readonly<Record<string, unknown>>;
38
43
  readonly registerCommand: (options: {
39
- readonly acceptsArguments?: boolean;
44
+ readonly acceptsArgs?: boolean;
40
45
  readonly description: string;
41
46
  readonly handler: (context: CommandContext) => Promise<{ readonly text: string }>;
42
47
  readonly name: string;
43
48
  }) => void;
44
49
  readonly registerService: (options: {
45
50
  readonly id: string;
46
- readonly start: () => void;
47
- readonly stop: () => void;
51
+ readonly start: (context?: unknown) => void;
52
+ readonly stop?: (context?: unknown) => void;
48
53
  }) => void;
49
54
  readonly registerTool: (
50
- options: {
55
+ tool: {
51
56
  readonly description: string;
57
+ readonly execute: (toolCallId: string, params: { readonly code: string }) => Promise<ToolResult>;
52
58
  readonly name: string;
53
- readonly parameters: {
54
- readonly properties: Readonly<Record<string, { readonly description: string; readonly type: string }>>;
55
- readonly required: readonly string[];
56
- readonly type: string;
57
- };
59
+ readonly parameters: unknown;
58
60
  },
59
- handler: (params: { readonly code: string }) => Promise<{
60
- readonly content: ReadonlyArray<{ readonly text: string; readonly type: string }>;
61
- readonly isError?: true;
62
- }>,
61
+ options?: { readonly name?: string; readonly optional?: boolean },
63
62
  ) => void;
64
- readonly sendMessage: (text: string) => void;
65
63
  }
66
64
 
67
65
  const CODE_PARAMETER = {
68
66
  properties: {
69
67
  code: { description: 'Async arrow function to execute', type: 'string' },
70
68
  },
71
- required: ['code'] as const,
69
+ required: ['code'],
72
70
  type: 'object',
73
71
  };
74
72
 
75
73
  export default function register(api: OpenClawApi, fetchFunction?: FetchFunction): void {
76
- const config = api.config.plugins?.entries?.tweetclaw?.config;
77
- if (config?.apiKey === undefined) {
74
+ const config: unknown = api.pluginConfig;
75
+ if (!isPluginConfig(config)) {
78
76
  api.logger.warn(
79
77
  "TweetClaw: No API key configured. Run: openclaw config set plugins.entries.tweetclaw.config.apiKey 'xq_YOUR_KEY'",
80
78
  );
@@ -84,23 +82,25 @@ export default function register(api: OpenClawApi, fetchFunction?: FetchFunction
84
82
  const { apiKey, baseUrl = 'https://xquik.com' } = config;
85
83
  const request = createProxiedRequest(baseUrl, apiKey, fetchFunction);
86
84
 
87
- // --- Tools (2-tool approach) ---
85
+ // --- Tools (2-tool approach, execute inside tool object) ---
88
86
  api.registerTool(
89
87
  {
90
88
  description: SEARCH_DESCRIPTION,
89
+ execute: async (_toolCallId, { code }) => handleExplore(code),
91
90
  name: 'explore',
92
91
  parameters: CODE_PARAMETER,
93
92
  },
94
- async ({ code }) => handleExplore(code),
93
+ { name: 'explore' },
95
94
  );
96
95
 
97
96
  api.registerTool(
98
97
  {
99
98
  description: EXECUTE_DESCRIPTION,
99
+ execute: async (_toolCallId, { code }) => handleTweetclaw({ apiKey, baseUrl, code, fetchFunction }),
100
100
  name: 'tweetclaw',
101
101
  parameters: CODE_PARAMETER,
102
102
  },
103
- async ({ code }) => handleTweetclaw({ apiKey, baseUrl, code, fetchFunction }),
103
+ { name: 'tweetclaw', optional: true },
104
104
  );
105
105
 
106
106
  // --- Commands (instant, no LLM) ---
@@ -114,7 +114,7 @@ export default function register(api: OpenClawApi, fetchFunction?: FetchFunction
114
114
  });
115
115
 
116
116
  api.registerCommand({
117
- acceptsArguments: true,
117
+ acceptsArgs: true,
118
118
  description: 'Show trending topics on X',
119
119
  handler: async ({ args }) => {
120
120
  const text = await handleXTrends(request, args);
@@ -136,7 +136,7 @@ export default function register(api: OpenClawApi, fetchFunction?: FetchFunction
136
136
  const username: string = isPollerEvent(event) && typeof event.xUsername === 'string'
137
137
  ? event.xUsername
138
138
  : '';
139
- api.sendMessage(`[TweetClaw] ${eventType} from @${username}`);
139
+ api.logger.info(`[TweetClaw] ${eventType} from @${username}`);
140
140
  }
141
141
  },
142
142
  request,
@@ -4,7 +4,7 @@ import type { EndpointInfo, ToolResult } from '../types.js';
4
4
 
5
5
  const categories = [...new Set(API_SPEC.map((endpoint) => endpoint.category))].toSorted((a, b) => a.localeCompare(b)).join(', ');
6
6
 
7
- const SEARCH_DESCRIPTION = `Search the X (Twitter) API spec for endpoints: tweet search, user lookup, media download, monitoring, giveaways, composition, and more. No network calls - runs against an in-memory endpoint catalog.
7
+ const SEARCH_DESCRIPTION = `Search the X (Twitter) API spec for endpoints: post tweets, reply, like, retweet, follow, DM, update profile, upload media, search tweets, look up users, extract data, monitor accounts, run giveaways, compose tweets, and more. No network calls - runs against an in-memory endpoint catalog.
8
8
 
9
9
  Write an async arrow function. The sandbox provides:
10
10
 
@@ -2,7 +2,7 @@ import { createProxiedRequest } from '../request.js';
2
2
  import { AsyncFunction, errorResult, specEndpoints, successResult } from './sandbox.js';
3
3
  import type { FetchFunction, RequestFunction, ToolResult } from '../types.js';
4
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.
5
+ const EXECUTE_DESCRIPTION = `Execute X (Twitter) API calls: post tweets, reply, like, retweet, follow, DM, update profile, upload media, search tweets, look up users, extract data, run giveaways, monitor accounts, compose tweets, and more. Write an async arrow function.
6
6
 
7
7
  The sandbox provides:
8
8
  \`\`\`typescript
@@ -20,6 +20,13 @@ declare const spec: { endpoints: EndpointInfo[] };
20
20
  Auth is injected automatically - never pass API keys.
21
21
  First use "explore" to find endpoints, then write code here to call them.
22
22
 
23
+ ## Important rules
24
+ - TWEET ACTIONS: SENDING a tweet ("tweet this", "post this") uses POST /api/v1/x/tweets. DRAFTING a tweet ("help me write", "compose") uses the 3-step compose flow. Never use compose when the user has text and wants to send it.
25
+ - CALL ORDERING: NEVER combine free and paid endpoints in Promise.all. Call free endpoints first (radar, styles, compose), then paid ones separately. If a paid call fails with 402, still use free data already fetched.
26
+ - WRITE ACTIONS: All require the "account" parameter (X username, e.g. "@myaccount"). Follow/unfollow/DM use numeric user ID in path - look up the user first via GET /api/v1/x/users/:username.
27
+ - CURRENT EVENTS: Use /api/v1/radar (free) for trending topics. Never use web search for trends.
28
+ - SUBSCRIPTION ERRORS: On 402, call POST /api/v1/subscribe (free) to get checkout URL.
29
+
23
30
  ## Workflows
24
31
 
25
32
  ### 1. Send a tweet (Subscription required)
@@ -103,7 +110,28 @@ async () => {
103
110
  }
104
111
  \`\`\`
105
112
 
106
- ### 7. Search tweets with pagination (Subscription required)
113
+ ### 7. Update profile, avatar, or banner
114
+ \`\`\`javascript
115
+ async () => {
116
+ // Update bio, name, location, URL
117
+ await xquik.request('/api/v1/x/profile', {
118
+ method: 'PATCH',
119
+ body: { account: '@myaccount', name: 'New Name', bio: 'Building cool stuff' }
120
+ });
121
+ // Update avatar (url must be HTTPS, max 700 KB)
122
+ await xquik.request('/api/v1/x/profile/avatar', {
123
+ method: 'PATCH',
124
+ body: { account: '@myaccount', url: 'https://example.com/avatar.jpg' }
125
+ });
126
+ // Update banner (max 2 MB)
127
+ return xquik.request('/api/v1/x/profile/banner', {
128
+ method: 'PATCH',
129
+ body: { account: '@myaccount', url: 'https://example.com/banner.jpg' }
130
+ });
131
+ }
132
+ \`\`\`
133
+
134
+ ### 8. Search tweets with pagination (Subscription required)
107
135
  \`\`\`javascript
108
136
  async () => {
109
137
  // Use limit param for more than 20 results (max 200)
@@ -113,14 +141,87 @@ async () => {
113
141
  }
114
142
  \`\`\`
115
143
 
116
- ### 8. Browse trending topics (FREE)
144
+ ### 9. Look up a user or tweet
145
+ \`\`\`javascript
146
+ async () => {
147
+ // User profile (name, bio, followers, following, verified, location)
148
+ const user = await xquik.request('/api/v1/x/users/elonmusk');
149
+ // Single tweet with full metrics
150
+ const tweet = await xquik.request('/api/v1/x/tweets/1234567890');
151
+ // Check if A follows B
152
+ const follows = await xquik.request('/api/v1/x/followers/check', {
153
+ query: { source: 'userA', target: 'userB' }
154
+ });
155
+ return { user, tweet, follows };
156
+ }
157
+ \`\`\`
158
+
159
+ ### 10. Monitor an account + set up webhook
160
+ \`\`\`javascript
161
+ async () => {
162
+ // Create monitor for new tweets, replies, follower changes
163
+ const monitor = await xquik.request('/api/v1/monitors', {
164
+ method: 'POST',
165
+ body: { username: 'elonmusk', eventTypes: ['tweet.new', 'tweet.reply', 'follower.gained'] }
166
+ });
167
+ // Set up webhook to receive events (save the secret!)
168
+ const webhook = await xquik.request('/api/v1/webhooks', {
169
+ method: 'POST',
170
+ body: { url: 'https://your-server.com/webhook', eventTypes: ['tweet.new', 'tweet.reply'] }
171
+ });
172
+ return { monitor, webhook };
173
+ }
174
+ \`\`\`
175
+
176
+ ### 11. Run a giveaway draw from tweet replies
177
+ \`\`\`javascript
178
+ async () => {
179
+ return xquik.request('/api/v1/draws', {
180
+ method: 'POST',
181
+ body: {
182
+ tweetUrl: 'https://x.com/user/status/1234567890',
183
+ winnerCount: 3,
184
+ backupCount: 2,
185
+ uniqueAuthorsOnly: true,
186
+ mustRetweet: true,
187
+ mustFollowUsername: 'myaccount',
188
+ filterMinFollowers: 50
189
+ }
190
+ });
191
+ }
192
+ \`\`\`
193
+
194
+ ### 12. Extract bulk data (followers, replies, communities)
195
+ \`\`\`javascript
196
+ async () => {
197
+ // Always estimate cost first
198
+ const estimate = await xquik.request('/api/v1/extractions/estimate', {
199
+ method: 'POST',
200
+ body: { toolType: 'follower_explorer', targetUsername: 'elonmusk', resultsLimit: 1000 }
201
+ });
202
+ if (!estimate.allowed) return { error: 'Would exceed quota', estimate };
203
+ // Create extraction job
204
+ const job = await xquik.request('/api/v1/extractions', {
205
+ method: 'POST',
206
+ body: { toolType: 'follower_explorer', targetUsername: 'elonmusk', resultsLimit: 1000 }
207
+ });
208
+ return job;
209
+ // 20 tool types: reply_extractor, repost_extractor, quote_extractor, thread_extractor,
210
+ // article_extractor, follower_explorer, following_explorer, verified_follower_explorer,
211
+ // mention_extractor, post_extractor, community_extractor, community_moderator_explorer,
212
+ // community_post_extractor, community_search, list_member_extractor, list_post_extractor,
213
+ // list_follower_explorer, space_explorer, people_search, tweet_search_extractor
214
+ }
215
+ \`\`\`
216
+
217
+ ### 13. Browse trending topics (FREE)
117
218
  \`\`\`javascript
118
219
  async () => {
119
220
  return xquik.request('/api/v1/radar');
120
221
  }
121
222
  \`\`\`
122
223
 
123
- ### 9. Analyze a user's writing style
224
+ ### 14. Analyze a user's writing style
124
225
  \`\`\`javascript
125
226
  async () => {
126
227
  // Returns cached style if available (free for all users)
@@ -132,7 +233,7 @@ async () => {
132
233
  }
133
234
  \`\`\`
134
235
 
135
- ### 10. Download media and get gallery link (Subscription required)
236
+ ### 15. Download media and get gallery link (Subscription required)
136
237
  \`\`\`javascript
137
238
  async () => {
138
239
  // Returns galleryUrl only (shareable gallery page with all media)
@@ -143,14 +244,42 @@ async () => {
143
244
  }
144
245
  \`\`\`
145
246
 
146
- ### 11. Subscribe (FREE - returns Stripe checkout URL)
247
+ ### 16. Set up Telegram alerts for monitor events (FREE)
248
+ \`\`\`javascript
249
+ async () => {
250
+ return xquik.request('/api/v1/integrations', {
251
+ method: 'POST',
252
+ body: {
253
+ type: 'telegram',
254
+ chatId: '123456789',
255
+ eventTypes: ['tweet.new', 'tweet.reply', 'draw.completed', 'extraction.completed']
256
+ }
257
+ });
258
+ }
259
+ \`\`\`
260
+
261
+ ### 17. Community actions (create, join, leave)
262
+ \`\`\`javascript
263
+ async () => {
264
+ // Join a community
265
+ await xquik.request('/api/v1/x/communities/99999/join', {
266
+ method: 'POST', body: { account: '@myaccount' }
267
+ });
268
+ // Leave a community
269
+ await xquik.request('/api/v1/x/communities/99999/join', {
270
+ method: 'DELETE', body: { account: '@myaccount' }
271
+ });
272
+ }
273
+ \`\`\`
274
+
275
+ ### 18. Subscribe (FREE - returns Stripe checkout URL)
147
276
  \`\`\`javascript
148
277
  async () => {
149
278
  return xquik.request('/api/v1/subscribe', { method: 'POST' });
150
279
  }
151
280
  \`\`\`
152
281
 
153
- ### 12. Draft & optimize tweet text (3-step compose flow, FREE)
282
+ ### 19. Draft & optimize tweet text (3-step compose flow, FREE)
154
283
  \`\`\`javascript
155
284
  async () => {
156
285
  // Use this ONLY when the user wants help WRITING tweet text.