@xquik/tweetclaw 1.0.0

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