@xquik/tweetclaw 1.6.4 → 1.6.6
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 +25 -14
- package/dist/api-spec.d.ts +3 -0
- package/dist/api-spec.js +1427 -0
- package/dist/api-spec.js.map +1 -0
- package/dist/commands/xstatus.d.ts +17 -0
- package/dist/commands/xstatus.js +52 -0
- package/dist/commands/xstatus.js.map +1 -0
- package/dist/commands/xtrends.d.ts +16 -0
- package/dist/commands/xtrends.js +39 -0
- package/dist/commands/xtrends.js.map +1 -0
- package/dist/index.d.ts +107 -0
- package/dist/index.js +249 -0
- package/dist/index.js.map +1 -0
- package/dist/mpp.d.ts +7 -0
- package/dist/mpp.js +39 -0
- package/dist/mpp.js.map +1 -0
- package/dist/request.d.ts +7 -0
- package/dist/request.js +88 -0
- package/dist/request.js.map +1 -0
- package/dist/services/event-poller.d.ts +7 -0
- package/dist/services/event-poller.js +69 -0
- package/dist/services/event-poller.js.map +1 -0
- package/dist/tools/catalog.d.ts +17 -0
- package/dist/tools/catalog.js +110 -0
- package/dist/tools/catalog.js.map +1 -0
- package/dist/tools/explore.d.ts +5 -0
- package/dist/tools/explore.js +28 -0
- package/dist/tools/explore.js.map +1 -0
- package/dist/tools/result.d.ts +5 -0
- package/dist/tools/result.js +15 -0
- package/dist/tools/result.js.map +1 -0
- package/dist/tools/tweetclaw.d.ts +13 -0
- package/dist/tools/tweetclaw.js +62 -0
- package/dist/tools/tweetclaw.js.map +1 -0
- package/dist/truncate.d.ts +3 -0
- package/dist/truncate.js +25 -0
- package/dist/truncate.js.map +1 -0
- package/dist/types.d.ts +64 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/openclaw.plugin.json +7 -8
- package/package.json +17 -17
- package/skills/tweetclaw/SKILL.md +37 -44
- package/src/api-spec.ts +491 -12
- package/src/index.ts +212 -10
- package/src/mpp.ts +9 -11
- package/src/request.ts +27 -2
- package/src/tools/catalog.ts +145 -0
- package/src/tools/explore.ts +18 -44
- package/src/tools/result.ts +19 -0
- package/src/tools/tweetclaw.ts +49 -296
- package/src/types.ts +19 -0
- package/src/tools/executor.ts +0 -125
package/src/tools/tweetclaw.ts
CHANGED
|
@@ -1,300 +1,40 @@
|
|
|
1
1
|
import { createProxiedRequest } from '../request.js';
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
2
|
+
import { resolveCatalogRequest, specEndpoints } from './catalog.js';
|
|
3
|
+
import { errorResult, successResult } from './result.js';
|
|
4
|
+
import type { FetchFunction, RequestFunction, ToolResult, TweetclawParams } from '../types.js';
|
|
4
5
|
|
|
5
|
-
const EXECUTE_DESCRIPTION = `
|
|
6
|
+
const EXECUTE_DESCRIPTION = `Invoke one Xquik API endpoint from the bundled TweetClaw catalog.
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
method?: string; // default: 'GET'
|
|
13
|
-
body?: unknown;
|
|
14
|
-
query?: Record<string, string>;
|
|
15
|
-
}): Promise<unknown>;
|
|
16
|
-
};
|
|
17
|
-
declare const spec: { endpoints: EndpointInfo[] };
|
|
18
|
-
\`\`\`
|
|
8
|
+
Use "explore" first to find the endpoint, then call this tool with structured parameters:
|
|
9
|
+
- path: concrete /api/v1/... path
|
|
10
|
+
- method: GET, POST, PATCH, PUT, or DELETE
|
|
11
|
+
- query: query parameters as an object
|
|
12
|
+
- body: JSON request body
|
|
19
13
|
|
|
20
|
-
Auth is injected automatically
|
|
21
|
-
First use "explore" to find endpoints, then write code here to call them.
|
|
14
|
+
Auth is injected automatically. Never pass API keys, signing keys, passwords, cookies, or TOTP secrets.
|
|
22
15
|
|
|
23
16
|
## Important rules
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
### 2. Reply to a tweet
|
|
46
|
-
\`\`\`javascript
|
|
47
|
-
async () => {
|
|
48
|
-
return xquik.request('/api/v1/x/tweets', {
|
|
49
|
-
method: 'POST',
|
|
50
|
-
body: { account: '@myaccount', text: 'Great point!', reply_to_tweet_id: '1234567890' }
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
\`\`\`
|
|
54
|
-
|
|
55
|
-
### 3. Like, retweet, follow (follow requires user ID lookup)
|
|
56
|
-
\`\`\`javascript
|
|
57
|
-
async () => {
|
|
58
|
-
// Like a tweet (tweet ID in path)
|
|
59
|
-
await xquik.request('/api/v1/x/tweets/1234567890/like', {
|
|
60
|
-
method: 'POST', body: { account: '@myaccount' }
|
|
61
|
-
});
|
|
62
|
-
// Retweet
|
|
63
|
-
await xquik.request('/api/v1/x/tweets/1234567890/retweet', {
|
|
64
|
-
method: 'POST', body: { account: '@myaccount' }
|
|
65
|
-
});
|
|
66
|
-
// Follow - requires numeric user ID, look up first
|
|
67
|
-
const user = await xquik.request('/api/v1/x/users/elonmusk');
|
|
68
|
-
await xquik.request(\`/api/v1/x/users/\${user.id}/follow\`, {
|
|
69
|
-
method: 'POST', body: { account: '@myaccount' }
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
\`\`\`
|
|
73
|
-
|
|
74
|
-
### 4. Undo actions (unlike, unfollow, delete tweet)
|
|
75
|
-
\`\`\`javascript
|
|
76
|
-
async () => {
|
|
77
|
-
await xquik.request('/api/v1/x/tweets/1234567890/like', {
|
|
78
|
-
method: 'DELETE', body: { account: '@myaccount' }
|
|
79
|
-
});
|
|
80
|
-
await xquik.request('/api/v1/x/users/44196397/follow', {
|
|
81
|
-
method: 'DELETE', body: { account: '@myaccount' }
|
|
82
|
-
});
|
|
83
|
-
await xquik.request('/api/v1/x/tweets/1234567890', {
|
|
84
|
-
method: 'DELETE', body: { account: '@myaccount' }
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
\`\`\`
|
|
88
|
-
|
|
89
|
-
### 5. Send DM (uses recipient user ID in path)
|
|
90
|
-
\`\`\`javascript
|
|
91
|
-
async () => {
|
|
92
|
-
return xquik.request('/api/v1/x/dm/44196397', {
|
|
93
|
-
method: 'POST',
|
|
94
|
-
body: { account: '@myaccount', text: 'Hey, check this out!' }
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
\`\`\`
|
|
98
|
-
|
|
99
|
-
### 6. Upload media via URL and tweet with image
|
|
100
|
-
\`\`\`javascript
|
|
101
|
-
async () => {
|
|
102
|
-
const media = await xquik.request('/api/v1/x/media', {
|
|
103
|
-
method: 'POST',
|
|
104
|
-
body: { account: '@myaccount', url: 'https://example.com/photo.jpg' }
|
|
105
|
-
});
|
|
106
|
-
return xquik.request('/api/v1/x/tweets', {
|
|
107
|
-
method: 'POST',
|
|
108
|
-
body: { account: '@myaccount', text: 'Check this out!', media_ids: [media.mediaId] }
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
\`\`\`
|
|
112
|
-
|
|
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)
|
|
135
|
-
\`\`\`javascript
|
|
136
|
-
async () => {
|
|
137
|
-
// Use limit param for more than 20 results (max 200)
|
|
138
|
-
return xquik.request('/api/v1/x/tweets/search', {
|
|
139
|
-
query: { q: 'from:elonmusk', limit: '50' }
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
\`\`\`
|
|
143
|
-
|
|
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, quotes, retweets
|
|
163
|
-
const monitor = await xquik.request('/api/v1/monitors', {
|
|
164
|
-
method: 'POST',
|
|
165
|
-
body: { username: 'elonmusk', eventTypes: ['tweet.new', 'tweet.reply', 'tweet.quote', 'tweet.retweet'] }
|
|
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
|
-
// 23 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
|
-
// favoriters, user_likes, user_media
|
|
215
|
-
}
|
|
216
|
-
\`\`\`
|
|
217
|
-
|
|
218
|
-
### 13. Browse trending topics (FREE)
|
|
219
|
-
\`\`\`javascript
|
|
220
|
-
async () => {
|
|
221
|
-
return xquik.request('/api/v1/radar');
|
|
222
|
-
}
|
|
223
|
-
\`\`\`
|
|
224
|
-
|
|
225
|
-
### 14. Analyze a user's writing style
|
|
226
|
-
\`\`\`javascript
|
|
227
|
-
async () => {
|
|
228
|
-
// Returns cached style if available (free for all users)
|
|
229
|
-
// Auto-refreshes from X if cache is older than 7 days (subscription required)
|
|
230
|
-
return xquik.request('/api/v1/styles', {
|
|
231
|
-
method: 'POST',
|
|
232
|
-
body: { username: 'dbdevletbahceli' }
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
\`\`\`
|
|
236
|
-
|
|
237
|
-
### 15. Download media and get gallery link (Subscription required)
|
|
238
|
-
\`\`\`javascript
|
|
239
|
-
async () => {
|
|
240
|
-
// Returns galleryUrl only (shareable gallery page with all media)
|
|
241
|
-
return xquik.request('/api/v1/x/media/download', {
|
|
242
|
-
method: 'POST',
|
|
243
|
-
body: { tweetInput: '1234567890' } // tweet ID or full URL
|
|
244
|
-
});
|
|
245
|
-
}
|
|
246
|
-
\`\`\`
|
|
247
|
-
|
|
248
|
-
### 16. Community actions (create, join, leave)
|
|
249
|
-
\`\`\`javascript
|
|
250
|
-
async () => {
|
|
251
|
-
// Join a community
|
|
252
|
-
await xquik.request('/api/v1/x/communities/99999/join', {
|
|
253
|
-
method: 'POST', body: { account: '@myaccount' }
|
|
254
|
-
});
|
|
255
|
-
// Leave a community
|
|
256
|
-
await xquik.request('/api/v1/x/communities/99999/join', {
|
|
257
|
-
method: 'DELETE', body: { account: '@myaccount' }
|
|
258
|
-
});
|
|
259
|
-
}
|
|
260
|
-
\`\`\`
|
|
261
|
-
|
|
262
|
-
### 17. Subscribe (FREE - returns checkout URL)
|
|
263
|
-
\`\`\`javascript
|
|
264
|
-
async () => {
|
|
265
|
-
return xquik.request('/api/v1/subscribe', { method: 'POST' });
|
|
266
|
-
}
|
|
267
|
-
\`\`\`
|
|
268
|
-
|
|
269
|
-
### 18. Draft & optimize tweet text (3-step compose flow, FREE)
|
|
270
|
-
\`\`\`javascript
|
|
271
|
-
async () => {
|
|
272
|
-
// Use this ONLY when the user wants help WRITING tweet text.
|
|
273
|
-
// To SEND a tweet, use POST /api/v1/x/tweets instead.
|
|
274
|
-
// Step 1: Get algorithm data
|
|
275
|
-
const compose = await xquik.request('/api/v1/compose', {
|
|
276
|
-
method: 'POST',
|
|
277
|
-
body: { step: 'compose', topic: 'AI agents' }
|
|
278
|
-
});
|
|
279
|
-
return compose; // Returns contentRules, followUpQuestions, scorerWeights
|
|
280
|
-
// After user answers: call with body { step: 'refine', goal, tone, topic }
|
|
281
|
-
// After drafting: call with body { step: 'score', draft }
|
|
282
|
-
}
|
|
283
|
-
\`\`\`
|
|
284
|
-
|
|
285
|
-
## Cost
|
|
286
|
-
- 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/x/accounts, /api/v1/support/*
|
|
287
|
-
- MPP pay-per-use (no account/subscription needed, 32 endpoints): GET /api/v1/x/tweets/:id ($0.00015/call), GET /api/v1/x/tweets/search ($0.00015/tweet), GET /api/v1/x/tweets/:id/quotes ($0.00015/tweet), GET /api/v1/x/tweets/:id/replies ($0.00015/tweet), GET /api/v1/x/tweets/:id/retweeters ($0.00015/user), GET /api/v1/x/tweets/:id/favoriters ($0.00015/user), GET /api/v1/x/tweets/:id/thread ($0.00015/tweet), GET /api/v1/x/users/:username ($0.00015/call), GET /api/v1/x/users/:id/tweets ($0.00015/tweet), GET /api/v1/x/users/:id/likes ($0.00015/tweet), GET /api/v1/x/users/:id/media ($0.00015/tweet), GET /api/v1/x/followers/check ($0.00105/call), GET /api/v1/x/articles/:tweetId ($0.00105/call), POST /api/v1/x/media/download ($0.00015/media), GET /api/v1/trends ($0.00045/call), GET /api/v1/x/trends ($0.00045/call), GET /api/v1/x/communities/:id/info ($0.00015/call), GET /api/v1/x/communities/:id/members ($0.00015/user), GET /api/v1/x/communities/:id/moderators ($0.00015/user), GET /api/v1/x/communities/:id/tweets ($0.00015/tweet), GET /api/v1/x/communities/search ($0.00015/community), GET /api/v1/x/communities/tweets ($0.00015/tweet), GET /api/v1/x/lists/:id/followers ($0.00015/user), GET /api/v1/x/lists/:id/members ($0.00015/user), GET /api/v1/x/lists/:id/tweets ($0.00015/tweet), GET /api/v1/x/users/batch ($0.00015/user), GET /api/v1/x/users/search ($0.00015/user), GET /api/v1/x/users/:id/followers ($0.00015/user), GET /api/v1/x/users/:id/followers-you-know ($0.00015/user), GET /api/v1/x/users/:id/following ($0.00015/user), GET /api/v1/x/users/:id/mentions ($0.00015/tweet), GET /api/v1/x/users/:id/verified-followers ($0.00015/user)
|
|
288
|
-
- Subscription required: /api/v1/styles (X API refresh when cache >7 days), /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
|
|
289
|
-
- 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/users/:id/remove-follower, 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
|
|
290
|
-
- Do not skip requests based on assumed subscription status. The API returns a clear 402 error if a subscription is required, which is the correct signal to offer a checkout URL.
|
|
291
|
-
- MPP MODE: When configured with a signing key (no API key), the mppx SDK auto-handles 402 challenges by paying on-chain. Only the 32 MPP-eligible endpoints work in this mode.
|
|
292
|
-
|
|
293
|
-
## Error handling
|
|
294
|
-
- If response contains "subscription is inactive" or status 402, call POST /api/v1/subscribe to get checkout URL
|
|
295
|
-
- 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
|
|
296
|
-
- If a paid call fails, still use free data already fetched (radar, styles, compose). Never discard free data or fall back to web search
|
|
297
|
-
- API errors include status code and message`;
|
|
17
|
+
- Only endpoints listed in the bundled catalog can be invoked. Unknown paths are rejected.
|
|
18
|
+
- The plugin only calls the configured Xquik API base URL and only /api/v1 paths.
|
|
19
|
+
- Account connection, re-authentication, API-key administration, subscription checkout, credit top-up, and support-ticket actions are dashboard-only.
|
|
20
|
+
- TWEET ACTIONS: SENDING a tweet ("tweet this", "post this") uses POST /api/v1/x/tweets. DRAFTING a tweet ("help me write", "compose") uses the compose flow.
|
|
21
|
+
- WRITE ACTIONS: Show the exact endpoint and payload to the user before approval. All write-like calls trigger an OpenClaw approval prompt.
|
|
22
|
+
- MPP MODE: When configured with a signing key and no API key, only MPP-eligible read endpoints are allowed.
|
|
23
|
+
- CURRENT EVENTS: Use /api/v1/radar for curated trends.
|
|
24
|
+
|
|
25
|
+
## Example: Send a tweet
|
|
26
|
+
{
|
|
27
|
+
"path": "/api/v1/x/tweets",
|
|
28
|
+
"method": "POST",
|
|
29
|
+
"body": { "account": "@myaccount", "text": "Hello world!" }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
## Example: Search tweets
|
|
33
|
+
{
|
|
34
|
+
"path": "/api/v1/x/tweets/search",
|
|
35
|
+
"method": "GET",
|
|
36
|
+
"query": { "q": "AI agents", "limit": 50 }
|
|
37
|
+
}`;
|
|
298
38
|
|
|
299
39
|
const EXECUTION_TIMEOUT_MS = 30_000;
|
|
300
40
|
const MS_PER_SECOND = 1000;
|
|
@@ -302,18 +42,31 @@ const MS_PER_SECOND = 1000;
|
|
|
302
42
|
interface TweetclawOptions {
|
|
303
43
|
readonly apiKey: string;
|
|
304
44
|
readonly baseUrl: string;
|
|
305
|
-
readonly code: string;
|
|
306
45
|
readonly fetchFunction?: FetchFunction | undefined;
|
|
46
|
+
readonly mppMode?: boolean | undefined;
|
|
47
|
+
readonly params: Readonly<TweetclawParams>;
|
|
307
48
|
readonly timeoutMs?: number | undefined;
|
|
308
49
|
}
|
|
309
50
|
|
|
310
51
|
async function handleTweetclaw(options: Readonly<TweetclawOptions>): Promise<ToolResult> {
|
|
311
|
-
const {
|
|
52
|
+
const {
|
|
53
|
+
apiKey,
|
|
54
|
+
baseUrl,
|
|
55
|
+
fetchFunction,
|
|
56
|
+
mppMode = false,
|
|
57
|
+
params,
|
|
58
|
+
timeoutMs = EXECUTION_TIMEOUT_MS,
|
|
59
|
+
} = options;
|
|
60
|
+
|
|
312
61
|
try {
|
|
62
|
+
const requestInfo = resolveCatalogRequest(params, { mppMode });
|
|
313
63
|
const request: RequestFunction = createProxiedRequest(baseUrl, apiKey, fetchFunction);
|
|
314
|
-
|
|
315
64
|
const result: unknown = await Promise.race([
|
|
316
|
-
|
|
65
|
+
request(requestInfo.path, {
|
|
66
|
+
...(requestInfo.body === undefined ? {} : { body: requestInfo.body }),
|
|
67
|
+
method: requestInfo.method,
|
|
68
|
+
...(requestInfo.query === undefined ? {} : { query: requestInfo.query }),
|
|
69
|
+
}),
|
|
317
70
|
new Promise<never>((_resolve, reject) => {
|
|
318
71
|
setTimeout(() => {
|
|
319
72
|
reject(new Error(`Execution timed out after ${String(timeoutMs / MS_PER_SECOND)}s`));
|
|
@@ -327,4 +80,4 @@ async function handleTweetclaw(options: Readonly<TweetclawOptions>): Promise<Too
|
|
|
327
80
|
}
|
|
328
81
|
}
|
|
329
82
|
|
|
330
|
-
export { EXECUTE_DESCRIPTION, handleTweetclaw };
|
|
83
|
+
export { EXECUTE_DESCRIPTION, handleTweetclaw, specEndpoints };
|
package/src/types.ts
CHANGED
|
@@ -29,6 +29,23 @@ type RequestFunction = (path: string, options?: Readonly<RequestOptions>) => Pro
|
|
|
29
29
|
|
|
30
30
|
type FetchFunction = typeof fetch;
|
|
31
31
|
|
|
32
|
+
interface ExploreParams {
|
|
33
|
+
readonly category?: string;
|
|
34
|
+
readonly free?: boolean;
|
|
35
|
+
readonly limit?: number;
|
|
36
|
+
readonly method?: string;
|
|
37
|
+
readonly mpp?: boolean;
|
|
38
|
+
readonly path?: string;
|
|
39
|
+
readonly query?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface TweetclawParams {
|
|
43
|
+
readonly body?: unknown;
|
|
44
|
+
readonly method?: string;
|
|
45
|
+
readonly path: string;
|
|
46
|
+
readonly query?: Readonly<Record<string, boolean | number | string>>;
|
|
47
|
+
}
|
|
48
|
+
|
|
32
49
|
interface ToolResult {
|
|
33
50
|
readonly content: ReadonlyArray<{ readonly text: string; readonly type: 'text' }>;
|
|
34
51
|
readonly isError?: true;
|
|
@@ -52,9 +69,11 @@ export type {
|
|
|
52
69
|
EndpointInfo,
|
|
53
70
|
EndpointParameter,
|
|
54
71
|
EventPollerOptions,
|
|
72
|
+
ExploreParams,
|
|
55
73
|
FetchFunction,
|
|
56
74
|
PluginConfig,
|
|
57
75
|
RequestFunction,
|
|
58
76
|
RequestOptions,
|
|
77
|
+
TweetclawParams,
|
|
59
78
|
ToolResult,
|
|
60
79
|
};
|
package/src/tools/executor.ts
DELETED
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
import vm from 'node:vm';
|
|
2
|
-
import { API_SPEC } from '../api-spec.js';
|
|
3
|
-
import { truncateResponse } from '../truncate.js';
|
|
4
|
-
import type { ToolResult } from '../types.js';
|
|
5
|
-
|
|
6
|
-
const specEndpoints: ReadonlyArray<Readonly<Record<string, unknown>>> = API_SPEC
|
|
7
|
-
.filter((endpoint) => endpoint.agentProhibited !== true)
|
|
8
|
-
.map((endpoint): Readonly<Record<string, unknown>> => ({ ...endpoint }));
|
|
9
|
-
|
|
10
|
-
function extractErrorMessage(error: unknown): string {
|
|
11
|
-
if (error instanceof Error) {
|
|
12
|
-
return `${error.constructor.name}: ${error.message}`;
|
|
13
|
-
}
|
|
14
|
-
return String(error);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function isAsyncFunctionConstructor(
|
|
18
|
-
value: unknown,
|
|
19
|
-
): value is new (...parameters: readonly string[]) => (...parameters: readonly unknown[]) => Promise<unknown> {
|
|
20
|
-
return typeof value === 'function';
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function getConstructorFromPrototype(proto: unknown): unknown {
|
|
24
|
-
if (typeof proto !== 'object' || proto === null) {
|
|
25
|
-
return undefined;
|
|
26
|
-
}
|
|
27
|
-
return 'constructor' in proto ? proto.constructor : undefined;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function resolveAsyncFunctionConstructor(prototype?: unknown): new (
|
|
31
|
-
...parameters: readonly string[]
|
|
32
|
-
) => (...parameters: readonly unknown[]) => Promise<unknown> {
|
|
33
|
-
const asyncPrototype: unknown = prototype ?? Object.getPrototypeOf(async (): Promise<void> => {});
|
|
34
|
-
const candidate: unknown = getConstructorFromPrototype(asyncPrototype);
|
|
35
|
-
if (!isAsyncFunctionConstructor(candidate)) {
|
|
36
|
-
throw new Error('AsyncFunction constructor not found');
|
|
37
|
-
}
|
|
38
|
-
return candidate;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const BLOCKED_PROPS: ReadonlySet<string | symbol> = new Set(['constructor', '__proto__', 'prototype']);
|
|
42
|
-
|
|
43
|
-
function isBlockedProperty(property: string | symbol): boolean {
|
|
44
|
-
return BLOCKED_PROPS.has(property);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function wrapValue(value: unknown): unknown {
|
|
48
|
-
if (value === null || value === undefined) return value;
|
|
49
|
-
if (typeof value !== 'object' && typeof value !== 'function') return value;
|
|
50
|
-
return createSafeProxy(value);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
async function wrapAsync(promise: Promise<unknown>): Promise<unknown> {
|
|
54
|
-
const resolved: unknown = await promise;
|
|
55
|
-
return wrapValue(resolved);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function createCallableProxy(bound: (...a: readonly unknown[]) => unknown): unknown {
|
|
59
|
-
const handler: ProxyHandler<typeof bound> = {
|
|
60
|
-
apply(_target: typeof bound, _thisArgument: unknown, argumentsList: unknown[]): unknown {
|
|
61
|
-
const result: unknown = bound(...argumentsList);
|
|
62
|
-
if (result instanceof Promise) {
|
|
63
|
-
return wrapAsync(result);
|
|
64
|
-
}
|
|
65
|
-
return wrapValue(result);
|
|
66
|
-
},
|
|
67
|
-
get(_target: typeof bound, property: string | symbol): unknown {
|
|
68
|
-
if (isBlockedProperty(property)) return undefined;
|
|
69
|
-
return Reflect.get(_target, property);
|
|
70
|
-
},
|
|
71
|
-
};
|
|
72
|
-
return new Proxy(bound, handler);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function createSafeProxy(target: unknown): unknown {
|
|
76
|
-
if (target === null || target === undefined) return target;
|
|
77
|
-
if (typeof target !== 'object' && typeof target !== 'function') return target;
|
|
78
|
-
|
|
79
|
-
const handler: ProxyHandler<Record<string | symbol, unknown>> = {
|
|
80
|
-
get(t: Record<string | symbol, unknown>, property: string | symbol): unknown {
|
|
81
|
-
if (isBlockedProperty(property)) return undefined;
|
|
82
|
-
const value: unknown = Reflect.get(t, property);
|
|
83
|
-
if (typeof value === 'function') {
|
|
84
|
-
const bound: (...a: readonly unknown[]) => unknown = value.bind(t);
|
|
85
|
-
return createCallableProxy(bound);
|
|
86
|
-
}
|
|
87
|
-
return wrapValue(value);
|
|
88
|
-
},
|
|
89
|
-
};
|
|
90
|
-
return new Proxy(target as Record<string | symbol, unknown>, handler);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function runInSandbox(code: string, globals: Readonly<Record<string, unknown>>): unknown {
|
|
94
|
-
const rawContext: Record<string, unknown> = Object.create(null) as Record<string, unknown>;
|
|
95
|
-
for (const key of Object.keys(globals)) {
|
|
96
|
-
const value: unknown = globals[key];
|
|
97
|
-
const safeValue: unknown = typeof value === 'object' && value !== null
|
|
98
|
-
? createSafeProxy(value)
|
|
99
|
-
: value;
|
|
100
|
-
Reflect.set(rawContext, key, safeValue);
|
|
101
|
-
}
|
|
102
|
-
const context: vm.Context = vm.createContext(rawContext);
|
|
103
|
-
return vm.runInNewContext(`(${code})()`, context);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function successResult(content: unknown): ToolResult {
|
|
107
|
-
return { content: [{ text: truncateResponse(content), type: 'text' as const }] };
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function errorResult(error: unknown): ToolResult {
|
|
111
|
-
return { content: [{ text: extractErrorMessage(error), type: 'text' as const }], isError: true };
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export {
|
|
115
|
-
BLOCKED_PROPS,
|
|
116
|
-
createSafeProxy,
|
|
117
|
-
errorResult,
|
|
118
|
-
extractErrorMessage,
|
|
119
|
-
getConstructorFromPrototype,
|
|
120
|
-
resolveAsyncFunctionConstructor,
|
|
121
|
-
runInSandbox,
|
|
122
|
-
specEndpoints,
|
|
123
|
-
successResult,
|
|
124
|
-
wrapValue,
|
|
125
|
-
};
|