social-autoposter 1.6.25 → 1.6.26
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/package.json +13 -2
- package/scripts/score_twitter_candidates.py +15 -4
- package/skill/lib/twitter-backend.sh +62 -0
- package/skill/run-twitter-cycle.sh +3 -1
- package/scripts/_gsc_roi_query.py +0 -231
- package/scripts/batch_send_dms.py +0 -148
- package/scripts/finalize_post_101.py +0 -170
- package/scripts/insert_li_notifs.py +0 -69
- package/scripts/li_notif_scan_process.py +0 -204
- package/scripts/migrate_dm_links.py +0 -128
- package/scripts/migrate_link_clicks.py +0 -97
- package/scripts/migrate_mentions_out_of_posts.py +0 -264
- package/scripts/migrate_newsletter_links.py +0 -108
- package/scripts/migrate_post_links.py +0 -88
- package/scripts/migrate_replies_stats.py +0 -45
- package/scripts/migrate_subreddit_bans_to_objects.py +0 -113
- package/scripts/phase_d_edit.py +0 -103
- package/scripts/phase_d_new_comments.py +0 -76
- package/scripts/realign_sequences.py +0 -60
- package/scripts/scratch_seo_gsc.py +0 -134
- package/scripts/scratch_seo_posthog.py +0 -179
- package/scripts/scratch_seo_volume.py +0 -136
- package/scripts/seed_dashboard_users.py +0 -94
- package/scripts/send_dashboard_invite.py +0 -141
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "social-autoposter",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.26",
|
|
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"
|
|
@@ -21,7 +21,18 @@
|
|
|
21
21
|
"!scripts/_seo_lane_roi.py",
|
|
22
22
|
"!scripts/check_improve_runs.py",
|
|
23
23
|
"!scripts/classify_all_dms.py",
|
|
24
|
-
"!scripts/
|
|
24
|
+
"!scripts/migrate_*.py",
|
|
25
|
+
"!scripts/_gsc_roi_query.py",
|
|
26
|
+
"!scripts/batch_send_dms.py",
|
|
27
|
+
"!scripts/finalize_post_*.py",
|
|
28
|
+
"!scripts/insert_li_notifs.py",
|
|
29
|
+
"!scripts/li_notif_scan_process.py",
|
|
30
|
+
"!scripts/phase_d_edit.py",
|
|
31
|
+
"!scripts/phase_d_new_comments.py",
|
|
32
|
+
"!scripts/realign_sequences.py",
|
|
33
|
+
"!scripts/scratch_*.py",
|
|
34
|
+
"!scripts/seed_dashboard_users.py",
|
|
35
|
+
"!scripts/send_dashboard_invite.py",
|
|
25
36
|
"scripts/*.sh",
|
|
26
37
|
"schema-postgres.sql",
|
|
27
38
|
"config.example.json",
|
|
@@ -30,6 +30,16 @@ from twitter_account import resolve_handle as _resolve_twitter_handle # noqa: E
|
|
|
30
30
|
from project_topics import topics_for_project # noqa: E402
|
|
31
31
|
|
|
32
32
|
|
|
33
|
+
# Freshness window (in hours) for the expire-stale gate that flips stale
|
|
34
|
+
# pending rows to status='expired'. Sourced from the FRESHNESS_HOURS env the
|
|
35
|
+
# cycle exports (run-twitter-cycle.sh) so the expiry ceiling is configured in
|
|
36
|
+
# ONE place. Falls back to 18 when unset (e.g. ad-hoc / --expire-only runs) to
|
|
37
|
+
# preserve the historical default. NOTE: the gate is on discovered_at
|
|
38
|
+
# (discovery age), not tweet_posted_at; for logic D (≤1h discovery freshness)
|
|
39
|
+
# the two are within ~1h of each other.
|
|
40
|
+
EXPIRE_FRESHNESS_HOURS = int(os.environ.get("FRESHNESS_HOURS") or "18")
|
|
41
|
+
|
|
42
|
+
|
|
33
43
|
# Real Twitter snowflake IDs are 18-19 digit numbers with full entropy in the
|
|
34
44
|
# low bits (sequence counter + worker/datacenter ID = bottom 22 bits ≈ bottom
|
|
35
45
|
# 7 decimal digits). An ID ending in 6+ zeros is statistically impossible
|
|
@@ -505,9 +515,10 @@ def upsert_candidates(tweets, config, batch_id=None, attempts_map=None, scored_s
|
|
|
505
515
|
print(f" Error inserting {url}: {e}", file=sys.stderr)
|
|
506
516
|
continue
|
|
507
517
|
|
|
508
|
-
# Expire old pending candidates
|
|
509
|
-
# (status flip), not a delete — we keep the row forever
|
|
510
|
-
|
|
518
|
+
# Expire old pending candidates past the freshness window. This is a
|
|
519
|
+
# freshness GATE (status flip), not a delete — we keep the row forever
|
|
520
|
+
# for analytics.
|
|
521
|
+
api_post("/api/v1/twitter-candidates/expire-stale", {"freshness_hours": EXPIRE_FRESHNESS_HOURS})
|
|
511
522
|
|
|
512
523
|
# NO PRUNING. We keep every twitter_candidates row forever (chosen, skipped,
|
|
513
524
|
# expired) so we can audit project routing, skip reasons, growth dynamics,
|
|
@@ -570,7 +581,7 @@ def main():
|
|
|
570
581
|
# it off and prints the count.
|
|
571
582
|
resp = api_post(
|
|
572
583
|
"/api/v1/twitter-candidates/expire-stale",
|
|
573
|
-
{"freshness_hours":
|
|
584
|
+
{"freshness_hours": EXPIRE_FRESHNESS_HOURS},
|
|
574
585
|
)
|
|
575
586
|
expired = (resp.get("data") or {}).get("expired_count", 0)
|
|
576
587
|
print(f"Expired {expired} old pending candidates (no row deletion)")
|
|
@@ -227,3 +227,65 @@ defer_if_foreign_for_backend() {
|
|
|
227
227
|
# the twitter-agent profile. Always return 1 (do not defer).
|
|
228
228
|
return 1
|
|
229
229
|
}
|
|
230
|
+
|
|
231
|
+
# --- browser-harness `-c` capability self-heal (added 2026-06-02) -----------
|
|
232
|
+
# A stale ~/Developer/browser-harness checkout that PREDATES the `-c` interface
|
|
233
|
+
# makes `browser-harness -c "<script>"` print its usage string instead of
|
|
234
|
+
# running the script. The Phase 1 scan loop in run-twitter-cycle.sh then yields
|
|
235
|
+
# zero tweets with no obvious cause. cli.js documents the same failure for the
|
|
236
|
+
# bh_run MCP path. When this bit the testing machine, the debugging agent saw
|
|
237
|
+
# the `-c` flag, WRONGLY assumed it was unsupported, and proposed rewriting the
|
|
238
|
+
# call to a nonexistent "stdin form" (browser-harness has no stdin mode — `-c`
|
|
239
|
+
# is the only interface; see run.py). This runs at source-time, before any
|
|
240
|
+
# `-c` call, so all twitter harness scripts (cycle/threads/engage/dm/followups)
|
|
241
|
+
# get auto-repair. Static probe is one grep when fresh (zero steady-state cost);
|
|
242
|
+
# the git+uv refresh only fires when the checkout is actually stale.
|
|
243
|
+
_sa_harness_log() {
|
|
244
|
+
# Use the caller's log() FUNCTION when present; `declare -F` matches only a
|
|
245
|
+
# shell function, never the macOS /usr/bin/log binary (command -v would).
|
|
246
|
+
if declare -F log >/dev/null 2>&1; then log "$*"; else echo "[$(date +%H:%M:%S)] $*" >&2; fi
|
|
247
|
+
}
|
|
248
|
+
_sa_resolve_uv() {
|
|
249
|
+
local c
|
|
250
|
+
c="$(command -v uv 2>/dev/null)" && { echo "$c"; return 0; }
|
|
251
|
+
for c in "$HOME/.local/bin/uv" /opt/homebrew/bin/uv /usr/local/bin/uv; do
|
|
252
|
+
[ -x "$c" ] && { echo "$c"; return 0; }
|
|
253
|
+
done
|
|
254
|
+
return 1
|
|
255
|
+
}
|
|
256
|
+
ensure_harness_c_support() {
|
|
257
|
+
# Run once per process even if several libs source this file.
|
|
258
|
+
[ -n "${_SA_HARNESS_C_CHECKED:-}" ] && return 0
|
|
259
|
+
export _SA_HARNESS_C_CHECKED=1
|
|
260
|
+
|
|
261
|
+
local src="$HOME/Developer/browser-harness"
|
|
262
|
+
local run_py="$src/src/browser_harness/run.py"
|
|
263
|
+
local bh="$HOME/.local/bin/browser-harness"
|
|
264
|
+
|
|
265
|
+
# Static capability probe — no daemon/Chrome needed. The CLI is an editable
|
|
266
|
+
# (`uv tool install -e`) install of $src, so its run.py is the source of
|
|
267
|
+
# truth for whether `-c` is recognized.
|
|
268
|
+
if [ -f "$run_py" ] && grep -q '"-c"' "$run_py"; then
|
|
269
|
+
return 0
|
|
270
|
+
fi
|
|
271
|
+
|
|
272
|
+
_sa_harness_log "[harness] browser-harness checkout missing/stale (no -c support) -> self-healing via git + uv..."
|
|
273
|
+
if [ -d "$src/.git" ]; then
|
|
274
|
+
git -C "$src" fetch --depth 1 origin HEAD >/dev/null 2>&1 \
|
|
275
|
+
&& git -C "$src" reset --hard FETCH_HEAD >/dev/null 2>&1
|
|
276
|
+
fi
|
|
277
|
+
local uv; uv="$(_sa_resolve_uv || true)"
|
|
278
|
+
if [ -n "$uv" ] && [ -d "$src" ]; then
|
|
279
|
+
"$uv" tool install --force -e "$src" >/dev/null 2>&1 || true
|
|
280
|
+
fi
|
|
281
|
+
[ -x "$bh" ] && "$bh" --reload >/dev/null 2>&1 || true
|
|
282
|
+
|
|
283
|
+
if [ -f "$run_py" ] && grep -q '"-c"' "$run_py"; then
|
|
284
|
+
_sa_harness_log "[harness] self-heal OK -> browser-harness -c is supported now"
|
|
285
|
+
return 0
|
|
286
|
+
fi
|
|
287
|
+
_sa_harness_log "[harness] ERROR: browser-harness still lacks -c after self-heal."
|
|
288
|
+
_sa_harness_log "[harness] FIX: run 'social-autoposter update' (re-clones/refreshes $src + reinstalls the CLI). The -c flag is CORRECT; browser-harness has NO stdin mode, so do NOT rewrite the cycle to a stdin form."
|
|
289
|
+
return 1
|
|
290
|
+
}
|
|
291
|
+
ensure_harness_c_support || true
|
|
@@ -147,7 +147,9 @@ FRESHNESS_HOURS=2
|
|
|
147
147
|
# since-rewrite hook) stays tightened to 1h, the winning D setting.
|
|
148
148
|
TWITTER_CYCLE_VARIANT=D
|
|
149
149
|
FRESHNESS_HOURS_DISCOVER=1
|
|
150
|
-
|
|
150
|
+
# Export FRESHNESS_HOURS too so score_twitter_candidates.py inherits it and
|
|
151
|
+
# drives the expire-stale gate from the same knob (was hardcoded 18h there).
|
|
152
|
+
export TWITTER_CYCLE_VARIANT FRESHNESS_HOURS_DISCOVER FRESHNESS_HOURS
|
|
151
153
|
# Hook env: ~/.claude/hooks/twitter-search-since-rewrite.py reads this and
|
|
152
154
|
# uses it in place of its hardcoded 6h default when present.
|
|
153
155
|
export FRESHNESS_HOURS_OVERRIDE=$FRESHNESS_HOURS_DISCOVER
|
|
@@ -1,231 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""One-off: GSC pipeline ROI per project (last 30d).
|
|
3
|
-
|
|
4
|
-
For each project in config.json with GSC-pipeline pages in the last 30 days,
|
|
5
|
-
queries PostHog (HogQL) for total unique-distinct_id visitors to those exact
|
|
6
|
-
$pathname values, and the top-3 pages by visitors. Joins with Postgres for
|
|
7
|
-
GSC clicks and spend.
|
|
8
|
-
|
|
9
|
-
Run with:
|
|
10
|
-
/opt/homebrew/bin/python3.11 scripts/_gsc_roi_query.py
|
|
11
|
-
"""
|
|
12
|
-
import json
|
|
13
|
-
import os
|
|
14
|
-
import subprocess
|
|
15
|
-
import sys
|
|
16
|
-
import urllib.parse
|
|
17
|
-
import urllib.request
|
|
18
|
-
from collections import defaultdict
|
|
19
|
-
|
|
20
|
-
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def load_env():
|
|
24
|
-
with open(os.path.join(ROOT, '.env')) as f:
|
|
25
|
-
for line in f:
|
|
26
|
-
line = line.strip()
|
|
27
|
-
if line and not line.startswith('#') and '=' in line:
|
|
28
|
-
k, v = line.split('=', 1)
|
|
29
|
-
os.environ.setdefault(k.strip(), v.strip())
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def ph_key():
|
|
33
|
-
return subprocess.check_output(
|
|
34
|
-
['security', 'find-generic-password', '-s', 'PostHog-Personal-API-Key-m13v', '-w'],
|
|
35
|
-
text=True,
|
|
36
|
-
).strip()
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def hogql(project_id: str, sql: str, key: str):
|
|
40
|
-
body = json.dumps({"query": {"kind": "HogQLQuery", "query": sql}}).encode()
|
|
41
|
-
req = urllib.request.Request(
|
|
42
|
-
f"https://us.posthog.com/api/projects/{project_id}/query/",
|
|
43
|
-
data=body,
|
|
44
|
-
headers={
|
|
45
|
-
"Authorization": f"Bearer {key}",
|
|
46
|
-
"Content-Type": "application/json",
|
|
47
|
-
},
|
|
48
|
-
method="POST",
|
|
49
|
-
)
|
|
50
|
-
with urllib.request.urlopen(req, timeout=60) as r:
|
|
51
|
-
return json.loads(r.read().decode())
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def main():
|
|
55
|
-
load_env()
|
|
56
|
-
import psycopg2
|
|
57
|
-
|
|
58
|
-
# Build project -> posthog project_id map from config.json
|
|
59
|
-
with open(os.path.join(ROOT, 'config.json')) as f:
|
|
60
|
-
cfg = json.load(f)
|
|
61
|
-
posthog_map = {}
|
|
62
|
-
site_map = {}
|
|
63
|
-
for p in cfg['projects']:
|
|
64
|
-
name = p['name']
|
|
65
|
-
ph = (p.get('posthog') or {}).get('project_id')
|
|
66
|
-
if ph:
|
|
67
|
-
posthog_map[name.lower()] = str(ph)
|
|
68
|
-
site_map[name.lower()] = (p.get('landing_pages') or {}).get('base_url') or p.get('website') or ''
|
|
69
|
-
|
|
70
|
-
# Pull GSC pages + spend per project (last 30d)
|
|
71
|
-
conn = psycopg2.connect(os.environ['DATABASE_URL'])
|
|
72
|
-
cur = conn.cursor()
|
|
73
|
-
cur.execute("""
|
|
74
|
-
SELECT gq.product, gq.page_url, gq.clicks, gq.impressions,
|
|
75
|
-
COALESCE(cs.total_cost_usd, 0)::float AS cost
|
|
76
|
-
FROM gsc_queries gq
|
|
77
|
-
LEFT JOIN claude_sessions cs ON cs.session_id = gq.claude_session_id
|
|
78
|
-
WHERE gq.status='done'
|
|
79
|
-
AND gq.completed_at >= NOW() - INTERVAL '30 days'
|
|
80
|
-
AND gq.page_url IS NOT NULL AND gq.page_url <> ''
|
|
81
|
-
""")
|
|
82
|
-
rows = cur.fetchall()
|
|
83
|
-
cur.close(); conn.close()
|
|
84
|
-
|
|
85
|
-
# Group by product
|
|
86
|
-
per_project = defaultdict(list)
|
|
87
|
-
for product, url, clicks, impr, cost in rows:
|
|
88
|
-
# Normalize: if URL is relative, leave path; if absolute, parse path
|
|
89
|
-
if url.startswith('http'):
|
|
90
|
-
u = urllib.parse.urlparse(url)
|
|
91
|
-
host = u.netloc
|
|
92
|
-
path = u.path or '/'
|
|
93
|
-
else:
|
|
94
|
-
host = ''
|
|
95
|
-
path = url
|
|
96
|
-
per_project[product].append({
|
|
97
|
-
"host": host, "path": path, "clicks": int(clicks or 0),
|
|
98
|
-
"impressions": int(impr or 0), "cost": float(cost or 0),
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
key = ph_key()
|
|
102
|
-
summary = []
|
|
103
|
-
|
|
104
|
-
for product, pages in per_project.items():
|
|
105
|
-
ph_proj = posthog_map.get(product.lower())
|
|
106
|
-
n_pages = len(pages)
|
|
107
|
-
spend = sum(p['cost'] for p in pages)
|
|
108
|
-
gsc_clicks = sum(p['clicks'] for p in pages)
|
|
109
|
-
gsc_impr = sum(p['impressions'] for p in pages)
|
|
110
|
-
|
|
111
|
-
if not ph_proj:
|
|
112
|
-
summary.append({
|
|
113
|
-
"product": product, "n_pages": n_pages, "spend": spend,
|
|
114
|
-
"visitors": None, "gsc_clicks": gsc_clicks, "gsc_impr": gsc_impr,
|
|
115
|
-
"top": [], "note": "no posthog project_id in config.json",
|
|
116
|
-
})
|
|
117
|
-
continue
|
|
118
|
-
|
|
119
|
-
# PostHog HogQL: count unique distinct_id per $pathname for the matching paths.
|
|
120
|
-
# Filter by path IN (...) — host doesn't matter inside a single PostHog project.
|
|
121
|
-
paths = sorted({p['path'] for p in pages if p['path']})
|
|
122
|
-
if not paths:
|
|
123
|
-
summary.append({
|
|
124
|
-
"product": product, "n_pages": n_pages, "spend": spend,
|
|
125
|
-
"visitors": 0, "gsc_clicks": gsc_clicks, "gsc_impr": gsc_impr,
|
|
126
|
-
"top": [], "note": "no paths to query",
|
|
127
|
-
})
|
|
128
|
-
continue
|
|
129
|
-
path_list = ", ".join(f"'{p.replace(chr(39), chr(39)+chr(39))}'" for p in paths)
|
|
130
|
-
|
|
131
|
-
sql = (
|
|
132
|
-
"SELECT properties.$pathname AS path, count(DISTINCT distinct_id) AS visitors "
|
|
133
|
-
"FROM events "
|
|
134
|
-
"WHERE event = '$pageview' "
|
|
135
|
-
f"AND timestamp > now() - INTERVAL 30 DAY "
|
|
136
|
-
f"AND properties.$pathname IN ({path_list}) "
|
|
137
|
-
"GROUP BY properties.$pathname "
|
|
138
|
-
"ORDER BY visitors DESC "
|
|
139
|
-
"LIMIT 500"
|
|
140
|
-
)
|
|
141
|
-
try:
|
|
142
|
-
res = hogql(ph_proj, sql, key)
|
|
143
|
-
results = res.get('results') or []
|
|
144
|
-
total_visitors = sum(r[1] for r in results)
|
|
145
|
-
top = [(r[0], r[1]) for r in results[:5]]
|
|
146
|
-
except Exception as e:
|
|
147
|
-
total_visitors = None
|
|
148
|
-
top = []
|
|
149
|
-
err = str(e)[:120]
|
|
150
|
-
summary.append({
|
|
151
|
-
"product": product, "n_pages": n_pages, "spend": spend,
|
|
152
|
-
"visitors": None, "gsc_clicks": gsc_clicks, "gsc_impr": gsc_impr,
|
|
153
|
-
"top": [], "note": f"PostHog error: {err}",
|
|
154
|
-
})
|
|
155
|
-
continue
|
|
156
|
-
|
|
157
|
-
summary.append({
|
|
158
|
-
"product": product, "n_pages": n_pages, "spend": spend,
|
|
159
|
-
"visitors": total_visitors, "gsc_clicks": gsc_clicks, "gsc_impr": gsc_impr,
|
|
160
|
-
"top": top, "ph_proj": ph_proj, "note": "",
|
|
161
|
-
})
|
|
162
|
-
|
|
163
|
-
# Sort by visitors desc (None at bottom)
|
|
164
|
-
summary.sort(key=lambda s: (s['visitors'] is None, -(s['visitors'] or 0)))
|
|
165
|
-
|
|
166
|
-
print()
|
|
167
|
-
print(f"{'Project':<14} {'Pages':>5} {'Spend':>9} {'Visitors':>9} {'GSCclicks':>9} {'GSCimpr':>8} {'$/visitor':>10} Top page (visitors)")
|
|
168
|
-
print("-" * 130)
|
|
169
|
-
for s in summary:
|
|
170
|
-
v = s['visitors']
|
|
171
|
-
vs = "n/a" if v is None else f"{v:,}"
|
|
172
|
-
cpv = "n/a" if not v else f"${s['spend']/v:.2f}"
|
|
173
|
-
top1 = s['top'][0] if s['top'] else ("", 0)
|
|
174
|
-
top1_str = f"{top1[0][:60]} ({top1[1]})" if top1[0] else "-"
|
|
175
|
-
print(f"{s['product']:<14} {s['n_pages']:>5} ${s['spend']:>7.0f} {vs:>9} {s['gsc_clicks']:>9} {s['gsc_impr']:>8} {cpv:>10} {top1_str}")
|
|
176
|
-
if s['note']:
|
|
177
|
-
print(f" note: {s['note']}")
|
|
178
|
-
|
|
179
|
-
print("\n\nTOP-3 GSC PAGES PER PROJECT BY POSTHOG VISITORS:")
|
|
180
|
-
print("=" * 100)
|
|
181
|
-
for s in summary:
|
|
182
|
-
if not s['top']:
|
|
183
|
-
continue
|
|
184
|
-
print(f"\n{s['product']} (ph={s.get('ph_proj','?')}):")
|
|
185
|
-
for path, v in s['top'][:3]:
|
|
186
|
-
print(f" {v:>6,} {path}")
|
|
187
|
-
|
|
188
|
-
# Now for the top-N pages overall across all projects, break down by $referrer / utm_source
|
|
189
|
-
print("\n\nTRAFFIC SOURCE BREAKDOWN (Top 5 GSC pages by visitors, per project):")
|
|
190
|
-
print("=" * 100)
|
|
191
|
-
for s in summary:
|
|
192
|
-
if not s['top']:
|
|
193
|
-
continue
|
|
194
|
-
ph_proj = s.get('ph_proj')
|
|
195
|
-
if not ph_proj:
|
|
196
|
-
continue
|
|
197
|
-
top_paths = [p for p, _ in s['top'][:5]]
|
|
198
|
-
path_list = ", ".join(f"'{p.replace(chr(39), chr(39)+chr(39))}'" for p in top_paths)
|
|
199
|
-
sql = (
|
|
200
|
-
"SELECT properties.$pathname AS path, "
|
|
201
|
-
"coalesce(properties.$referring_domain, 'direct/none') AS ref, "
|
|
202
|
-
"count(DISTINCT distinct_id) AS visitors "
|
|
203
|
-
"FROM events "
|
|
204
|
-
"WHERE event = '$pageview' "
|
|
205
|
-
"AND timestamp > now() - INTERVAL 30 DAY "
|
|
206
|
-
f"AND properties.$pathname IN ({path_list}) "
|
|
207
|
-
"GROUP BY path, ref "
|
|
208
|
-
"ORDER BY path, visitors DESC "
|
|
209
|
-
"LIMIT 200"
|
|
210
|
-
)
|
|
211
|
-
try:
|
|
212
|
-
r = hogql(ph_proj, sql, key)
|
|
213
|
-
res = r.get('results') or []
|
|
214
|
-
except Exception as e:
|
|
215
|
-
print(f"\n{s['product']}: PostHog source-breakdown error: {str(e)[:80]}")
|
|
216
|
-
continue
|
|
217
|
-
by_path = defaultdict(list)
|
|
218
|
-
for path, ref, v in res:
|
|
219
|
-
by_path[path].append((ref, v))
|
|
220
|
-
print(f"\n{s['product']}:")
|
|
221
|
-
for path in top_paths:
|
|
222
|
-
srcs = by_path.get(path) or []
|
|
223
|
-
srcs.sort(key=lambda x: -x[1])
|
|
224
|
-
total = sum(v for _, v in srcs)
|
|
225
|
-
head = ", ".join(f"{ref}={v}" for ref, v in srcs[:5])
|
|
226
|
-
print(f" [{total:>5,}] {path}")
|
|
227
|
-
print(f" {head}")
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
if __name__ == "__main__":
|
|
231
|
-
main()
|
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Batch send Reddit DMs with retries and DB updates."""
|
|
3
|
-
|
|
4
|
-
import json
|
|
5
|
-
import os
|
|
6
|
-
import subprocess
|
|
7
|
-
import sys
|
|
8
|
-
import time
|
|
9
|
-
|
|
10
|
-
REPO_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
11
|
-
sys.path.insert(0, os.path.join(REPO_DIR, "scripts"))
|
|
12
|
-
from db import load_env
|
|
13
|
-
|
|
14
|
-
load_env()
|
|
15
|
-
DB_URL = os.environ["DATABASE_URL"]
|
|
16
|
-
|
|
17
|
-
DMS = [
|
|
18
|
-
(675, "RobotEnthusiast1980", "glass break sensor testing", "did you get around to testing the placement yet? curious how the 7m spec holds up in your actual space"),
|
|
19
|
-
(676, "EverySecondCountss", "zed + mcp daily driver", "did you end up getting a stable daily driver setup with zed or still tweaking configs? genuinely want to try it but don't want to spend a week debugging glue"),
|
|
20
|
-
(677, "Worf-", "NVR software choice", "solid plan on the axis cameras. what NVR software are you leaning toward? if you go with something like frigate the used ones should just work over onvif"),
|
|
21
|
-
(678, "raja-rancho", "good automation channels", "what videos or channels were most useful when you were starting out with automations? always looking for good recs to share"),
|
|
22
|
-
(679, "ShermanHoax", "sysco alternatives", "once that pricing creep sets in its brutal. have you found any regional distributors worth working with as a backup?"),
|
|
23
|
-
(680, "Anonymously_Joe", "cold ferment specs", "cold ferment is the move for volume. how long are you fermenting and what hydration? always curious about other shops setups"),
|
|
24
|
-
(681, "Lost_Drunken_Sailor", "cutting phone orders", "have you actually seen places pull off online only without losing the older regulars? curious if the drop off is as bad as i imagine"),
|
|
25
|
-
(682, "rocketstart1", "tool integration gaps", "the human glue thing is exactly the problem. which tools are you using day to day? curious if anything covers the custom integrations zapier cant handle"),
|
|
26
|
-
(683, "ghl92479", "AST based test gen", "the AST based test generation is clever. what parser are you using for the tree, or just feeding file structure to the agent directly?"),
|
|
27
|
-
(684, "Other_Grocery_9829", "alternatives to stealth", "since you mentioned puppeteer stealth, what are you actually automating? curious if theres a native API approach that could skip the detection game"),
|
|
28
|
-
(685, "Hot-Confection-3459", "liquid ai in production", "lol the trade secrets slip was great. seriously though are you using liquid models in production? curious how they compare for coding tasks"),
|
|
29
|
-
(686, "Triple-F-Jeff", "lorex web interface fix", "did you get the web interface working? accessing cameras directly by IP usually bypasses the NVR firmware limitations"),
|
|
30
|
-
(687, "socialmatehq", "supabase affiliate tracking", "your supabase CRM setup sounds clean. curious about the affiliate tracking, did you build custom tables or find an existing schema?"),
|
|
31
|
-
(688, "Zatetics", "corrections file approach", "the skills and memory approach you described is exactly my workflow too. what does your corrections file look like, do you append or prune old entries?"),
|
|
32
|
-
(689, "partstable", "corrections database ROI", "800 tokens preventing 90% of mistakes is incredible ROI. how do you decide when a correction gets promoted vs retired?"),
|
|
33
|
-
(690, "stumptowndoug", "multi agent file conflicts", "nice work shipping the fs watcher. how does it handle when multiple agents write the same file? thats where my polling setup kept breaking"),
|
|
34
|
-
(691, "Virtual_Armadillo126", "perception vs permissions", "your point about what agents can see vs do is underrated. have you seen any framework that cleanly separates perception from permission boundaries?"),
|
|
35
|
-
(692, "Limp_Cauliflower5192", "wildest user edge case", "first week after shipping is always humbling. what was the wildest edge case a real user found in your agent?"),
|
|
36
|
-
(693, "Intelligent-School64", "batch processing technique", "going from 90s to 15s latency is massive. would love to hear more about the batch processing approach you landed on"),
|
|
37
|
-
(694, "chocboyfish", "lighting upgrade idea", "did the lighting swap idea resonate? even going from overhead fluorescent to warmer pendants can change the whole vibe for cheap"),
|
|
38
|
-
(695, "Individual-Love-9342", "per agent cost tracking", "the postgres logging approach has been solid for us. what granularity are you tracking, per call or per task?"),
|
|
39
|
-
(696, "symmetry_seeking", "doc drift detection", "hierarchy docs drifting from code is the exact problem. have you tried any automated checks to flag when docs and code diverge?"),
|
|
40
|
-
(697, "commanderjack_EDH", "camera ONVIF check", "did you check the model number yet? if they do ONVIF you have tons of options for third party NVR software even without the app"),
|
|
41
|
-
(698, "Silver_Ad4449", "tiktok content approach", "smart move going deep on one platform. what kind of content is working for you on tiktok? the algorithm rewards very different stuff"),
|
|
42
|
-
(699, "upvotes2doge", "ARIA tree on different sites", "curious if youve tried inspector jake on SPAs specifically, some seem to have pretty sparse accessibility trees which limits what the agent can actually target"),
|
|
43
|
-
(700, "Abject_Elevator5461", "online ordering shift rate", "the ATP difference between phone and online is legit. curious what percentage of orders you see shift when places push for online"),
|
|
44
|
-
(701, "JimiJohhnySRV", "hallway camera setup", "the activity zone trick for high traffic areas is a game changer. what camera did you end up using for the hallway?"),
|
|
45
|
-
(702, "handsnerfin", "outline before AI approach", "the outline before asking approach really does change retention. curious if youve found specific problem types where it helps more"),
|
|
46
|
-
(703, "Objective_River_5218", "global skill noise", "interesting that global skills havent gotten noisy for you yet. how many are in your knowledge base right now?"),
|
|
47
|
-
(704, "bobo-the-merciful", "nelson quarterdeck rhythm", "the admiral pattern is well thought out. does the quarterdeck rhythm catch things the escalation path misses in practice?"),
|
|
48
|
-
(705, "MrBrightside_119", "stuck session detection", "the cron based stuck detection is clever. whats your time threshold and how often does it false positive?"),
|
|
49
|
-
(706, "Pritom14", "AX tree token savings", "landed on the same AX tree plus screenshot combo. have you benchmarked token savings with tree data vs pure screenshots through your proxy?"),
|
|
50
|
-
(707, "EbbCommon9300", "autonomy session risk", "the near miss data angle is fascinating. are you scoping risk per agent session or across delegated action chains?"),
|
|
51
|
-
(708, "Beautiful_Ad789", "workflow format", "totally agree vibe workflow is its own thing. what format did you end up using instead of json for workflow definitions?"),
|
|
52
|
-
(709, "eazyigz123", "hard block rollback rate", "the hard block vs soft memory distinction is interesting. how often have you had to roll blocks back when context changed?"),
|
|
53
|
-
(710, "entheosoul", "MCP structured output case", "fair pushback on MCP vs bash. the structured output case is where MCP shines but agreed its not always worth the overhead"),
|
|
54
|
-
(711, "dawsonvpowell", "Jobber vs Housecall gaps", "everything in one system makes sense. which of those tools did you go with and where were the gaps?"),
|
|
55
|
-
(712, "KiteWhisperer", "clinic manager tipping point", "thats the goal for year two. what patient volume did you find makes a clinic manager pay for themselves?"),
|
|
56
|
-
(713, "Ambitious_Bridge_180", "other OD communities", "just checked out ODs on Finance, great group. any other communities youd recommend for the business ops side?"),
|
|
57
|
-
(714, "EthanWalker483", "lead building fix", "monday morning reminder emails are exactly it. did switching to a database fix the lead building or just shift the bottleneck?"),
|
|
58
|
-
]
|
|
59
|
-
|
|
60
|
-
def send_dm(dm_id, author, subject, body):
|
|
61
|
-
max_retries = 3
|
|
62
|
-
for attempt in range(1, max_retries + 1):
|
|
63
|
-
print(f"[{dm_id}] Attempt {attempt}: Sending to {author}...")
|
|
64
|
-
try:
|
|
65
|
-
result = subprocess.run(
|
|
66
|
-
["python3", os.path.join(REPO_DIR, "scripts", "reddit_browser.py"),
|
|
67
|
-
"compose-dm", author, subject, body],
|
|
68
|
-
capture_output=True, text=True, timeout=60
|
|
69
|
-
)
|
|
70
|
-
data = json.loads(result.stdout.strip().split('\n')[-1] if result.stdout.strip() else '{}')
|
|
71
|
-
except Exception as e:
|
|
72
|
-
data = {"ok": False, "error": str(e)}
|
|
73
|
-
|
|
74
|
-
if data.get("ok"):
|
|
75
|
-
print(f"[{dm_id}] SUCCESS: Sent to {author}")
|
|
76
|
-
# Update DB
|
|
77
|
-
try:
|
|
78
|
-
subprocess.run(
|
|
79
|
-
["psql", DB_URL, "-c",
|
|
80
|
-
f"UPDATE dms SET status='sent', our_dm_content=$${body}$$, sent_at=NOW() WHERE id={dm_id};"],
|
|
81
|
-
capture_output=True, timeout=15
|
|
82
|
-
)
|
|
83
|
-
except Exception:
|
|
84
|
-
pass
|
|
85
|
-
# Log outbound
|
|
86
|
-
try:
|
|
87
|
-
subprocess.run(
|
|
88
|
-
["python3", os.path.join(REPO_DIR, "scripts", "dm_conversation.py"),
|
|
89
|
-
"log-outbound", "--dm-id", str(dm_id), "--content", body],
|
|
90
|
-
capture_output=True, timeout=15
|
|
91
|
-
)
|
|
92
|
-
except Exception:
|
|
93
|
-
pass
|
|
94
|
-
# Stamp chat_url from the sender's returned thread_url so the dashboard
|
|
95
|
-
# "open chat" button points at the actual room. set-url refuses anything
|
|
96
|
-
# that isn't a real DM-thread URL, so bad data can't leak in.
|
|
97
|
-
thread_url = data.get("thread_url") or ""
|
|
98
|
-
if thread_url:
|
|
99
|
-
try:
|
|
100
|
-
subprocess.run(
|
|
101
|
-
["python3", os.path.join(REPO_DIR, "scripts", "dm_conversation.py"),
|
|
102
|
-
"set-url", "--dm-id", str(dm_id), "--url", thread_url],
|
|
103
|
-
capture_output=True, timeout=15
|
|
104
|
-
)
|
|
105
|
-
except Exception:
|
|
106
|
-
pass
|
|
107
|
-
return True
|
|
108
|
-
|
|
109
|
-
error = data.get("error", "unknown")
|
|
110
|
-
print(f"[{dm_id}] FAILED (attempt {attempt}): {error}")
|
|
111
|
-
if attempt < max_retries:
|
|
112
|
-
print(f"[{dm_id}] Waiting 30s before retry...")
|
|
113
|
-
time.sleep(30)
|
|
114
|
-
|
|
115
|
-
# All retries exhausted
|
|
116
|
-
print(f"[{dm_id}] ERROR: All retries exhausted for {author}")
|
|
117
|
-
try:
|
|
118
|
-
subprocess.run(
|
|
119
|
-
["psql", DB_URL, "-c",
|
|
120
|
-
f"UPDATE dms SET status='error', skip_reason=$$Failed after 3 attempts: {error}$$ WHERE id={dm_id};"],
|
|
121
|
-
capture_output=True, timeout=15
|
|
122
|
-
)
|
|
123
|
-
except Exception:
|
|
124
|
-
pass
|
|
125
|
-
return False
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
if __name__ == "__main__":
|
|
129
|
-
# Force unbuffered output
|
|
130
|
-
sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', buffering=1)
|
|
131
|
-
sys.stderr = os.fdopen(sys.stderr.fileno(), 'w', buffering=1)
|
|
132
|
-
|
|
133
|
-
succeeded = 0
|
|
134
|
-
failed = 0
|
|
135
|
-
for dm_id, author, subject, body in DMS:
|
|
136
|
-
if send_dm(dm_id, author, subject, body):
|
|
137
|
-
succeeded += 1
|
|
138
|
-
else:
|
|
139
|
-
failed += 1
|
|
140
|
-
sys.stdout.flush()
|
|
141
|
-
# Small delay between DMs
|
|
142
|
-
time.sleep(2)
|
|
143
|
-
|
|
144
|
-
print(f"\n=== SUMMARY ===")
|
|
145
|
-
print(f"Succeeded: {succeeded}")
|
|
146
|
-
print(f"Failed: {failed}")
|
|
147
|
-
print(f"Total: {succeeded + failed}")
|
|
148
|
-
sys.stdout.flush()
|