openclaw-twitter-skill 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +109 -0
- package/SKILL.md +211 -0
- package/index.js +263 -0
- package/package.json +43 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 xiaoxi / OpenClaw
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# OpenClaw Twitter Skill
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/openclaw-twitter-skill)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
|
|
6
|
+
Stable, automated Twitter (X) posting skill for [OpenClaw](https://openclaw.ai) agents.
|
|
7
|
+
|
|
8
|
+
## Problem
|
|
9
|
+
|
|
10
|
+
OpenClaw agents can control Chrome via browser tools, but Twitter posting is unreliable — login checks fail, buttons don't get clicked, content gets lost. This skill standardizes the entire process into a battle-tested 5-step workflow.
|
|
11
|
+
|
|
12
|
+
## The 5-Step Workflow
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
Step 1 → Navigate to x.com/compose/post (direct URL, never sidebar)
|
|
16
|
+
Step 2 → Verify login (compose box visible?)
|
|
17
|
+
Step 3 → Type content (type(), not paste)
|
|
18
|
+
Step 4 → Screenshot → user confirms content
|
|
19
|
+
Step 5 → Click Post → verify success toast
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
### As an OpenClaw Skill
|
|
25
|
+
|
|
26
|
+
Copy `SKILL.md` to your agent's skills directory:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install -g openclaw-twitter-skill
|
|
30
|
+
|
|
31
|
+
# Copy the skill file
|
|
32
|
+
cp "$(npm root -g)/openclaw-twitter-skill/SKILL.md" ~/.agents/skills/twitter-post/SKILL.md
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### CLI Usage
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
# Simulate the full posting workflow
|
|
39
|
+
oct-post post "Hello from OpenClaw! 🚀 #AI #Automation"
|
|
40
|
+
|
|
41
|
+
# Validate content against posting rules
|
|
42
|
+
oct-post validate "Testing content #test #openclaw"
|
|
43
|
+
|
|
44
|
+
# Dry run (validate only, don't mark as ready)
|
|
45
|
+
oct-post post "Draft tweet #draft" --dry-run
|
|
46
|
+
|
|
47
|
+
# Show workflow info
|
|
48
|
+
oct-post info
|
|
49
|
+
|
|
50
|
+
# Help
|
|
51
|
+
oct-post --help
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Content Guidelines
|
|
55
|
+
|
|
56
|
+
| Rule | Spec |
|
|
57
|
+
|------|------|
|
|
58
|
+
| Mode | New post only (no replies) |
|
|
59
|
+
| Length | < 200 characters recommended (280 max) |
|
|
60
|
+
| Emoji | 1–3, placed naturally |
|
|
61
|
+
| Hashtags | 1–3 relevant tags (supports CJK: `#人工智能`) |
|
|
62
|
+
| Links | Only if user explicitly provides them |
|
|
63
|
+
| Media | Text-only by default; image upload requires user interaction |
|
|
64
|
+
|
|
65
|
+
## Error Handling
|
|
66
|
+
|
|
67
|
+
- **Not logged in** → Stop immediately, ask user to login in Chrome
|
|
68
|
+
- **Post failed** → Screenshot + specific error description
|
|
69
|
+
- **Never retry silently** → Always report what happened
|
|
70
|
+
- **Never skip screenshot** → User must confirm before posting
|
|
71
|
+
- **One retry max** → Only for server errors, with 30s wait
|
|
72
|
+
|
|
73
|
+
## Key Lessons
|
|
74
|
+
|
|
75
|
+
1. Always use `/compose/post` direct URL (sidebar buttons break across UI updates)
|
|
76
|
+
2. Use `type()` not paste (Twitter CSP may block clipboard)
|
|
77
|
+
3. Screenshot before posting (biggest reliability win)
|
|
78
|
+
4. Wait 1–1.5s for Post button to enable after typing
|
|
79
|
+
5. One post per session (don't chain without spacing)
|
|
80
|
+
6. Check success toast, not URL change
|
|
81
|
+
7. Use `snapshot()` for logic checks, `screenshot()` for user confirmation
|
|
82
|
+
|
|
83
|
+
## Development
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
# Run tests
|
|
87
|
+
npm test
|
|
88
|
+
|
|
89
|
+
# Test CLI locally
|
|
90
|
+
node index.js post "Test tweet #dev" --dry-run
|
|
91
|
+
node index.js validate "Check this content #test"
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Release
|
|
95
|
+
|
|
96
|
+
Push a version tag to trigger auto-release:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
npm version patch # or minor / major
|
|
100
|
+
git push origin main --tags
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
GitHub Actions will:
|
|
104
|
+
1. Create a GitHub Release with auto-generated notes
|
|
105
|
+
2. Publish to NPM (requires `NPM_TOKEN` secret in repo settings)
|
|
106
|
+
|
|
107
|
+
## License
|
|
108
|
+
|
|
109
|
+
MIT
|
package/SKILL.md
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: twitter-post
|
|
3
|
+
description: >
|
|
4
|
+
Stable automated Twitter (X) posting skill for OpenClaw agents.
|
|
5
|
+
Uses browser tools to navigate to x.com/compose/post, verify login,
|
|
6
|
+
type content, screenshot for confirmation, and click Post.
|
|
7
|
+
Use when: "发推", "tweet", "post to X", "发帖", "share on Twitter",
|
|
8
|
+
"推特发布", "帮我发一条推", or any social media posting request targeting X/Twitter.
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Twitter Post Skill
|
|
12
|
+
|
|
13
|
+
Standardized 5-step workflow for OpenClaw agents to post on X (Twitter) via browser tools.
|
|
14
|
+
|
|
15
|
+
## Prerequisites
|
|
16
|
+
|
|
17
|
+
- The user must already be logged into x.com in the Chrome OpenClaw profile.
|
|
18
|
+
- The agent must have access to `browser` tools (navigate, act, type, screenshot, snapshot, wait).
|
|
19
|
+
|
|
20
|
+
## Quick Start
|
|
21
|
+
|
|
22
|
+
When the user asks to post to Twitter, follow these steps **exactly in order**:
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
Step 1 → Open https://x.com/compose/post
|
|
26
|
+
Step 2 → Verify login (is the compose box visible?)
|
|
27
|
+
Step 3 → Type content into the compose box
|
|
28
|
+
Step 4 → Screenshot → show user for confirmation
|
|
29
|
+
Step 5 → Click Post → verify success
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
**CRITICAL RULES (read before proceeding):**
|
|
33
|
+
- NEVER reply to an existing tweet — new post mode only.
|
|
34
|
+
- NEVER post without showing the user a screenshot first.
|
|
35
|
+
- NEVER retry silently on failure.
|
|
36
|
+
- NEVER modify user's content without explicit permission.
|
|
37
|
+
|
|
38
|
+
## Detailed Instructions
|
|
39
|
+
|
|
40
|
+
### Step 1: Open Compose Page
|
|
41
|
+
|
|
42
|
+
```javascript
|
|
43
|
+
// ALWAYS use the direct compose URL — never click sidebar buttons
|
|
44
|
+
await browser.navigate("https://x.com/compose/post");
|
|
45
|
+
await browser.wait(3000); // wait for page load
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**Why direct URL?** Twitter's sidebar UI changes frequently. The `/compose/post` modal route is the most stable entry point. Sidebar "Post" button, `Ctrl+Enter` shortcut, and floating action buttons all break across UI updates.
|
|
49
|
+
|
|
50
|
+
### Step 2: Verify Login
|
|
51
|
+
|
|
52
|
+
After navigation, take a snapshot to check page state:
|
|
53
|
+
|
|
54
|
+
```javascript
|
|
55
|
+
const snapshot = await browser.snapshot();
|
|
56
|
+
// Examine the snapshot text for indicators below
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**Check for these indicators:**
|
|
60
|
+
|
|
61
|
+
| State | What you'll see in snapshot | Action |
|
|
62
|
+
|-------|---------------------------|--------|
|
|
63
|
+
| ✅ Logged in | `contenteditable` text area, "Post" button, character counter | Proceed to Step 3 |
|
|
64
|
+
| ❌ Not logged in | "Sign in" / "Log in" text, email/password fields | **STOP** — report to user |
|
|
65
|
+
| ⚠️ Rate limited | "Rate limit" / "Try again later" / error banner | **STOP** — screenshot and report |
|
|
66
|
+
| ⚠️ Account suspended | "Your account is suspended" message | **STOP** — screenshot and report |
|
|
67
|
+
|
|
68
|
+
**If not logged in, respond with:**
|
|
69
|
+
> ⚠️ Twitter 未登录。请先在 Chrome OpenClaw profile 中手动登录 x.com,然后重试。
|
|
70
|
+
|
|
71
|
+
Do NOT attempt to enter credentials or guess passwords. This is a hard stop.
|
|
72
|
+
|
|
73
|
+
### Step 3: Type Content
|
|
74
|
+
|
|
75
|
+
```javascript
|
|
76
|
+
// Click the compose text area first to focus it
|
|
77
|
+
await browser.act("click the tweet compose text area");
|
|
78
|
+
await browser.wait(500);
|
|
79
|
+
|
|
80
|
+
// Type the content character by character (do NOT paste)
|
|
81
|
+
await browser.type("Your tweet content here #OpenClaw");
|
|
82
|
+
await browser.wait(1000); // wait for content to settle
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Content rules (MUST follow):**
|
|
86
|
+
|
|
87
|
+
| Rule | Spec |
|
|
88
|
+
|------|------|
|
|
89
|
+
| Mode | New post ONLY — never reply to existing tweets |
|
|
90
|
+
| Length | Maximum 200 characters (leave room for rendering) |
|
|
91
|
+
| Emoji | 1–3 emojis, placed naturally (not emoji spam) |
|
|
92
|
+
| Hashtags | 1–3 relevant hashtags at the end |
|
|
93
|
+
| Language | Match user's language preference |
|
|
94
|
+
| Links | Only if the user explicitly provides them |
|
|
95
|
+
| Media | Text-only by default (see Media Attachments section below) |
|
|
96
|
+
|
|
97
|
+
**Content template:**
|
|
98
|
+
```
|
|
99
|
+
[Core message, 1-2 sentences] [1-2 emoji]
|
|
100
|
+
|
|
101
|
+
#Tag1 #Tag2
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**Why type() not paste?** Twitter's CSP and anti-bot measures may block clipboard paste events. `type()` simulates real keyboard input and is consistently more reliable.
|
|
105
|
+
|
|
106
|
+
### Step 4: Screenshot for Confirmation
|
|
107
|
+
|
|
108
|
+
```javascript
|
|
109
|
+
// MANDATORY — never skip this step
|
|
110
|
+
const screenshot = await browser.screenshot();
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Present the screenshot to the user and say:
|
|
114
|
+
> 📸 请确认推文内容是否正确。确认后我将点击发布。
|
|
115
|
+
|
|
116
|
+
**Wait for user confirmation before proceeding.** If the user wants changes:
|
|
117
|
+
1. Select all text in the compose box: `await browser.act("select all text in the compose area");`
|
|
118
|
+
2. Delete it: `await browser.act("press Backspace");`
|
|
119
|
+
3. Type the new content
|
|
120
|
+
4. Screenshot again
|
|
121
|
+
|
|
122
|
+
### Step 5: Click Post & Verify
|
|
123
|
+
|
|
124
|
+
```javascript
|
|
125
|
+
// Wait for the Post button to be enabled (it takes ~500ms-1s after typing)
|
|
126
|
+
await browser.wait(1500);
|
|
127
|
+
|
|
128
|
+
// Click the Post button
|
|
129
|
+
await browser.act('click the "Post" button');
|
|
130
|
+
|
|
131
|
+
// Wait for the post to be submitted
|
|
132
|
+
await browser.wait(3000);
|
|
133
|
+
|
|
134
|
+
// Take a verification screenshot
|
|
135
|
+
const result = await browser.screenshot();
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
**Success indicators (check in screenshot/snapshot):**
|
|
139
|
+
- "Your post was sent" toast notification at the bottom
|
|
140
|
+
- Compose modal closes and returns to timeline
|
|
141
|
+
- A "View" link appears in the toast
|
|
142
|
+
|
|
143
|
+
**Failure indicators:**
|
|
144
|
+
- Post button still visible → it wasn't clicked
|
|
145
|
+
- "Something went wrong" toast → Twitter server error
|
|
146
|
+
- Error popup / red text → content validation error
|
|
147
|
+
- Page unchanged → JavaScript error, nothing happened
|
|
148
|
+
|
|
149
|
+
**On success, respond:**
|
|
150
|
+
> ✅ 推文已发布成功!
|
|
151
|
+
|
|
152
|
+
**On failure, respond with screenshot:**
|
|
153
|
+
> ❌ 发布失败。截图如上,请检查。[具体错误描述]
|
|
154
|
+
|
|
155
|
+
## Media Attachments (Optional)
|
|
156
|
+
|
|
157
|
+
If the user wants to attach an image:
|
|
158
|
+
|
|
159
|
+
```javascript
|
|
160
|
+
// After Step 3 (typing content), before Step 4 (screenshot):
|
|
161
|
+
await browser.act('click the "Add photos or video" button (media icon)');
|
|
162
|
+
await browser.wait(1000);
|
|
163
|
+
// The file picker will open — this requires user interaction
|
|
164
|
+
// Report to user: "请在弹出的文件选择器中选择要上传的图片"
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
**Limitations:**
|
|
168
|
+
- File picker is a native OS dialog — the agent cannot select files programmatically.
|
|
169
|
+
- Wait for the image to upload and thumbnail to appear before proceeding.
|
|
170
|
+
- If no media button is found, proceed with text-only post.
|
|
171
|
+
|
|
172
|
+
## Multi-Tweet Handling
|
|
173
|
+
|
|
174
|
+
If the user provides multiple tweets to post:
|
|
175
|
+
|
|
176
|
+
1. **Post them ONE AT A TIME** — complete the full 5-step cycle for each.
|
|
177
|
+
2. **Wait at least 30 seconds between posts** to avoid rate limiting.
|
|
178
|
+
3. **Get confirmation for each tweet separately** — do not batch confirmations.
|
|
179
|
+
4. **If any tweet fails, STOP** — do not continue with remaining tweets.
|
|
180
|
+
|
|
181
|
+
## Error Handling Rules
|
|
182
|
+
|
|
183
|
+
1. **NEVER retry silently.** If posting fails, report the error with a screenshot immediately.
|
|
184
|
+
2. **NEVER guess or fill in credentials.** Login is the user's responsibility.
|
|
185
|
+
3. **NEVER post without user confirmation** (Step 4 screenshot approval).
|
|
186
|
+
4. **NEVER modify the user's content** without telling them.
|
|
187
|
+
5. **Report specific errors:** "Button disabled", "Page not loaded", "Rate limited" — not just "failed".
|
|
188
|
+
6. **One retry maximum:** For "Something went wrong" server errors only, wait 30 seconds and retry the full workflow once. If it fails again, stop.
|
|
189
|
+
|
|
190
|
+
## Common Failure Modes & Fixes
|
|
191
|
+
|
|
192
|
+
| Problem | Cause | Fix |
|
|
193
|
+
|---------|-------|-----|
|
|
194
|
+
| Compose box not found | Page didn't fully load | `wait(5000)` then `snapshot()` to check again |
|
|
195
|
+
| Post button disabled | Content empty or invalid | Verify text was actually typed via `snapshot()` |
|
|
196
|
+
| "Something went wrong" | Twitter server error | Wait 30s, retry once, then report |
|
|
197
|
+
| Page shows login form | Session expired | Ask user to re-login in Chrome profile |
|
|
198
|
+
| Content truncated | Typed too fast | Add `wait(500)` between segments if content is long |
|
|
199
|
+
| Modal doesn't open | URL didn't navigate properly | Try `navigate()` again with a fresh page load |
|
|
200
|
+
| "Post" button not found | UI layout changed | Use `snapshot()` to find the actual button text/label |
|
|
201
|
+
|
|
202
|
+
## Lessons Learned (经验教训)
|
|
203
|
+
|
|
204
|
+
1. **`/compose/post` is king** — Sidebar "Post" button and keyboard shortcuts are unreliable across UI updates. Always navigate directly.
|
|
205
|
+
2. **Type, don't paste** — `type()` simulates human input; paste may be blocked by Twitter's CSP.
|
|
206
|
+
3. **Always screenshot before posting** — This is the single biggest reliability improvement. Users catch errors, and you have evidence of what was attempted.
|
|
207
|
+
4. **Wait before clicking Post** — The Post button takes 500ms–1s to become active after content is entered. Clicking too early does nothing.
|
|
208
|
+
5. **One post per session** — Don't chain multiple posts without spacing. Complete one full cycle before starting another.
|
|
209
|
+
6. **Check toast, not URL** — After posting, the URL may not change immediately. Look for the success toast notification.
|
|
210
|
+
7. **snapshot() over screenshot() for logic** — Use `snapshot()` (DOM text) for programmatic checks, `screenshot()` (image) for user-facing confirmation.
|
|
211
|
+
8. **act() is your friend** — When specific selectors break, describe the action in natural language: `act('click the blue Post button')` is more robust than CSS selectors.
|
package/index.js
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* OpenClaw Twitter Skill CLI (oct-post)
|
|
5
|
+
*
|
|
6
|
+
* Provides a CLI interface for the standardized 5-step Twitter posting workflow.
|
|
7
|
+
* In standalone mode, this validates and previews the workflow.
|
|
8
|
+
* When used by an OpenClaw agent, the SKILL.md drives the actual browser execution.
|
|
9
|
+
*
|
|
10
|
+
* Zero dependencies — uses only Node.js built-in modules.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
|
|
18
|
+
// ─── Helpers ───────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
const NO_COLOR = process.env.NO_COLOR || !process.stdout.isTTY;
|
|
21
|
+
|
|
22
|
+
const C = NO_COLOR
|
|
23
|
+
? { reset: '', red: '', green: '', yellow: '', cyan: '', dim: '', bold: '' }
|
|
24
|
+
: {
|
|
25
|
+
reset: '\x1b[0m',
|
|
26
|
+
red: '\x1b[31m',
|
|
27
|
+
green: '\x1b[32m',
|
|
28
|
+
yellow: '\x1b[33m',
|
|
29
|
+
cyan: '\x1b[36m',
|
|
30
|
+
dim: '\x1b[2m',
|
|
31
|
+
bold: '\x1b[1m',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function log(icon, msg) { console.log(`${icon} ${msg}`); }
|
|
35
|
+
function ok(msg) { log(`${C.green}✔${C.reset}`, msg); }
|
|
36
|
+
function warn(msg) { log(`${C.yellow}⚠${C.reset}`, msg); }
|
|
37
|
+
function fail(msg) { log(`${C.red}✖${C.reset}`, msg); }
|
|
38
|
+
function info(msg) { log(`${C.cyan}ℹ${C.reset}`, msg); }
|
|
39
|
+
function step(n, msg) { console.log(`\n${C.cyan}[Step ${n}/5]${C.reset} ${msg}`); }
|
|
40
|
+
|
|
41
|
+
function getPkg() {
|
|
42
|
+
return JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf-8'));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ─── Content Validation ────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Validate tweet content against posting rules.
|
|
49
|
+
* Returns { valid, errors[], warnings[], stats{} }
|
|
50
|
+
*/
|
|
51
|
+
function validateContent(text) {
|
|
52
|
+
const errors = [];
|
|
53
|
+
const warnings = [];
|
|
54
|
+
|
|
55
|
+
if (!text || text.trim().length === 0) {
|
|
56
|
+
errors.push('Content is empty.');
|
|
57
|
+
return { valid: false, errors, warnings, stats: {} };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const trimmed = text.trim();
|
|
61
|
+
const len = trimmed.length;
|
|
62
|
+
|
|
63
|
+
// Hashtags: support ASCII and Unicode word chars (CJK, etc.)
|
|
64
|
+
const hashtags = trimmed.match(/#[\w\u4e00-\u9fff\u3400-\u4dbf\uF900-\uFAFF]+/gu) || [];
|
|
65
|
+
|
|
66
|
+
// Emojis: broad Unicode emoji detection
|
|
67
|
+
const emojis = trimmed.match(
|
|
68
|
+
/[\u{1F600}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}\u{1F300}-\u{1F5FF}\u{1F680}-\u{1F6FF}\u{1F1E0}-\u{1F1FF}\u{1FA00}-\u{1FA6F}\u{1FA70}-\u{1FAFF}\u{2702}-\u{27B0}\u{FE00}-\u{FE0F}\u{200D}\u{20E3}]/gu
|
|
69
|
+
) || [];
|
|
70
|
+
|
|
71
|
+
// Hard limit: Twitter's 280 chars
|
|
72
|
+
if (len > 280) {
|
|
73
|
+
errors.push(`Exceeds Twitter limit: ${len}/280 characters.`);
|
|
74
|
+
} else if (len > 200) {
|
|
75
|
+
warnings.push(`Content is ${len} chars. Recommended < 200 for readability.`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Hashtag guidance
|
|
79
|
+
if (hashtags.length > 3) {
|
|
80
|
+
warnings.push(`Too many hashtags (${hashtags.length}). Recommended 1–3.`);
|
|
81
|
+
} else if (hashtags.length === 0) {
|
|
82
|
+
warnings.push('No hashtags found. Consider adding 1–3 relevant tags.');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Emoji guidance
|
|
86
|
+
if (emojis.length > 5) {
|
|
87
|
+
warnings.push(`Many emojis (${emojis.length}). Recommended 1–3.`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Check for potential reply patterns
|
|
91
|
+
if (trimmed.startsWith('@')) {
|
|
92
|
+
warnings.push('Content starts with @mention — this may create a reply instead of a new post.');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
valid: errors.length === 0,
|
|
97
|
+
errors,
|
|
98
|
+
warnings,
|
|
99
|
+
stats: { length: len, hashtags: hashtags.length, emojis: emojis.length },
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Expose for testing
|
|
104
|
+
module.exports = { validateContent };
|
|
105
|
+
|
|
106
|
+
// ─── Commands ──────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
function cmdPost(content, opts) {
|
|
109
|
+
console.log(`\n${'─'.repeat(50)}`);
|
|
110
|
+
console.log(` 🐦 OpenClaw Twitter Posting Workflow`);
|
|
111
|
+
console.log(`${'─'.repeat(50)}`);
|
|
112
|
+
|
|
113
|
+
const result = validateContent(content);
|
|
114
|
+
|
|
115
|
+
if (!result.valid) {
|
|
116
|
+
result.errors.forEach(e => fail(e));
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
result.warnings.forEach(w => warn(w));
|
|
120
|
+
|
|
121
|
+
info(`Content: ${result.stats.length} chars | ${result.stats.hashtags} hashtags | ${result.stats.emojis} emojis`);
|
|
122
|
+
|
|
123
|
+
step(1, 'Navigate to https://x.com/compose/post');
|
|
124
|
+
ok('Open compose page');
|
|
125
|
+
|
|
126
|
+
step(2, 'Verify login status');
|
|
127
|
+
ok('Check for compose text area');
|
|
128
|
+
|
|
129
|
+
step(3, 'Type content');
|
|
130
|
+
console.log(`${C.dim} "${content}"${C.reset}`);
|
|
131
|
+
ok('Content entered');
|
|
132
|
+
|
|
133
|
+
step(4, 'Screenshot for confirmation');
|
|
134
|
+
if (opts.skipConfirm) {
|
|
135
|
+
warn('--skip-confirm: Skipping user confirmation (use with caution)');
|
|
136
|
+
} else {
|
|
137
|
+
ok('Screenshot taken → awaiting user confirmation');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
step(5, 'Click Post & verify');
|
|
141
|
+
ok('Click "Post" button → check success toast');
|
|
142
|
+
|
|
143
|
+
console.log(`\n${'─'.repeat(50)}`);
|
|
144
|
+
if (opts.dryRun) {
|
|
145
|
+
warn('DRY RUN — No actual posting. Workflow validated successfully.');
|
|
146
|
+
} else {
|
|
147
|
+
ok('Workflow ready. Agent will execute via browser tools.');
|
|
148
|
+
}
|
|
149
|
+
console.log(`${'─'.repeat(50)}\n`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function cmdValidate(content) {
|
|
153
|
+
console.log('\n🔍 Validating tweet content...\n');
|
|
154
|
+
const result = validateContent(content);
|
|
155
|
+
|
|
156
|
+
if (result.valid) {
|
|
157
|
+
ok('Content is valid');
|
|
158
|
+
} else {
|
|
159
|
+
result.errors.forEach(e => fail(e));
|
|
160
|
+
}
|
|
161
|
+
result.warnings.forEach(w => warn(w));
|
|
162
|
+
|
|
163
|
+
console.log(`\n📊 Stats:`);
|
|
164
|
+
console.log(` Length: ${result.stats.length}/280 (recommended < 200)`);
|
|
165
|
+
console.log(` Hashtags: ${result.stats.hashtags} (recommended 1–3)`);
|
|
166
|
+
console.log(` Emojis: ${result.stats.emojis} (recommended 1–3)`);
|
|
167
|
+
|
|
168
|
+
process.exit(result.valid ? 0 : 1);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function cmdInfo() {
|
|
172
|
+
const pkg = getPkg();
|
|
173
|
+
console.log(`\n🐦 ${C.bold}${pkg.name}${C.reset} v${pkg.version}`);
|
|
174
|
+
console.log(` ${pkg.description}\n`);
|
|
175
|
+
console.log(`${C.bold}Workflow:${C.reset} 5-step standardized posting`);
|
|
176
|
+
console.log(' 1. Open → https://x.com/compose/post');
|
|
177
|
+
console.log(' 2. Login → verify compose box visible');
|
|
178
|
+
console.log(' 3. Type → enter content (< 200 chars)');
|
|
179
|
+
console.log(' 4. Shot → screenshot for user confirmation');
|
|
180
|
+
console.log(' 5. Post → click button & verify success\n');
|
|
181
|
+
console.log(`${C.dim}Homepage: ${pkg.homepage}${C.reset}`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ─── CLI Parsing (zero-dependency) ─────────────────────────
|
|
185
|
+
|
|
186
|
+
function printHelp() {
|
|
187
|
+
const pkg = getPkg();
|
|
188
|
+
console.log(`
|
|
189
|
+
${C.bold}${pkg.name}${C.reset} v${pkg.version}
|
|
190
|
+
|
|
191
|
+
${C.bold}USAGE${C.reset}
|
|
192
|
+
oct-post <command> [options]
|
|
193
|
+
|
|
194
|
+
${C.bold}COMMANDS${C.reset}
|
|
195
|
+
post <content> Simulate the 5-step posting workflow
|
|
196
|
+
validate <content> Validate tweet content against rules
|
|
197
|
+
info Show skill info and workflow summary
|
|
198
|
+
|
|
199
|
+
${C.bold}OPTIONS${C.reset}
|
|
200
|
+
--dry-run Validate only, don't mark as ready to post
|
|
201
|
+
--skip-confirm Skip screenshot confirmation step
|
|
202
|
+
-h, --help Show this help message
|
|
203
|
+
-v, --version Show version number
|
|
204
|
+
|
|
205
|
+
${C.bold}EXAMPLES${C.reset}
|
|
206
|
+
oct-post post "Hello from OpenClaw! 🚀 #AI #Automation"
|
|
207
|
+
oct-post post "Draft tweet #draft" --dry-run
|
|
208
|
+
oct-post validate "Testing content #test"
|
|
209
|
+
oct-post info
|
|
210
|
+
`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function main() {
|
|
214
|
+
const args = process.argv.slice(2);
|
|
215
|
+
|
|
216
|
+
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
|
|
217
|
+
printHelp();
|
|
218
|
+
process.exit(0);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (args[0] === '--version' || args[0] === '-v') {
|
|
222
|
+
console.log(getPkg().version);
|
|
223
|
+
process.exit(0);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const cmd = args[0];
|
|
227
|
+
|
|
228
|
+
switch (cmd) {
|
|
229
|
+
case 'post': {
|
|
230
|
+
const content = args.find((a, i) => i > 0 && !a.startsWith('--'));
|
|
231
|
+
if (!content) {
|
|
232
|
+
fail('Missing content. Usage: oct-post post "your tweet"');
|
|
233
|
+
process.exit(1);
|
|
234
|
+
}
|
|
235
|
+
cmdPost(content, {
|
|
236
|
+
dryRun: args.includes('--dry-run'),
|
|
237
|
+
skipConfirm: args.includes('--skip-confirm'),
|
|
238
|
+
});
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
case 'validate': {
|
|
242
|
+
const content = args.find((a, i) => i > 0 && !a.startsWith('--'));
|
|
243
|
+
if (!content) {
|
|
244
|
+
fail('Missing content. Usage: oct-post validate "your tweet"');
|
|
245
|
+
process.exit(1);
|
|
246
|
+
}
|
|
247
|
+
cmdValidate(content);
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
case 'info':
|
|
251
|
+
cmdInfo();
|
|
252
|
+
break;
|
|
253
|
+
default:
|
|
254
|
+
fail(`Unknown command: ${cmd}`);
|
|
255
|
+
printHelp();
|
|
256
|
+
process.exit(1);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Only run CLI when executed directly (not when require'd for testing)
|
|
261
|
+
if (require.main === module) {
|
|
262
|
+
main();
|
|
263
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "openclaw-twitter-skill",
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "OpenClaw agent skill for stable automated Twitter (X) posting via browser tools. Standardized 5-step workflow with login check, content validation, screenshot confirmation, and error reporting.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"oct-post": "index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"index.js",
|
|
11
|
+
"SKILL.md",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"test": "node tests/test.js",
|
|
17
|
+
"prepublishOnly": "node tests/test.js"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"openclaw",
|
|
21
|
+
"twitter",
|
|
22
|
+
"x",
|
|
23
|
+
"automation",
|
|
24
|
+
"skill",
|
|
25
|
+
"agent",
|
|
26
|
+
"browser",
|
|
27
|
+
"social-media",
|
|
28
|
+
"posting"
|
|
29
|
+
],
|
|
30
|
+
"author": "xiaoxi <xiaoxi@openclaw.ai>",
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/adminlove520/openclaw-twitter-skill.git"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://github.com/adminlove520/openclaw-twitter-skill#readme",
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/adminlove520/openclaw-twitter-skill/issues"
|
|
39
|
+
},
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=16.0.0"
|
|
42
|
+
}
|
|
43
|
+
}
|