social-autoposter 1.5.1 → 1.6.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "social-autoposter",
3
- "version": "1.5.1",
3
+ "version": "1.6.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"
@@ -17,6 +17,7 @@ import time
17
17
 
18
18
  sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
19
19
  import db as dbmod
20
+ from version import read_version as read_autoposter_version
20
21
 
21
22
  CONFIG_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "config.json")
22
23
 
@@ -391,9 +392,9 @@ def cmd_log_post(args):
391
392
  thread_title, thread_content, our_url, our_content, our_account,
392
393
  source_summary, project_name, status, posted_at, feedback_report_used,
393
394
  engagement_style, search_topic, language, claude_session_id,
394
- generation_trace, link_source)
395
+ generation_trace, link_source, autoposter_version)
395
396
  VALUES ('github', %s, %s, %s, %s, '', %s, %s, %s, '', %s, 'active', NOW(), TRUE,
396
- %s, %s, %s, %s::uuid, %s::jsonb, %s) RETURNING id""",
397
+ %s, %s, %s, %s::uuid, %s::jsonb, %s, %s) RETURNING id""",
397
398
  [args.thread_url, args.thread_author, args.thread_author, args.thread_title,
398
399
  args.our_url, args.our_text, args.account, args.project,
399
400
  getattr(args, "engagement_style", None),
@@ -401,7 +402,8 @@ def cmd_log_post(args):
401
402
  (getattr(args, "language", None) or "en"),
402
403
  session_id,
403
404
  generation_trace_json,
404
- getattr(args, "link_source", None)],
405
+ getattr(args, "link_source", None),
406
+ read_autoposter_version()],
405
407
  )
406
408
  row = cur.fetchone()
407
409
  new_id = row[0] if row else None
@@ -23,7 +23,10 @@ import sys
23
23
  from datetime import datetime
24
24
 
25
25
  sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
26
- import db as dbmod
26
+
27
+
28
+ def _use_legacy_neon() -> bool:
29
+ return os.environ.get("SOCIAL_AUTOPOSTER_LEGACY_NEON") == "1"
27
30
 
28
31
  PROJECTS_ROOT = os.path.expanduser("~/.claude/projects")
29
32
 
@@ -483,6 +486,124 @@ def parse_transcript(path: str):
483
486
  }
484
487
 
485
488
 
489
+ _BACKFILL_TABLES = (
490
+ "posts", "replies", "dms", "dm_messages",
491
+ "seo_escalations", "seo_keywords", "seo_page_improvements", "gsc_queries",
492
+ )
493
+
494
+
495
+ def _persist_via_api(args, parsed, started, ended, duration_ms, orch_cost, cycle_id):
496
+ """Upsert claude_sessions row + backfill model column via HTTP routes.
497
+
498
+ Two calls:
499
+ POST /api/v1/claude-sessions -> upsert by session_id
500
+ POST /api/v1/claude-sessions/backfill-model -> stamp model on activity rows
501
+ """
502
+ from http_api import api_post
503
+ api_post(
504
+ "/api/v1/claude-sessions",
505
+ {
506
+ "session_id": args.session_id,
507
+ "script": args.script,
508
+ "started_at": started,
509
+ "ended_at": ended,
510
+ "duration_ms": duration_ms,
511
+ "total_cost_usd": round(parsed["total_cost_usd"], 6),
512
+ "orchestrator_cost_usd": orch_cost,
513
+ "input_tokens": parsed["totals"]["input"],
514
+ "output_tokens": parsed["totals"]["output"],
515
+ "cache_read_tokens": parsed["totals"]["cache_read"],
516
+ "cache_creation_tokens": parsed["totals"]["cache_creation"],
517
+ "model_breakdown": parsed["by_model"],
518
+ "model": parsed["primary_model"],
519
+ "cycle_id": cycle_id,
520
+ "task_call_count": parsed.get("task_call_count", 0),
521
+ "subagent_count": parsed.get("subagent_count", 0),
522
+ "subagent_cost_usd": parsed.get("subagent_cost_usd", 0.0),
523
+ "subagent_breakdown": parsed.get("subagent_breakdown") or None,
524
+ },
525
+ )
526
+
527
+ resp = api_post(
528
+ "/api/v1/claude-sessions/backfill-model",
529
+ {
530
+ "session_id": args.session_id,
531
+ "model": parsed["primary_model"],
532
+ "tables": list(_BACKFILL_TABLES),
533
+ },
534
+ )
535
+ data = (resp or {}).get("data") or {}
536
+ backfill_counts = data.get("backfilled") or {}
537
+ for t in _BACKFILL_TABLES:
538
+ backfill_counts.setdefault(t, 0)
539
+ return backfill_counts
540
+
541
+
542
+ def _persist_via_neon(args, parsed, started, ended, duration_ms, orch_cost, cycle_id):
543
+ """Legacy path: direct psycopg2 to Neon. Gated by SOCIAL_AUTOPOSTER_LEGACY_NEON=1."""
544
+ import db as dbmod
545
+ dbmod.load_env()
546
+ conn = dbmod.get_conn()
547
+ subagent_breakdown_json = (
548
+ json.dumps(parsed["subagent_breakdown"]) if parsed.get("subagent_breakdown") else None
549
+ )
550
+ conn.execute(
551
+ """INSERT INTO claude_sessions (
552
+ session_id, script, started_at, ended_at, duration_ms,
553
+ total_cost_usd, orchestrator_cost_usd,
554
+ input_tokens, output_tokens,
555
+ cache_read_tokens, cache_creation_tokens, model_breakdown, model,
556
+ cycle_id,
557
+ task_call_count, subagent_count, subagent_cost_usd, subagent_breakdown
558
+ ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s::jsonb, %s, %s,
559
+ %s, %s, %s, %s::jsonb)
560
+ ON CONFLICT (session_id) DO UPDATE SET
561
+ ended_at = EXCLUDED.ended_at,
562
+ duration_ms = EXCLUDED.duration_ms,
563
+ total_cost_usd = EXCLUDED.total_cost_usd,
564
+ orchestrator_cost_usd = COALESCE(EXCLUDED.orchestrator_cost_usd,
565
+ claude_sessions.orchestrator_cost_usd),
566
+ input_tokens = EXCLUDED.input_tokens,
567
+ output_tokens = EXCLUDED.output_tokens,
568
+ cache_read_tokens = EXCLUDED.cache_read_tokens,
569
+ cache_creation_tokens = EXCLUDED.cache_creation_tokens,
570
+ model_breakdown = EXCLUDED.model_breakdown,
571
+ model = EXCLUDED.model,
572
+ cycle_id = COALESCE(EXCLUDED.cycle_id, claude_sessions.cycle_id),
573
+ task_call_count = EXCLUDED.task_call_count,
574
+ subagent_count = EXCLUDED.subagent_count,
575
+ subagent_cost_usd = EXCLUDED.subagent_cost_usd,
576
+ subagent_breakdown = EXCLUDED.subagent_breakdown
577
+ """,
578
+ [
579
+ args.session_id, args.script, started, ended, duration_ms,
580
+ round(parsed["total_cost_usd"], 6),
581
+ orch_cost,
582
+ parsed["totals"]["input"], parsed["totals"]["output"],
583
+ parsed["totals"]["cache_read"], parsed["totals"]["cache_creation"],
584
+ json.dumps(parsed["by_model"]),
585
+ parsed["primary_model"],
586
+ cycle_id,
587
+ parsed.get("task_call_count", 0),
588
+ parsed.get("subagent_count", 0),
589
+ parsed.get("subagent_cost_usd", 0.0),
590
+ subagent_breakdown_json,
591
+ ],
592
+ )
593
+
594
+ backfill_counts = {}
595
+ for table in _BACKFILL_TABLES:
596
+ cur = conn.execute(
597
+ f"UPDATE {table} SET model = %s "
598
+ f"WHERE claude_session_id = %s AND model IS NULL",
599
+ [parsed["primary_model"], args.session_id],
600
+ )
601
+ backfill_counts[table] = cur.rowcount
602
+ conn.commit()
603
+ conn.close()
604
+ return backfill_counts
605
+
606
+
486
607
  def main():
487
608
  parser = argparse.ArgumentParser()
488
609
  parser.add_argument("--session-id", required=True)
@@ -558,74 +679,12 @@ def main():
558
679
  else None
559
680
  )
560
681
 
561
- dbmod.load_env()
562
- conn = dbmod.get_conn()
563
- subagent_breakdown_json = (
564
- json.dumps(parsed["subagent_breakdown"]) if parsed.get("subagent_breakdown") else None
565
- )
566
- conn.execute(
567
- """INSERT INTO claude_sessions (
568
- session_id, script, started_at, ended_at, duration_ms,
569
- total_cost_usd, orchestrator_cost_usd,
570
- input_tokens, output_tokens,
571
- cache_read_tokens, cache_creation_tokens, model_breakdown, model,
572
- cycle_id,
573
- task_call_count, subagent_count, subagent_cost_usd, subagent_breakdown
574
- ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s::jsonb, %s, %s,
575
- %s, %s, %s, %s::jsonb)
576
- ON CONFLICT (session_id) DO UPDATE SET
577
- ended_at = EXCLUDED.ended_at,
578
- duration_ms = EXCLUDED.duration_ms,
579
- total_cost_usd = EXCLUDED.total_cost_usd,
580
- orchestrator_cost_usd = COALESCE(EXCLUDED.orchestrator_cost_usd,
581
- claude_sessions.orchestrator_cost_usd),
582
- input_tokens = EXCLUDED.input_tokens,
583
- output_tokens = EXCLUDED.output_tokens,
584
- cache_read_tokens = EXCLUDED.cache_read_tokens,
585
- cache_creation_tokens = EXCLUDED.cache_creation_tokens,
586
- model_breakdown = EXCLUDED.model_breakdown,
587
- model = EXCLUDED.model,
588
- cycle_id = COALESCE(EXCLUDED.cycle_id, claude_sessions.cycle_id),
589
- task_call_count = EXCLUDED.task_call_count,
590
- subagent_count = EXCLUDED.subagent_count,
591
- subagent_cost_usd = EXCLUDED.subagent_cost_usd,
592
- subagent_breakdown = EXCLUDED.subagent_breakdown
593
- """,
594
- [
595
- args.session_id, args.script, started, ended, duration_ms,
596
- round(parsed["total_cost_usd"], 6),
597
- orch_cost,
598
- parsed["totals"]["input"], parsed["totals"]["output"],
599
- parsed["totals"]["cache_read"], parsed["totals"]["cache_creation"],
600
- json.dumps(parsed["by_model"]),
601
- parsed["primary_model"],
602
- cycle_id,
603
- parsed.get("task_call_count", 0),
604
- parsed.get("subagent_count", 0),
605
- parsed.get("subagent_cost_usd", 0.0),
606
- subagent_breakdown_json,
607
- ],
608
- )
609
-
610
- # Backfill dominant model onto any activity rows stamped with this session.
611
- # Only overwrites rows where model IS NULL so re-runs of log_claude_session
612
- # against the same session_id stay idempotent. Covers social tables
613
- # (posts/replies/dms/dm_messages) plus SEO pipeline tables that stamp
614
- # claude_session_id (seo_escalations, seo_keywords, seo_page_improvements,
615
- # gsc_queries).
616
- backfill_counts = {}
617
- for table in (
618
- "posts", "replies", "dms", "dm_messages",
619
- "seo_escalations", "seo_keywords", "seo_page_improvements", "gsc_queries",
620
- ):
621
- cur = conn.execute(
622
- f"UPDATE {table} SET model = %s "
623
- f"WHERE claude_session_id = %s AND model IS NULL",
624
- [parsed["primary_model"], args.session_id],
625
- )
626
- backfill_counts[table] = cur.rowcount
627
- conn.commit()
628
- conn.close()
682
+ if _use_legacy_neon():
683
+ backfill_counts = _persist_via_neon(args, parsed, started, ended, duration_ms,
684
+ orch_cost, cycle_id)
685
+ else:
686
+ backfill_counts = _persist_via_api(args, parsed, started, ended, duration_ms,
687
+ orch_cost, cycle_id)
629
688
 
630
689
  print(json.dumps({
631
690
  "logged": True,
@@ -20,6 +20,7 @@ from datetime import datetime, timezone
20
20
 
21
21
  sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
22
22
  from http_api import api_get, api_post
23
+ from version import read_version as read_autoposter_version
23
24
 
24
25
  USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)"
25
26
 
@@ -776,6 +777,12 @@ def cmd_log_post(args):
776
777
  # post_reddit.py based on which URL Claude baked into the reply text.
777
778
  if getattr(args, "link_source", None):
778
779
  body["link_source"] = args.link_source
780
+ # autoposter_version: social-autoposter package.json version at the moment
781
+ # we posted. Powers per-release attribution: "did 1.5.0 outperform 1.4.x
782
+ # on Reddit?". None when package.json + env are both missing.
783
+ autoposter_version = read_autoposter_version()
784
+ if autoposter_version:
785
+ body["autoposter_version"] = autoposter_version
779
786
  resp = api_post("/api/v1/posts", body, ok_on_conflict=True)
780
787
  err = resp.get("error") if isinstance(resp, dict) else None
781
788
  if err: