social-autoposter 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +85 -0
- package/SKILL.md +317 -0
- package/bin/cli.js +158 -0
- package/config.example.json +51 -0
- package/launchd/com.m13v.social-autoposter.plist +28 -0
- package/launchd/com.m13v.social-engage.plist +28 -0
- package/launchd/com.m13v.social-stats.plist +28 -0
- package/package.json +42 -0
- package/schema.sql +82 -0
- package/scripts/find_threads.py +194 -0
- package/scripts/scan_replies.py +370 -0
- package/scripts/update_stats.py +208 -0
- package/setup/SKILL.md +180 -0
- package/skill/SKILL.md +283 -0
- package/skill/engage.sh +109 -0
- package/skill/run.sh +41 -0
- package/skill/stats.sh +38 -0
- package/syncfield.sh +78 -0
package/README.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# social-autoposter
|
|
2
|
+
|
|
3
|
+
Automated social posting pipeline for Reddit, X/Twitter, LinkedIn, and Moltbook. Install as an AI agent skill or use the standalone Python scripts.
|
|
4
|
+
|
|
5
|
+
[**Browse the data in Datasette Lite**](https://lite.datasette.io/?url=https://raw.githubusercontent.com/m13v/social-autoposter/main/social_posts.db)
|
|
6
|
+
|
|
7
|
+
## Install as a skill
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx social-autoposter init
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Then tell your agent: **"set up social autoposter"** — the setup skill walks you through config, DB creation, browser logins, and a test run.
|
|
14
|
+
|
|
15
|
+
To update scripts without touching your config or database:
|
|
16
|
+
```bash
|
|
17
|
+
npx social-autoposter update
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Or set up manually:
|
|
21
|
+
```bash
|
|
22
|
+
cp config.example.json config.json # edit with your accounts
|
|
23
|
+
sqlite3 social_posts.db < schema.sql # create the database
|
|
24
|
+
bash setup.sh # symlinks + launchd (macOS)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## How it works
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
SKILL.md (the playbook)
|
|
31
|
+
│
|
|
32
|
+
├── /social-autoposter → find thread, draft, post, log
|
|
33
|
+
├── /social-autoposter stats → update engagement via API
|
|
34
|
+
├── /social-autoposter engage → scan replies, respond
|
|
35
|
+
└── /social-autoposter audit → browser-based full audit
|
|
36
|
+
│
|
|
37
|
+
├── scripts/find_threads.py → thread discovery (no browser)
|
|
38
|
+
├── scripts/scan_replies.py → reply scanning (no browser)
|
|
39
|
+
└── scripts/update_stats.py → stats fetching (no browser)
|
|
40
|
+
│
|
|
41
|
+
├── skill/run.sh → launchd wrapper (hourly)
|
|
42
|
+
├── skill/stats.sh → launchd wrapper (6-hourly)
|
|
43
|
+
└── skill/engage.sh → launchd wrapper (2-hourly)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Structure
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
social-autoposter/
|
|
50
|
+
├── SKILL.md <- skill playbook (generic, publishable)
|
|
51
|
+
├── config.example.json <- config template (accounts, subreddits, content angle)
|
|
52
|
+
├── schema.sql <- DB schema
|
|
53
|
+
├── setup.sh <- creates symlinks, loads launchd agents
|
|
54
|
+
├── setup/
|
|
55
|
+
│ └── SKILL.md <- interactive setup wizard skill
|
|
56
|
+
├── scripts/
|
|
57
|
+
│ ├── find_threads.py <- find candidate threads via Reddit/Moltbook API
|
|
58
|
+
│ ├── scan_replies.py <- scan for new replies to our posts via API
|
|
59
|
+
│ └── update_stats.py <- fetch engagement stats via API
|
|
60
|
+
├── skill/
|
|
61
|
+
│ ├── SKILL.md <- personal skill (hardcoded accounts)
|
|
62
|
+
│ ├── run.sh <- hourly posting (launchd wrapper)
|
|
63
|
+
│ ├── stats.sh <- 6-hourly stats (launchd wrapper)
|
|
64
|
+
│ ├── engage.sh <- 2-hourly engagement (launchd wrapper)
|
|
65
|
+
│ └── logs/ <- runtime logs (gitignored)
|
|
66
|
+
├── social_posts.db <- SQLite database (committed for Datasette)
|
|
67
|
+
├── syncfield.sh <- sync SQLite -> Neon Postgres
|
|
68
|
+
└── launchd/ <- macOS LaunchAgent plists
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## For other AI agents
|
|
72
|
+
|
|
73
|
+
The skill is designed to work with any agent that has:
|
|
74
|
+
- **Shell access** (to run Python scripts and sqlite3)
|
|
75
|
+
- **Browser automation** (Playwright, Selenium, etc. for posting)
|
|
76
|
+
- **An LLM** (for drafting comments in the right tone)
|
|
77
|
+
|
|
78
|
+
The Python scripts handle thread discovery, reply scanning, and stats updates without needing a browser or LLM. The SKILL.md is the playbook — any agent reads it and executes the workflows with its own tools.
|
|
79
|
+
|
|
80
|
+
## Accounts
|
|
81
|
+
|
|
82
|
+
- **Reddit**: u/Deep_Ad1959
|
|
83
|
+
- **X/Twitter**: @m13v_
|
|
84
|
+
- **LinkedIn**: Matthew Diakonov
|
|
85
|
+
- **Moltbook**: matthew-autoposter
|
package/SKILL.md
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: social-autoposter
|
|
3
|
+
description: "Automate social media posting across Reddit, X/Twitter, LinkedIn, and Moltbook. Find threads, post comments, track engagement stats, and handle reply engagement. Use when: 'post to social', 'social autoposter', 'find threads', 'audit social posts', 'update post stats', 'engage replies'. Run /social-autoposter to start."
|
|
4
|
+
user_invocable: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Social Autoposter
|
|
8
|
+
|
|
9
|
+
Automates finding, posting, and tracking social media comments. Works with any agent that has browser automation and shell access.
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
| Command | What it does |
|
|
14
|
+
|---------|-------------|
|
|
15
|
+
| `/social-autoposter` | Full posting run (find threads + post + log) |
|
|
16
|
+
| `/social-autoposter stats` | Update engagement stats via API |
|
|
17
|
+
| `/social-autoposter engage` | Scan and reply to responses on our posts |
|
|
18
|
+
| `/social-autoposter audit` | Full browser audit of all posts |
|
|
19
|
+
|
|
20
|
+
## Config
|
|
21
|
+
|
|
22
|
+
All personal settings live in `config.json` (copy from `config.example.json`). The skill reads this at runtime.
|
|
23
|
+
|
|
24
|
+
Key fields:
|
|
25
|
+
- `accounts` — platform usernames/handles
|
|
26
|
+
- `subreddits` — target subreddits to monitor
|
|
27
|
+
- `content_angle` — your unique perspective for authentic comments
|
|
28
|
+
- `projects` — your products/repos to mention when relevant (with topic keywords)
|
|
29
|
+
- `database` — path to SQLite DB
|
|
30
|
+
|
|
31
|
+
## Helper Scripts
|
|
32
|
+
|
|
33
|
+
Standalone Python scripts any agent can call directly. No LLM needed for these.
|
|
34
|
+
|
|
35
|
+
| Script | What it does |
|
|
36
|
+
|--------|-------------|
|
|
37
|
+
| `scripts/find_threads.py` | Find candidate threads via Reddit JSON + Moltbook API |
|
|
38
|
+
| `scripts/scan_replies.py` | Scan for new replies to our posts via API |
|
|
39
|
+
| `scripts/update_stats.py` | Fetch engagement stats via API, update DB |
|
|
40
|
+
|
|
41
|
+
Examples:
|
|
42
|
+
```bash
|
|
43
|
+
python3 scripts/find_threads.py --topic "macOS automation" --limit 5
|
|
44
|
+
python3 scripts/scan_replies.py
|
|
45
|
+
python3 scripts/update_stats.py --quiet
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Critical: No Parallel Posting
|
|
51
|
+
|
|
52
|
+
**NEVER launch multiple agents or parallel tasks for posting.** All posting operations (comments, replies, thread creation) MUST be done sequentially in a single agent. Reasons:
|
|
53
|
+
- There is only one shared browser — parallel agents fight over it and cause timeouts
|
|
54
|
+
- Parallel agents cause duplicate posts (same comment posted twice on the same thread)
|
|
55
|
+
- Browser lock conflicts lead to unpredictable failures
|
|
56
|
+
|
|
57
|
+
This applies to ALL posting workflows: comments on existing threads, self-replies with links, new thread creation, and reply engagement. Even if you have 5 threads to post on, do them one at a time in the same agent.
|
|
58
|
+
|
|
59
|
+
**After each post, always verify** by reloading the page and confirming the comment appears exactly once before moving to the next post.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Workflow: Post (`/social-autoposter`)
|
|
64
|
+
|
|
65
|
+
Find a thread, draft a comment, post it, log it.
|
|
66
|
+
|
|
67
|
+
### 1. Rate limit check
|
|
68
|
+
|
|
69
|
+
```sql
|
|
70
|
+
SELECT COUNT(*) FROM posts WHERE posted_at >= datetime('now', '-24 hours')
|
|
71
|
+
```
|
|
72
|
+
If 40+ posts in the last 24 hours, stop. Max 40/day.
|
|
73
|
+
|
|
74
|
+
### 2. Find candidate threads
|
|
75
|
+
|
|
76
|
+
**Option A — Use the helper script (preferred, no browser needed):**
|
|
77
|
+
```bash
|
|
78
|
+
python3 scripts/find_threads.py --include-moltbook
|
|
79
|
+
```
|
|
80
|
+
This returns a JSON list of candidate threads with dedup already applied.
|
|
81
|
+
|
|
82
|
+
**Option B — Browse manually with browser automation:**
|
|
83
|
+
Browse `/new` and `/hot` across target subreddits (from `config.json`). Also check Moltbook via API.
|
|
84
|
+
|
|
85
|
+
### 3. Pick the best thread
|
|
86
|
+
|
|
87
|
+
Requirements:
|
|
88
|
+
- You have a genuine angle from your `content_angle` in `config.json`
|
|
89
|
+
- The thread hasn't been posted in before (check `thread_url` in DB)
|
|
90
|
+
- Your last 5 comments don't repeat the same talking points:
|
|
91
|
+
```sql
|
|
92
|
+
SELECT our_content FROM posts ORDER BY id DESC LIMIT 5
|
|
93
|
+
```
|
|
94
|
+
- If no thread fits naturally, **stop**. Better to skip than force a bad comment.
|
|
95
|
+
|
|
96
|
+
### 4. Read the thread
|
|
97
|
+
|
|
98
|
+
Before commenting, read the full thread:
|
|
99
|
+
- Check tone (casual/technical/professional)
|
|
100
|
+
- Read top comments for length and style cues
|
|
101
|
+
- Note thread age (skip stale threads)
|
|
102
|
+
- Identify the best comment to reply to (high-upvote comments get more visibility)
|
|
103
|
+
|
|
104
|
+
### 5. Draft the comment
|
|
105
|
+
|
|
106
|
+
Follow the **Content Rules** section below. Key points:
|
|
107
|
+
- 2-3 sentences max, match thread energy
|
|
108
|
+
- First person, specific details from your experience
|
|
109
|
+
- No product links in top-level comments (use Tiered Reply Strategy for that)
|
|
110
|
+
- If it sounds like a blog post, rewrite it
|
|
111
|
+
|
|
112
|
+
### 6. Post it
|
|
113
|
+
|
|
114
|
+
**Reddit** (browser automation):
|
|
115
|
+
- Navigate to `old.reddit.com` thread URL
|
|
116
|
+
- Find the reply box (textarea with class `usertext-edit`)
|
|
117
|
+
- Type the comment, click submit
|
|
118
|
+
- Wait 2-3 seconds, verify the comment appeared
|
|
119
|
+
- Capture the permalink of the new comment
|
|
120
|
+
- Close the tab
|
|
121
|
+
|
|
122
|
+
**X/Twitter** (browser automation):
|
|
123
|
+
- Navigate to the tweet
|
|
124
|
+
- Type reply in the reply box, click Reply
|
|
125
|
+
- Verify the reply posted
|
|
126
|
+
- Capture the URL
|
|
127
|
+
|
|
128
|
+
**LinkedIn** (browser automation):
|
|
129
|
+
- Navigate to the post
|
|
130
|
+
- Type comment, click Post
|
|
131
|
+
- No stable URL available, note as posted
|
|
132
|
+
|
|
133
|
+
**Moltbook** (API — no browser needed):
|
|
134
|
+
```bash
|
|
135
|
+
source ~/social-autoposter/.env
|
|
136
|
+
curl -s -X POST -H "Authorization: Bearer $MOLTBOOK_API_KEY" \
|
|
137
|
+
-H "Content-Type: application/json" \
|
|
138
|
+
-d '{"title": "...", "content": "...", "type": "text", "submolt_name": "general"}' \
|
|
139
|
+
"https://www.moltbook.com/api/v1/posts"
|
|
140
|
+
```
|
|
141
|
+
Verify: fetch post by UUID, check `verification_status` is `"verified"`.
|
|
142
|
+
On Moltbook, write as an agent: "my human" not "I".
|
|
143
|
+
Rate limit: max 1 post per 30 minutes.
|
|
144
|
+
|
|
145
|
+
### 7. Log to database
|
|
146
|
+
|
|
147
|
+
```sql
|
|
148
|
+
INSERT INTO posts (platform, thread_url, thread_author, thread_author_handle,
|
|
149
|
+
thread_title, thread_content, our_url, our_content, our_account,
|
|
150
|
+
source_summary, status, posted_at)
|
|
151
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'active', datetime('now'));
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### 8. Sync (if configured)
|
|
155
|
+
|
|
156
|
+
If `sync_script` is set in `config.json`, run it to push data to a remote database.
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## Workflow: Stats (`/social-autoposter stats`)
|
|
161
|
+
|
|
162
|
+
Update engagement metrics for all posts. No browser needed.
|
|
163
|
+
|
|
164
|
+
**Preferred: use the helper script:**
|
|
165
|
+
```bash
|
|
166
|
+
python3 scripts/update_stats.py
|
|
167
|
+
python3 scripts/update_stats.py --quiet # summary only
|
|
168
|
+
python3 scripts/update_stats.py --json # machine-readable output
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
This fetches:
|
|
172
|
+
- **Reddit**: comment scores and thread stats via public JSON API. Detects deleted/removed.
|
|
173
|
+
- **Moltbook**: upvotes and comment counts via REST API. Detects deleted posts.
|
|
174
|
+
|
|
175
|
+
Updates `upvotes`, `comments_count`, `thread_engagement`, `engagement_updated_at` in the DB.
|
|
176
|
+
|
|
177
|
+
For **X/Twitter** stats (requires browser): use `/social-autoposter audit` instead.
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## Workflow: Engage (`/social-autoposter engage`)
|
|
182
|
+
|
|
183
|
+
Discover replies to our posts and respond to them.
|
|
184
|
+
|
|
185
|
+
### Phase A: Scan for replies (no browser needed)
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
python3 scripts/scan_replies.py
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
This scans Reddit JSON API + Moltbook API for new replies. Inserts into `replies` table as `pending` or `skipped`.
|
|
192
|
+
|
|
193
|
+
Skip reasons: `too_short` (<5 words), `filtered_author` (AutoModerator, [deleted], self), `too_old` (>7 days), `deleted`.
|
|
194
|
+
|
|
195
|
+
### Phase B: Respond to pending replies (needs browser/API)
|
|
196
|
+
|
|
197
|
+
Query pending replies:
|
|
198
|
+
```sql
|
|
199
|
+
SELECT r.id, r.platform, r.their_author, r.their_content, r.their_comment_url,
|
|
200
|
+
r.depth, p.thread_title, p.our_content
|
|
201
|
+
FROM replies r
|
|
202
|
+
JOIN posts p ON r.post_id = p.id
|
|
203
|
+
WHERE r.status='pending'
|
|
204
|
+
ORDER BY r.discovered_at ASC LIMIT 10
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
For each pending reply:
|
|
208
|
+
1. Draft a response (2-4 sentences, casual, expand the topic, ask follow-ups)
|
|
209
|
+
2. Apply the **Tiered Reply Strategy** for project mentions
|
|
210
|
+
3. Post via browser (Reddit/X) or API (Moltbook)
|
|
211
|
+
4. Update the reply record:
|
|
212
|
+
```sql
|
|
213
|
+
UPDATE replies SET status='replied', our_reply_content=?, our_reply_url=?,
|
|
214
|
+
replied_at=datetime('now') WHERE id=?
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Max 5 replies per run.
|
|
218
|
+
|
|
219
|
+
### Phase C: X/Twitter replies (browser required)
|
|
220
|
+
|
|
221
|
+
X has no public API for notifications. To discover X replies:
|
|
222
|
+
1. Navigate to `https://x.com/notifications/mentions`
|
|
223
|
+
2. Extract mentions replying to your account
|
|
224
|
+
3. Filter out already-tracked reply IDs, light acknowledgments, and your own replies
|
|
225
|
+
4. Respond to substantive replies (max 5)
|
|
226
|
+
5. Log everything to `replies` table
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## Workflow: Audit (`/social-autoposter audit`)
|
|
231
|
+
|
|
232
|
+
Full browser-based audit of all posts. Use for X/Twitter stats or visual verification.
|
|
233
|
+
|
|
234
|
+
1. Query all posts with URLs:
|
|
235
|
+
```sql
|
|
236
|
+
SELECT id, platform, our_url, status, upvotes, views, comments_count
|
|
237
|
+
FROM posts WHERE our_url IS NOT NULL ORDER BY posted_at DESC
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
2. Visit each URL via browser automation. Check:
|
|
241
|
+
- `active`: visible and accessible
|
|
242
|
+
- `deleted`: 404 or "this tweet has been deleted"
|
|
243
|
+
- `removed`: marked as removed by moderator
|
|
244
|
+
- `inactive`: thread locked or archived
|
|
245
|
+
|
|
246
|
+
3. Capture engagement metrics (upvotes, views, comments) and update DB.
|
|
247
|
+
|
|
248
|
+
4. Report summary: total checked, by status, top performers.
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## Content Rules
|
|
253
|
+
|
|
254
|
+
1. **Write like you're texting a coworker.** Lowercase fine. Sentence fragments fine. Never start with "Makes sense" or "The nuance here is." If it sounds like a blog post, rewrite it.
|
|
255
|
+
|
|
256
|
+
2. **First person, specific.** Use concrete numbers and real experiences, not generalizations. Say "I" not "you" or "one."
|
|
257
|
+
|
|
258
|
+
3. **Reply to top comments, not just OP.** High-upvote comments get more eyeballs.
|
|
259
|
+
|
|
260
|
+
4. **Only comment when you have a real angle from your work.** Use `content_angle` from `config.json`. If the thread doesn't connect to your experience, skip it.
|
|
261
|
+
|
|
262
|
+
5. **No product links in top-level comments.** Earn attention first. Links come later via the Tiered Reply Strategy.
|
|
263
|
+
|
|
264
|
+
6. **On Moltbook, write as an agent.** Use "my human" not "I". First-person agent perspective.
|
|
265
|
+
|
|
266
|
+
7. **Log everything.** Every thread and comment goes in the database.
|
|
267
|
+
|
|
268
|
+
### Bad vs Good
|
|
269
|
+
|
|
270
|
+
BAD: "Makes sense — Claude already tries to `| tail -n 50` on its own but by then the tokens are already in context."
|
|
271
|
+
GOOD: "gonna try this — I run 5 agents in parallel and my API bill is becoming a second rent payment"
|
|
272
|
+
|
|
273
|
+
BAD: "What everyone here is describing is basically specification-driven development."
|
|
274
|
+
GOOD: "I spend more time writing CLAUDE.md specs than I ever spent writing code. the irony is I'm basically doing waterfall now and shipping faster than ever."
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## Tiered Reply Strategy
|
|
279
|
+
|
|
280
|
+
When replying to comments on our posts, escalate project mentions based on conversation context.
|
|
281
|
+
|
|
282
|
+
**Tier 1 — Default (no link):** Genuine engagement. Expand the topic, ask follow-ups. Most replies.
|
|
283
|
+
|
|
284
|
+
**Tier 2 — Natural mention:** Conversation touches something you're building. Mention the project casually. Include link only if it adds value. Triggers:
|
|
285
|
+
- "what are you working on" / "what tool do you use"
|
|
286
|
+
- They describe a problem matching a project's `topics` from `config.json`
|
|
287
|
+
- Conversation is 2+ replies deep (genuine interest)
|
|
288
|
+
|
|
289
|
+
**Tier 3 — Direct ask:** They explicitly ask for a link, to try it, if it's open source. Give it immediately.
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
## Database Schema
|
|
294
|
+
|
|
295
|
+
```sql
|
|
296
|
+
-- Core tables (see schema.sql for full DDL)
|
|
297
|
+
posts -- everything we post (platform, urls, content, engagement, status)
|
|
298
|
+
threads -- threads we've discovered
|
|
299
|
+
our_posts -- backward-compat post tracking
|
|
300
|
+
replies -- replies to our posts and our responses
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
Key fields in `posts`: `id, platform, thread_url, thread_title, our_url, our_content, our_account, posted_at, status, upvotes, comments_count, views, source_summary`
|
|
304
|
+
|
|
305
|
+
Key fields in `replies`: `id, post_id, platform, their_author, their_content, our_reply_content, status (pending|replied|skipped|error), depth`
|
|
306
|
+
|
|
307
|
+
---
|
|
308
|
+
|
|
309
|
+
## Platform Reference
|
|
310
|
+
|
|
311
|
+
**Reddit:** Use `old.reddit.com` for reliable automation. Comment box: textarea with class `usertext-edit`. No posting API — browser only.
|
|
312
|
+
|
|
313
|
+
**X/Twitter:** Reply to existing tweets. 1-2 sentences ideal. No public API for notifications — browser only.
|
|
314
|
+
|
|
315
|
+
**LinkedIn:** Professional tone, brief. Comments don't have stable URLs. Browser only.
|
|
316
|
+
|
|
317
|
+
**Moltbook:** Full REST API, no browser needed. Base: `https://www.moltbook.com/api/v1`. Auth: `Bearer $MOLTBOOK_API_KEY`. Agent-first platform — write as an agent.
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const { spawnSync } = require('child_process');
|
|
8
|
+
|
|
9
|
+
const DEST = path.join(os.homedir(), 'social-autoposter');
|
|
10
|
+
const PKG_ROOT = path.join(__dirname, '..');
|
|
11
|
+
|
|
12
|
+
// Files/dirs to copy from npm package to ~/social-autoposter
|
|
13
|
+
const COPY_TARGETS = [
|
|
14
|
+
'scripts',
|
|
15
|
+
'schema.sql',
|
|
16
|
+
'config.example.json',
|
|
17
|
+
'SKILL.md',
|
|
18
|
+
'skill',
|
|
19
|
+
'setup',
|
|
20
|
+
'launchd',
|
|
21
|
+
'syncfield.sh',
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
// Never overwrite these user files during update
|
|
25
|
+
const USER_FILES = new Set(['config.json', 'social_posts.db', '.env']);
|
|
26
|
+
|
|
27
|
+
function copyDir(src, dest) {
|
|
28
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
29
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
30
|
+
const srcPath = path.join(src, entry.name);
|
|
31
|
+
const destPath = path.join(dest, entry.name);
|
|
32
|
+
if (entry.isDirectory()) {
|
|
33
|
+
copyDir(srcPath, destPath);
|
|
34
|
+
} else {
|
|
35
|
+
fs.copyFileSync(srcPath, destPath);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function linkOrRelink(target, linkPath) {
|
|
41
|
+
try { fs.rmSync(linkPath, { recursive: true, force: true }); } catch {}
|
|
42
|
+
fs.symlinkSync(target, linkPath);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function init() {
|
|
46
|
+
console.log('Setting up social-autoposter in', DEST);
|
|
47
|
+
fs.mkdirSync(DEST, { recursive: true });
|
|
48
|
+
|
|
49
|
+
// Copy all package files
|
|
50
|
+
for (const f of COPY_TARGETS) {
|
|
51
|
+
const src = path.join(PKG_ROOT, f);
|
|
52
|
+
const dest = path.join(DEST, f);
|
|
53
|
+
if (!fs.existsSync(src)) continue;
|
|
54
|
+
const stat = fs.statSync(src);
|
|
55
|
+
if (stat.isDirectory()) {
|
|
56
|
+
copyDir(src, dest);
|
|
57
|
+
} else {
|
|
58
|
+
fs.copyFileSync(src, dest);
|
|
59
|
+
}
|
|
60
|
+
console.log(' copied', f);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// config.json — only if it doesn't exist
|
|
64
|
+
const configDest = path.join(DEST, 'config.json');
|
|
65
|
+
if (!fs.existsSync(configDest)) {
|
|
66
|
+
fs.copyFileSync(path.join(PKG_ROOT, 'config.example.json'), configDest);
|
|
67
|
+
console.log(' created config.json from template');
|
|
68
|
+
} else {
|
|
69
|
+
console.log(' config.json exists — skipping');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Create DB from schema if missing
|
|
73
|
+
const dbPath = path.join(DEST, 'social_posts.db');
|
|
74
|
+
if (!fs.existsSync(dbPath)) {
|
|
75
|
+
const schemaPath = path.join(DEST, 'schema.sql');
|
|
76
|
+
const result = spawnSync('sqlite3', [dbPath], {
|
|
77
|
+
input: fs.readFileSync(schemaPath),
|
|
78
|
+
stdio: ['pipe', 'inherit', 'inherit'],
|
|
79
|
+
});
|
|
80
|
+
if (result.status === 0) {
|
|
81
|
+
console.log(' created social_posts.db');
|
|
82
|
+
} else {
|
|
83
|
+
console.warn(' WARNING: sqlite3 failed — create DB manually:');
|
|
84
|
+
console.warn(' sqlite3 ~/social-autoposter/social_posts.db < ~/social-autoposter/schema.sql');
|
|
85
|
+
}
|
|
86
|
+
} else {
|
|
87
|
+
console.log(' social_posts.db exists — skipping');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Skill symlink: ~/.claude/skills/social-autoposter -> ~/social-autoposter/skill
|
|
91
|
+
const skillsDir = path.join(os.homedir(), '.claude', 'skills');
|
|
92
|
+
fs.mkdirSync(skillsDir, { recursive: true });
|
|
93
|
+
linkOrRelink(path.join(DEST, 'skill'), path.join(skillsDir, 'social-autoposter'));
|
|
94
|
+
console.log(' ~/.claude/skills/social-autoposter ->', path.join(DEST, 'skill'));
|
|
95
|
+
|
|
96
|
+
// DB symlink: ~/.claude/social_posts.db -> ~/social-autoposter/social_posts.db
|
|
97
|
+
const claudeDir = path.join(os.homedir(), '.claude');
|
|
98
|
+
try {
|
|
99
|
+
linkOrRelink(dbPath, path.join(claudeDir, 'social_posts.db'));
|
|
100
|
+
console.log(' ~/.claude/social_posts.db ->', dbPath);
|
|
101
|
+
} catch {}
|
|
102
|
+
|
|
103
|
+
console.log('');
|
|
104
|
+
console.log('Done! Next steps:');
|
|
105
|
+
console.log(' 1. Edit ~/social-autoposter/config.json with your accounts');
|
|
106
|
+
console.log(' 2. Tell your Claude agent: "set up social autoposter"');
|
|
107
|
+
console.log(' (uses the setup/SKILL.md wizard for browser login verification)');
|
|
108
|
+
console.log(' 3. Or configure manually and run: bash ~/social-autoposter/setup.sh');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function update() {
|
|
112
|
+
if (!fs.existsSync(DEST)) {
|
|
113
|
+
console.error('Not installed. Run: npx social-autoposter init');
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
console.log('Updating social-autoposter...');
|
|
118
|
+
|
|
119
|
+
for (const f of COPY_TARGETS) {
|
|
120
|
+
if (USER_FILES.has(f)) {
|
|
121
|
+
console.log(' skipping', f, '(user file)');
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const src = path.join(PKG_ROOT, f);
|
|
125
|
+
const dest = path.join(DEST, f);
|
|
126
|
+
if (!fs.existsSync(src)) continue;
|
|
127
|
+
const stat = fs.statSync(src);
|
|
128
|
+
if (stat.isDirectory()) {
|
|
129
|
+
copyDir(src, dest);
|
|
130
|
+
} else {
|
|
131
|
+
fs.copyFileSync(src, dest);
|
|
132
|
+
}
|
|
133
|
+
console.log(' updated', f);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Re-symlink skill in case it broke
|
|
137
|
+
const skillsDir = path.join(os.homedir(), '.claude', 'skills');
|
|
138
|
+
try {
|
|
139
|
+
linkOrRelink(path.join(DEST, 'skill'), path.join(skillsDir, 'social-autoposter'));
|
|
140
|
+
console.log(' re-linked ~/.claude/skills/social-autoposter');
|
|
141
|
+
} catch {}
|
|
142
|
+
|
|
143
|
+
console.log('');
|
|
144
|
+
console.log('Update complete. config.json and social_posts.db were preserved.');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const cmd = process.argv[2];
|
|
148
|
+
if (cmd === 'init') {
|
|
149
|
+
init();
|
|
150
|
+
} else if (cmd === 'update') {
|
|
151
|
+
update();
|
|
152
|
+
} else {
|
|
153
|
+
console.log('social-autoposter — automated social posting for Claude agents');
|
|
154
|
+
console.log('');
|
|
155
|
+
console.log('Usage:');
|
|
156
|
+
console.log(' npx social-autoposter init first-time setup');
|
|
157
|
+
console.log(' npx social-autoposter update update scripts, preserve config');
|
|
158
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"database": "~/social-autoposter/social_posts.db",
|
|
3
|
+
"prompt_db": "~/claude-prompt-db/prompts.db",
|
|
4
|
+
"sync_script": "~/social-autoposter/syncfield.sh",
|
|
5
|
+
"stats_script": "~/.claude/skills/social-autoposter/stats.sh",
|
|
6
|
+
|
|
7
|
+
"accounts": {
|
|
8
|
+
"reddit": {
|
|
9
|
+
"username": "your-reddit-username",
|
|
10
|
+
"login_method": "browser"
|
|
11
|
+
},
|
|
12
|
+
"twitter": {
|
|
13
|
+
"handle": "@your-twitter-handle",
|
|
14
|
+
"login_method": "browser"
|
|
15
|
+
},
|
|
16
|
+
"linkedin": {
|
|
17
|
+
"name": "Your Name",
|
|
18
|
+
"login_method": "browser"
|
|
19
|
+
},
|
|
20
|
+
"moltbook": {
|
|
21
|
+
"username": "your-moltbook-username",
|
|
22
|
+
"api_key_env": "MOLTBOOK_API_KEY",
|
|
23
|
+
"env_file": "~/social-autoposter/.env"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
"subreddits": ["ClaudeAI", "programming", "webdev", "devops"],
|
|
28
|
+
|
|
29
|
+
"content_angle": "Describe your unique experience/perspective that gives you authentic angles for comments. Example: 'Building a macOS desktop AI agent. Experience with Swift, browser automation, MCP tools, and running 5 Claude agents in parallel.'",
|
|
30
|
+
|
|
31
|
+
"projects": [
|
|
32
|
+
{
|
|
33
|
+
"name": "My Main Project",
|
|
34
|
+
"description": "One-line description of what it does",
|
|
35
|
+
"website": "https://example.com",
|
|
36
|
+
"github": "",
|
|
37
|
+
"topics": ["desktop automation", "AI agents", "browser control"],
|
|
38
|
+
"_comment": "Topics are keywords that trigger mentioning this project in replies. When a conversation touches these topics, the tiered reply strategy may naturally mention this project."
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"name": "My Open Source Tool",
|
|
42
|
+
"description": "What the tool does",
|
|
43
|
+
"website": "",
|
|
44
|
+
"github": "https://github.com/you/your-repo",
|
|
45
|
+
"topics": ["macOS automation", "accessibility APIs"]
|
|
46
|
+
}
|
|
47
|
+
],
|
|
48
|
+
|
|
49
|
+
"open_source_links": [],
|
|
50
|
+
"launchd_label": "com.yourname.social-stats"
|
|
51
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<key>Label</key>
|
|
6
|
+
<string>com.m13v.social-autoposter</string>
|
|
7
|
+
<key>ProgramArguments</key>
|
|
8
|
+
<array>
|
|
9
|
+
<string>/bin/bash</string>
|
|
10
|
+
<string>/Users/matthewdi/.claude/skills/social-autoposter/skill/run.sh</string>
|
|
11
|
+
</array>
|
|
12
|
+
<key>StartInterval</key>
|
|
13
|
+
<integer>3600</integer>
|
|
14
|
+
<key>StandardOutPath</key>
|
|
15
|
+
<string>/Users/matthewdi/.claude/skills/social-autoposter/logs/launchd-stdout.log</string>
|
|
16
|
+
<key>StandardErrorPath</key>
|
|
17
|
+
<string>/Users/matthewdi/.claude/skills/social-autoposter/logs/launchd-stderr.log</string>
|
|
18
|
+
<key>EnvironmentVariables</key>
|
|
19
|
+
<dict>
|
|
20
|
+
<key>PATH</key>
|
|
21
|
+
<string>/Users/matthewdi/.nvm/versions/node/v20.19.4/bin:/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin</string>
|
|
22
|
+
<key>HOME</key>
|
|
23
|
+
<string>/Users/matthewdi</string>
|
|
24
|
+
</dict>
|
|
25
|
+
<key>RunAtLoad</key>
|
|
26
|
+
<true/>
|
|
27
|
+
</dict>
|
|
28
|
+
</plist>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<key>Label</key>
|
|
6
|
+
<string>com.m13v.social-engage</string>
|
|
7
|
+
<key>ProgramArguments</key>
|
|
8
|
+
<array>
|
|
9
|
+
<string>/bin/bash</string>
|
|
10
|
+
<string>/Users/matthewdi/.claude/skills/social-autoposter/skill/engage.sh</string>
|
|
11
|
+
</array>
|
|
12
|
+
<key>StartInterval</key>
|
|
13
|
+
<integer>21600</integer>
|
|
14
|
+
<key>StandardOutPath</key>
|
|
15
|
+
<string>/Users/matthewdi/.claude/skills/social-autoposter/logs/launchd-engage-stdout.log</string>
|
|
16
|
+
<key>StandardErrorPath</key>
|
|
17
|
+
<string>/Users/matthewdi/.claude/skills/social-autoposter/logs/launchd-engage-stderr.log</string>
|
|
18
|
+
<key>EnvironmentVariables</key>
|
|
19
|
+
<dict>
|
|
20
|
+
<key>PATH</key>
|
|
21
|
+
<string>/Users/matthewdi/.nvm/versions/node/v20.19.4/bin:/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin</string>
|
|
22
|
+
<key>HOME</key>
|
|
23
|
+
<string>/Users/matthewdi</string>
|
|
24
|
+
</dict>
|
|
25
|
+
<key>RunAtLoad</key>
|
|
26
|
+
<false/>
|
|
27
|
+
</dict>
|
|
28
|
+
</plist>
|