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.
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  resolveIntelligenceDir
3
- } from "./chunk-YBURTADE.js";
3
+ } from "./chunk-JO3BXAUQ.js";
4
4
 
5
5
  // src/intelligence-lab.ts
6
6
  import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
@@ -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-IYD3TCSE.js";
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-CMZLPU3Z.js";
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 : process.env.X_CONSUMER_KEY || "";
256
- const consumerSecret = typeof xRaw.consumerSecret === "string" ? xRaw.consumerSecret : process.env.X_CONSUMER_SECRET || "";
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 envPrefix2 = `X_ACCESS_TOKEN_${key.toUpperCase().replace(/-/g, "_")}`;
262
- const accessToken = typeof val.accessToken === "string" ? val.accessToken : process.env[envPrefix2] || "";
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, or use X_CONSUMER_KEY / X_CONSUMER_SECRET env vars." };
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
- api.logger.info(`${logPrefix} Registered 3 X/Twitter read-only tools (social intel). Profiles: ${xConfig.ok ? Object.keys(xConfig.profiles).join(", ") || "none" : "unconfigured"}`);
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 runtimeKey = process.env.TRADERCLAW_WALLET_PRIVATE_KEY || "";
850
- return runtimeKey.trim() || void 0;
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 masked token. Use to verify Gateway setup is correct.",
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 xToolCount = config.xConfig?.ok ? 3 : 0;
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;
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  IntelligenceLab
3
- } from "../chunk-IYD3TCSE.js";
4
- import "../chunk-CMZLPU3Z.js";
3
+ } from "../chunk-C24QA3MQ.js";
4
+ import "../chunk-JO3BXAUQ.js";
5
5
  export {
6
6
  IntelligenceLab
7
7
  };
@@ -11,7 +11,7 @@ import {
11
11
  resolveMemoryDir,
12
12
  resolveWorkspaceRoot,
13
13
  writeWorkspaceFile
14
- } from "../chunk-CMZLPU3Z.js";
14
+ } from "../chunk-JO3BXAUQ.js";
15
15
  export {
16
16
  atomicWriteFile,
17
17
  generateBulletinDigest,
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: "Forbidden (403). This read endpoint requires a paid X API tier (pay-as-you-go or Basic).",
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 || process.env.X_CONSUMER_KEY;
287
- const consumerSecret = x.consumerSecret || process.env.X_CONSUMER_SECRET;
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, or set X_CONSUMER_KEY / X_CONSUMER_SECRET env vars." };
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 envPrefix = `X_ACCESS_TOKEN_${agentId.toUpperCase().replace(/-/g, "_")}`;
299
- const accessToken = profile.accessToken || process.env[envPrefix];
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 : (process.env.X_CONSUMER_KEY || "");
9
- const consumerSecret = typeof xRaw.consumerSecret === "string" ? xRaw.consumerSecret : (process.env.X_CONSUMER_SECRET || "");
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 envPrefix = `X_ACCESS_TOKEN_${key.toUpperCase().replace(/-/g, "_")}`;
16
- const accessToken = typeof val.accessToken === "string" ? val.accessToken : (process.env[envPrefix] || "");
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, or use X_CONSUMER_KEY / X_CONSUMER_SECRET env vars." };
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
- api.logger.info(`${logPrefix} Registered 3 X/Twitter read-only tools (social intel). Profiles: ${xConfig.ok ? Object.keys(xConfig.profiles).join(", ") || "none" : "unconfigured"}`);
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
  }
@@ -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.48",
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: REPORTincludes DEEP ANALYSIS section (Bitquery/intelligence lab/trust checks used)
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 |