social-autoposter 1.0.9 → 1.1.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/.env.example CHANGED
@@ -7,5 +7,5 @@
7
7
  MOLTBOOK_API_KEY=
8
8
 
9
9
  # Shared Neon Postgres database — pre-filled, read/write access, no delete
10
- # All social-autoposter users write to this shared DB (used by syncfield.sh)
10
+ # All social-autoposter users write to this shared Neon Postgres DB
11
11
  DATABASE_URL=postgresql://social_autoposter_public:sap_public_2026@ep-empty-bird-ai7uh8cy-pooler.c-4.us-east-1.aws.neon.tech/neondb?sslmode=require
package/README.md CHANGED
@@ -2,8 +2,6 @@
2
2
 
3
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
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
5
  ## Install as a skill
8
6
 
9
7
  ```bash
@@ -20,7 +18,7 @@ npx social-autoposter update
20
18
  Or set up manually:
21
19
  ```bash
22
20
  cp config.example.json config.json # edit with your accounts
23
- sqlite3 social_posts.db < schema.sql # create the database
21
+ psql "$DATABASE_URL" -f schema-postgres.sql # initialize the Neon DB
24
22
  bash setup.sh # symlinks + launchd (macOS)
25
23
  ```
26
24
 
@@ -49,11 +47,12 @@ SKILL.md (the playbook)
49
47
  social-autoposter/
50
48
  ├── SKILL.md <- skill playbook (generic, publishable)
51
49
  ├── config.example.json <- config template (accounts, subreddits, content angle)
52
- ├── schema.sql <- DB schema
50
+ ├── schema-postgres.sql <- Neon Postgres DB schema
53
51
  ├── setup.sh <- creates symlinks, loads launchd agents
54
52
  ├── setup/
55
53
  │ └── SKILL.md <- interactive setup wizard skill
56
54
  ├── scripts/
55
+ │ ├── db.py <- Neon Postgres connection wrapper
57
56
  │ ├── find_threads.py <- find candidate threads via Reddit/Moltbook API
58
57
  │ ├── scan_replies.py <- scan for new replies to our posts via API
59
58
  │ └── update_stats.py <- fetch engagement stats via API
@@ -63,15 +62,13 @@ social-autoposter/
63
62
  │ ├── stats.sh <- 6-hourly stats (launchd wrapper)
64
63
  │ ├── engage.sh <- 2-hourly engagement (launchd wrapper)
65
64
  │ └── logs/ <- runtime logs (gitignored)
66
- ├── social_posts.db <- SQLite database (committed for Datasette)
67
- ├── syncfield.sh <- sync SQLite -> Neon Postgres
68
65
  └── launchd/ <- macOS LaunchAgent plists
69
66
  ```
70
67
 
71
68
  ## For other AI agents
72
69
 
73
70
  The skill is designed to work with any agent that has:
74
- - **Shell access** (to run Python scripts and sqlite3)
71
+ - **Shell access** (to run Python scripts and psql)
75
72
  - **Browser automation** (Playwright, Selenium, etc. for posting)
76
73
  - **An LLM** (for drafting comments in the right tone)
77
74
 
package/SKILL.md CHANGED
@@ -26,7 +26,7 @@ Key fields:
26
26
  - `subreddits` — target subreddits to monitor
27
27
  - `content_angle` — your unique perspective for authentic comments
28
28
  - `projects` — your products/repos to mention when relevant (with topic keywords)
29
- - `database` — path to SQLite DB
29
+ - `database` — unused (DB is Neon Postgres via `DATABASE_URL` in `.env`)
30
30
 
31
31
  ## Helper Scripts
32
32
 
@@ -67,7 +67,7 @@ Find a thread, draft a comment, post it, log it.
67
67
  ### 1. Rate limit check
68
68
 
69
69
  ```sql
70
- SELECT COUNT(*) FROM posts WHERE posted_at >= datetime('now', '-24 hours')
70
+ SELECT COUNT(*) FROM posts WHERE posted_at >= NOW() - INTERVAL '24 hours'
71
71
  ```
72
72
  If 40+ posts in the last 24 hours, stop. Max 40/day.
73
73
 
@@ -148,13 +148,9 @@ Rate limit: max 1 post per 30 minutes.
148
148
  INSERT INTO posts (platform, thread_url, thread_author, thread_author_handle,
149
149
  thread_title, thread_content, our_url, our_content, our_account,
150
150
  source_summary, status, posted_at)
151
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'active', datetime('now'));
151
+ VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, 'active', NOW());
152
152
  ```
153
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
154
  ---
159
155
 
160
156
  ## Workflow: Stats (`/social-autoposter stats`)
@@ -210,8 +206,8 @@ For each pending reply:
210
206
  3. Post via browser (Reddit/X) or API (Moltbook)
211
207
  4. Update the reply record:
212
208
  ```sql
213
- UPDATE replies SET status='replied', our_reply_content=?, our_reply_url=?,
214
- replied_at=datetime('now') WHERE id=?
209
+ UPDATE replies SET status='replied', our_reply_content=%s, our_reply_url=%s,
210
+ replied_at=NOW() WHERE id=%s
215
211
  ```
216
212
 
217
213
  Max 5 replies per run.
package/bin/cli.js CHANGED
@@ -8,6 +8,7 @@ const { spawnSync } = require('child_process');
8
8
 
9
9
  const DEST = path.join(os.homedir(), 'social-autoposter');
10
10
  const PKG_ROOT = path.join(__dirname, '..');
11
+ const HOME = os.homedir();
11
12
 
12
13
  // Files/dirs to copy from npm package to ~/social-autoposter
13
14
  const COPY_TARGETS = [
@@ -18,8 +19,6 @@ const COPY_TARGETS = [
18
19
  'SKILL.md',
19
20
  'skill',
20
21
  'setup',
21
- 'launchd',
22
- 'syncfield.sh',
23
22
  ];
24
23
 
25
24
  // Never overwrite these user files during update
@@ -43,6 +42,80 @@ function linkOrRelink(target, linkPath) {
43
42
  fs.symlinkSync(target, linkPath);
44
43
  }
45
44
 
45
+ function generatePlists() {
46
+ // Detect PATH for launchd (include node, homebrew, system)
47
+ const nodeBin = path.dirname(process.execPath);
48
+ const pathDirs = new Set([nodeBin, '/opt/homebrew/bin', '/usr/local/bin', '/usr/bin', '/bin']);
49
+ const launchdPath = [...pathDirs].join(':');
50
+
51
+ const plists = [
52
+ {
53
+ file: 'com.m13v.social-autoposter.plist',
54
+ label: 'com.m13v.social-autoposter',
55
+ script: `${DEST}/skill/run.sh`,
56
+ interval: 3600,
57
+ runAtLoad: true,
58
+ stdoutLog: `${DEST}/skill/logs/launchd-stdout.log`,
59
+ stderrLog: `${DEST}/skill/logs/launchd-stderr.log`,
60
+ },
61
+ {
62
+ file: 'com.m13v.social-stats.plist',
63
+ label: 'com.m13v.social-stats',
64
+ script: `${DEST}/skill/stats.sh`,
65
+ interval: 21600,
66
+ runAtLoad: false,
67
+ stdoutLog: `${DEST}/skill/logs/launchd-stats-stdout.log`,
68
+ stderrLog: `${DEST}/skill/logs/launchd-stats-stderr.log`,
69
+ },
70
+ {
71
+ file: 'com.m13v.social-engage.plist',
72
+ label: 'com.m13v.social-engage',
73
+ script: `${DEST}/skill/engage.sh`,
74
+ interval: 21600,
75
+ runAtLoad: false,
76
+ stdoutLog: `${DEST}/skill/logs/launchd-engage-stdout.log`,
77
+ stderrLog: `${DEST}/skill/logs/launchd-engage-stderr.log`,
78
+ },
79
+ ];
80
+
81
+ const launchdDir = path.join(DEST, 'launchd');
82
+ fs.mkdirSync(launchdDir, { recursive: true });
83
+
84
+ for (const p of plists) {
85
+ const xml = `<?xml version="1.0" encoding="UTF-8"?>
86
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
87
+ <plist version="1.0">
88
+ <dict>
89
+ \t<key>Label</key>
90
+ \t<string>${p.label}</string>
91
+ \t<key>ProgramArguments</key>
92
+ \t<array>
93
+ \t\t<string>/bin/bash</string>
94
+ \t\t<string>${p.script}</string>
95
+ \t</array>
96
+ \t<key>StartInterval</key>
97
+ \t<integer>${p.interval}</integer>
98
+ \t<key>StandardOutPath</key>
99
+ \t<string>${p.stdoutLog}</string>
100
+ \t<key>StandardErrorPath</key>
101
+ \t<string>${p.stderrLog}</string>
102
+ \t<key>EnvironmentVariables</key>
103
+ \t<dict>
104
+ \t\t<key>PATH</key>
105
+ \t\t<string>${launchdPath}</string>
106
+ \t\t<key>HOME</key>
107
+ \t\t<string>${HOME}</string>
108
+ \t</dict>
109
+ \t<key>RunAtLoad</key>
110
+ \t<${p.runAtLoad}/>
111
+ </dict>
112
+ </plist>
113
+ `;
114
+ fs.writeFileSync(path.join(launchdDir, p.file), xml);
115
+ }
116
+ console.log(' generated launchd plists with correct paths');
117
+ }
118
+
46
119
  function init() {
47
120
  console.log('Setting up social-autoposter in', DEST);
48
121
  fs.mkdirSync(DEST, { recursive: true });
@@ -61,6 +134,9 @@ function init() {
61
134
  console.log(' copied', f);
62
135
  }
63
136
 
137
+ // Generate launchd plists with user's actual HOME
138
+ generatePlists();
139
+
64
140
  // config.json — only if it doesn't exist
65
141
  const configDest = path.join(DEST, 'config.json');
66
142
  if (!fs.existsSync(configDest)) {
@@ -135,6 +211,9 @@ function update() {
135
211
  console.log(' updated', f);
136
212
  }
137
213
 
214
+ // Regenerate launchd plists with correct paths
215
+ generatePlists();
216
+
138
217
  // Re-symlink skill and setup skill in case they broke
139
218
  const skillsDir = path.join(os.homedir(), '.claude', 'skills');
140
219
  try {
@@ -147,7 +226,7 @@ function update() {
147
226
  } catch {}
148
227
 
149
228
  console.log('');
150
- console.log('Update complete. config.json and social_posts.db were preserved.');
229
+ console.log('Update complete. config.json was preserved.');
151
230
  }
152
231
 
153
232
  const cmd = process.argv[2];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "social-autoposter",
3
- "version": "1.0.9",
3
+ "version": "1.1.0",
4
4
  "description": "Automated social posting pipeline for Reddit, X/Twitter, LinkedIn, and Moltbook. Install as a Claude Code agent skill.",
5
5
  "bin": {
6
6
  "social-autoposter": "bin/cli.js"
@@ -16,9 +16,7 @@
16
16
  "skill/run.sh",
17
17
  "skill/stats.sh",
18
18
  "skill/engage.sh",
19
- "setup/SKILL.md",
20
- "launchd/",
21
- "syncfield.sh"
19
+ "setup/SKILL.md"
22
20
  ],
23
21
  "keywords": [
24
22
  "social-media",
@@ -1,8 +1,8 @@
1
- -- schema-postgres.sql — Neon Postgres schema mirroring SQLite for SyncField
1
+ -- schema-postgres.sql — Neon Postgres schema (primary database)
2
2
  -- Run once: psql "$DATABASE_URL" -f schema-postgres.sql
3
3
 
4
4
  CREATE TABLE IF NOT EXISTS posts (
5
- id INTEGER PRIMARY KEY,
5
+ id SERIAL PRIMARY KEY,
6
6
  platform TEXT NOT NULL,
7
7
  thread_url TEXT NOT NULL,
8
8
  thread_author TEXT,
@@ -31,8 +31,30 @@ CREATE TABLE IF NOT EXISTS posts (
31
31
 
32
32
  CREATE INDEX IF NOT EXISTS idx_posts_platform ON posts(platform);
33
33
 
34
+ CREATE TABLE IF NOT EXISTS threads (
35
+ id SERIAL PRIMARY KEY,
36
+ platform TEXT NOT NULL,
37
+ url TEXT NOT NULL UNIQUE,
38
+ author TEXT,
39
+ author_handle TEXT,
40
+ title TEXT,
41
+ content TEXT,
42
+ engagement TEXT,
43
+ discovered_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
44
+ );
45
+
46
+ CREATE TABLE IF NOT EXISTS our_posts (
47
+ id SERIAL PRIMARY KEY,
48
+ thread_id INTEGER REFERENCES threads(id),
49
+ platform TEXT NOT NULL,
50
+ url TEXT,
51
+ content TEXT NOT NULL,
52
+ account TEXT,
53
+ posted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
54
+ );
55
+
34
56
  CREATE TABLE IF NOT EXISTS campaigns (
35
- id INTEGER PRIMARY KEY,
57
+ id SERIAL PRIMARY KEY,
36
58
  name TEXT NOT NULL,
37
59
  prompt TEXT NOT NULL,
38
60
  platforms TEXT DEFAULT 'x,reddit,moltbook',
@@ -44,7 +66,7 @@ CREATE TABLE IF NOT EXISTS campaigns (
44
66
  );
45
67
 
46
68
  CREATE TABLE IF NOT EXISTS replies (
47
- id INTEGER PRIMARY KEY,
69
+ id SERIAL PRIMARY KEY,
48
70
  post_id INTEGER REFERENCES posts(id),
49
71
  platform TEXT NOT NULL,
50
72
  their_comment_id TEXT NOT NULL,
@@ -65,7 +87,7 @@ CREATE TABLE IF NOT EXISTS replies (
65
87
  );
66
88
 
67
89
  CREATE TABLE IF NOT EXISTS thread_comments (
68
- id INTEGER PRIMARY KEY,
90
+ id SERIAL PRIMARY KEY,
69
91
  thread_id INTEGER,
70
92
  author TEXT,
71
93
  author_handle TEXT,
@@ -74,10 +96,3 @@ CREATE TABLE IF NOT EXISTS thread_comments (
74
96
  discovered_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
75
97
  );
76
98
 
77
- CREATE TABLE IF NOT EXISTS _syncfield_meta (
78
- key TEXT PRIMARY KEY,
79
- value TEXT
80
- );
81
-
82
- INSERT INTO _syncfield_meta (key, value) VALUES ('last_sync', '1970-01-01T00:00:00Z')
83
- ON CONFLICT (key) DO NOTHING;
package/setup/SKILL.md CHANGED
@@ -16,8 +16,7 @@ Interactive setup wizard for social-autoposter. Walk the user through configurat
16
16
  ## Prerequisites
17
17
 
18
18
  - Node.js 16+ (for `npx`)
19
- - `sqlite3` available on PATH
20
- - Python 3.9+ for running helper scripts
19
+ - Python 3.9+ with `pip3` for running helper scripts
21
20
  - A browser automation tool (Playwright MCP, Selenium, etc.) for platform login verification
22
21
 
23
22
  ---
@@ -31,7 +30,7 @@ Run these steps in order. Ask the user for input at each step. Don't skip ahead.
31
30
  Check if already installed:
32
31
 
33
32
  ```bash
34
- ls ~/social-autoposter/schema.sql 2>/dev/null && echo "FOUND" || echo "NOT_FOUND"
33
+ ls ~/social-autoposter/schema-postgres.sql 2>/dev/null && echo "FOUND" || echo "NOT_FOUND"
35
34
  ```
36
35
 
37
36
  If NOT_FOUND, install:
@@ -237,11 +236,17 @@ If no: "You can run manually anytime with `/social-autoposter`"
237
236
 
238
237
  ### Step 8: Summary
239
238
 
240
- Print a summary:
239
+ Read `config.json` accounts and compute each platform's stats URL:
240
+ - Twitter/X handle (strip leading `@`): `https://s4l.ai/stats/HANDLE`
241
+ - Reddit username: `https://s4l.ai/stats/USERNAME`
242
+ - LinkedIn name (URL-encoded spaces as `%20`): `https://s4l.ai/stats/NAME`
243
+ - Moltbook username: `https://s4l.ai/stats/MOLTBOOK_USERNAME`
244
+
245
+ Print a summary with real values substituted:
241
246
  ```
242
247
  Social Autoposter Setup Complete
243
248
 
244
- Installed: ~/social-autoposter (v1.0.8 via npm)
249
+ Installed: ~/social-autoposter (v1.0.9 via npm)
245
250
  Database: Neon Postgres (DATABASE_URL in .env)
246
251
  Config: ~/social-autoposter/config.json
247
252
  Env: ~/social-autoposter/.env
@@ -256,6 +261,14 @@ Social Autoposter Setup Complete
256
261
  Rate limit: 40 posts per 24 hours
257
262
  Automation: launchd (hourly post, 6h stats, 2h engage)
258
263
 
264
+ Your live stats pages:
265
+ X/Twitter: https://s4l.ai/stats/HANDLE
266
+ Reddit: https://s4l.ai/stats/USERNAME
267
+ LinkedIn: https://s4l.ai/stats/NAME
268
+ Moltbook: https://s4l.ai/stats/MOLTBOOK_USERNAME
269
+
259
270
  Try it: /social-autoposter
260
271
  Update: npx social-autoposter update
261
272
  ```
273
+
274
+ Tell the user: "Your stats pages are ready — they'll show posts as soon as your first run completes and syncs to Neon (happens automatically after each post run). Bookmark the links above."
package/skill/SKILL.md CHANGED
@@ -18,6 +18,10 @@ Automates finding, posting, and tracking social media comments and original post
18
18
  | `/social-autoposter engage` | Scan and reply to responses on our posts |
19
19
  | `/social-autoposter audit` | Full browser audit of all posts |
20
20
 
21
+ **View your posts live:** `https://s4l.ai/stats/[your_handle]`
22
+ — e.g. `https://s4l.ai/stats/m13v_` (Twitter handle without `@`), `https://s4l.ai/stats/Deep_Ad1959` (Reddit), `https://s4l.ai/stats/matthew-autoposter` (Moltbook).
23
+ The handles come from `config.json → accounts.*.handle/username`. Each platform account has its own URL.
24
+
21
25
  ---
22
26
 
23
27
  ## FIRST: Read config
@@ -37,7 +41,7 @@ Key fields you'll use throughout every workflow:
37
41
  - `subreddits` — list of subreddits to monitor and post in
38
42
  - `content_angle` — the user's unique perspective for writing authentic comments
39
43
  - `projects` — products/repos to mention naturally when relevant (each has `name`, `description`, `website`, `github`, `topics`)
40
- - `database` — unused (DB is Neon Postgres via `DATABASE_URL` in `.env`)
44
+ - `database` — unused (Neon Postgres via `DATABASE_URL` in `.env`)
41
45
 
42
46
  Use these values everywhere below instead of any hardcoded names or links.
43
47
 
@@ -201,6 +205,8 @@ After posting, you MUST:
201
205
  python3 ~/social-autoposter/scripts/update_stats.py
202
206
  ```
203
207
 
208
+ After running, view updated stats at `https://s4l.ai/stats/[handle]`. Changes appear on the website within ~5 minutes.
209
+
204
210
  ---
205
211
 
206
212
  ## Workflow: Engage (`/social-autoposter engage`)
package/skill/engage.sh CHANGED
@@ -12,10 +12,14 @@ set -euo pipefail
12
12
  [ -f "$HOME/social-autoposter/.env" ] && source "$HOME/social-autoposter/.env"
13
13
 
14
14
  REPO_DIR="$HOME/social-autoposter"
15
- DB="$REPO_DIR/social_posts.db"
16
15
  SKILL_FILE="$REPO_DIR/skill/SKILL.md"
17
16
  LOG_DIR="$REPO_DIR/skill/logs"
18
17
 
18
+ if [ -z "${DATABASE_URL:-}" ]; then
19
+ echo "ERROR: DATABASE_URL not set in ~/social-autoposter/.env"
20
+ exit 1
21
+ fi
22
+
19
23
  mkdir -p "$LOG_DIR"
20
24
  LOG_FILE="$LOG_DIR/engage-$(date +%Y-%m-%d_%H%M%S).log"
21
25
 
@@ -27,12 +31,12 @@ log "=== Engagement Loop Run: $(date) ==="
27
31
  # PHASE A: Scan for replies (Python, no Claude needed)
28
32
  # ═══════════════════════════════════════════════════════
29
33
  log "Phase A: Scanning for replies..."
30
- python3 "$REPO_DIR/scripts/scan_replies.py" --db "$DB" 2>&1 | tee -a "$LOG_FILE" || true
34
+ python3 "$REPO_DIR/scripts/scan_replies.py" 2>&1 | tee -a "$LOG_FILE" || true
31
35
 
32
36
  # ═══════════════════════════════════════════════════════
33
37
  # PHASE B: X/Twitter discovery + all reply engagement
34
38
  # ═══════════════════════════════════════════════════════
35
- PENDING_COUNT=$(sqlite3 "$DB" "SELECT COUNT(*) FROM replies WHERE status='pending';")
39
+ PENDING_COUNT=$(psql "$DATABASE_URL" -t -A -c "SELECT COUNT(*) FROM replies WHERE status='pending';")
36
40
  log "Phase B: $PENDING_COUNT pending replies to handle"
37
41
 
38
42
  # Always run Phase B — it handles both X/Twitter discovery and pending replies
@@ -63,17 +67,19 @@ There are $PENDING_COUNT pending replies in the database.
63
67
  - **Tier 3 (direct ask):** They ask for link/tool/source. Give it immediately.
64
68
 
65
69
  $(if [ "$PENDING_COUNT" -gt 0 ]; then
66
- sqlite3 -json "$DB" "
67
- SELECT r.id, r.platform, r.their_author, r.their_content, r.their_comment_url,
68
- r.their_comment_id, r.depth,
69
- p.thread_title, p.thread_url, p.our_content, p.our_url,
70
- CASE WHEN p.thread_url = p.our_url THEN 1 ELSE 0 END as is_our_original_post
71
- FROM replies r
72
- JOIN posts p ON r.post_id = p.id
73
- WHERE r.status='pending'
74
- ORDER BY
75
- CASE WHEN p.thread_url = p.our_url THEN 0 ELSE 1 END,
76
- r.discovered_at ASC;"
70
+ psql "$DATABASE_URL" -t -A -c "
71
+ SELECT json_agg(q) FROM (
72
+ SELECT r.id, r.platform, r.their_author, r.their_content, r.their_comment_url,
73
+ r.their_comment_id, r.depth,
74
+ p.thread_title, p.thread_url, p.our_content, p.our_url,
75
+ CASE WHEN p.thread_url = p.our_url THEN 1 ELSE 0 END as is_our_original_post
76
+ FROM replies r
77
+ JOIN posts p ON r.post_id = p.id
78
+ WHERE r.status='pending'
79
+ ORDER BY
80
+ CASE WHEN p.thread_url = p.our_url THEN 0 ELSE 1 END,
81
+ r.discovered_at ASC
82
+ ) q;"
77
83
  else
78
84
  echo "No pending replies."
79
85
  fi)
@@ -88,21 +94,13 @@ CRITICAL: Close browser tabs after every page visit (browser_tabs action 'close'
88
94
  # ═══════════════════════════════════════════════════════
89
95
  log "Phase C: Cleanup"
90
96
 
91
- TOTAL_PENDING=$(sqlite3 "$DB" "SELECT COUNT(*) FROM replies WHERE status='pending';")
92
- TOTAL_REPLIED=$(sqlite3 "$DB" "SELECT COUNT(*) FROM replies WHERE status='replied';")
93
- TOTAL_SKIPPED=$(sqlite3 "$DB" "SELECT COUNT(*) FROM replies WHERE status='skipped';")
94
- TOTAL_ERRORS=$(sqlite3 "$DB" "SELECT COUNT(*) FROM replies WHERE status='error';")
97
+ TOTAL_PENDING=$(psql "$DATABASE_URL" -t -A -c "SELECT COUNT(*) FROM replies WHERE status='pending';")
98
+ TOTAL_REPLIED=$(psql "$DATABASE_URL" -t -A -c "SELECT COUNT(*) FROM replies WHERE status='replied';")
99
+ TOTAL_SKIPPED=$(psql "$DATABASE_URL" -t -A -c "SELECT COUNT(*) FROM replies WHERE status='skipped';")
100
+ TOTAL_ERRORS=$(psql "$DATABASE_URL" -t -A -c "SELECT COUNT(*) FROM replies WHERE status='error';")
95
101
 
96
102
  log "Replies summary: pending=$TOTAL_PENDING replied=$TOTAL_REPLIED skipped=$TOTAL_SKIPPED errors=$TOTAL_ERRORS"
97
103
 
98
- # Git sync
99
- cd "$REPO_DIR"
100
- git add social_posts.db
101
- git diff --cached --quiet || git commit -m "engage $(date '+%Y-%m-%d %H:%M')" && git push 2>/dev/null || true
102
-
103
- # Sync SQLite → Neon Postgres
104
- bash "$REPO_DIR/syncfield.sh" || true
105
-
106
104
  # Delete old logs
107
105
  find "$LOG_DIR" -name "engage-*.log" -mtime +7 -delete 2>/dev/null || true
108
106
 
package/skill/stats.sh CHANGED
@@ -7,7 +7,6 @@
7
7
  set -euo pipefail
8
8
 
9
9
  REPO_DIR="$HOME/social-autoposter"
10
- DB="$REPO_DIR/social_posts.db"
11
10
  LOG_DIR="$REPO_DIR/skill/logs"
12
11
  QUIET="${1:-}"
13
12
 
@@ -22,17 +21,9 @@ echo "[$(date +%H:%M:%S)] Starting stats update" | tee "$LOGFILE"
22
21
 
23
22
  # Run the Python stats script
24
23
  if [ "$QUIET" = "--quiet" ]; then
25
- python3 "$REPO_DIR/scripts/update_stats.py" --db "$DB" --quiet 2>&1 | tee -a "$LOGFILE"
24
+ python3 "$REPO_DIR/scripts/update_stats.py" --quiet 2>&1 | tee -a "$LOGFILE"
26
25
  else
27
- python3 "$REPO_DIR/scripts/update_stats.py" --db "$DB" 2>&1 | tee -a "$LOGFILE"
26
+ python3 "$REPO_DIR/scripts/update_stats.py" 2>&1 | tee -a "$LOGFILE"
28
27
  fi
29
28
 
30
29
  echo "[$(date +%H:%M:%S)] Stats update complete" | tee -a "$LOGFILE"
31
-
32
- # Sync DB to GitHub for Datasette Lite
33
- cd "$REPO_DIR"
34
- git add social_posts.db
35
- git diff --cached --quiet || git commit -m "stats $(date '+%Y-%m-%d %H:%M')" && git push 2>/dev/null || true
36
-
37
- # Sync SQLite → Neon Postgres
38
- bash "$REPO_DIR/syncfield.sh" || true
@@ -1,28 +0,0 @@
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>
@@ -1,28 +0,0 @@
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>
@@ -1,28 +0,0 @@
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-stats</string>
7
- <key>ProgramArguments</key>
8
- <array>
9
- <string>/bin/bash</string>
10
- <string>/Users/matthewdi/.claude/skills/social-autoposter/skill/stats.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-stats-stdout.log</string>
16
- <key>StandardErrorPath</key>
17
- <string>/Users/matthewdi/.claude/skills/social-autoposter/logs/launchd-stats-stderr.log</string>
18
- <key>EnvironmentVariables</key>
19
- <dict>
20
- <key>PATH</key>
21
- <string>/opt/homebrew/bin:/usr/local/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>
package/syncfield.sh DELETED
@@ -1,78 +0,0 @@
1
- #!/usr/bin/env bash
2
- # syncfield.sh — Sync SQLite → Neon Postgres (idempotent upsert)
3
- # Called after git push in stats.sh and engage.sh
4
- # Requires: sqlite3, psql, DATABASE_URL in .env
5
-
6
- set -euo pipefail
7
-
8
- DB="$HOME/social-autoposter/social_posts.db"
9
-
10
- # Load secrets
11
- # shellcheck source=/dev/null
12
- [ -f "$HOME/social-autoposter/.env" ] && source "$HOME/social-autoposter/.env"
13
-
14
- if [ -z "${DATABASE_URL:-}" ]; then
15
- echo "syncfield: DATABASE_URL not set, skipping sync"
16
- exit 0
17
- fi
18
-
19
- TMPDIR="${TMPDIR:-/tmp}"
20
-
21
- sync_table() {
22
- local table="$1"
23
- local columns="$2"
24
- local conflict_col="${3:-id}"
25
- local csv_file="$TMPDIR/syncfield_${table}.csv"
26
-
27
- # Export from SQLite as CSV
28
- sqlite3 -header -csv "$DB" "SELECT $columns FROM $table;" > "$csv_file"
29
-
30
- local row_count
31
- row_count=$(wc -l < "$csv_file" | tr -d ' ')
32
- row_count=$((row_count - 1)) # subtract header
33
-
34
- if [ "$row_count" -le 0 ]; then
35
- rm -f "$csv_file"
36
- return
37
- fi
38
-
39
- # Build column list for SET clause (exclude conflict column)
40
- local set_clause=""
41
- IFS=',' read -ra cols <<< "$columns"
42
- for col in "${cols[@]}"; do
43
- col=$(echo "$col" | tr -d ' ')
44
- if [ "$col" != "$conflict_col" ]; then
45
- if [ -n "$set_clause" ]; then
46
- set_clause="$set_clause, "
47
- fi
48
- set_clause="${set_clause}${col} = EXCLUDED.${col}"
49
- fi
50
- done
51
-
52
- # Upsert via temp table + INSERT ON CONFLICT
53
- psql "$DATABASE_URL" -q <<SQL
54
- CREATE TEMP TABLE _tmp_${table} (LIKE ${table} INCLUDING ALL);
55
- \\copy _tmp_${table}($columns) FROM '$csv_file' WITH (FORMAT csv, HEADER true, NULL '');
56
- INSERT INTO ${table}($columns)
57
- SELECT $columns FROM _tmp_${table}
58
- ON CONFLICT ($conflict_col) DO UPDATE SET $set_clause;
59
- DROP TABLE _tmp_${table};
60
- SQL
61
-
62
- echo "syncfield: synced $table ($row_count rows)"
63
- rm -f "$csv_file"
64
- }
65
-
66
- # Sync each table
67
- sync_table "posts" "id,platform,thread_url,thread_author,thread_author_handle,thread_title,thread_content,thread_engagement,our_url,our_content,our_account,posted_at,discovered_at,status,status_checked_at,engagement_updated_at,upvotes,comments_count,views,source_turn_id,source_summary,top_comment_author,top_comment_content,top_comment_upvotes,top_comment_url"
68
-
69
- sync_table "campaigns" "id,name,prompt,platforms,status,max_posts_per_day,posts_made,created_at,updated_at"
70
-
71
- sync_table "replies" "id,post_id,platform,their_comment_id,their_author,their_content,their_comment_url,our_reply_id,our_reply_content,our_reply_url,parent_reply_id,moltbook_post_uuid,moltbook_parent_comment_uuid,depth,status,skip_reason,discovered_at,replied_at"
72
-
73
- sync_table "thread_comments" "id,thread_id,author,author_handle,content,engagement,discovered_at"
74
-
75
- # Update sync timestamp
76
- psql "$DATABASE_URL" -q -c "INSERT INTO _syncfield_meta (key, value) VALUES ('last_sync', NOW()::text) ON CONFLICT (key) DO UPDATE SET value = NOW()::text;"
77
-
78
- echo "syncfield: sync complete at $(date '+%Y-%m-%d %H:%M:%S')"