@clawnch/clawtomaton 0.7.0 → 0.8.1
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/dist/agent/index.d.ts.map +1 -1
- package/dist/agent/index.js +16 -1
- package/dist/agent/index.js.map +1 -1
- package/dist/agent/prompt.d.ts.map +1 -1
- package/dist/agent/prompt.js +28 -0
- package/dist/agent/prompt.js.map +1 -1
- package/dist/heartbeat/index.d.ts +24 -4
- package/dist/heartbeat/index.d.ts.map +1 -1
- package/dist/heartbeat/index.js +146 -6
- package/dist/heartbeat/index.js.map +1 -1
- package/dist/skills/check-balance.js +1 -1
- package/dist/skills/check-balance.js.map +1 -1
- package/dist/skills/check-price.d.ts.map +1 -1
- package/dist/skills/check-price.js +8 -4
- package/dist/skills/check-price.js.map +1 -1
- package/dist/skills/clawnx.d.ts +20 -4
- package/dist/skills/clawnx.d.ts.map +1 -1
- package/dist/skills/clawnx.js +718 -23
- package/dist/skills/clawnx.js.map +1 -1
- package/dist/skills/deploy.d.ts.map +1 -1
- package/dist/skills/deploy.js +10 -0
- package/dist/skills/deploy.js.map +1 -1
- package/dist/skills/hummingbot.d.ts +27 -0
- package/dist/skills/hummingbot.d.ts.map +1 -0
- package/dist/skills/hummingbot.js +457 -0
- package/dist/skills/hummingbot.js.map +1 -0
- package/dist/skills/index.d.ts +3 -1
- package/dist/skills/index.d.ts.map +1 -1
- package/dist/skills/index.js +9 -1
- package/dist/skills/index.js.map +1 -1
- package/dist/skills/portfolio.js +1 -1
- package/dist/skills/portfolio.js.map +1 -1
- package/dist/skills/services.d.ts +18 -0
- package/dist/skills/services.d.ts.map +1 -0
- package/dist/skills/services.js +409 -0
- package/dist/skills/services.js.map +1 -0
- package/package.json +2 -1
package/dist/skills/clawnx.js
CHANGED
|
@@ -1,28 +1,43 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Skill: clawnx — X/Twitter integration via ClawnX from @clawnch/
|
|
2
|
+
* Skill: clawnx — Full X/Twitter integration via ClawnX from @clawnch/clawnx.
|
|
3
|
+
*
|
|
4
|
+
* 45+ actions covering every surface of the X API v2:
|
|
5
|
+
*
|
|
6
|
+
* Content: post_tweet, post_thread, post_with_media, upload_media, delete_tweet, get_tweet, search
|
|
7
|
+
* Engagement: like, unlike, retweet, unretweet, bookmark, unbookmark, list_bookmarks, list_likes,
|
|
8
|
+
* liking_users, retweeted_by, quote_tweets
|
|
9
|
+
* Social: follow, unfollow, list_followers, list_following, block, unblock, mute, unmute,
|
|
10
|
+
* list_blocked, list_muted, get_user, search_users, lookup_users
|
|
11
|
+
* Timelines: get_timeline, home_timeline, get_mentions, get_my_profile
|
|
12
|
+
* DMs: send_dm, send_dm_to_conversation, list_dms, get_dm_conversation
|
|
13
|
+
* Threads: get_conversation
|
|
14
|
+
* Lists: create_list, delete_list, get_list, get_user_lists, add_list_member,
|
|
15
|
+
* remove_list_member, list_members, list_tweets
|
|
16
|
+
* Streaming: stream_start, stream_stop, stream_rules_set, stream_rules_get
|
|
17
|
+
* Orchestration: action_chain (multi-step with PREV_TWEET_ID substitution)
|
|
18
|
+
* Media: upload_media, post_with_media
|
|
19
|
+
*
|
|
20
|
+
* All X API logic lives in @clawnch/clawnx — this file is a thin Clawtomaton
|
|
21
|
+
* skill adapter that maps action names + params to ClawnX method calls.
|
|
3
22
|
*
|
|
4
|
-
* Lets the agent post tweets, search, engage, and monitor X/Twitter.
|
|
5
23
|
* Requires X API credentials in environment variables:
|
|
6
24
|
* X_API_KEY, X_API_SECRET, X_ACCESS_TOKEN, X_ACCESS_TOKEN_SECRET, X_BEARER_TOKEN
|
|
7
|
-
*
|
|
8
|
-
* ClawnX is lazy-loaded from @clawnch/sdk (optional dependency).
|
|
9
25
|
*/
|
|
10
26
|
/** Lazy singleton — created once on first use */
|
|
11
27
|
let cachedClient = null;
|
|
12
28
|
async function getClawnX() {
|
|
13
29
|
if (cachedClient)
|
|
14
30
|
return cachedClient;
|
|
15
|
-
let
|
|
31
|
+
let mod;
|
|
16
32
|
try {
|
|
17
|
-
// @ts-ignore — @clawnch/
|
|
18
|
-
|
|
33
|
+
// @ts-ignore — @clawnch/clawnx is an optional dependency
|
|
34
|
+
mod = await import('@clawnch/clawnx');
|
|
19
35
|
}
|
|
20
36
|
catch {
|
|
21
|
-
throw new Error('@clawnch/
|
|
37
|
+
throw new Error('@clawnch/clawnx not installed. Run: npm install @clawnch/clawnx\n' +
|
|
22
38
|
'Then set env vars: X_API_KEY, X_API_SECRET, X_ACCESS_TOKEN, X_ACCESS_TOKEN_SECRET, X_BEARER_TOKEN');
|
|
23
39
|
}
|
|
24
|
-
|
|
25
|
-
cachedClient = new sdk.ClawnX();
|
|
40
|
+
cachedClient = new mod.ClawnX();
|
|
26
41
|
return cachedClient;
|
|
27
42
|
}
|
|
28
43
|
function formatTweet(tweet) {
|
|
@@ -39,20 +54,28 @@ function formatUser(user) {
|
|
|
39
54
|
}
|
|
40
55
|
export const clawnxSkill = {
|
|
41
56
|
name: 'clawnx',
|
|
42
|
-
description: 'X/Twitter:
|
|
43
|
-
'
|
|
57
|
+
description: 'Full X/Twitter integration: 45+ actions covering content, engagement, social graph, DMs, lists, ' +
|
|
58
|
+
'streaming, media, and multi-step orchestration. Agent-native with structured metadata, action chaining, ' +
|
|
59
|
+
'and atomic thread posting. Requires X API credentials.',
|
|
44
60
|
parameters: [
|
|
45
61
|
{
|
|
46
62
|
name: 'action',
|
|
47
63
|
type: 'string',
|
|
48
|
-
description: 'Action:
|
|
49
|
-
'
|
|
64
|
+
description: 'Action to perform. Content: post_tweet, post_thread, post_with_media, upload_media, delete_tweet, get_tweet, search. ' +
|
|
65
|
+
'Engagement: like, unlike, retweet, unretweet, bookmark, unbookmark, list_bookmarks, list_likes, liking_users, retweeted_by, quote_tweets. ' +
|
|
66
|
+
'Social: follow, unfollow, list_followers, list_following, block, unblock, mute, unmute, list_blocked, list_muted, get_user, search_users, lookup_users. ' +
|
|
67
|
+
'Timelines: get_timeline, home_timeline, get_mentions, get_my_profile. ' +
|
|
68
|
+
'DMs: send_dm, send_dm_to_conversation, list_dms, get_dm_conversation. ' +
|
|
69
|
+
'Threads: get_conversation. ' +
|
|
70
|
+
'Lists: create_list, delete_list, get_list, get_user_lists, add_list_member, remove_list_member, list_members, list_tweets. ' +
|
|
71
|
+
'Streaming: stream_start, stream_stop, stream_rules_set, stream_rules_get. ' +
|
|
72
|
+
'Orchestration: action_chain.',
|
|
50
73
|
required: true,
|
|
51
74
|
},
|
|
52
75
|
{
|
|
53
76
|
name: 'text',
|
|
54
77
|
type: 'string',
|
|
55
|
-
description: 'Tweet text (for post_tweet). Max 280 chars.',
|
|
78
|
+
description: 'Tweet text (for post_tweet, post_with_media) or DM text (for send_dm). Max 280 chars for tweets.',
|
|
56
79
|
required: false,
|
|
57
80
|
},
|
|
58
81
|
{
|
|
@@ -64,31 +87,105 @@ export const clawnxSkill = {
|
|
|
64
87
|
{
|
|
65
88
|
name: 'query',
|
|
66
89
|
type: 'string',
|
|
67
|
-
description: 'Search query (for search
|
|
90
|
+
description: 'Search query (for search, search_users). Supports X query syntax ($TICKER, from:user, etc.).',
|
|
68
91
|
required: false,
|
|
69
92
|
},
|
|
70
93
|
{
|
|
71
94
|
name: 'tweet_id',
|
|
72
95
|
type: 'string',
|
|
73
|
-
description: 'Tweet ID or URL (for get_tweet, like, retweet, delete_tweet).',
|
|
96
|
+
description: 'Tweet ID or URL (for get_tweet, like, unlike, retweet, unretweet, delete_tweet, bookmark, unbookmark, get_conversation, liking_users, retweeted_by, quote_tweets).',
|
|
74
97
|
required: false,
|
|
75
98
|
},
|
|
76
99
|
{
|
|
77
100
|
name: 'username',
|
|
78
101
|
type: 'string',
|
|
79
|
-
description: 'Username without @ (for get_user, follow, get_timeline).',
|
|
102
|
+
description: 'Username without @ (for get_user, follow, unfollow, get_timeline, list_followers, list_following, block, unblock, mute, unmute, send_dm, list_likes, lookup_users).',
|
|
103
|
+
required: false,
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: 'usernames',
|
|
107
|
+
type: 'string',
|
|
108
|
+
description: 'Comma-separated usernames for lookup_users (e.g. "alice,bob,carol").',
|
|
80
109
|
required: false,
|
|
81
110
|
},
|
|
82
111
|
{
|
|
83
112
|
name: 'reply_to',
|
|
84
113
|
type: 'string',
|
|
85
|
-
description: 'Tweet ID or URL to reply to (for post_tweet).',
|
|
114
|
+
description: 'Tweet ID or URL to reply to (for post_tweet, post_with_media).',
|
|
86
115
|
required: false,
|
|
87
116
|
},
|
|
88
117
|
{
|
|
89
118
|
name: 'quote',
|
|
90
119
|
type: 'string',
|
|
91
|
-
description: 'Tweet ID or URL to quote (for post_tweet).',
|
|
120
|
+
description: 'Tweet ID or URL to quote (for post_tweet, post_with_media).',
|
|
121
|
+
required: false,
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
name: 'media_url',
|
|
125
|
+
type: 'string',
|
|
126
|
+
description: 'URL to media file for upload_media or post_with_media. Supports images, GIFs, videos.',
|
|
127
|
+
required: false,
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
name: 'media_type',
|
|
131
|
+
type: 'string',
|
|
132
|
+
description: 'MIME type override (e.g. "image/jpeg", "video/mp4"). Auto-detected if omitted.',
|
|
133
|
+
required: false,
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
name: 'count',
|
|
137
|
+
type: 'number',
|
|
138
|
+
description: 'Number of results to return for list/pagination actions (default: 20, max: 100).',
|
|
139
|
+
required: false,
|
|
140
|
+
default: 20,
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
name: 'list_id',
|
|
144
|
+
type: 'string',
|
|
145
|
+
description: 'List ID (for list actions: get_list, delete_list, add_list_member, remove_list_member, list_members, list_tweets).',
|
|
146
|
+
required: false,
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
name: 'list_name',
|
|
150
|
+
type: 'string',
|
|
151
|
+
description: 'List name (for create_list).',
|
|
152
|
+
required: false,
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
name: 'list_description',
|
|
156
|
+
type: 'string',
|
|
157
|
+
description: 'List description (for create_list).',
|
|
158
|
+
required: false,
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
name: 'list_private',
|
|
162
|
+
type: 'boolean',
|
|
163
|
+
description: 'Whether the list is private (for create_list). Default: false.',
|
|
164
|
+
required: false,
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
name: 'conversation_id',
|
|
168
|
+
type: 'string',
|
|
169
|
+
description: 'DM conversation ID (for send_dm_to_conversation, get_dm_conversation).',
|
|
170
|
+
required: false,
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
name: 'stream_rules',
|
|
174
|
+
type: 'string',
|
|
175
|
+
description: 'JSON array of stream filter rules for stream_rules_set. E.g. [{"value":"$CLAWNCH","tag":"token"}].',
|
|
176
|
+
required: false,
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
name: 'stream_duration',
|
|
180
|
+
type: 'number',
|
|
181
|
+
description: 'Stream duration in seconds for stream_start (default: 30, max: 300).',
|
|
182
|
+
required: false,
|
|
183
|
+
default: 30,
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
name: 'chain_steps',
|
|
187
|
+
type: 'string',
|
|
188
|
+
description: 'JSON array of action steps for action_chain. Each step is {action, ...params}. Use "PREV_TWEET_ID" to reference the previous step\'s tweet ID.',
|
|
92
189
|
required: false,
|
|
93
190
|
},
|
|
94
191
|
],
|
|
@@ -112,6 +209,7 @@ export const clawnxSkill = {
|
|
|
112
209
|
callId: '',
|
|
113
210
|
success: true,
|
|
114
211
|
result: `Posted tweet ${tweet.id}: ${tweet.text}\nhttps://x.com/i/status/${tweet.id}`,
|
|
212
|
+
metadata: { tweet_id: tweet.id },
|
|
115
213
|
};
|
|
116
214
|
}
|
|
117
215
|
case 'post_thread': {
|
|
@@ -124,11 +222,12 @@ export const clawnxSkill = {
|
|
|
124
222
|
}
|
|
125
223
|
const threadTweets = texts.map((t) => ({ text: t }));
|
|
126
224
|
const result = await x.postThread(threadTweets);
|
|
127
|
-
const ids = result.
|
|
225
|
+
const ids = result.tweetIds;
|
|
128
226
|
return {
|
|
129
227
|
callId: '',
|
|
130
228
|
success: true,
|
|
131
229
|
result: `Posted ${ids.length}-tweet thread.\nFirst: https://x.com/i/status/${ids[0]}\nIDs: ${ids.join(', ')}`,
|
|
230
|
+
metadata: { tweet_ids: ids, first_tweet_id: ids[0] },
|
|
132
231
|
};
|
|
133
232
|
}
|
|
134
233
|
case 'search': {
|
|
@@ -189,7 +288,7 @@ export const clawnxSkill = {
|
|
|
189
288
|
const username = params.username;
|
|
190
289
|
if (!username)
|
|
191
290
|
return { callId: '', success: false, result: null, error: 'username is required for get_timeline' };
|
|
192
|
-
const result = await x.getUserTimeline(
|
|
291
|
+
const result = await x.getUserTimeline(username, { maxResults: 10 });
|
|
193
292
|
const tweets = result.data ?? [];
|
|
194
293
|
if (tweets.length === 0)
|
|
195
294
|
return { callId: '', success: true, result: `No recent tweets from @${username}.` };
|
|
@@ -207,12 +306,608 @@ export const clawnxSkill = {
|
|
|
207
306
|
await x.deleteTweet(tweetId);
|
|
208
307
|
return { callId: '', success: true, result: `Deleted tweet ${tweetId}` };
|
|
209
308
|
}
|
|
309
|
+
// ==================================================================
|
|
310
|
+
// Undo engagement
|
|
311
|
+
// ==================================================================
|
|
312
|
+
case 'unlike': {
|
|
313
|
+
const tweetId = params.tweet_id;
|
|
314
|
+
if (!tweetId)
|
|
315
|
+
return { callId: '', success: false, result: null, error: 'tweet_id is required for unlike' };
|
|
316
|
+
await x.unlikeTweet(tweetId);
|
|
317
|
+
return { callId: '', success: true, result: `Unliked tweet ${tweetId}` };
|
|
318
|
+
}
|
|
319
|
+
case 'unretweet': {
|
|
320
|
+
const tweetId = params.tweet_id;
|
|
321
|
+
if (!tweetId)
|
|
322
|
+
return { callId: '', success: false, result: null, error: 'tweet_id is required for unretweet' };
|
|
323
|
+
await x.unretweet(tweetId);
|
|
324
|
+
return { callId: '', success: true, result: `Unretweeted ${tweetId}` };
|
|
325
|
+
}
|
|
326
|
+
case 'unfollow': {
|
|
327
|
+
const username = params.username;
|
|
328
|
+
if (!username)
|
|
329
|
+
return { callId: '', success: false, result: null, error: 'username is required for unfollow' };
|
|
330
|
+
await x.unfollowUser(username);
|
|
331
|
+
return { callId: '', success: true, result: `Unfollowed @${username}` };
|
|
332
|
+
}
|
|
333
|
+
// ==================================================================
|
|
334
|
+
// Bookmarks
|
|
335
|
+
// ==================================================================
|
|
336
|
+
case 'bookmark': {
|
|
337
|
+
const tweetId = params.tweet_id;
|
|
338
|
+
if (!tweetId)
|
|
339
|
+
return { callId: '', success: false, result: null, error: 'tweet_id is required for bookmark' };
|
|
340
|
+
await x.bookmarkTweet(tweetId);
|
|
341
|
+
return { callId: '', success: true, result: `Bookmarked tweet ${tweetId}` };
|
|
342
|
+
}
|
|
343
|
+
case 'unbookmark': {
|
|
344
|
+
const tweetId = params.tweet_id;
|
|
345
|
+
if (!tweetId)
|
|
346
|
+
return { callId: '', success: false, result: null, error: 'tweet_id is required for unbookmark' };
|
|
347
|
+
await x.unbookmarkTweet(tweetId);
|
|
348
|
+
return { callId: '', success: true, result: `Removed bookmark for tweet ${tweetId}` };
|
|
349
|
+
}
|
|
350
|
+
case 'list_bookmarks': {
|
|
351
|
+
const count = params.count ?? 20;
|
|
352
|
+
const result = await x.getBookmarks({ maxResults: Math.min(count, 100) });
|
|
353
|
+
const tweets = result.data ?? [];
|
|
354
|
+
if (tweets.length === 0)
|
|
355
|
+
return { callId: '', success: true, result: 'No bookmarks.' };
|
|
356
|
+
const lines = tweets.map(formatTweet);
|
|
357
|
+
return { callId: '', success: true, result: `${tweets.length} bookmarked tweets:\n${lines.join('\n')}` };
|
|
358
|
+
}
|
|
359
|
+
// ==================================================================
|
|
360
|
+
// Block / Mute
|
|
361
|
+
// ==================================================================
|
|
362
|
+
case 'block': {
|
|
363
|
+
const username = params.username;
|
|
364
|
+
if (!username)
|
|
365
|
+
return { callId: '', success: false, result: null, error: 'username is required for block' };
|
|
366
|
+
await x.blockUser(username);
|
|
367
|
+
return { callId: '', success: true, result: `Blocked @${username}` };
|
|
368
|
+
}
|
|
369
|
+
case 'unblock': {
|
|
370
|
+
const username = params.username;
|
|
371
|
+
if (!username)
|
|
372
|
+
return { callId: '', success: false, result: null, error: 'username is required for unblock' };
|
|
373
|
+
await x.unblockUser(username);
|
|
374
|
+
return { callId: '', success: true, result: `Unblocked @${username}` };
|
|
375
|
+
}
|
|
376
|
+
case 'mute': {
|
|
377
|
+
const username = params.username;
|
|
378
|
+
if (!username)
|
|
379
|
+
return { callId: '', success: false, result: null, error: 'username is required for mute' };
|
|
380
|
+
await x.muteUser(username);
|
|
381
|
+
return { callId: '', success: true, result: `Muted @${username}` };
|
|
382
|
+
}
|
|
383
|
+
case 'unmute': {
|
|
384
|
+
const username = params.username;
|
|
385
|
+
if (!username)
|
|
386
|
+
return { callId: '', success: false, result: null, error: 'username is required for unmute' };
|
|
387
|
+
await x.unmuteUser(username);
|
|
388
|
+
return { callId: '', success: true, result: `Unmuted @${username}` };
|
|
389
|
+
}
|
|
390
|
+
case 'list_blocked': {
|
|
391
|
+
const count = params.count ?? 20;
|
|
392
|
+
const result = await x.getBlockedUsers({ maxResults: Math.min(count, 100) });
|
|
393
|
+
const users = result.data ?? [];
|
|
394
|
+
if (users.length === 0)
|
|
395
|
+
return { callId: '', success: true, result: 'No blocked users.' };
|
|
396
|
+
const lines = users.map(formatUser);
|
|
397
|
+
return { callId: '', success: true, result: `${users.length} blocked users:\n${lines.join('\n')}` };
|
|
398
|
+
}
|
|
399
|
+
case 'list_muted': {
|
|
400
|
+
const count = params.count ?? 20;
|
|
401
|
+
const result = await x.getMutedUsers({ maxResults: Math.min(count, 100) });
|
|
402
|
+
const users = result.data ?? [];
|
|
403
|
+
if (users.length === 0)
|
|
404
|
+
return { callId: '', success: true, result: 'No muted users.' };
|
|
405
|
+
const lines = users.map(formatUser);
|
|
406
|
+
return { callId: '', success: true, result: `${users.length} muted users:\n${lines.join('\n')}` };
|
|
407
|
+
}
|
|
408
|
+
// ==================================================================
|
|
409
|
+
// Social graph
|
|
410
|
+
// ==================================================================
|
|
411
|
+
case 'list_followers': {
|
|
412
|
+
const username = params.username;
|
|
413
|
+
if (!username)
|
|
414
|
+
return { callId: '', success: false, result: null, error: 'username is required for list_followers' };
|
|
415
|
+
const count = params.count ?? 20;
|
|
416
|
+
const result = await x.getFollowers(username, { maxResults: Math.min(count, 100) });
|
|
417
|
+
const users = result.data ?? [];
|
|
418
|
+
if (users.length === 0)
|
|
419
|
+
return { callId: '', success: true, result: `@${username} has no followers (or account is private).` };
|
|
420
|
+
const lines = users.map(formatUser);
|
|
421
|
+
return {
|
|
422
|
+
callId: '',
|
|
423
|
+
success: true,
|
|
424
|
+
result: `@${username}'s followers (${users.length}):\n${lines.join('\n')}`,
|
|
425
|
+
metadata: { count: users.length, username },
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
case 'list_following': {
|
|
429
|
+
const username = params.username;
|
|
430
|
+
if (!username)
|
|
431
|
+
return { callId: '', success: false, result: null, error: 'username is required for list_following' };
|
|
432
|
+
const count = params.count ?? 20;
|
|
433
|
+
const result = await x.getFollowing(username, { maxResults: Math.min(count, 100) });
|
|
434
|
+
const users = result.data ?? [];
|
|
435
|
+
if (users.length === 0)
|
|
436
|
+
return { callId: '', success: true, result: `@${username} follows nobody.` };
|
|
437
|
+
const lines = users.map(formatUser);
|
|
438
|
+
return {
|
|
439
|
+
callId: '',
|
|
440
|
+
success: true,
|
|
441
|
+
result: `@${username} follows (${users.length}):\n${lines.join('\n')}`,
|
|
442
|
+
metadata: { count: users.length, username },
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
// ==================================================================
|
|
446
|
+
// Engagement lookups
|
|
447
|
+
// ==================================================================
|
|
448
|
+
case 'list_likes': {
|
|
449
|
+
const username = params.username;
|
|
450
|
+
if (!username)
|
|
451
|
+
return { callId: '', success: false, result: null, error: 'username is required for list_likes' };
|
|
452
|
+
const count = params.count ?? 20;
|
|
453
|
+
const result = await x.getLikedTweets(username, { maxResults: Math.min(count, 100) });
|
|
454
|
+
const tweets = result.data ?? [];
|
|
455
|
+
if (tweets.length === 0)
|
|
456
|
+
return { callId: '', success: true, result: `@${username} has no public likes.` };
|
|
457
|
+
const lines = tweets.map(formatTweet);
|
|
458
|
+
return { callId: '', success: true, result: `@${username}'s liked tweets (${tweets.length}):\n${lines.join('\n')}` };
|
|
459
|
+
}
|
|
460
|
+
case 'liking_users': {
|
|
461
|
+
const tweetId = params.tweet_id;
|
|
462
|
+
if (!tweetId)
|
|
463
|
+
return { callId: '', success: false, result: null, error: 'tweet_id is required for liking_users' };
|
|
464
|
+
const count = params.count ?? 20;
|
|
465
|
+
const result = await x.getLikingUsers(tweetId, { maxResults: Math.min(count, 100) });
|
|
466
|
+
const users = result.data ?? [];
|
|
467
|
+
if (users.length === 0)
|
|
468
|
+
return { callId: '', success: true, result: 'No users liked this tweet.' };
|
|
469
|
+
const lines = users.map(formatUser);
|
|
470
|
+
return {
|
|
471
|
+
callId: '',
|
|
472
|
+
success: true,
|
|
473
|
+
result: `Users who liked ${tweetId} (${users.length}):\n${lines.join('\n')}`,
|
|
474
|
+
metadata: { count: users.length },
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
case 'retweeted_by': {
|
|
478
|
+
const tweetId = params.tweet_id;
|
|
479
|
+
if (!tweetId)
|
|
480
|
+
return { callId: '', success: false, result: null, error: 'tweet_id is required for retweeted_by' };
|
|
481
|
+
const count = params.count ?? 20;
|
|
482
|
+
const result = await x.getRetweetedBy(tweetId, { maxResults: Math.min(count, 100) });
|
|
483
|
+
const users = result.data ?? [];
|
|
484
|
+
if (users.length === 0)
|
|
485
|
+
return { callId: '', success: true, result: 'No users retweeted this tweet.' };
|
|
486
|
+
const lines = users.map(formatUser);
|
|
487
|
+
return {
|
|
488
|
+
callId: '',
|
|
489
|
+
success: true,
|
|
490
|
+
result: `Users who retweeted ${tweetId} (${users.length}):\n${lines.join('\n')}`,
|
|
491
|
+
metadata: { count: users.length },
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
case 'quote_tweets': {
|
|
495
|
+
const tweetId = params.tweet_id;
|
|
496
|
+
if (!tweetId)
|
|
497
|
+
return { callId: '', success: false, result: null, error: 'tweet_id is required for quote_tweets' };
|
|
498
|
+
const count = params.count ?? 20;
|
|
499
|
+
const result = await x.getQuoteTweets(tweetId, { maxResults: Math.min(count, 100) });
|
|
500
|
+
const tweets = result.data ?? [];
|
|
501
|
+
if (tweets.length === 0)
|
|
502
|
+
return { callId: '', success: true, result: 'No quote tweets.' };
|
|
503
|
+
const lines = tweets.map(formatTweet);
|
|
504
|
+
return { callId: '', success: true, result: `Quote tweets of ${tweetId} (${tweets.length}):\n${lines.join('\n')}` };
|
|
505
|
+
}
|
|
506
|
+
// ==================================================================
|
|
507
|
+
// Home timeline
|
|
508
|
+
// ==================================================================
|
|
509
|
+
case 'home_timeline': {
|
|
510
|
+
const count = params.count ?? 20;
|
|
511
|
+
const result = await x.getHomeTimeline({ maxResults: Math.min(count, 100) });
|
|
512
|
+
const tweets = result.data ?? [];
|
|
513
|
+
if (tweets.length === 0)
|
|
514
|
+
return { callId: '', success: true, result: 'Home timeline is empty.' };
|
|
515
|
+
const lines = tweets.map(formatTweet);
|
|
516
|
+
return { callId: '', success: true, result: `Home timeline (${tweets.length} tweets):\n${lines.join('\n')}` };
|
|
517
|
+
}
|
|
518
|
+
// ==================================================================
|
|
519
|
+
// Tweet metrics (detailed)
|
|
520
|
+
// ==================================================================
|
|
521
|
+
case 'get_tweet_metrics': {
|
|
522
|
+
const tweetId = params.tweet_id;
|
|
523
|
+
if (!tweetId)
|
|
524
|
+
return { callId: '', success: false, result: null, error: 'tweet_id is required for get_tweet_metrics' };
|
|
525
|
+
const result = await x.getTweetMetrics(tweetId);
|
|
526
|
+
const t = result.data;
|
|
527
|
+
const m = t.public_metrics;
|
|
528
|
+
const lines = [
|
|
529
|
+
`Tweet ${t.id}: ${t.text?.substring(0, 100)}${(t.text?.length ?? 0) > 100 ? '...' : ''}`,
|
|
530
|
+
m ? ` Likes: ${m.like_count} Retweets: ${m.retweet_count} Replies: ${m.reply_count} Quotes: ${m.quote_count ?? 0} Impressions: ${m.impression_count ?? 'N/A'} Bookmarks: ${m.bookmark_count ?? 'N/A'}` : ' Metrics unavailable',
|
|
531
|
+
];
|
|
532
|
+
return { callId: '', success: true, result: lines.join('\n'), metadata: { metrics: m } };
|
|
533
|
+
}
|
|
534
|
+
// ==================================================================
|
|
535
|
+
// Conversation / thread retrieval
|
|
536
|
+
// ==================================================================
|
|
537
|
+
case 'get_conversation': {
|
|
538
|
+
const tweetId = params.tweet_id;
|
|
539
|
+
if (!tweetId)
|
|
540
|
+
return { callId: '', success: false, result: null, error: 'tweet_id is required for get_conversation' };
|
|
541
|
+
const count = params.count ?? 20;
|
|
542
|
+
const result = await x.getConversation(tweetId, { maxResults: Math.min(count, 100) });
|
|
543
|
+
const tweets = result.data ?? [];
|
|
544
|
+
if (tweets.length === 0)
|
|
545
|
+
return { callId: '', success: true, result: 'No conversation thread found.' };
|
|
546
|
+
const lines = tweets.map(formatTweet);
|
|
547
|
+
return { callId: '', success: true, result: `Conversation thread (${tweets.length} tweets):\n${lines.join('\n')}` };
|
|
548
|
+
}
|
|
549
|
+
// ==================================================================
|
|
550
|
+
// Direct Messages
|
|
551
|
+
// ==================================================================
|
|
552
|
+
case 'send_dm': {
|
|
553
|
+
const username = params.username;
|
|
554
|
+
const text = params.text;
|
|
555
|
+
if (!username)
|
|
556
|
+
return { callId: '', success: false, result: null, error: 'username is required for send_dm' };
|
|
557
|
+
if (!text)
|
|
558
|
+
return { callId: '', success: false, result: null, error: 'text is required for send_dm' };
|
|
559
|
+
const result = await x.sendDM(username, { text });
|
|
560
|
+
const eventId = result.data?.dm_event_id ?? result.data?.id ?? 'sent';
|
|
561
|
+
return {
|
|
562
|
+
callId: '',
|
|
563
|
+
success: true,
|
|
564
|
+
result: `DM sent to @${username}: "${text.substring(0, 60)}${text.length > 60 ? '...' : ''}"\nEvent: ${eventId}`,
|
|
565
|
+
metadata: { dm_event_id: eventId, recipient: username },
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
case 'send_dm_to_conversation': {
|
|
569
|
+
const conversationId = params.conversation_id;
|
|
570
|
+
const text = params.text;
|
|
571
|
+
if (!conversationId)
|
|
572
|
+
return { callId: '', success: false, result: null, error: 'conversation_id is required for send_dm_to_conversation' };
|
|
573
|
+
if (!text)
|
|
574
|
+
return { callId: '', success: false, result: null, error: 'text is required for send_dm_to_conversation' };
|
|
575
|
+
const result = await x.sendDMToConversation(conversationId, { text });
|
|
576
|
+
const eventId = result.data?.dm_event_id ?? result.data?.id ?? 'sent';
|
|
577
|
+
return {
|
|
578
|
+
callId: '',
|
|
579
|
+
success: true,
|
|
580
|
+
result: `DM sent to conversation ${conversationId}: "${text.substring(0, 60)}${text.length > 60 ? '...' : ''}"\nEvent: ${eventId}`,
|
|
581
|
+
metadata: { dm_event_id: eventId, conversation_id: conversationId },
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
case 'list_dms': {
|
|
585
|
+
const count = params.count ?? 20;
|
|
586
|
+
const result = await x.getDMEvents({ maxResults: Math.min(count, 100) });
|
|
587
|
+
const events = result.data ?? [];
|
|
588
|
+
if (events.length === 0)
|
|
589
|
+
return { callId: '', success: true, result: 'No recent DM events.' };
|
|
590
|
+
const lines = events.map((e) => {
|
|
591
|
+
const sender = e.sender_id ?? 'unknown';
|
|
592
|
+
const text = e.text ?? e.dm_conversation_id ?? '';
|
|
593
|
+
const time = e.created_at ? ` (${e.created_at})` : '';
|
|
594
|
+
return ` ${e.id}: [${sender}] ${text.substring(0, 80)}${text.length > 80 ? '...' : ''}${time}`;
|
|
595
|
+
});
|
|
596
|
+
return { callId: '', success: true, result: `${events.length} recent DM events:\n${lines.join('\n')}` };
|
|
597
|
+
}
|
|
598
|
+
case 'get_dm_conversation': {
|
|
599
|
+
const conversationId = params.conversation_id;
|
|
600
|
+
if (!conversationId)
|
|
601
|
+
return { callId: '', success: false, result: null, error: 'conversation_id is required for get_dm_conversation' };
|
|
602
|
+
const count = params.count ?? 20;
|
|
603
|
+
const result = await x.getDMConversation(conversationId, { maxResults: Math.min(count, 100) });
|
|
604
|
+
const events = result.data ?? [];
|
|
605
|
+
if (events.length === 0)
|
|
606
|
+
return { callId: '', success: true, result: 'No messages in this conversation.' };
|
|
607
|
+
const lines = events.map((e) => {
|
|
608
|
+
const sender = e.sender_id ?? 'unknown';
|
|
609
|
+
const text = e.text ?? '';
|
|
610
|
+
const time = e.created_at ? ` (${e.created_at})` : '';
|
|
611
|
+
return ` [${sender}] ${text}${time}`;
|
|
612
|
+
});
|
|
613
|
+
return { callId: '', success: true, result: `Conversation ${conversationId} (${events.length} messages):\n${lines.join('\n')}` };
|
|
614
|
+
}
|
|
615
|
+
// ==================================================================
|
|
616
|
+
// Lists management
|
|
617
|
+
// ==================================================================
|
|
618
|
+
case 'create_list': {
|
|
619
|
+
const name = params.list_name;
|
|
620
|
+
if (!name)
|
|
621
|
+
return { callId: '', success: false, result: null, error: 'list_name is required for create_list' };
|
|
622
|
+
const result = await x.createList({
|
|
623
|
+
name,
|
|
624
|
+
description: params.list_description ?? '',
|
|
625
|
+
private: params.list_private ?? false,
|
|
626
|
+
});
|
|
627
|
+
const list = result.data;
|
|
628
|
+
return {
|
|
629
|
+
callId: '',
|
|
630
|
+
success: true,
|
|
631
|
+
result: `List created: "${list.name}" (ID: ${list.id})`,
|
|
632
|
+
metadata: { list_id: list.id, list_name: list.name },
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
case 'delete_list': {
|
|
636
|
+
const listId = params.list_id;
|
|
637
|
+
if (!listId)
|
|
638
|
+
return { callId: '', success: false, result: null, error: 'list_id is required for delete_list' };
|
|
639
|
+
await x.deleteList(listId);
|
|
640
|
+
return { callId: '', success: true, result: `Deleted list ${listId}` };
|
|
641
|
+
}
|
|
642
|
+
case 'get_list': {
|
|
643
|
+
const listId = params.list_id;
|
|
644
|
+
if (!listId)
|
|
645
|
+
return { callId: '', success: false, result: null, error: 'list_id is required for get_list' };
|
|
646
|
+
const result = await x.getList(listId);
|
|
647
|
+
const l = result.data;
|
|
648
|
+
return { callId: '', success: true, result: `List "${l.name}" (${l.id}): ${l.description ?? 'no description'} | ${l.member_count ?? '?'} members | ${l.private ? 'private' : 'public'}` };
|
|
649
|
+
}
|
|
650
|
+
case 'get_user_lists': {
|
|
651
|
+
const username = params.username;
|
|
652
|
+
if (!username)
|
|
653
|
+
return { callId: '', success: false, result: null, error: 'username is required for get_user_lists' };
|
|
654
|
+
const count = params.count ?? 20;
|
|
655
|
+
const result = await x.getUserLists(username, { maxResults: Math.min(count, 100) });
|
|
656
|
+
const lists = result.data ?? [];
|
|
657
|
+
if (lists.length === 0)
|
|
658
|
+
return { callId: '', success: true, result: `@${username} has no public lists.` };
|
|
659
|
+
const lines = lists.map((l) => ` ${l.id}: "${l.name}" — ${l.member_count ?? '?'} members`);
|
|
660
|
+
return { callId: '', success: true, result: `@${username}'s lists (${lists.length}):\n${lines.join('\n')}` };
|
|
661
|
+
}
|
|
662
|
+
case 'add_list_member': {
|
|
663
|
+
const listId = params.list_id;
|
|
664
|
+
const username = params.username;
|
|
665
|
+
if (!listId)
|
|
666
|
+
return { callId: '', success: false, result: null, error: 'list_id is required for add_list_member' };
|
|
667
|
+
if (!username)
|
|
668
|
+
return { callId: '', success: false, result: null, error: 'username is required for add_list_member' };
|
|
669
|
+
await x.addListMember(listId, username);
|
|
670
|
+
return { callId: '', success: true, result: `Added @${username} to list ${listId}` };
|
|
671
|
+
}
|
|
672
|
+
case 'remove_list_member': {
|
|
673
|
+
const listId = params.list_id;
|
|
674
|
+
const username = params.username;
|
|
675
|
+
if (!listId)
|
|
676
|
+
return { callId: '', success: false, result: null, error: 'list_id is required for remove_list_member' };
|
|
677
|
+
if (!username)
|
|
678
|
+
return { callId: '', success: false, result: null, error: 'username is required for remove_list_member' };
|
|
679
|
+
await x.removeListMember(listId, username);
|
|
680
|
+
return { callId: '', success: true, result: `Removed @${username} from list ${listId}` };
|
|
681
|
+
}
|
|
682
|
+
case 'list_members': {
|
|
683
|
+
const listId = params.list_id;
|
|
684
|
+
if (!listId)
|
|
685
|
+
return { callId: '', success: false, result: null, error: 'list_id is required for list_members' };
|
|
686
|
+
const count = params.count ?? 20;
|
|
687
|
+
const result = await x.getListMembers(listId, { maxResults: Math.min(count, 100) });
|
|
688
|
+
const users = result.data ?? [];
|
|
689
|
+
if (users.length === 0)
|
|
690
|
+
return { callId: '', success: true, result: 'No members in this list.' };
|
|
691
|
+
const lines = users.map(formatUser);
|
|
692
|
+
return { callId: '', success: true, result: `List ${listId} members (${users.length}):\n${lines.join('\n')}` };
|
|
693
|
+
}
|
|
694
|
+
case 'list_tweets': {
|
|
695
|
+
const listId = params.list_id;
|
|
696
|
+
if (!listId)
|
|
697
|
+
return { callId: '', success: false, result: null, error: 'list_id is required for list_tweets' };
|
|
698
|
+
const count = params.count ?? 20;
|
|
699
|
+
const result = await x.getListTweets(listId, { maxResults: Math.min(count, 100) });
|
|
700
|
+
const tweets = result.data ?? [];
|
|
701
|
+
if (tweets.length === 0)
|
|
702
|
+
return { callId: '', success: true, result: 'No tweets in this list.' };
|
|
703
|
+
const lines = tweets.map(formatTweet);
|
|
704
|
+
return { callId: '', success: true, result: `List ${listId} tweets (${tweets.length}):\n${lines.join('\n')}` };
|
|
705
|
+
}
|
|
706
|
+
// ==================================================================
|
|
707
|
+
// User lookup & search
|
|
708
|
+
// ==================================================================
|
|
709
|
+
case 'search_users': {
|
|
710
|
+
const query = params.query;
|
|
711
|
+
if (!query)
|
|
712
|
+
return { callId: '', success: false, result: null, error: 'query is required for search_users' };
|
|
713
|
+
const count = params.count ?? 20;
|
|
714
|
+
const result = await x.searchUsers(query, { maxResults: Math.min(count, 100) });
|
|
715
|
+
const users = result.data ?? [];
|
|
716
|
+
if (users.length === 0)
|
|
717
|
+
return { callId: '', success: true, result: `No users match "${query}".` };
|
|
718
|
+
const lines = users.map(formatUser);
|
|
719
|
+
return { callId: '', success: true, result: `Users matching "${query}" (${users.length}):\n${lines.join('\n')}` };
|
|
720
|
+
}
|
|
721
|
+
case 'lookup_users': {
|
|
722
|
+
const usernamesStr = params.usernames;
|
|
723
|
+
if (!usernamesStr)
|
|
724
|
+
return { callId: '', success: false, result: null, error: 'usernames (comma-separated) is required for lookup_users' };
|
|
725
|
+
const usernames = usernamesStr.split(',').map(u => u.trim()).filter(Boolean);
|
|
726
|
+
if (usernames.length === 0)
|
|
727
|
+
return { callId: '', success: false, result: null, error: 'No valid usernames provided' };
|
|
728
|
+
const result = await x.getUsersByUsernames(usernames);
|
|
729
|
+
const users = result.data ?? [];
|
|
730
|
+
if (users.length === 0)
|
|
731
|
+
return { callId: '', success: true, result: 'No users found.' };
|
|
732
|
+
const lines = users.map(formatUser);
|
|
733
|
+
return { callId: '', success: true, result: `${users.length} users:\n${lines.join('\n')}` };
|
|
734
|
+
}
|
|
735
|
+
// ==================================================================
|
|
736
|
+
// Media upload
|
|
737
|
+
// ==================================================================
|
|
738
|
+
case 'upload_media': {
|
|
739
|
+
const mediaUrl = params.media_url;
|
|
740
|
+
if (!mediaUrl)
|
|
741
|
+
return { callId: '', success: false, result: null, error: 'media_url is required for upload_media' };
|
|
742
|
+
const result = await x.uploadMediaFromUrl({
|
|
743
|
+
url: mediaUrl,
|
|
744
|
+
mimeType: params.media_type,
|
|
745
|
+
});
|
|
746
|
+
return {
|
|
747
|
+
callId: '',
|
|
748
|
+
success: true,
|
|
749
|
+
result: `Media uploaded. media_id: ${result.media_id_string}\nUse this ID with post_with_media or post_tweet.`,
|
|
750
|
+
metadata: { media_id: result.media_id_string },
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
case 'post_with_media': {
|
|
754
|
+
const text = params.text;
|
|
755
|
+
const mediaUrl = params.media_url;
|
|
756
|
+
if (!text)
|
|
757
|
+
return { callId: '', success: false, result: null, error: 'text is required for post_with_media' };
|
|
758
|
+
if (!mediaUrl)
|
|
759
|
+
return { callId: '', success: false, result: null, error: 'media_url is required for post_with_media' };
|
|
760
|
+
// Upload media first
|
|
761
|
+
const media = await x.uploadMediaFromUrl({
|
|
762
|
+
url: mediaUrl,
|
|
763
|
+
mimeType: params.media_type,
|
|
764
|
+
});
|
|
765
|
+
// Post tweet with media
|
|
766
|
+
const opts = { text, mediaIds: [media.media_id_string] };
|
|
767
|
+
if (params.reply_to)
|
|
768
|
+
opts.replyTo = params.reply_to;
|
|
769
|
+
if (params.quote)
|
|
770
|
+
opts.quoteTweetId = params.quote;
|
|
771
|
+
const result = await x.postTweet(opts);
|
|
772
|
+
const tweet = result.data;
|
|
773
|
+
return {
|
|
774
|
+
callId: '',
|
|
775
|
+
success: true,
|
|
776
|
+
result: `Posted tweet with media ${tweet.id}: ${tweet.text}\nhttps://x.com/i/status/${tweet.id}`,
|
|
777
|
+
metadata: { tweet_id: tweet.id, media_id: media.media_id_string },
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
// ==================================================================
|
|
781
|
+
// New: Streaming
|
|
782
|
+
// ==================================================================
|
|
783
|
+
case 'stream_start': {
|
|
784
|
+
const durationSec = Math.min(params.stream_duration ?? 30, 300);
|
|
785
|
+
// Stop any active stream first
|
|
786
|
+
x.stopStream();
|
|
787
|
+
const tweets = await x.streamFiltered({
|
|
788
|
+
durationMs: durationSec * 1000,
|
|
789
|
+
maxTweets: 50,
|
|
790
|
+
});
|
|
791
|
+
if (tweets.length === 0) {
|
|
792
|
+
return { callId: '', success: true, result: 'Stream completed. No tweets matched your filter rules. Set rules with stream_rules_set first.' };
|
|
793
|
+
}
|
|
794
|
+
const lines = tweets.map(formatTweet);
|
|
795
|
+
return {
|
|
796
|
+
callId: '',
|
|
797
|
+
success: true,
|
|
798
|
+
result: `Streamed ${tweets.length} tweets in ${durationSec}s:\n${lines.join('\n')}`,
|
|
799
|
+
metadata: { tweet_count: tweets.length, duration_sec: durationSec },
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
case 'stream_stop': {
|
|
803
|
+
x.stopStream();
|
|
804
|
+
return { callId: '', success: true, result: 'Stream stopped.' };
|
|
805
|
+
}
|
|
806
|
+
case 'stream_rules_set': {
|
|
807
|
+
const rulesJson = params.stream_rules;
|
|
808
|
+
if (!rulesJson)
|
|
809
|
+
return { callId: '', success: false, result: null, error: 'stream_rules (JSON array) is required for stream_rules_set' };
|
|
810
|
+
let rules;
|
|
811
|
+
try {
|
|
812
|
+
rules = JSON.parse(rulesJson);
|
|
813
|
+
}
|
|
814
|
+
catch {
|
|
815
|
+
return { callId: '', success: false, result: null, error: 'stream_rules is not valid JSON. Must be a JSON array of {value, tag?} objects.' };
|
|
816
|
+
}
|
|
817
|
+
if (!Array.isArray(rules) || rules.length === 0) {
|
|
818
|
+
return { callId: '', success: false, result: null, error: 'stream_rules must be a non-empty JSON array of {value, tag?} objects' };
|
|
819
|
+
}
|
|
820
|
+
const result = await x.setStreamRules(rules);
|
|
821
|
+
const added = result.length ?? 0;
|
|
822
|
+
return {
|
|
823
|
+
callId: '',
|
|
824
|
+
success: true,
|
|
825
|
+
result: `Stream rules updated. ${added} rules active.\nRules: ${rules.map(r => `"${r.value}"${r.tag ? ` (${r.tag})` : ''}`).join(', ')}`,
|
|
826
|
+
metadata: { rules_count: added },
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
case 'stream_rules_get': {
|
|
830
|
+
const rules = await x.getStreamRules();
|
|
831
|
+
if (rules.length === 0) {
|
|
832
|
+
return { callId: '', success: true, result: 'No stream filter rules configured. Use stream_rules_set to add rules.' };
|
|
833
|
+
}
|
|
834
|
+
const lines = rules.map((r) => ` ${r.id}: "${r.value}"${r.tag ? ` (${r.tag})` : ''}`);
|
|
835
|
+
return { callId: '', success: true, result: `${rules.length} active stream rules:\n${lines.join('\n')}` };
|
|
836
|
+
}
|
|
837
|
+
// ==================================================================
|
|
838
|
+
// New: Action chaining
|
|
839
|
+
// ==================================================================
|
|
840
|
+
case 'action_chain': {
|
|
841
|
+
const stepsJson = params.chain_steps;
|
|
842
|
+
if (!stepsJson)
|
|
843
|
+
return { callId: '', success: false, result: null, error: 'chain_steps (JSON array) is required for action_chain' };
|
|
844
|
+
let steps;
|
|
845
|
+
try {
|
|
846
|
+
steps = JSON.parse(stepsJson);
|
|
847
|
+
}
|
|
848
|
+
catch {
|
|
849
|
+
return { callId: '', success: false, result: null, error: 'chain_steps is not valid JSON. Must be a JSON array of action objects.' };
|
|
850
|
+
}
|
|
851
|
+
if (!Array.isArray(steps) || steps.length === 0) {
|
|
852
|
+
return { callId: '', success: false, result: null, error: 'chain_steps must be a non-empty JSON array of action objects' };
|
|
853
|
+
}
|
|
854
|
+
const results = [];
|
|
855
|
+
let prevTweetId = null;
|
|
856
|
+
for (let i = 0; i < steps.length; i++) {
|
|
857
|
+
const step = { ...steps[i] };
|
|
858
|
+
// Replace PREV_TWEET_ID placeholder with actual tweet ID from previous step
|
|
859
|
+
for (const [key, val] of Object.entries(step)) {
|
|
860
|
+
if (val === 'PREV_TWEET_ID') {
|
|
861
|
+
if (!prevTweetId) {
|
|
862
|
+
results.push(`Step ${i + 1} (${step.action}): FAILED — references PREV_TWEET_ID but no previous step produced a tweet ID`);
|
|
863
|
+
return {
|
|
864
|
+
callId: '',
|
|
865
|
+
success: false,
|
|
866
|
+
result: `Action chain halted at step ${i + 1}:\n${results.join('\n')}`,
|
|
867
|
+
error: `Step ${i + 1} references PREV_TWEET_ID but no previous step produced a tweet ID`,
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
step[key] = prevTweetId;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
// Recursively execute each step through this same skill
|
|
874
|
+
const stepResult = await clawnxSkill.execute(step, {});
|
|
875
|
+
results.push(`Step ${i + 1} (${step.action}): ${stepResult.success ? 'OK' : 'FAILED'} — ${stepResult.result ?? stepResult.error}`);
|
|
876
|
+
// Extract tweet ID for chaining
|
|
877
|
+
if (stepResult.metadata?.tweet_id) {
|
|
878
|
+
prevTweetId = stepResult.metadata.tweet_id;
|
|
879
|
+
}
|
|
880
|
+
else if (stepResult.metadata?.first_tweet_id) {
|
|
881
|
+
prevTweetId = stepResult.metadata.first_tweet_id;
|
|
882
|
+
}
|
|
883
|
+
// Stop chain on failure
|
|
884
|
+
if (!stepResult.success) {
|
|
885
|
+
results.push(`Chain halted at step ${i + 1}.`);
|
|
886
|
+
break;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
return {
|
|
890
|
+
callId: '',
|
|
891
|
+
success: true,
|
|
892
|
+
result: `Action chain completed (${steps.length} steps):\n${results.join('\n')}`,
|
|
893
|
+
metadata: { steps_executed: results.length, last_tweet_id: prevTweetId },
|
|
894
|
+
};
|
|
895
|
+
}
|
|
210
896
|
default:
|
|
211
897
|
return {
|
|
212
898
|
callId: '',
|
|
213
899
|
success: false,
|
|
214
900
|
result: null,
|
|
215
|
-
error: `Unknown ClawnX action: "${action}". Valid:
|
|
901
|
+
error: `Unknown ClawnX action: "${action}". Valid actions: ` +
|
|
902
|
+
'Content: post_tweet, post_thread, post_with_media, upload_media, delete_tweet, get_tweet, search. ' +
|
|
903
|
+
'Engagement: like, unlike, retweet, unretweet, bookmark, unbookmark, list_bookmarks, list_likes, liking_users, retweeted_by, quote_tweets. ' +
|
|
904
|
+
'Social: follow, unfollow, list_followers, list_following, block, unblock, mute, unmute, list_blocked, list_muted, get_user, search_users, lookup_users. ' +
|
|
905
|
+
'Timelines: get_timeline, home_timeline, get_mentions, get_my_profile, get_tweet_metrics. ' +
|
|
906
|
+
'DMs: send_dm, send_dm_to_conversation, list_dms, get_dm_conversation. ' +
|
|
907
|
+
'Threads: get_conversation. ' +
|
|
908
|
+
'Lists: create_list, delete_list, get_list, get_user_lists, add_list_member, remove_list_member, list_members, list_tweets. ' +
|
|
909
|
+
'Streaming: stream_start, stream_stop, stream_rules_set, stream_rules_get. ' +
|
|
910
|
+
'Orchestration: action_chain.',
|
|
216
911
|
};
|
|
217
912
|
}
|
|
218
913
|
}
|