solana-traderclaw 1.0.48 → 1.0.50
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/{chunk-NZTB624I.js → chunk-C24QA3MQ.js} +1 -1
- package/dist/{chunk-CMZLPU3Z.js → chunk-JO3BXAUQ.js} +0 -4
- package/dist/index.js +121 -25
- package/dist/src/intelligence-lab.js +2 -2
- package/dist/src/runtime-layout.js +1 -1
- package/lib/x-client.mjs +75 -7
- package/lib/x-tools.mjs +43 -16
- package/openclaw.plugin.json +17 -0
- package/package.json +3 -7
- package/skills/solana-trader/SKILL.md +5 -1
- package/skills/solana-trader/refs/x-credentials.md +165 -0
- package/skills/solana-trader/refs/x-journal.md +62 -0
- package/bin/gateway-persistence-linux.mjs +0 -275
- package/bin/installer-step-engine.mjs +0 -2029
- package/bin/llm-model-preference.mjs +0 -229
- package/bin/openclaw-trader.mjs +0 -3146
- package/bin/traderclaw.cjs +0 -13
- package/dist/chunk-3RG5ZIWI.js +0 -10
- package/dist/chunk-F3UKCPA4.js +0 -377
- package/dist/chunk-GYXPEC7O.js +0 -454
- package/dist/chunk-IYD3TCSE.js +0 -366
- package/dist/chunk-YBURTADE.js +0 -169
|
@@ -6,10 +6,6 @@ function resolveWorkspaceRoot(configOverride) {
|
|
|
6
6
|
if (configOverride && configOverride.trim()) {
|
|
7
7
|
return configOverride.trim().replace(/^~/, homedir());
|
|
8
8
|
}
|
|
9
|
-
const envDir = process.env.OPENCLAW_WORKSPACE_DIR;
|
|
10
|
-
if (envDir && envDir.trim()) {
|
|
11
|
-
return envDir.trim().replace(/^~/, homedir());
|
|
12
|
-
}
|
|
13
9
|
return join(homedir(), ".openclaw", "workspace");
|
|
14
10
|
}
|
|
15
11
|
function resolveMemoryDir(workspaceRoot) {
|
package/dist/index.js
CHANGED
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
} from "./chunk-T4YWGIIR.js";
|
|
18
18
|
import {
|
|
19
19
|
IntelligenceLab
|
|
20
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-C24QA3MQ.js";
|
|
21
21
|
import {
|
|
22
22
|
scrubUntrustedText
|
|
23
23
|
} from "./chunk-AI6MTHUN.js";
|
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
generateStateMd,
|
|
33
33
|
resolveMemoryDir,
|
|
34
34
|
resolveWorkspaceRoot
|
|
35
|
-
} from "./chunk-
|
|
35
|
+
} from "./chunk-JO3BXAUQ.js";
|
|
36
36
|
|
|
37
37
|
// index.ts
|
|
38
38
|
import { Type } from "@sinclair/typebox";
|
|
@@ -125,9 +125,10 @@ async function xApiFetch(method, endpoint, credentials, { body = null, queryPara
|
|
|
125
125
|
};
|
|
126
126
|
}
|
|
127
127
|
if (response.status === 403) {
|
|
128
|
+
const isWrite = method === "POST" || method === "PUT" || method === "DELETE";
|
|
128
129
|
return {
|
|
129
130
|
ok: false,
|
|
130
|
-
error: "Forbidden (403). This read endpoint requires a paid X API tier (pay-as-you-go or Basic).",
|
|
131
|
+
error: isWrite ? "Forbidden (403). X rejected this write request. Check that App permissions are set to Read+Write in the X developer portal and regenerate your access tokens after any permission change." : "Forbidden (403). This read endpoint requires a paid X API tier (pay-as-you-go or Basic). This does NOT affect posting \u2014 x_post_tweet and x_reply_tweet still work on Free tier.",
|
|
131
132
|
status: 403,
|
|
132
133
|
data: responseData
|
|
133
134
|
};
|
|
@@ -146,6 +147,62 @@ async function xApiFetch(method, endpoint, credentials, { body = null, queryPara
|
|
|
146
147
|
rateLimitRemaining: rateLimitRemaining ? parseInt(rateLimitRemaining) : void 0
|
|
147
148
|
};
|
|
148
149
|
}
|
|
150
|
+
function validateTweetText(text) {
|
|
151
|
+
if (!text || typeof text !== "string") {
|
|
152
|
+
return { valid: false, error: "Tweet text is required and must be a non-empty string." };
|
|
153
|
+
}
|
|
154
|
+
const trimmed = text.trim();
|
|
155
|
+
if (trimmed.length === 0) {
|
|
156
|
+
return { valid: false, error: "Tweet text cannot be empty." };
|
|
157
|
+
}
|
|
158
|
+
if (trimmed.length > MAX_TWEET_LENGTH) {
|
|
159
|
+
return { valid: false, error: `Tweet exceeds ${MAX_TWEET_LENGTH} characters (got ${trimmed.length}). Shorten the text.` };
|
|
160
|
+
}
|
|
161
|
+
return { valid: true, text: trimmed };
|
|
162
|
+
}
|
|
163
|
+
async function postTweet(credentials, text) {
|
|
164
|
+
const validation = validateTweetText(text);
|
|
165
|
+
if (!validation.valid) return { ok: false, error: validation.error };
|
|
166
|
+
const result = await xApiFetch("POST", "/tweets", credentials, {
|
|
167
|
+
body: { text: validation.text }
|
|
168
|
+
});
|
|
169
|
+
if (result.ok && result.data?.data?.id) {
|
|
170
|
+
const tweetId = result.data.data.id;
|
|
171
|
+
const username = credentials.username || "unknown";
|
|
172
|
+
return {
|
|
173
|
+
ok: true,
|
|
174
|
+
tweetId,
|
|
175
|
+
tweetUrl: `https://x.com/${username}/status/${tweetId}`,
|
|
176
|
+
text: validation.text
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
return result;
|
|
180
|
+
}
|
|
181
|
+
async function replyToTweet(credentials, tweetId, text) {
|
|
182
|
+
const validation = validateTweetText(text);
|
|
183
|
+
if (!validation.valid) return { ok: false, error: validation.error };
|
|
184
|
+
if (!tweetId || typeof tweetId !== "string") {
|
|
185
|
+
return { ok: false, error: "tweetId is required to reply." };
|
|
186
|
+
}
|
|
187
|
+
const result = await xApiFetch("POST", "/tweets", credentials, {
|
|
188
|
+
body: {
|
|
189
|
+
text: validation.text,
|
|
190
|
+
reply: { in_reply_to_tweet_id: tweetId }
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
if (result.ok && result.data?.data?.id) {
|
|
194
|
+
const replyId = result.data.data.id;
|
|
195
|
+
const username = credentials.username || "unknown";
|
|
196
|
+
return {
|
|
197
|
+
ok: true,
|
|
198
|
+
replyId,
|
|
199
|
+
replyUrl: `https://x.com/${username}/status/${replyId}`,
|
|
200
|
+
inReplyTo: tweetId,
|
|
201
|
+
text: validation.text
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
return result;
|
|
205
|
+
}
|
|
149
206
|
async function readMentions(credentials, { maxResults = 10, sinceId = null, paginationToken = null } = {}) {
|
|
150
207
|
if (!credentials.userId) {
|
|
151
208
|
return { ok: false, error: "userId is required to read mentions. Set it in the agent's X profile config." };
|
|
@@ -252,15 +309,14 @@ async function getThread(credentials, tweetId, { maxResults = 20 } = {}) {
|
|
|
252
309
|
// lib/x-tools.mjs
|
|
253
310
|
function parseXConfig(obj) {
|
|
254
311
|
const xRaw = obj?.x && typeof obj.x === "object" && !Array.isArray(obj.x) ? obj.x : {};
|
|
255
|
-
const consumerKey = typeof xRaw.consumerKey === "string" ? xRaw.consumerKey :
|
|
256
|
-
const consumerSecret = typeof xRaw.consumerSecret === "string" ? xRaw.consumerSecret :
|
|
312
|
+
const consumerKey = typeof xRaw.consumerKey === "string" ? xRaw.consumerKey : "";
|
|
313
|
+
const consumerSecret = typeof xRaw.consumerSecret === "string" ? xRaw.consumerSecret : "";
|
|
257
314
|
const profiles = {};
|
|
258
315
|
if (xRaw.profiles && typeof xRaw.profiles === "object" && !Array.isArray(xRaw.profiles)) {
|
|
259
316
|
for (const [key, val] of Object.entries(xRaw.profiles)) {
|
|
260
317
|
if (val && typeof val === "object" && !Array.isArray(val)) {
|
|
261
|
-
const
|
|
262
|
-
const
|
|
263
|
-
const accessTokenSecret = typeof val.accessTokenSecret === "string" ? val.accessTokenSecret : process.env[`${envPrefix2}_SECRET`] || "";
|
|
318
|
+
const accessToken = typeof val.accessToken === "string" ? val.accessToken : "";
|
|
319
|
+
const accessTokenSecret = typeof val.accessTokenSecret === "string" ? val.accessTokenSecret : "";
|
|
264
320
|
if (accessToken && accessTokenSecret) {
|
|
265
321
|
profiles[key] = {
|
|
266
322
|
accessToken,
|
|
@@ -272,13 +328,6 @@ function parseXConfig(obj) {
|
|
|
272
328
|
}
|
|
273
329
|
}
|
|
274
330
|
}
|
|
275
|
-
const defaultAgentId = typeof obj?.agentId === "string" ? obj.agentId : "cto";
|
|
276
|
-
const envPrefix = `X_ACCESS_TOKEN_${defaultAgentId.toUpperCase().replace(/-/g, "_")}`;
|
|
277
|
-
const envAccessToken = process.env[envPrefix] || "";
|
|
278
|
-
const envAccessTokenSecret = process.env[`${envPrefix}_SECRET`] || "";
|
|
279
|
-
if (!profiles[defaultAgentId] && envAccessToken && envAccessTokenSecret) {
|
|
280
|
-
profiles[defaultAgentId] = { accessToken: envAccessToken, accessTokenSecret: envAccessTokenSecret };
|
|
281
|
-
}
|
|
282
331
|
if (consumerKey && consumerSecret) {
|
|
283
332
|
return { ok: true, consumerKey, consumerSecret, profiles };
|
|
284
333
|
}
|
|
@@ -287,7 +336,7 @@ function parseXConfig(obj) {
|
|
|
287
336
|
function resolveAgentCredentials(xConfig, callerAgentId, requestedAgentId, fallbackAgentId) {
|
|
288
337
|
const agentId = callerAgentId || requestedAgentId || fallbackAgentId || "cto";
|
|
289
338
|
if (!xConfig || !xConfig.ok) {
|
|
290
|
-
return { ok: false, error: "X/Twitter configuration missing. Set 'x.consumerKey', 'x.consumerSecret', and 'x.profiles.<agentId>' in plugin config
|
|
339
|
+
return { ok: false, error: "X/Twitter configuration missing. Set 'x.consumerKey', 'x.consumerSecret', and 'x.profiles.<agentId>' in plugin config." };
|
|
291
340
|
}
|
|
292
341
|
const profile = xConfig.profiles[agentId];
|
|
293
342
|
if (!profile) {
|
|
@@ -308,6 +357,7 @@ function resolveAgentCredentials(xConfig, callerAgentId, requestedAgentId, fallb
|
|
|
308
357
|
}
|
|
309
358
|
function registerXTools(api, Type2, xConfig, fallbackAgentId, logPrefix, options) {
|
|
310
359
|
const checkPermission = options?.checkPermission || null;
|
|
360
|
+
const enableWriteTools = options?.enableWriteTools ?? false;
|
|
311
361
|
const json = (data) => ({
|
|
312
362
|
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
313
363
|
});
|
|
@@ -326,6 +376,37 @@ function registerXTools(api, Type2, xConfig, fallbackAgentId, logPrefix, options
|
|
|
326
376
|
return json({ error: err instanceof Error ? err.message : String(err) });
|
|
327
377
|
}
|
|
328
378
|
};
|
|
379
|
+
if (enableWriteTools) {
|
|
380
|
+
api.registerTool({
|
|
381
|
+
name: "x_post_tweet",
|
|
382
|
+
description: "Post a tweet to X/Twitter from the calling agent's configured profile. Max 280 characters.",
|
|
383
|
+
parameters: Type2.Object({
|
|
384
|
+
text: Type2.String({ description: "Tweet text (max 280 characters)" }),
|
|
385
|
+
agentId: Type2.Optional(Type2.String({ description: "Override agent ID (default: caller's agent identity)" }))
|
|
386
|
+
}),
|
|
387
|
+
execute: wrapExecute("x_post_tweet", async (_id, params) => {
|
|
388
|
+
const callerAgentId = params._agentId;
|
|
389
|
+
const creds = resolveAgentCredentials(xConfig, callerAgentId, params.agentId, fallbackAgentId);
|
|
390
|
+
if (!creds.ok) return { error: creds.error };
|
|
391
|
+
return postTweet(creds.credentials, params.text);
|
|
392
|
+
})
|
|
393
|
+
});
|
|
394
|
+
api.registerTool({
|
|
395
|
+
name: "x_reply_tweet",
|
|
396
|
+
description: "Reply to a specific tweet on X/Twitter. Max 280 characters.",
|
|
397
|
+
parameters: Type2.Object({
|
|
398
|
+
tweetId: Type2.String({ description: "The tweet ID to reply to" }),
|
|
399
|
+
text: Type2.String({ description: "Reply text (max 280 characters)" }),
|
|
400
|
+
agentId: Type2.Optional(Type2.String({ description: "Override agent ID (default: caller's agent identity)" }))
|
|
401
|
+
}),
|
|
402
|
+
execute: wrapExecute("x_reply_tweet", async (_id, params) => {
|
|
403
|
+
const callerAgentId = params._agentId;
|
|
404
|
+
const creds = resolveAgentCredentials(xConfig, callerAgentId, params.agentId, fallbackAgentId);
|
|
405
|
+
if (!creds.ok) return { error: creds.error };
|
|
406
|
+
return replyToTweet(creds.credentials, params.tweetId, params.text);
|
|
407
|
+
})
|
|
408
|
+
});
|
|
409
|
+
}
|
|
329
410
|
api.registerTool({
|
|
330
411
|
name: "x_read_mentions",
|
|
331
412
|
description: "Read recent mentions of the agent's X profile. Requires pay-as-you-go or Basic tier X API access.",
|
|
@@ -384,7 +465,9 @@ function registerXTools(api, Type2, xConfig, fallbackAgentId, logPrefix, options
|
|
|
384
465
|
});
|
|
385
466
|
})
|
|
386
467
|
});
|
|
387
|
-
|
|
468
|
+
const toolCount = enableWriteTools ? 5 : 3;
|
|
469
|
+
const writeNote = enableWriteTools ? "" : " (write tools disabled \u2014 set beta.xPosting: true in plugin config to enable x_post_tweet and x_reply_tweet)";
|
|
470
|
+
api.logger.info(`${logPrefix} Registered ${toolCount} X/Twitter tools${writeNote}. Profiles: ${xConfig.ok ? Object.keys(xConfig.profiles).join(", ") || "none" : "unconfigured"}`);
|
|
388
471
|
}
|
|
389
472
|
|
|
390
473
|
// lib/web-fetch.mjs
|
|
@@ -687,6 +770,7 @@ function parseConfig(raw) {
|
|
|
687
770
|
const externalUserId = typeof obj.externalUserId === "string" ? obj.externalUserId : void 0;
|
|
688
771
|
const refreshToken = typeof obj.refreshToken === "string" ? obj.refreshToken : void 0;
|
|
689
772
|
const walletPublicKey = typeof obj.walletPublicKey === "string" ? obj.walletPublicKey : void 0;
|
|
773
|
+
const walletPrivateKey = typeof obj.walletPrivateKey === "string" ? obj.walletPrivateKey : void 0;
|
|
690
774
|
const apiTimeout = typeof obj.apiTimeout === "number" ? obj.apiTimeout : 12e4;
|
|
691
775
|
const agentId = typeof obj.agentId === "string" ? obj.agentId : void 0;
|
|
692
776
|
const gatewayBaseUrl = typeof obj.gatewayBaseUrl === "string" ? obj.gatewayBaseUrl : void 0;
|
|
@@ -698,6 +782,8 @@ function parseConfig(raw) {
|
|
|
698
782
|
const dailyLogRetentionDays = typeof obj.dailyLogRetentionDays === "number" ? obj.dailyLogRetentionDays : 30;
|
|
699
783
|
const recoverySecret = typeof obj.recoverySecret === "string" ? obj.recoverySecret : void 0;
|
|
700
784
|
const xConfig = parseXConfig(obj);
|
|
785
|
+
const betaRaw = obj.beta && typeof obj.beta === "object" && !Array.isArray(obj.beta) ? obj.beta : {};
|
|
786
|
+
const beta = { xPosting: betaRaw.xPosting === true };
|
|
701
787
|
return {
|
|
702
788
|
orchestratorUrl,
|
|
703
789
|
walletId,
|
|
@@ -705,6 +791,7 @@ function parseConfig(raw) {
|
|
|
705
791
|
externalUserId,
|
|
706
792
|
refreshToken,
|
|
707
793
|
walletPublicKey,
|
|
794
|
+
walletPrivateKey,
|
|
708
795
|
recoverySecret,
|
|
709
796
|
apiTimeout,
|
|
710
797
|
agentId,
|
|
@@ -715,7 +802,8 @@ function parseConfig(raw) {
|
|
|
715
802
|
bootstrapDecisionCount,
|
|
716
803
|
bootstrapBulletinWindowHours,
|
|
717
804
|
dailyLogRetentionDays,
|
|
718
|
-
xConfig
|
|
805
|
+
xConfig,
|
|
806
|
+
beta
|
|
719
807
|
};
|
|
720
808
|
}
|
|
721
809
|
function buildTraderClawWelcomeMessage(apiKeyForDisplay) {
|
|
@@ -846,8 +934,8 @@ var solanaTraderPlugin = {
|
|
|
846
934
|
refreshToken: effectiveRefreshToken,
|
|
847
935
|
walletPublicKey: effectiveWalletPublicKey,
|
|
848
936
|
walletPrivateKeyProvider: () => {
|
|
849
|
-
const
|
|
850
|
-
return
|
|
937
|
+
const k = config.walletPrivateKey;
|
|
938
|
+
return typeof k === "string" && k.trim() ? k.trim() : void 0;
|
|
851
939
|
},
|
|
852
940
|
recoverySecretProvider: async () => {
|
|
853
941
|
const fromDisk = readRecoverySecretFromDisk();
|
|
@@ -1971,12 +2059,18 @@ var solanaTraderPlugin = {
|
|
|
1971
2059
|
});
|
|
1972
2060
|
api.registerTool({
|
|
1973
2061
|
name: "solana_gateway_credentials_set",
|
|
1974
|
-
description: "Register or update your OpenClaw Gateway credentials with the orchestrator. This enables event-to-agent forwarding \u2014 when subscriptions include agentId, the orchestrator delivers each stream event to your Gateway via /v1/responses. Call this once during initial setup (Step 0). The gatewayBaseUrl is your self-hosted OpenClaw Gateway's public URL. The gatewayToken is the Bearer token for authenticating forwarded events.",
|
|
2062
|
+
description: "Register or update your OpenClaw Gateway credentials with the orchestrator. This enables event-to-agent forwarding \u2014 when subscriptions include agentId, the orchestrator delivers each stream event to your Gateway via /v1/responses. Call this once during initial setup (Step 0). The gatewayBaseUrl is your self-hosted OpenClaw Gateway's public URL. The gatewayToken is the Bearer token for authenticating forwarded events. Optional forwardTelegramChatId stores your Telegram chat id on this credential row so agent replies are announced to that chat (same row as api_key + agentId). Omit forwardTelegramChatId to leave the stored value unchanged; pass null to clear.",
|
|
1975
2063
|
parameters: Type.Object({
|
|
1976
2064
|
gatewayBaseUrl: Type.String({ description: "Your OpenClaw Gateway's public HTTPS URL (e.g., 'https://gateway.example.com')" }),
|
|
1977
2065
|
gatewayToken: Type.String({ description: "Bearer token for authenticating forwarded events to your Gateway" }),
|
|
1978
2066
|
agentId: Type.Optional(Type.String({ description: "Agent ID to associate credentials with (default: 'main'). Omit to store as the default fallback." })),
|
|
1979
|
-
active: Type.Optional(Type.Boolean({ description: "Whether forwarding is active (default: true)" }))
|
|
2067
|
+
active: Type.Optional(Type.Boolean({ description: "Whether forwarding is active (default: true)" })),
|
|
2068
|
+
forwardTelegramChatId: Type.Optional(
|
|
2069
|
+
Type.Union([
|
|
2070
|
+
Type.String({ description: "Telegram chat id (digits, optional leading -) for routing agent responses" }),
|
|
2071
|
+
Type.Null({ description: "Clear stored Telegram chat id" })
|
|
2072
|
+
])
|
|
2073
|
+
)
|
|
1980
2074
|
}),
|
|
1981
2075
|
execute: wrapExecute("solana_gateway_credentials_set", async (_id, params) => {
|
|
1982
2076
|
const body = {
|
|
@@ -1985,12 +2079,13 @@ var solanaTraderPlugin = {
|
|
|
1985
2079
|
};
|
|
1986
2080
|
if (params.agentId) body.agentId = params.agentId;
|
|
1987
2081
|
if (params.active !== void 0) body.active = params.active;
|
|
2082
|
+
if (params.forwardTelegramChatId !== void 0) body.forwardTelegramChatId = params.forwardTelegramChatId;
|
|
1988
2083
|
return put("/api/agents/gateway-credentials", body);
|
|
1989
2084
|
})
|
|
1990
2085
|
});
|
|
1991
2086
|
api.registerTool({
|
|
1992
2087
|
name: "solana_gateway_credentials_get",
|
|
1993
|
-
description: "Get the currently registered Gateway credentials for event-to-agent forwarding. Returns the gatewayBaseUrl, agentId, active status, and
|
|
2088
|
+
description: "Get the currently registered Gateway credentials for event-to-agent forwarding. Returns the gatewayBaseUrl, agentId, active status, forwardTelegramChatId (when set), and metadata. Use to verify Gateway setup is correct.",
|
|
1994
2089
|
parameters: Type.Object({}),
|
|
1995
2090
|
execute: wrapExecute(
|
|
1996
2091
|
"solana_gateway_credentials_get",
|
|
@@ -3400,9 +3495,10 @@ Context compaction triggered. STATE.md synced from last persisted state. Decisio
|
|
|
3400
3495
|
}
|
|
3401
3496
|
}
|
|
3402
3497
|
});
|
|
3403
|
-
registerXTools(api, Type, config.xConfig, config.agentId || "cto", "[solana-trader]");
|
|
3498
|
+
registerXTools(api, Type, config.xConfig, config.agentId || "cto", "[solana-trader]", { enableWriteTools: config.beta?.xPosting ?? false });
|
|
3404
3499
|
registerWebFetchTool(api, Type, "[solana-trader]");
|
|
3405
|
-
const
|
|
3500
|
+
const xWriteEnabled = config.beta?.xPosting ?? false;
|
|
3501
|
+
const xToolCount = config.xConfig?.ok ? xWriteEnabled ? 5 : 3 : 0;
|
|
3406
3502
|
const webFetchCount = 1;
|
|
3407
3503
|
const intelligenceToolCount = 17;
|
|
3408
3504
|
const baseToolCount = 76;
|
package/lib/x-client.mjs
CHANGED
|
@@ -116,9 +116,12 @@ async function xApiFetch(method, endpoint, credentials, { body = null, queryPara
|
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
if (response.status === 403) {
|
|
119
|
+
const isWrite = method === "POST" || method === "PUT" || method === "DELETE";
|
|
119
120
|
return {
|
|
120
121
|
ok: false,
|
|
121
|
-
error:
|
|
122
|
+
error: isWrite
|
|
123
|
+
? "Forbidden (403). X rejected this write request. Check that App permissions are set to Read+Write in the X developer portal and regenerate your access tokens after any permission change."
|
|
124
|
+
: "Forbidden (403). This read endpoint requires a paid X API tier (pay-as-you-go or Basic). This does NOT affect posting — x_post_tweet and x_reply_tweet still work on Free tier.",
|
|
122
125
|
status: 403,
|
|
123
126
|
data: responseData,
|
|
124
127
|
};
|
|
@@ -140,6 +143,72 @@ async function xApiFetch(method, endpoint, credentials, { body = null, queryPara
|
|
|
140
143
|
};
|
|
141
144
|
}
|
|
142
145
|
|
|
146
|
+
export function validateTweetText(text) {
|
|
147
|
+
if (!text || typeof text !== "string") {
|
|
148
|
+
return { valid: false, error: "Tweet text is required and must be a non-empty string." };
|
|
149
|
+
}
|
|
150
|
+
const trimmed = text.trim();
|
|
151
|
+
if (trimmed.length === 0) {
|
|
152
|
+
return { valid: false, error: "Tweet text cannot be empty." };
|
|
153
|
+
}
|
|
154
|
+
if (trimmed.length > MAX_TWEET_LENGTH) {
|
|
155
|
+
return { valid: false, error: `Tweet exceeds ${MAX_TWEET_LENGTH} characters (got ${trimmed.length}). Shorten the text.` };
|
|
156
|
+
}
|
|
157
|
+
return { valid: true, text: trimmed };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export async function postTweet(credentials, text) {
|
|
161
|
+
const validation = validateTweetText(text);
|
|
162
|
+
if (!validation.valid) return { ok: false, error: validation.error };
|
|
163
|
+
|
|
164
|
+
const result = await xApiFetch("POST", "/tweets", credentials, {
|
|
165
|
+
body: { text: validation.text },
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
if (result.ok && result.data?.data?.id) {
|
|
169
|
+
const tweetId = result.data.data.id;
|
|
170
|
+
const username = credentials.username || "unknown";
|
|
171
|
+
return {
|
|
172
|
+
ok: true,
|
|
173
|
+
tweetId,
|
|
174
|
+
tweetUrl: `https://x.com/${username}/status/${tweetId}`,
|
|
175
|
+
text: validation.text,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return result;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export async function replyToTweet(credentials, tweetId, text) {
|
|
183
|
+
const validation = validateTweetText(text);
|
|
184
|
+
if (!validation.valid) return { ok: false, error: validation.error };
|
|
185
|
+
|
|
186
|
+
if (!tweetId || typeof tweetId !== "string") {
|
|
187
|
+
return { ok: false, error: "tweetId is required to reply." };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const result = await xApiFetch("POST", "/tweets", credentials, {
|
|
191
|
+
body: {
|
|
192
|
+
text: validation.text,
|
|
193
|
+
reply: { in_reply_to_tweet_id: tweetId },
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
if (result.ok && result.data?.data?.id) {
|
|
198
|
+
const replyId = result.data.data.id;
|
|
199
|
+
const username = credentials.username || "unknown";
|
|
200
|
+
return {
|
|
201
|
+
ok: true,
|
|
202
|
+
replyId,
|
|
203
|
+
replyUrl: `https://x.com/${username}/status/${replyId}`,
|
|
204
|
+
inReplyTo: tweetId,
|
|
205
|
+
text: validation.text,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return result;
|
|
210
|
+
}
|
|
211
|
+
|
|
143
212
|
export async function readMentions(credentials, { maxResults = 10, sinceId = null, paginationToken = null } = {}) {
|
|
144
213
|
if (!credentials.userId) {
|
|
145
214
|
return { ok: false, error: "userId is required to read mentions. Set it in the agent's X profile config." };
|
|
@@ -283,11 +352,11 @@ export function resolveCredentials(pluginConfig, agentId) {
|
|
|
283
352
|
return { ok: false, error: "X/Twitter configuration missing. Set 'x.consumerKey', 'x.consumerSecret', and 'x.profiles.<agentId>' in plugin config." };
|
|
284
353
|
}
|
|
285
354
|
|
|
286
|
-
const consumerKey = x.consumerKey
|
|
287
|
-
const consumerSecret = x.consumerSecret
|
|
355
|
+
const consumerKey = x.consumerKey;
|
|
356
|
+
const consumerSecret = x.consumerSecret;
|
|
288
357
|
|
|
289
358
|
if (!consumerKey || !consumerSecret) {
|
|
290
|
-
return { ok: false, error: "X App credentials missing. Set 'x.consumerKey' and 'x.consumerSecret' in plugin config
|
|
359
|
+
return { ok: false, error: "X App credentials missing. Set 'x.consumerKey' and 'x.consumerSecret' in plugin config." };
|
|
291
360
|
}
|
|
292
361
|
|
|
293
362
|
const profile = x.profiles?.[agentId] || x.profiles?.["default"];
|
|
@@ -295,9 +364,8 @@ export function resolveCredentials(pluginConfig, agentId) {
|
|
|
295
364
|
return { ok: false, error: `No X profile configured for agent '${agentId}'. Add 'x.profiles.${agentId}' (or 'x.profiles.default') with accessToken and accessTokenSecret.` };
|
|
296
365
|
}
|
|
297
366
|
|
|
298
|
-
const
|
|
299
|
-
const
|
|
300
|
-
const accessTokenSecret = profile.accessTokenSecret || process.env[`${envPrefix}_SECRET`];
|
|
367
|
+
const accessToken = profile.accessToken;
|
|
368
|
+
const accessTokenSecret = profile.accessTokenSecret;
|
|
301
369
|
|
|
302
370
|
if (!accessToken || !accessTokenSecret) {
|
|
303
371
|
return { ok: false, error: `X access tokens missing for agent '${agentId}'. Set accessToken and accessTokenSecret in the profile config.` };
|
package/lib/x-tools.mjs
CHANGED
|
@@ -1,20 +1,19 @@
|
|
|
1
|
-
import { readMentions, searchTweets, getThread } from "./x-client.mjs";
|
|
1
|
+
import { postTweet, replyToTweet, readMentions, searchTweets, getThread } from "./x-client.mjs";
|
|
2
2
|
|
|
3
3
|
export function parseXConfig(obj) {
|
|
4
4
|
const xRaw = (obj?.x && typeof obj.x === "object" && !Array.isArray(obj.x))
|
|
5
5
|
? obj.x
|
|
6
6
|
: {};
|
|
7
7
|
|
|
8
|
-
const consumerKey = typeof xRaw.consumerKey === "string" ? xRaw.consumerKey :
|
|
9
|
-
const consumerSecret = typeof xRaw.consumerSecret === "string" ? xRaw.consumerSecret :
|
|
8
|
+
const consumerKey = typeof xRaw.consumerKey === "string" ? xRaw.consumerKey : "";
|
|
9
|
+
const consumerSecret = typeof xRaw.consumerSecret === "string" ? xRaw.consumerSecret : "";
|
|
10
10
|
const profiles = {};
|
|
11
11
|
|
|
12
12
|
if (xRaw.profiles && typeof xRaw.profiles === "object" && !Array.isArray(xRaw.profiles)) {
|
|
13
13
|
for (const [key, val] of Object.entries(xRaw.profiles)) {
|
|
14
14
|
if (val && typeof val === "object" && !Array.isArray(val)) {
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
const accessTokenSecret = typeof val.accessTokenSecret === "string" ? val.accessTokenSecret : (process.env[`${envPrefix}_SECRET`] || "");
|
|
15
|
+
const accessToken = typeof val.accessToken === "string" ? val.accessToken : "";
|
|
16
|
+
const accessTokenSecret = typeof val.accessTokenSecret === "string" ? val.accessTokenSecret : "";
|
|
18
17
|
if (accessToken && accessTokenSecret) {
|
|
19
18
|
profiles[key] = {
|
|
20
19
|
accessToken,
|
|
@@ -27,14 +26,6 @@ export function parseXConfig(obj) {
|
|
|
27
26
|
}
|
|
28
27
|
}
|
|
29
28
|
|
|
30
|
-
const defaultAgentId = typeof obj?.agentId === "string" ? obj.agentId : "cto";
|
|
31
|
-
const envPrefix = `X_ACCESS_TOKEN_${defaultAgentId.toUpperCase().replace(/-/g, "_")}`;
|
|
32
|
-
const envAccessToken = process.env[envPrefix] || "";
|
|
33
|
-
const envAccessTokenSecret = process.env[`${envPrefix}_SECRET`] || "";
|
|
34
|
-
if (!profiles[defaultAgentId] && envAccessToken && envAccessTokenSecret) {
|
|
35
|
-
profiles[defaultAgentId] = { accessToken: envAccessToken, accessTokenSecret: envAccessTokenSecret };
|
|
36
|
-
}
|
|
37
|
-
|
|
38
29
|
if (consumerKey && consumerSecret) {
|
|
39
30
|
return { ok: true, consumerKey, consumerSecret, profiles };
|
|
40
31
|
}
|
|
@@ -45,7 +36,7 @@ export function resolveAgentCredentials(xConfig, callerAgentId, requestedAgentId
|
|
|
45
36
|
const agentId = callerAgentId || requestedAgentId || fallbackAgentId || "cto";
|
|
46
37
|
|
|
47
38
|
if (!xConfig || !xConfig.ok) {
|
|
48
|
-
return { ok: false, error: "X/Twitter configuration missing. Set 'x.consumerKey', 'x.consumerSecret', and 'x.profiles.<agentId>' in plugin config
|
|
39
|
+
return { ok: false, error: "X/Twitter configuration missing. Set 'x.consumerKey', 'x.consumerSecret', and 'x.profiles.<agentId>' in plugin config." };
|
|
49
40
|
}
|
|
50
41
|
|
|
51
42
|
const profile = xConfig.profiles[agentId];
|
|
@@ -69,6 +60,7 @@ export function resolveAgentCredentials(xConfig, callerAgentId, requestedAgentId
|
|
|
69
60
|
|
|
70
61
|
export function registerXTools(api, Type, xConfig, fallbackAgentId, logPrefix, options) {
|
|
71
62
|
const checkPermission = options?.checkPermission || null;
|
|
63
|
+
const enableWriteTools = options?.enableWriteTools ?? false;
|
|
72
64
|
|
|
73
65
|
const json = (data) => ({
|
|
74
66
|
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
@@ -91,6 +83,39 @@ export function registerXTools(api, Type, xConfig, fallbackAgentId, logPrefix, o
|
|
|
91
83
|
}
|
|
92
84
|
};
|
|
93
85
|
|
|
86
|
+
if (enableWriteTools) {
|
|
87
|
+
api.registerTool({
|
|
88
|
+
name: "x_post_tweet",
|
|
89
|
+
description: "Post a tweet to X/Twitter from the calling agent's configured profile. Max 280 characters.",
|
|
90
|
+
parameters: Type.Object({
|
|
91
|
+
text: Type.String({ description: "Tweet text (max 280 characters)" }),
|
|
92
|
+
agentId: Type.Optional(Type.String({ description: "Override agent ID (default: caller's agent identity)" })),
|
|
93
|
+
}),
|
|
94
|
+
execute: wrapExecute("x_post_tweet", async (_id, params) => {
|
|
95
|
+
const callerAgentId = params._agentId;
|
|
96
|
+
const creds = resolveAgentCredentials(xConfig, callerAgentId, params.agentId, fallbackAgentId);
|
|
97
|
+
if (!creds.ok) return { error: creds.error };
|
|
98
|
+
return postTweet(creds.credentials, params.text);
|
|
99
|
+
}),
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
api.registerTool({
|
|
103
|
+
name: "x_reply_tweet",
|
|
104
|
+
description: "Reply to a specific tweet on X/Twitter. Max 280 characters.",
|
|
105
|
+
parameters: Type.Object({
|
|
106
|
+
tweetId: Type.String({ description: "The tweet ID to reply to" }),
|
|
107
|
+
text: Type.String({ description: "Reply text (max 280 characters)" }),
|
|
108
|
+
agentId: Type.Optional(Type.String({ description: "Override agent ID (default: caller's agent identity)" })),
|
|
109
|
+
}),
|
|
110
|
+
execute: wrapExecute("x_reply_tweet", async (_id, params) => {
|
|
111
|
+
const callerAgentId = params._agentId;
|
|
112
|
+
const creds = resolveAgentCredentials(xConfig, callerAgentId, params.agentId, fallbackAgentId);
|
|
113
|
+
if (!creds.ok) return { error: creds.error };
|
|
114
|
+
return replyToTweet(creds.credentials, params.tweetId, params.text);
|
|
115
|
+
}),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
94
119
|
api.registerTool({
|
|
95
120
|
name: "x_read_mentions",
|
|
96
121
|
description: "Read recent mentions of the agent's X profile. Requires pay-as-you-go or Basic tier X API access.",
|
|
@@ -152,5 +177,7 @@ export function registerXTools(api, Type, xConfig, fallbackAgentId, logPrefix, o
|
|
|
152
177
|
}),
|
|
153
178
|
});
|
|
154
179
|
|
|
155
|
-
|
|
180
|
+
const toolCount = enableWriteTools ? 5 : 3;
|
|
181
|
+
const writeNote = enableWriteTools ? "" : " (write tools disabled — set beta.xPosting: true in plugin config to enable x_post_tweet and x_reply_tweet)";
|
|
182
|
+
api.logger.info(`${logPrefix} Registered ${toolCount} X/Twitter tools${writeNote}. Profiles: ${xConfig.ok ? Object.keys(xConfig.profiles).join(", ") || "none" : "unconfigured"}`);
|
|
156
183
|
}
|
package/openclaw.plugin.json
CHANGED
|
@@ -84,6 +84,18 @@
|
|
|
84
84
|
"default": 30,
|
|
85
85
|
"description": "Number of days to retain daily log files before pruning"
|
|
86
86
|
},
|
|
87
|
+
"beta": {
|
|
88
|
+
"type": "object",
|
|
89
|
+
"description": "Beta feature flags. Enable experimental capabilities that are not yet enabled by default.",
|
|
90
|
+
"properties": {
|
|
91
|
+
"xPosting": {
|
|
92
|
+
"type": "boolean",
|
|
93
|
+
"default": false,
|
|
94
|
+
"description": "Enable X/Twitter write tools: x_post_tweet and x_reply_tweet. Requires your X App permissions set to Read+Write and tokens regenerated. When false (default), only the 3 read tools are registered (x_read_mentions, x_search_tweets, x_get_thread)."
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
"additionalProperties": false
|
|
98
|
+
},
|
|
87
99
|
"x": {
|
|
88
100
|
"type": "object",
|
|
89
101
|
"description": "X/Twitter configuration for read-only social intel tools",
|
|
@@ -182,6 +194,11 @@
|
|
|
182
194
|
"dailyLogRetentionDays": {
|
|
183
195
|
"label": "Daily Log Retention (days)",
|
|
184
196
|
"advanced": true
|
|
197
|
+
},
|
|
198
|
+
"beta": {
|
|
199
|
+
"label": "Beta Features",
|
|
200
|
+
"description": "Enable experimental features. Set beta.xPosting: true to activate x_post_tweet and x_reply_tweet.",
|
|
201
|
+
"advanced": true
|
|
185
202
|
}
|
|
186
203
|
}
|
|
187
204
|
}
|
package/package.json
CHANGED
|
@@ -1,19 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "solana-traderclaw",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.50",
|
|
4
4
|
"description": "TraderClaw V1-Upgraded — Solana trading for OpenClaw with intelligence lab, tool envelopes, prompt scrubbing, read-only X social intel, and split skill docs",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": "./dist/index.js"
|
|
9
9
|
},
|
|
10
|
-
"bin": {
|
|
11
|
-
"traderclaw": "bin/traderclaw.cjs"
|
|
12
|
-
},
|
|
13
10
|
"files": [
|
|
14
11
|
"dist/",
|
|
15
|
-
"bin/*.mjs",
|
|
16
|
-
"bin/*.cjs",
|
|
17
12
|
"skills/",
|
|
18
13
|
"config/",
|
|
19
14
|
"lib/",
|
|
@@ -60,6 +55,7 @@
|
|
|
60
55
|
"openclaw": {
|
|
61
56
|
"extensions": [
|
|
62
57
|
"./dist/index.js"
|
|
63
|
-
]
|
|
58
|
+
],
|
|
59
|
+
"hooks": []
|
|
64
60
|
}
|
|
65
61
|
}
|
|
@@ -274,7 +274,9 @@ Weights must sum to ~1.0. Evolve based on trade outcomes via `strategy_evolution
|
|
|
274
274
|
↓
|
|
275
275
|
11. Step 8: MEMORY — state_save, daily_log, decision_log, team_bulletin_post, context_snapshot_write
|
|
276
276
|
↓
|
|
277
|
-
12. Step 9:
|
|
277
|
+
12. Step 9: X POST — x_post_tweet *(beta — only available when `beta.xPosting: true` in plugin config)*
|
|
278
|
+
↓
|
|
279
|
+
13. Step 10: REPORT — includes DEEP ANALYSIS section (Bitquery/intelligence lab/trust checks used)
|
|
278
280
|
↓
|
|
279
281
|
13. SLEEP
|
|
280
282
|
```
|
|
@@ -577,6 +579,8 @@ All decision making, evaluation, and learning MUST use SOL-based values.
|
|
|
577
579
|
| `refs/position-management.md` | Step 7 MONITOR, house money, social exhaustion |
|
|
578
580
|
| `refs/review-learning.md` | Steps 8, 8.5 — review, structured learning log |
|
|
579
581
|
| `refs/strategy-evolution.md` | Step 9 EVOLVE, ADL, VFM, named patterns |
|
|
582
|
+
| `refs/x-credentials.md` | X/Twitter API credentials and configuration |
|
|
583
|
+
| `refs/x-journal.md` | X/Twitter posting guidelines and templates |
|
|
580
584
|
| `refs/cron-jobs.md` | All cron job definitions and workflows |
|
|
581
585
|
| `refs/api-reference.md` | API contract, endpoints, auth flow, error codes |
|
|
582
586
|
| `refs/memory-tags.md` | Complete memory tag vocabulary |
|