@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.
Files changed (53) hide show
  1. package/README.md +25 -14
  2. package/dist/api-spec.d.ts +3 -0
  3. package/dist/api-spec.js +1427 -0
  4. package/dist/api-spec.js.map +1 -0
  5. package/dist/commands/xstatus.d.ts +17 -0
  6. package/dist/commands/xstatus.js +52 -0
  7. package/dist/commands/xstatus.js.map +1 -0
  8. package/dist/commands/xtrends.d.ts +16 -0
  9. package/dist/commands/xtrends.js +39 -0
  10. package/dist/commands/xtrends.js.map +1 -0
  11. package/dist/index.d.ts +107 -0
  12. package/dist/index.js +249 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/mpp.d.ts +7 -0
  15. package/dist/mpp.js +39 -0
  16. package/dist/mpp.js.map +1 -0
  17. package/dist/request.d.ts +7 -0
  18. package/dist/request.js +88 -0
  19. package/dist/request.js.map +1 -0
  20. package/dist/services/event-poller.d.ts +7 -0
  21. package/dist/services/event-poller.js +69 -0
  22. package/dist/services/event-poller.js.map +1 -0
  23. package/dist/tools/catalog.d.ts +17 -0
  24. package/dist/tools/catalog.js +110 -0
  25. package/dist/tools/catalog.js.map +1 -0
  26. package/dist/tools/explore.d.ts +5 -0
  27. package/dist/tools/explore.js +28 -0
  28. package/dist/tools/explore.js.map +1 -0
  29. package/dist/tools/result.d.ts +5 -0
  30. package/dist/tools/result.js +15 -0
  31. package/dist/tools/result.js.map +1 -0
  32. package/dist/tools/tweetclaw.d.ts +13 -0
  33. package/dist/tools/tweetclaw.js +62 -0
  34. package/dist/tools/tweetclaw.js.map +1 -0
  35. package/dist/truncate.d.ts +3 -0
  36. package/dist/truncate.js +25 -0
  37. package/dist/truncate.js.map +1 -0
  38. package/dist/types.d.ts +64 -0
  39. package/dist/types.js +2 -0
  40. package/dist/types.js.map +1 -0
  41. package/openclaw.plugin.json +7 -8
  42. package/package.json +17 -17
  43. package/skills/tweetclaw/SKILL.md +37 -44
  44. package/src/api-spec.ts +491 -12
  45. package/src/index.ts +212 -10
  46. package/src/mpp.ts +9 -11
  47. package/src/request.ts +27 -2
  48. package/src/tools/catalog.ts +145 -0
  49. package/src/tools/explore.ts +18 -44
  50. package/src/tools/result.ts +19 -0
  51. package/src/tools/tweetclaw.ts +49 -296
  52. package/src/types.ts +19 -0
  53. package/src/tools/executor.ts +0 -125
@@ -1,300 +1,40 @@
1
1
  import { createProxiedRequest } from '../request.js';
2
- import { errorResult, runInSandbox, specEndpoints, successResult } from './executor.js';
3
- import type { FetchFunction, RequestFunction, ToolResult } from '../types.js';
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 = `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
+ const EXECUTE_DESCRIPTION = `Invoke one Xquik API endpoint from the bundled TweetClaw catalog.
6
7
 
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
- \`\`\`
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 - never pass API keys.
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
- - 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
-
30
- ## Workflows
31
-
32
- ### 1. Send a tweet (Subscription required)
33
- \`\`\`javascript
34
- async () => {
35
- // First, find connected accounts
36
- const { accounts } = await xquik.request('/api/v1/x/accounts');
37
- // Post the tweet directly
38
- return xquik.request('/api/v1/x/tweets', {
39
- method: 'POST',
40
- body: { account: accounts[0].xUsername, text: 'Hello world!' }
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 { apiKey, baseUrl, code, fetchFunction, timeoutMs = EXECUTION_TIMEOUT_MS } = options;
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
- runInSandbox(code, { xquik: { request }, spec: { endpoints: specEndpoints } }),
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
  };
@@ -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
- };