social-autoposter 1.6.52 → 1.6.53

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/bin/cli.js CHANGED
@@ -520,31 +520,43 @@ function installBrowserHarness() {
520
520
  }
521
521
 
522
522
  // Step 2 + 3: clone + `uv tool install -e .` browser-harness.
523
+ //
524
+ // PINNED to a known-good upstream commit instead of tracking origin/HEAD.
525
+ // The installer used to fetch+reset --hard to HEAD on every run, so any
526
+ // upstream change shipped to users untested (this is how the two-blank-tab
527
+ // regression in upstream daemon.py attach behavior could reach users). Our
528
+ // launch-at-real-URL fix in server.py/twitter-backend.sh neutralizes that
529
+ // class of bug regardless, but pinning stops surprise upstream drift. Bump
530
+ // BROWSER_HARNESS_PIN deliberately after validating a newer upstream against
531
+ // the shipped server.py contract.
532
+ const BROWSER_HARNESS_PIN = '6d20866664ea3d9691b27bbf64f42ae097437dc3';
523
533
  const harnessDir = path.join(HOME, 'Developer', 'browser-harness');
534
+ const pinHarness = () => {
535
+ // Fetch the exact pinned commit (GitHub serves arbitrary SHAs) and hard-
536
+ // reset onto it. Works for a fresh clone and an existing checkout alike.
537
+ const fetch = spawnSync('git', ['-C', harnessDir, 'fetch', '--depth', '1', 'origin', BROWSER_HARNESS_PIN], { stdio: 'inherit' });
538
+ if (fetch.status !== 0) {
539
+ console.warn(` WARNING: could not fetch pinned browser-harness commit ${BROWSER_HARNESS_PIN.slice(0, 9)}; using existing checkout.`);
540
+ return;
541
+ }
542
+ const reset = spawnSync('git', ['-C', harnessDir, 'reset', '--hard', 'FETCH_HEAD'], { stdio: 'inherit' });
543
+ if (reset.status !== 0) {
544
+ console.warn(' WARNING: could not reset browser-harness clone to pinned commit; using existing checkout.');
545
+ }
546
+ };
524
547
  if (!fs.existsSync(harnessDir)) {
525
548
  fs.mkdirSync(path.dirname(harnessDir), { recursive: true });
526
549
  console.log(' cloning browser-harness from GitHub...');
527
550
  const clone = spawnSync('git', ['clone', '--depth', '1', 'https://github.com/browser-use/browser-harness', harnessDir], { stdio: 'inherit' });
528
551
  if (clone.status !== 0) {
529
552
  console.warn(' WARNING: git clone failed; twitter-harness will not work until you clone manually.');
530
- }
531
- } else {
532
- // Refresh the existing clone instead of silently reusing it. server.py
533
- // invokes `browser-harness -c <script>`; a stale checkout that predates the
534
- // `-c` interface (or otherwise drifted from upstream) makes every bh_run
535
- // return the CLI usage string while looking "installed". fetch+reset --hard
536
- // to current upstream so the installed CLI always matches the shipped
537
- // server.py contract.
538
- console.log(` browser-harness clone exists -> ${harnessDir}; updating to latest...`);
539
- const fetch = spawnSync('git', ['-C', harnessDir, 'fetch', '--depth', '1', 'origin', 'HEAD'], { stdio: 'inherit' });
540
- if (fetch.status === 0) {
541
- const reset = spawnSync('git', ['-C', harnessDir, 'reset', '--hard', 'FETCH_HEAD'], { stdio: 'inherit' });
542
- if (reset.status !== 0) {
543
- console.warn(' WARNING: could not reset browser-harness clone to latest; using existing checkout.');
544
- }
545
553
  } else {
546
- console.warn(' WARNING: could not fetch browser-harness updates; using existing checkout.');
554
+ console.log(` pinning browser-harness to ${BROWSER_HARNESS_PIN.slice(0, 9)}...`);
555
+ pinHarness();
547
556
  }
557
+ } else {
558
+ console.log(` browser-harness clone exists -> ${harnessDir}; pinning to ${BROWSER_HARNESS_PIN.slice(0, 9)}...`);
559
+ pinHarness();
548
560
  }
549
561
 
550
562
  if (uvBin && fs.existsSync(harnessDir)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "social-autoposter",
3
- "version": "1.6.52",
3
+ "version": "1.6.53",
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"
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env python3
2
+ """One-shot: create recurring accelerator-application reminders in
3
+ matt@mediar.ai's Google Calendar via DWD (same SA as the gmail keepalive),
4
+ requesting a calendar scope. If the scope isn't authorized in the mediar.ai
5
+ Workspace DWD config, this fails with unauthorized_client and we fall back.
6
+ """
7
+ import json, time, urllib.parse, urllib.request, urllib.error
8
+ import google.auth
9
+ from google.auth.transport.requests import Request
10
+ from googleapiclient.discovery import build
11
+
12
+ SA_EMAIL = "gmail-dwd-impersonator@gmail-api-integration-486018.iam.gserviceaccount.com"
13
+ TARGET_USER = "matt@mediar.ai"
14
+ SCOPE = "https://www.googleapis.com/auth/calendar"
15
+
16
+
17
+ def mint_access_token():
18
+ creds, _ = google.auth.default(
19
+ scopes=["https://www.googleapis.com/auth/cloud-platform"]
20
+ )
21
+ creds.refresh(Request())
22
+ iat = int(time.time()); exp = iat + 3600
23
+ claim = {"iss": SA_EMAIL, "sub": TARGET_USER, "scope": SCOPE,
24
+ "aud": "https://oauth2.googleapis.com/token", "iat": iat, "exp": exp}
25
+ iam = build("iamcredentials", "v1", credentials=creds, cache_discovery=False)
26
+ signed = iam.projects().serviceAccounts().signJwt(
27
+ name=f"projects/-/serviceAccounts/{SA_EMAIL}",
28
+ body={"payload": json.dumps(claim)}).execute()
29
+ body = urllib.parse.urlencode({
30
+ "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
31
+ "assertion": signed["signedJwt"]}).encode()
32
+ req = urllib.request.Request("https://oauth2.googleapis.com/token", data=body,
33
+ headers={"Content-Type": "application/x-www-form-urlencoded"})
34
+ try:
35
+ with urllib.request.urlopen(req, timeout=30) as resp:
36
+ return json.loads(resp.read())["access_token"]
37
+ except urllib.error.HTTPError as e:
38
+ raise RuntimeError(f"token exchange {e.code}: {e.read().decode()}") from None
39
+
40
+
41
+ def main():
42
+ from google.oauth2.credentials import Credentials
43
+ token = mint_access_token()
44
+ cal = build("calendar", "v3", credentials=Credentials(token=token),
45
+ cache_discovery=False)
46
+
47
+ events = [
48
+ {
49
+ "summary": "Apply to a16z Speedrun (next cohort) - S4L",
50
+ "start": "2026-09-15",
51
+ "rrule": "RRULE:FREQ=MONTHLY;INTERVAL=4",
52
+ "description": (
53
+ "Reapply S4L to a16z Speedrun. Speedrun runs ~3 cohorts/year; "
54
+ "this fires every 4 months so you catch the next deadline.\n\n"
55
+ "Apply: https://speedrun.a16z.com/apply (start with email i@m13v.com)\n"
56
+ "Status / update existing app: https://speedrun.a16z.com/application-login\n\n"
57
+ "Reuse the saved answer set (pitch, traction, funding, founder bio). "
58
+ "Last filled 2026-06-03; still need citizenship, university, years of "
59
+ "experience, last-round date, and the 3 investor emails."
60
+ ),
61
+ },
62
+ {
63
+ "summary": "Apply to PearX (next batch) - S4L",
64
+ "start": "2026-10-01",
65
+ "rrule": "RRULE:FREQ=MONTHLY;INTERVAL=6",
66
+ "description": (
67
+ "Reapply S4L to PearX. Pear runs 2 batches/year (summer + winter); "
68
+ "this fires every 6 months for the next window.\n\n"
69
+ "Apply: https://pear.vc/pearx-application/ (Airtable form; long-text "
70
+ "fields are contenteditable divs)\n\n"
71
+ "Reuse the saved answer set. PearX S26 app was filled 2026-06-03 "
72
+ "(left for review, not submitted)."
73
+ ),
74
+ },
75
+ ]
76
+
77
+ created = []
78
+ for ev in events:
79
+ body = {
80
+ "summary": ev["summary"],
81
+ "description": ev["description"],
82
+ "start": {"date": ev["start"]},
83
+ "end": {"date": ev["start"]},
84
+ "recurrence": [ev["rrule"]],
85
+ "reminders": {"useDefault": False, "overrides": [
86
+ {"method": "popup", "minutes": 24 * 60},
87
+ {"method": "email", "minutes": 24 * 60},
88
+ ]},
89
+ "transparency": "transparent",
90
+ }
91
+ out = cal.events().insert(calendarId="primary", body=body).execute()
92
+ created.append((ev["summary"], out.get("htmlLink")))
93
+
94
+ for s, link in created:
95
+ print(f"CREATED: {s}\n {link}")
96
+
97
+
98
+ if __name__ == "__main__":
99
+ main()
@@ -152,6 +152,24 @@ def _now_iso():
152
152
  return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
153
153
 
154
154
 
155
+ def _fmt_local(iso_or_dt):
156
+ """Render a UTC ISO string (or datetime) in the machine's local timezone for
157
+ human-readable emails, e.g. '2026-06-03 16:35 PDT'. Falls back to the raw
158
+ input on any parse failure so the email still goes out."""
159
+ try:
160
+ dt = iso_or_dt
161
+ if isinstance(dt, str):
162
+ dt = _parse_ts(dt)
163
+ if dt is None:
164
+ return str(iso_or_dt)
165
+ if dt.tzinfo is None:
166
+ dt = dt.replace(tzinfo=timezone.utc)
167
+ local = dt.astimezone()
168
+ return local.strftime("%Y-%m-%d %I:%M %p %Z").replace(" 0", " ")
169
+ except Exception:
170
+ return str(iso_or_dt)
171
+
172
+
155
173
  def _ensure_dir():
156
174
  os.makedirs(STATE_DIR, exist_ok=True)
157
175
 
@@ -1125,7 +1143,7 @@ def _cmd_recover_record(args):
1125
1143
  "",
1126
1144
  "We are NOT giving up: pipelines stay paused and we will make",
1127
1145
  "one more login attempt after the restriction lifts, at",
1128
- " " + str(retry_at) + " (UTC)",
1146
+ " " + _fmt_local(retry_at) + " (" + str(retry_at) + " UTC)",
1129
1147
  "This is attempt " + str(attempts) + " of "
1130
1148
  + str(RECOVERY_RESTRICTED_MAX_ATTEMPTS) + ".",
1131
1149
  "",