@m13v/s4l 1.6.197-rc.7
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 +143 -0
- package/SKILL.md +342 -0
- package/bin/cli.js +980 -0
- package/bin/cookie-helper.js +315 -0
- package/bin/platform.js +59 -0
- package/bin/scheduler/index.js +12 -0
- package/bin/scheduler/launchd.js +518 -0
- package/browser-agent-configs/all-agents-mcp.json +68 -0
- package/browser-agent-configs/linkedin-agent-mcp.json +16 -0
- package/browser-agent-configs/linkedin-agent.json +17 -0
- package/browser-agent-configs/linkedin-harness-mcp.json +21 -0
- package/browser-agent-configs/reddit-agent-mcp.json +16 -0
- package/browser-agent-configs/reddit-agent.json +17 -0
- package/browser-agent-configs/twitter-harness-mcp.json +18 -0
- package/config.example.json +45 -0
- package/mcp/dist/index.js +4212 -0
- package/mcp/dist/onboarding.js +200 -0
- package/mcp/dist/panel.html +176 -0
- package/mcp/dist/product-link.html +102 -0
- package/mcp/dist/repo.js +222 -0
- package/mcp/dist/runtime.js +1079 -0
- package/mcp/dist/screencast.js +323 -0
- package/mcp/dist/setup.js +545 -0
- package/mcp/dist/telemetry.js +306 -0
- package/mcp/dist/twitterAuth.js +138 -0
- package/mcp/dist/version.js +271 -0
- package/mcp/dist/version.json +4 -0
- package/mcp/install-runtime.mjs +70 -0
- package/mcp/install.mjs +169 -0
- package/mcp/manifest.json +80 -0
- package/mcp/menubar/dashboard_server.py +213 -0
- package/mcp/menubar/s4l_card.py +1314 -0
- package/mcp/menubar/s4l_log_relay.py +179 -0
- package/mcp/menubar/s4l_menubar.py +2439 -0
- package/mcp/menubar/s4l_state.py +891 -0
- package/mcp/package.json +34 -0
- package/mcp/shared/doctor.cjs +437 -0
- package/mcp/shared/onboarding-ledger.cjs +324 -0
- package/mcp-servers/browser-harness/server.py +968 -0
- package/package.json +160 -0
- package/requirements.txt +20 -0
- package/scripts/_compute_allowlist.py +58 -0
- package/scripts/_db_update.py +20 -0
- package/scripts/_filt.py +9 -0
- package/scripts/_li_notif_match.py +76 -0
- package/scripts/_li_notif_orchestrate.py +126 -0
- package/scripts/_lock_preempt_test.py +60 -0
- package/scripts/_run_icp_precheck.py +57 -0
- package/scripts/a16z_pearx_calendar_reminders.py +99 -0
- package/scripts/account_resolver.py +141 -0
- package/scripts/active_campaigns.py +114 -0
- package/scripts/active_users.py +190 -0
- package/scripts/amplitude_24h_signups.py +468 -0
- package/scripts/amplitude_signups.py +177 -0
- package/scripts/apply_onboarding_selections.py +131 -0
- package/scripts/audience_pages.py +243 -0
- package/scripts/audit_helper.py +120 -0
- package/scripts/author_history_block.py +353 -0
- package/scripts/autopilot_stall_watch.py +284 -0
- package/scripts/backfill_twitter_attempts_topic.py +81 -0
- package/scripts/backfill_twitter_log_post_no_id.py +322 -0
- package/scripts/bench_dashboard.sh +138 -0
- package/scripts/bh_send.py +39 -0
- package/scripts/build_persona.py +409 -0
- package/scripts/bulk_icp.py +18 -0
- package/scripts/campaign_bump.py +51 -0
- package/scripts/capture_thread_media.py +288 -0
- package/scripts/check_browser_lock_health.sh +81 -0
- package/scripts/check_external_pool_depth.py +253 -0
- package/scripts/check_unread_web_chats.py +28 -0
- package/scripts/claim_web_chat.py +47 -0
- package/scripts/classify_run_error.py +158 -0
- package/scripts/claude_job.py +988 -0
- package/scripts/clean_stale_singleton.sh +56 -0
- package/scripts/cleanup_harness_tabs.py +68 -0
- package/scripts/copy_browser_cookies.py +454 -0
- package/scripts/counterparty_history.py +350 -0
- package/scripts/db.py +57 -0
- package/scripts/discover_claude_profiles.py +120 -0
- package/scripts/discover_linkedin_candidates.py +984 -0
- package/scripts/dm_conversation.py +682 -0
- package/scripts/dm_db_update.py +69 -0
- package/scripts/dm_engage_helper.py +161 -0
- package/scripts/dm_outreach_helper.py +147 -0
- package/scripts/dm_outreach_twitter_helper.py +129 -0
- package/scripts/dm_send_log.py +106 -0
- package/scripts/dm_short_links.py +1084 -0
- package/scripts/dump_web_chat_history.py +47 -0
- package/scripts/engage_github.py +640 -0
- package/scripts/engage_reddit.py +1235 -0
- package/scripts/engage_twitter_helper.py +301 -0
- package/scripts/engagement_styles.py +1787 -0
- package/scripts/enrich_twitter_candidates.py +82 -0
- package/scripts/feedback_digest.py +448 -0
- package/scripts/fetch_prospect_profile.py +312 -0
- package/scripts/fetch_twitter_t1.py +134 -0
- package/scripts/find_threads.py +530 -0
- package/scripts/follow_gate_log.py +59 -0
- package/scripts/funnel_per_day.py +194 -0
- package/scripts/generate_daily_human_style.py +494 -0
- package/scripts/generation_trace.py +173 -0
- package/scripts/get_run_cost.py +107 -0
- package/scripts/github_engage_helper.py +93 -0
- package/scripts/github_tools.py +509 -0
- package/scripts/harness_overlay.py +556 -0
- package/scripts/harvest_twitter_following.py +243 -0
- package/scripts/heartbeat.sh +70 -0
- package/scripts/history_context.py +284 -0
- package/scripts/http_api.py +206 -0
- package/scripts/human_dm_replies_helper.py +169 -0
- package/scripts/identity.py +302 -0
- package/scripts/ig_batch_creator.sh +93 -0
- package/scripts/ig_post_type_picker.py +243 -0
- package/scripts/ig_scrape_transcribe.sh +91 -0
- package/scripts/ingest_human_dm_replies.py +271 -0
- package/scripts/ingest_web_chat_replies.py +229 -0
- package/scripts/install_fleet.py +187 -0
- package/scripts/invent_mcp_server.py +350 -0
- package/scripts/invent_topics.py +1462 -0
- package/scripts/learned_preferences.py +263 -0
- package/scripts/li_discovery.py +161 -0
- package/scripts/link_edit_helper.py +142 -0
- package/scripts/link_tail.py +592 -0
- package/scripts/linkedin_api.py +561 -0
- package/scripts/linkedin_browser.py +730 -0
- package/scripts/linkedin_cooldown.py +128 -0
- package/scripts/linkedin_exclusions.py +234 -0
- package/scripts/linkedin_killswitch.py +1333 -0
- package/scripts/linkedin_search_topic_schema.py +49 -0
- package/scripts/linkedin_unipile.py +658 -0
- package/scripts/linkedin_url.py +228 -0
- package/scripts/log_claude_session.py +636 -0
- package/scripts/log_draft.py +143 -0
- package/scripts/log_linkedin_search_attempts.py +126 -0
- package/scripts/log_post.py +651 -0
- package/scripts/log_run.py +364 -0
- package/scripts/log_thread_media.py +108 -0
- package/scripts/log_twitter_search_attempts.py +150 -0
- package/scripts/log_twitter_skips.py +211 -0
- package/scripts/lookup_post.py +78 -0
- package/scripts/mark_web_chat_processed.py +32 -0
- package/scripts/mcp_lock_proxy.py +370 -0
- package/scripts/memory_snapshot.py +972 -0
- package/scripts/merge_review_queue.py +215 -0
- package/scripts/mint_external_pool.py +182 -0
- package/scripts/mint_kent_pool.py +249 -0
- package/scripts/moltbook_post.py +320 -0
- package/scripts/moltbook_tools.py +159 -0
- package/scripts/pending_threads.py +188 -0
- package/scripts/pick_ig_account.py +177 -0
- package/scripts/pick_project.py +208 -0
- package/scripts/pick_search_topic.py +771 -0
- package/scripts/pick_thread_target.py +279 -0
- package/scripts/pick_twitter_thread_target.py +202 -0
- package/scripts/podlog_fetch_batch.sh +32 -0
- package/scripts/post_github.py +1311 -0
- package/scripts/post_reddit.py +2668 -0
- package/scripts/precompute_dashboard_stats.py +204 -0
- package/scripts/preflight.sh +297 -0
- package/scripts/progress.py +88 -0
- package/scripts/project_excludes.py +353 -0
- package/scripts/project_slugs.py +91 -0
- package/scripts/project_stats.py +241 -0
- package/scripts/project_stats_json.py +1563 -0
- package/scripts/project_topics.py +192 -0
- package/scripts/qualified_query_bank.py +436 -0
- package/scripts/reap_stale_claude_sessions.py +867 -0
- package/scripts/reddit_browser.py +2549 -0
- package/scripts/reddit_browser_fetch.py +141 -0
- package/scripts/reddit_browser_lock.py +593 -0
- package/scripts/reddit_chat_sync.py +710 -0
- package/scripts/reddit_query_bank.py +200 -0
- package/scripts/reddit_threads_helper.py +151 -0
- package/scripts/reddit_tools.py +956 -0
- package/scripts/refresh_instagram_tokens.py +280 -0
- package/scripts/release-mcpb.sh +497 -0
- package/scripts/reply_db.py +334 -0
- package/scripts/reply_insert.py +98 -0
- package/scripts/reply_risk_digest.py +761 -0
- package/scripts/reset-test-machine.sh +602 -0
- package/scripts/restore_twitter_session.py +177 -0
- package/scripts/ripen_reddit_plan.py +478 -0
- package/scripts/run_claude.sh +433 -0
- package/scripts/run_moltbook_cycle.py +555 -0
- package/scripts/s4l_box_update.sh +226 -0
- package/scripts/s4l_channel.py +103 -0
- package/scripts/s4l_ctl.sh +75 -0
- package/scripts/s4l_env.py +47 -0
- package/scripts/saps_activity.py +126 -0
- package/scripts/saps_mode.py +328 -0
- package/scripts/scan_dm_candidates.py +580 -0
- package/scripts/scan_github_replies.py +168 -0
- package/scripts/scan_instagram_comments.py +481 -0
- package/scripts/scan_moltbook_replies.py +252 -0
- package/scripts/scan_pii.py +190 -0
- package/scripts/scan_reddit_replies.py +377 -0
- package/scripts/scan_twitter_mentions_browser.py +327 -0
- package/scripts/scan_twitter_thread_followups.py +299 -0
- package/scripts/scan_x_profile.py +384 -0
- package/scripts/schedule_state.py +202 -0
- package/scripts/scheduled_tasks_snapshot.py +123 -0
- package/scripts/score_linkedin_candidates.py +419 -0
- package/scripts/score_twitter_candidates.py +718 -0
- package/scripts/scrape_linkedin_comment_stats.py +1755 -0
- package/scripts/scrape_linkedin_stats_browser.py +52 -0
- package/scripts/scrape_reddit_views.py +365 -0
- package/scripts/seed_search_queries.py +453 -0
- package/scripts/seed_search_topics.py +127 -0
- package/scripts/send_web_chat_reply.py +130 -0
- package/scripts/sentry_init.py +128 -0
- package/scripts/setup_twitter_auth.py +1320 -0
- package/scripts/snapshot.py +583 -0
- package/scripts/stats.py +2702 -0
- package/scripts/stats_helper.py +52 -0
- package/scripts/strike_alert.py +783 -0
- package/scripts/sweep_post_link_clicks.py +107 -0
- package/scripts/sync_ig_to_posts.py +147 -0
- package/scripts/test_browser_lock.py +189 -0
- package/scripts/test_installation_api.sh +52 -0
- package/scripts/test_percard_posting.py +142 -0
- package/scripts/top_dud_linkedin_queries.py +71 -0
- package/scripts/top_dud_reddit_queries.py +67 -0
- package/scripts/top_dud_twitter_queries.py +71 -0
- package/scripts/top_dud_twitter_topics.py +102 -0
- package/scripts/top_linkedin_queries.py +55 -0
- package/scripts/top_omitted_reddit_topics.py +91 -0
- package/scripts/top_performers.py +588 -0
- package/scripts/top_search_topics.py +180 -0
- package/scripts/top_twitter_queries.py +190 -0
- package/scripts/twitter_access_check.py +382 -0
- package/scripts/twitter_account.py +41 -0
- package/scripts/twitter_batch_phase.py +126 -0
- package/scripts/twitter_browser.py +2804 -0
- package/scripts/twitter_cookie_mirror.py +130 -0
- package/scripts/twitter_cycle_helper.py +310 -0
- package/scripts/twitter_gen_links.py +287 -0
- package/scripts/twitter_post_plan.py +1188 -0
- package/scripts/twitter_scan.py +324 -0
- package/scripts/twitter_supply_signal.py +57 -0
- package/scripts/twitter_threads_helper.py +152 -0
- package/scripts/unclaim_web_chat.py +29 -0
- package/scripts/update_instagram_stats.py +261 -0
- package/scripts/update_linkedin_stats_from_feed.py +328 -0
- package/scripts/version.py +72 -0
- package/scripts/watchdog_hung_runs.py +343 -0
- package/scripts/write_generation_trace.py +73 -0
- package/setup/SKILL.md +277 -0
- package/skill/amplitude-24h-signups.sh +38 -0
- package/skill/archive-old-logs.sh +40 -0
- package/skill/audit-dm-staleness.sh +42 -0
- package/skill/audit-linkedin.sh +14 -0
- package/skill/audit-moltbook.sh +4 -0
- package/skill/audit-reddit-resurrect.sh +67 -0
- package/skill/audit-reddit.sh +4 -0
- package/skill/audit-twitter.sh +4 -0
- package/skill/audit.sh +287 -0
- package/skill/backfill-twitter-attempts-topic.sh +19 -0
- package/skill/backfill-twitter-ghost-posts.sh +24 -0
- package/skill/check-external-pool-depth.sh +7 -0
- package/skill/check-web-chats.sh +203 -0
- package/skill/dm-outreach-linkedin.sh +250 -0
- package/skill/dm-outreach-reddit.sh +274 -0
- package/skill/dm-outreach-twitter.sh +265 -0
- package/skill/engage-dm-replies-linkedin.sh +4 -0
- package/skill/engage-dm-replies-reddit.sh +4 -0
- package/skill/engage-dm-replies-twitter.sh +4 -0
- package/skill/engage-dm-replies.sh +1597 -0
- package/skill/engage-linkedin.sh +581 -0
- package/skill/engage-moltbook.sh +36 -0
- package/skill/engage-reddit.sh +146 -0
- package/skill/engage-twitter.sh +467 -0
- package/skill/github-engage.sh +176 -0
- package/skill/ingest-web-chat-replies.sh +38 -0
- package/skill/invent-supply-test.sh +100 -0
- package/skill/invent-topics.sh +50 -0
- package/skill/lib/linkedin-backend.sh +364 -0
- package/skill/lib/platform.sh +48 -0
- package/skill/lib/reddit-backend.sh +234 -0
- package/skill/lib/twitter-backend.sh +314 -0
- package/skill/link-edit-github.sh +136 -0
- package/skill/link-edit-moltbook.sh +117 -0
- package/skill/link-edit-reddit.sh +201 -0
- package/skill/linkedin-presence.sh +182 -0
- package/skill/linkedin-recovery.sh +282 -0
- package/skill/lock.sh +647 -0
- package/skill/memory-snapshot.sh +39 -0
- package/skill/precompute-stats.sh +35 -0
- package/skill/prewarm-funnel.sh +104 -0
- package/skill/refresh-instagram-tokens.sh +57 -0
- package/skill/refresh-twitter-following.sh +52 -0
- package/skill/reply-risk-digest.sh +31 -0
- package/skill/run-cycle-update-guard.sh +44 -0
- package/skill/run-draft-and-publish.sh +123 -0
- package/skill/run-generate-daily-style.sh +50 -0
- package/skill/run-github-launchd.sh +62 -0
- package/skill/run-github.sh +102 -0
- package/skill/run-instagram-daily.sh +149 -0
- package/skill/run-instagram-render.sh +875 -0
- package/skill/run-linkedin-launchd.sh +81 -0
- package/skill/run-linkedin-unipile.sh +130 -0
- package/skill/run-linkedin.sh +1593 -0
- package/skill/run-moltbook-launchd.sh +61 -0
- package/skill/run-moltbook.sh +38 -0
- package/skill/run-overlay-watch.sh +100 -0
- package/skill/run-reddit-search-launchd.sh +64 -0
- package/skill/run-reddit-search.sh +505 -0
- package/skill/run-reddit-threads-double.sh +32 -0
- package/skill/run-reddit-threads.sh +847 -0
- package/skill/run-scan-moltbook-replies.sh +57 -0
- package/skill/run-twitter-cycle-launchd.sh +63 -0
- package/skill/run-twitter-cycle-singleton.sh +62 -0
- package/skill/run-twitter-cycle.sh +2408 -0
- package/skill/run-twitter-threads.sh +592 -0
- package/skill/scan-instagram-replies.sh +61 -0
- package/skill/scan-twitter-followups.sh +57 -0
- package/skill/social-autoposter-update.sh +66 -0
- package/skill/stats-instagram.sh +72 -0
- package/skill/stats-linkedin.sh +271 -0
- package/skill/stats-moltbook.sh +4 -0
- package/skill/stats-reddit.sh +4 -0
- package/skill/stats-twitter.sh +4 -0
- package/skill/stats.sh +521 -0
- package/skill/strike-alert.sh +18 -0
- package/skill/styles.sh +87 -0
- package/skill/sweep-link-clicks.sh +40 -0
- package/skill/topics.sh +51 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Per-project learned_preferences: the agent-owned config block distilled from
|
|
3
|
+
human card decisions (review_events).
|
|
4
|
+
|
|
5
|
+
The feedback loop:
|
|
6
|
+
1. The menubar review card ships every approve/reject (with reason chips,
|
|
7
|
+
free-text note, link-click interactions, dwell) to /api/v1/review-events.
|
|
8
|
+
2. scripts/feedback_digest.py (scheduled) claims unprocessed events per
|
|
9
|
+
project and asks Claude for a conservative mutation plan.
|
|
10
|
+
3. apply_mutations() here writes that plan into config.json under the
|
|
11
|
+
project's `learned_preferences` block, whitelist-enforced, with flock +
|
|
12
|
+
backup + atomic replace.
|
|
13
|
+
4. Enforcement is SOFT (prompt-level, never a deterministic filter): the
|
|
14
|
+
twitter prep prompt embeds every project entry verbatim via
|
|
15
|
+
ALL_PROJECTS_JSON, so the block (with its self-describing _instruction)
|
|
16
|
+
reaches the judging/drafting model automatically. prompt_block() renders
|
|
17
|
+
the same content for prompts that want an explicit section.
|
|
18
|
+
|
|
19
|
+
Block shape (inside a config.json project entry):
|
|
20
|
+
|
|
21
|
+
"learned_preferences": {
|
|
22
|
+
"_instruction": "<how the drafting model should apply this block>",
|
|
23
|
+
"enabled": true,
|
|
24
|
+
"audience_avoid": ["crypto/web3-native authors ..."],
|
|
25
|
+
"audience_prefer": [],
|
|
26
|
+
"thread_avoid": ["engagement-bait question threads ..."],
|
|
27
|
+
"draft_style_notes": [],
|
|
28
|
+
"updated_at": "...",
|
|
29
|
+
"history": [{"ts", "change", "rationale", "source_events": [ids]}]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
WHITELIST: this module writes ONLY learned_preferences plus (append-only)
|
|
33
|
+
voice.never and content_guardrails.do_not. Nothing else in a project entry is
|
|
34
|
+
touchable through this path; unknown keys in a mutation plan are dropped and
|
|
35
|
+
counted, never applied. Facts (content_angle, links, identity fields) are
|
|
36
|
+
deliberately unreachable so a bad digest can never poison grounding.
|
|
37
|
+
"""
|
|
38
|
+
from __future__ import annotations
|
|
39
|
+
|
|
40
|
+
import datetime
|
|
41
|
+
import fcntl
|
|
42
|
+
import json
|
|
43
|
+
import os
|
|
44
|
+
import shutil
|
|
45
|
+
import sys
|
|
46
|
+
from pathlib import Path
|
|
47
|
+
|
|
48
|
+
# Lists the digest may fully manage inside learned_preferences.
|
|
49
|
+
MANAGED_LISTS = ("audience_avoid", "audience_prefer", "thread_avoid", "draft_style_notes")
|
|
50
|
+
# Existing config fields the digest may APPEND to (never remove from).
|
|
51
|
+
APPEND_ONLY_FIELDS = ("voice_never_add", "guardrails_do_not_add")
|
|
52
|
+
|
|
53
|
+
MAX_ENTRIES_PER_LIST = 10
|
|
54
|
+
MAX_ENTRY_CHARS = 200
|
|
55
|
+
MAX_HISTORY = 50
|
|
56
|
+
|
|
57
|
+
# Travels inside the JSON the prep prompt embeds (ALL_PROJECTS_JSON is the
|
|
58
|
+
# full project entry), so the drafting model reads its own operating manual
|
|
59
|
+
# for the block. Soft steering by design: judgment, not a hard ban.
|
|
60
|
+
DEFAULT_INSTRUCTION = (
|
|
61
|
+
"Human review feedback for this project, distilled from the user's "
|
|
62
|
+
"approve/reject decisions on draft cards. When judging candidates and "
|
|
63
|
+
"drafting replies: treat audience_avoid and thread_avoid as strong "
|
|
64
|
+
"negative signals (prefer rejecting matching candidates, citing "
|
|
65
|
+
"'learned_preference' in the reason), treat audience_prefer as a "
|
|
66
|
+
"positive signal, and follow draft_style_notes when writing. These are "
|
|
67
|
+
"preferences, not hard bans; use judgment when a candidate is "
|
|
68
|
+
"exceptionally on-topic despite a match."
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def config_path() -> str:
|
|
73
|
+
explicit = os.environ.get("S4L_CONFIG_PATH")
|
|
74
|
+
if explicit:
|
|
75
|
+
return explicit
|
|
76
|
+
repo = os.environ.get("S4L_REPO_DIR")
|
|
77
|
+
if repo:
|
|
78
|
+
return os.path.join(repo, "config.json")
|
|
79
|
+
return os.path.expanduser("~/social-autoposter/config.json")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _now_iso() -> str:
|
|
83
|
+
return datetime.datetime.now(datetime.timezone.utc).isoformat()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def normalized(block) -> dict:
|
|
87
|
+
"""Coerce whatever is in config into the canonical block shape."""
|
|
88
|
+
b = block if isinstance(block, dict) else {}
|
|
89
|
+
out = {
|
|
90
|
+
"_instruction": str(b.get("_instruction") or DEFAULT_INSTRUCTION),
|
|
91
|
+
"enabled": b.get("enabled", True) is not False,
|
|
92
|
+
}
|
|
93
|
+
for key in MANAGED_LISTS:
|
|
94
|
+
vals = b.get(key)
|
|
95
|
+
out[key] = [str(v).strip()[:MAX_ENTRY_CHARS] for v in vals if str(v).strip()] if isinstance(vals, list) else []
|
|
96
|
+
out["updated_at"] = b.get("updated_at")
|
|
97
|
+
hist = b.get("history")
|
|
98
|
+
out["history"] = list(hist)[-MAX_HISTORY:] if isinstance(hist, list) else []
|
|
99
|
+
return out
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def get_block(project_cfg) -> dict:
|
|
103
|
+
return normalized((project_cfg or {}).get("learned_preferences"))
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def prompt_block(project_cfg) -> str:
|
|
107
|
+
"""Explicit prompt section for callers that don't embed the raw project
|
|
108
|
+
JSON (mirrors the engagement_styles STYLES_BLOCK pattern). Empty string
|
|
109
|
+
when the block is disabled or has no entries."""
|
|
110
|
+
b = get_block(project_cfg)
|
|
111
|
+
if not b["enabled"]:
|
|
112
|
+
return ""
|
|
113
|
+
lines = []
|
|
114
|
+
labels = {
|
|
115
|
+
"audience_avoid": "Avoid audiences/authors like",
|
|
116
|
+
"audience_prefer": "Prefer audiences/authors like",
|
|
117
|
+
"thread_avoid": "Avoid threads like",
|
|
118
|
+
"draft_style_notes": "Drafting notes",
|
|
119
|
+
}
|
|
120
|
+
for key in MANAGED_LISTS:
|
|
121
|
+
for v in b[key]:
|
|
122
|
+
lines.append(f"- {labels[key]}: {v}")
|
|
123
|
+
if not lines:
|
|
124
|
+
return ""
|
|
125
|
+
return (
|
|
126
|
+
"## LEARNED PREFERENCES (from this user's own approve/reject decisions)\n"
|
|
127
|
+
+ b["_instruction"]
|
|
128
|
+
+ "\n"
|
|
129
|
+
+ "\n".join(lines)
|
|
130
|
+
+ "\n"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _validate_add_list(raw, cap=MAX_ENTRIES_PER_LIST):
|
|
135
|
+
out = []
|
|
136
|
+
if not isinstance(raw, list):
|
|
137
|
+
return out
|
|
138
|
+
for v in raw:
|
|
139
|
+
s = str(v).strip()
|
|
140
|
+
if s:
|
|
141
|
+
out.append(s[:MAX_ENTRY_CHARS])
|
|
142
|
+
if len(out) >= cap:
|
|
143
|
+
break
|
|
144
|
+
return out
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def apply_mutations(project_name: str, plan: dict, source_event_ids=None, cfg_path: str | None = None) -> dict:
|
|
148
|
+
"""Apply a digest mutation plan to config.json. Returns a summary dict:
|
|
149
|
+
{ok, applied: [change strings], dropped: [reasons], project_found: bool}.
|
|
150
|
+
|
|
151
|
+
plan shape (all optional):
|
|
152
|
+
{"changes": {<managed list>: {"add": [...], "remove": [...]}},
|
|
153
|
+
"voice_never_add": [...], "guardrails_do_not_add": [...],
|
|
154
|
+
"rationale": "..."}
|
|
155
|
+
|
|
156
|
+
flock on <config>.lock serializes against other writers (setup tools, a
|
|
157
|
+
concurrent digest); backup + atomic replace mirrors mcp/src/setup.ts
|
|
158
|
+
applySetup(). Unknown keys are DROPPED, never applied: the whitelist is
|
|
159
|
+
enforced here in code, not in the digest prompt.
|
|
160
|
+
"""
|
|
161
|
+
cfg_path = cfg_path or config_path()
|
|
162
|
+
applied, dropped = [], []
|
|
163
|
+
plan = plan if isinstance(plan, dict) else {}
|
|
164
|
+
changes = plan.get("changes") if isinstance(plan.get("changes"), dict) else {}
|
|
165
|
+
rationale = str(plan.get("rationale") or "").strip()[:500]
|
|
166
|
+
|
|
167
|
+
lock_path = cfg_path + ".lock"
|
|
168
|
+
Path(lock_path).parent.mkdir(parents=True, exist_ok=True)
|
|
169
|
+
with open(lock_path, "w") as lock_f:
|
|
170
|
+
fcntl.flock(lock_f, fcntl.LOCK_EX)
|
|
171
|
+
try:
|
|
172
|
+
cfg = json.loads(Path(cfg_path).read_text())
|
|
173
|
+
except Exception as e:
|
|
174
|
+
return {"ok": False, "error": f"config unreadable: {e}", "applied": [], "dropped": [], "project_found": False}
|
|
175
|
+
projects = cfg.get("projects") or []
|
|
176
|
+
proj = next((p for p in projects if p.get("name") == project_name), None)
|
|
177
|
+
if proj is None:
|
|
178
|
+
return {"ok": False, "error": "project not in config", "applied": [], "dropped": [], "project_found": False}
|
|
179
|
+
|
|
180
|
+
block = get_block(proj)
|
|
181
|
+
|
|
182
|
+
for key, ops in changes.items():
|
|
183
|
+
if key not in MANAGED_LISTS:
|
|
184
|
+
dropped.append(f"unknown list '{key}'")
|
|
185
|
+
continue
|
|
186
|
+
if not isinstance(ops, dict):
|
|
187
|
+
dropped.append(f"bad ops for '{key}'")
|
|
188
|
+
continue
|
|
189
|
+
for v in _validate_add_list(ops.get("remove")):
|
|
190
|
+
# Fuzzy-tolerant remove: exact match only; a miss is not an error.
|
|
191
|
+
if v in block[key]:
|
|
192
|
+
block[key].remove(v)
|
|
193
|
+
applied.append(f"{key} removed: {v}")
|
|
194
|
+
for v in _validate_add_list(ops.get("add")):
|
|
195
|
+
if v in block[key]:
|
|
196
|
+
continue
|
|
197
|
+
if len(block[key]) >= MAX_ENTRIES_PER_LIST:
|
|
198
|
+
dropped.append(f"{key} at cap ({MAX_ENTRIES_PER_LIST}), skipped: {v}")
|
|
199
|
+
continue
|
|
200
|
+
block[key].append(v)
|
|
201
|
+
applied.append(f"{key} added: {v}")
|
|
202
|
+
|
|
203
|
+
# Append-only extensions of existing curated fields.
|
|
204
|
+
for v in _validate_add_list(plan.get("voice_never_add"), cap=3):
|
|
205
|
+
voice = proj.setdefault("voice", {})
|
|
206
|
+
never = voice.setdefault("never", [])
|
|
207
|
+
if isinstance(never, list) and v not in never:
|
|
208
|
+
never.append(v)
|
|
209
|
+
applied.append(f"voice.never added: {v}")
|
|
210
|
+
for v in _validate_add_list(plan.get("guardrails_do_not_add"), cap=3):
|
|
211
|
+
guard = proj.setdefault("content_guardrails", {})
|
|
212
|
+
do_not = guard.setdefault("do_not", [])
|
|
213
|
+
if isinstance(do_not, list) and v not in do_not:
|
|
214
|
+
do_not.append(v)
|
|
215
|
+
applied.append(f"content_guardrails.do_not added: {v}")
|
|
216
|
+
|
|
217
|
+
for key in set(plan.keys()) - {"changes", "voice_never_add", "guardrails_do_not_add", "rationale", "project"}:
|
|
218
|
+
dropped.append(f"unknown top-level key '{key}'")
|
|
219
|
+
|
|
220
|
+
if not applied:
|
|
221
|
+
return {"ok": True, "applied": [], "dropped": dropped, "project_found": True}
|
|
222
|
+
|
|
223
|
+
block["updated_at"] = _now_iso()
|
|
224
|
+
block["history"] = (block["history"] + [
|
|
225
|
+
{
|
|
226
|
+
"ts": _now_iso(),
|
|
227
|
+
"change": "; ".join(applied)[:1000],
|
|
228
|
+
"rationale": rationale,
|
|
229
|
+
"source_events": list(source_event_ids or [])[:100],
|
|
230
|
+
}
|
|
231
|
+
])[-MAX_HISTORY:]
|
|
232
|
+
proj["learned_preferences"] = block
|
|
233
|
+
|
|
234
|
+
# Backup + atomic replace (same shape as setup.ts applySetup).
|
|
235
|
+
stamp = _now_iso().replace(":", "-").replace(".", "-")
|
|
236
|
+
try:
|
|
237
|
+
if os.path.exists(cfg_path):
|
|
238
|
+
shutil.copyfile(cfg_path, f"{cfg_path}.bak-{stamp}")
|
|
239
|
+
except Exception:
|
|
240
|
+
pass
|
|
241
|
+
tmp = cfg_path + ".tmp"
|
|
242
|
+
Path(tmp).write_text(json.dumps(cfg, indent=2) + "\n")
|
|
243
|
+
os.replace(tmp, cfg_path)
|
|
244
|
+
|
|
245
|
+
return {"ok": True, "applied": applied, "dropped": dropped, "project_found": True}
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
if __name__ == "__main__":
|
|
249
|
+
import argparse
|
|
250
|
+
|
|
251
|
+
ap = argparse.ArgumentParser(description="Inspect/render learned_preferences")
|
|
252
|
+
ap.add_argument("command", choices=["show", "block"], help="show = raw JSON, block = prompt block")
|
|
253
|
+
ap.add_argument("project")
|
|
254
|
+
args = ap.parse_args()
|
|
255
|
+
cfg = json.loads(Path(config_path()).read_text())
|
|
256
|
+
proj = next((p for p in (cfg.get("projects") or []) if p.get("name") == args.project), None)
|
|
257
|
+
if proj is None:
|
|
258
|
+
print(f"project {args.project!r} not found", file=sys.stderr)
|
|
259
|
+
sys.exit(1)
|
|
260
|
+
if args.command == "show":
|
|
261
|
+
print(json.dumps(get_block(proj), indent=2))
|
|
262
|
+
else:
|
|
263
|
+
print(prompt_block(proj))
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""LinkedIn Phase A discovery helpers, HTTP-backed (no DATABASE_URL).
|
|
3
|
+
|
|
4
|
+
Replaces the raw psql SELECT/INSERT lines that engage-linkedin.sh Phase A
|
|
5
|
+
embedded in its Claude prompt (Steps 1-3 dedup reads, Step 7 reply insert /
|
|
6
|
+
post create). Everything routes through the s4l.ai HTTP API so the engage
|
|
7
|
+
runner works on a machine with no direct DB access.
|
|
8
|
+
|
|
9
|
+
Subcommands (each prints to stdout in the same shape the old psql `-t -A`
|
|
10
|
+
output produced, so the prompt's downstream parsing is unchanged):
|
|
11
|
+
|
|
12
|
+
comment-ids
|
|
13
|
+
One their_comment_id per line. Phase A Step 1 dedup list.
|
|
14
|
+
|
|
15
|
+
engaged-pairs
|
|
16
|
+
One "author|||our_url" per line. Phase A Step 2 dedup list.
|
|
17
|
+
|
|
18
|
+
posts
|
|
19
|
+
One "id|our_url" per line. Phase A Step 3 post-matching index.
|
|
20
|
+
|
|
21
|
+
insert-reply --post-id N --comment-urn URN --author A --content C --href URL
|
|
22
|
+
Find-or-create a pending linkedin reply. Idempotent: a duplicate
|
|
23
|
+
(platform, their_comment_id) returns 409 which we treat as success.
|
|
24
|
+
Prints the resulting reply id (or "gated"/"duplicate") for the log.
|
|
25
|
+
|
|
26
|
+
create-post --activity-id ID --project NAME --author A
|
|
27
|
+
Create (or reuse) a linkedin post row for a discovered thread when no
|
|
28
|
+
existing post matched the activity id. Prints the post id. 409
|
|
29
|
+
duplicate_thread reuses the existing row's id.
|
|
30
|
+
|
|
31
|
+
All three read subcommands hit GET /api/v1/linkedin-discovery-context once
|
|
32
|
+
and slice the field they need, so a caller that needs all three can also just
|
|
33
|
+
run `context` to dump the raw JSON.
|
|
34
|
+
"""
|
|
35
|
+
import argparse
|
|
36
|
+
import json
|
|
37
|
+
import os
|
|
38
|
+
import sys
|
|
39
|
+
|
|
40
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
41
|
+
from http_api import api_get, api_post
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _context():
|
|
45
|
+
resp = api_get("/api/v1/linkedin-discovery-context")
|
|
46
|
+
return resp.get("data") or {}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def cmd_comment_ids():
|
|
50
|
+
for cid in _context().get("existing_comment_ids") or []:
|
|
51
|
+
print(cid)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def cmd_engaged_pairs():
|
|
55
|
+
for pair in _context().get("engaged_pairs") or []:
|
|
56
|
+
print(pair)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def cmd_posts():
|
|
60
|
+
for p in _context().get("posts") or []:
|
|
61
|
+
print(f"{p['id']}|{p['our_url']}")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def cmd_context():
|
|
65
|
+
print(json.dumps(_context(), indent=2))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def cmd_insert_reply(args):
|
|
69
|
+
resp = api_post(
|
|
70
|
+
"/api/v1/replies",
|
|
71
|
+
{
|
|
72
|
+
"platform": "linkedin",
|
|
73
|
+
"post_id": args.post_id,
|
|
74
|
+
"their_comment_id": args.comment_urn,
|
|
75
|
+
"their_author": args.author,
|
|
76
|
+
"their_content": args.content or "",
|
|
77
|
+
"their_comment_url": args.href or "",
|
|
78
|
+
"depth": 1,
|
|
79
|
+
"status": "pending",
|
|
80
|
+
},
|
|
81
|
+
ok_on_conflict=True,
|
|
82
|
+
)
|
|
83
|
+
error = resp.get("error") or {}
|
|
84
|
+
if error.get("code") == "duplicate_reply":
|
|
85
|
+
print("duplicate")
|
|
86
|
+
return
|
|
87
|
+
data = resp.get("data") or {}
|
|
88
|
+
# The blocklist / velocity gate returns ok with reply:null + gated reason.
|
|
89
|
+
if data.get("gated"):
|
|
90
|
+
print("gated:%s" % data.get("gated"))
|
|
91
|
+
return
|
|
92
|
+
reply = data.get("reply") or {}
|
|
93
|
+
print(reply.get("id", "inserted"))
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def cmd_create_post(args):
|
|
97
|
+
our_url = (
|
|
98
|
+
"https://www.linkedin.com/feed/update/"
|
|
99
|
+
"urn:li:activity:%s/" % args.activity_id
|
|
100
|
+
)
|
|
101
|
+
resp = api_post(
|
|
102
|
+
"/api/v1/posts",
|
|
103
|
+
{
|
|
104
|
+
"platform": "linkedin",
|
|
105
|
+
"thread_url": our_url,
|
|
106
|
+
"our_url": our_url,
|
|
107
|
+
"our_content": "[discovered via notification, no original content tracked]",
|
|
108
|
+
"project": args.project or "general",
|
|
109
|
+
"thread_author": args.author or "(unknown)",
|
|
110
|
+
"our_account": "Matthew Diakonov",
|
|
111
|
+
"engagement_style": "discovered_via_notification",
|
|
112
|
+
"status": "active",
|
|
113
|
+
},
|
|
114
|
+
ok_on_conflict=True,
|
|
115
|
+
)
|
|
116
|
+
error = resp.get("error") or {}
|
|
117
|
+
if error.get("code") == "duplicate_thread":
|
|
118
|
+
print((error.get("details") or {}).get("existing_post_id"))
|
|
119
|
+
return
|
|
120
|
+
post = (resp.get("data") or {}).get("post") or {}
|
|
121
|
+
print(post.get("id", ""))
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def main():
|
|
125
|
+
p = argparse.ArgumentParser(description="LinkedIn Phase A discovery helpers (HTTP).")
|
|
126
|
+
sub = p.add_subparsers(dest="cmd", required=True)
|
|
127
|
+
|
|
128
|
+
sub.add_parser("comment-ids")
|
|
129
|
+
sub.add_parser("engaged-pairs")
|
|
130
|
+
sub.add_parser("posts")
|
|
131
|
+
sub.add_parser("context")
|
|
132
|
+
|
|
133
|
+
ins = sub.add_parser("insert-reply")
|
|
134
|
+
ins.add_argument("--post-id", type=int, required=True)
|
|
135
|
+
ins.add_argument("--comment-urn", required=True)
|
|
136
|
+
ins.add_argument("--author", required=True)
|
|
137
|
+
ins.add_argument("--content", default="")
|
|
138
|
+
ins.add_argument("--href", default="")
|
|
139
|
+
|
|
140
|
+
cp = sub.add_parser("create-post")
|
|
141
|
+
cp.add_argument("--activity-id", required=True)
|
|
142
|
+
cp.add_argument("--project", default="general")
|
|
143
|
+
cp.add_argument("--author", default="")
|
|
144
|
+
|
|
145
|
+
args = p.parse_args()
|
|
146
|
+
if args.cmd == "comment-ids":
|
|
147
|
+
cmd_comment_ids()
|
|
148
|
+
elif args.cmd == "engaged-pairs":
|
|
149
|
+
cmd_engaged_pairs()
|
|
150
|
+
elif args.cmd == "posts":
|
|
151
|
+
cmd_posts()
|
|
152
|
+
elif args.cmd == "context":
|
|
153
|
+
cmd_context()
|
|
154
|
+
elif args.cmd == "insert-reply":
|
|
155
|
+
cmd_insert_reply(args)
|
|
156
|
+
elif args.cmd == "create-post":
|
|
157
|
+
cmd_create_post(args)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
if __name__ == "__main__":
|
|
161
|
+
main()
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""link_edit_helper.py — CLI wrapper used by the three link-edit pipelines
|
|
3
|
+
(skill/link-edit-{reddit,github,moltbook}.sh) to replace the inline
|
|
4
|
+
`psql "$DATABASE_URL"` one-liners they used to embed. The direct-Postgres lane
|
|
5
|
+
was removed 2026-06-01; DATABASE_URL is deliberately ignored, no DB, no
|
|
6
|
+
fallback. Every subcommand prints exactly what the corresponding psql call
|
|
7
|
+
printed so the surrounding shell capture ($(...)) and string/int compares are
|
|
8
|
+
unchanged.
|
|
9
|
+
|
|
10
|
+
Subcommands:
|
|
11
|
+
eligible --platform P [--age-hours N] [--min-upvotes-exclusive N]
|
|
12
|
+
[--page-gen-rate-pct N] [--order upvotes|posted_at]
|
|
13
|
+
-> GET /api/v1/posts/link-edit-eligible?...
|
|
14
|
+
-> prints the json_agg() array of eligible posts, or the literal `null`
|
|
15
|
+
when none match (matches Postgres json_agg-over-empty-set, so the
|
|
16
|
+
shell `[ "$EDITABLE" = "null" ]` guard still fires).
|
|
17
|
+
mark-edited --post-id N --content "<text>" [--source "<src>"]
|
|
18
|
+
-> PATCH /api/v1/posts/N { link_edited_now, link_edit_content, link_source? }
|
|
19
|
+
(was: UPDATE posts SET link_edited_at=NOW(), link_edit_content=..,
|
|
20
|
+
link_source=.. WHERE id=..)
|
|
21
|
+
mark-skipped --post-id N --reason "<reason>"
|
|
22
|
+
-> PATCH /api/v1/posts/N { link_edited_now, link_edit_content:"SKIPPED: <reason>" }
|
|
23
|
+
(was: UPDATE posts SET link_edited_at=NOW(),
|
|
24
|
+
link_edit_content='SKIPPED: ..' WHERE id=..)
|
|
25
|
+
set-project --post-id N --project "<name>"
|
|
26
|
+
-> PATCH /api/v1/posts/N { project_name }
|
|
27
|
+
(was: UPDATE posts SET project_name=.. WHERE id=..)
|
|
28
|
+
edited-count --platform P
|
|
29
|
+
-> GET /api/v1/posts/count?platform=P&link_edited=true
|
|
30
|
+
-> prints the integer all-time edited count (was: SELECT COUNT(*) FROM
|
|
31
|
+
posts WHERE platform=P AND link_edited_at IS NOT NULL)
|
|
32
|
+
"""
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import argparse
|
|
36
|
+
import json
|
|
37
|
+
import os
|
|
38
|
+
import sys
|
|
39
|
+
|
|
40
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
41
|
+
from http_api import api_get, api_patch # noqa: E402
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def cmd_eligible(args) -> int:
|
|
45
|
+
query = {
|
|
46
|
+
"platform": args.platform,
|
|
47
|
+
"age_hours": args.age_hours,
|
|
48
|
+
"page_gen_rate_pct": args.page_gen_rate_pct,
|
|
49
|
+
"order": args.order,
|
|
50
|
+
}
|
|
51
|
+
if args.min_upvotes_exclusive is not None:
|
|
52
|
+
query["min_upvotes_exclusive"] = args.min_upvotes_exclusive
|
|
53
|
+
resp = api_get("/api/v1/posts/link-edit-eligible", query=query)
|
|
54
|
+
posts = (resp.get("data") or {}).get("posts") or []
|
|
55
|
+
if not posts:
|
|
56
|
+
# Mirror Postgres json_agg() over an empty set, which returns NULL and
|
|
57
|
+
# which psql -t -A prints as the bare string `null`. The shells guard
|
|
58
|
+
# on `[ "$EDITABLE" = "null" ]`, so emit exactly that.
|
|
59
|
+
print("null")
|
|
60
|
+
return 0
|
|
61
|
+
json.dump(posts, sys.stdout, separators=(",", ":"))
|
|
62
|
+
sys.stdout.write("\n")
|
|
63
|
+
return 0
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _patch_post(post_id: int, body: dict) -> int:
|
|
67
|
+
api_patch(f"/api/v1/posts/{post_id}", body=body)
|
|
68
|
+
return 0
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def cmd_mark_edited(args) -> int:
|
|
72
|
+
body: dict = {
|
|
73
|
+
"link_edited_now": True,
|
|
74
|
+
"link_edit_content": args.content,
|
|
75
|
+
}
|
|
76
|
+
if args.source is not None:
|
|
77
|
+
body["link_source"] = args.source
|
|
78
|
+
return _patch_post(args.post_id, body)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def cmd_mark_skipped(args) -> int:
|
|
82
|
+
return _patch_post(
|
|
83
|
+
args.post_id,
|
|
84
|
+
{
|
|
85
|
+
"link_edited_now": True,
|
|
86
|
+
"link_edit_content": f"SKIPPED: {args.reason}",
|
|
87
|
+
},
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def cmd_set_project(args) -> int:
|
|
92
|
+
return _patch_post(args.post_id, {"project_name": args.project})
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def cmd_edited_count(args) -> int:
|
|
96
|
+
resp = api_get(
|
|
97
|
+
"/api/v1/posts/count",
|
|
98
|
+
query={"platform": args.platform, "link_edited": "true"},
|
|
99
|
+
)
|
|
100
|
+
print(int((resp.get("data") or {}).get("count") or 0))
|
|
101
|
+
return 0
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def main() -> int:
|
|
105
|
+
p = argparse.ArgumentParser()
|
|
106
|
+
sub = p.add_subparsers(dest="cmd", required=True)
|
|
107
|
+
|
|
108
|
+
e = sub.add_parser("eligible")
|
|
109
|
+
e.add_argument("--platform", required=True)
|
|
110
|
+
e.add_argument("--age-hours", type=int, default=6)
|
|
111
|
+
e.add_argument("--min-upvotes-exclusive", type=int, default=None)
|
|
112
|
+
e.add_argument("--page-gen-rate-pct", type=int, default=0)
|
|
113
|
+
e.add_argument("--order", default="upvotes", choices=["upvotes", "posted_at"])
|
|
114
|
+
|
|
115
|
+
me = sub.add_parser("mark-edited")
|
|
116
|
+
me.add_argument("--post-id", type=int, required=True)
|
|
117
|
+
me.add_argument("--content", required=True)
|
|
118
|
+
me.add_argument("--source", default=None)
|
|
119
|
+
|
|
120
|
+
ms = sub.add_parser("mark-skipped")
|
|
121
|
+
ms.add_argument("--post-id", type=int, required=True)
|
|
122
|
+
ms.add_argument("--reason", required=True)
|
|
123
|
+
|
|
124
|
+
sp = sub.add_parser("set-project")
|
|
125
|
+
sp.add_argument("--post-id", type=int, required=True)
|
|
126
|
+
sp.add_argument("--project", required=True)
|
|
127
|
+
|
|
128
|
+
ec = sub.add_parser("edited-count")
|
|
129
|
+
ec.add_argument("--platform", required=True)
|
|
130
|
+
|
|
131
|
+
args = p.parse_args()
|
|
132
|
+
return {
|
|
133
|
+
"eligible": cmd_eligible,
|
|
134
|
+
"mark-edited": cmd_mark_edited,
|
|
135
|
+
"mark-skipped": cmd_mark_skipped,
|
|
136
|
+
"set-project": cmd_set_project,
|
|
137
|
+
"edited-count": cmd_edited_count,
|
|
138
|
+
}[args.cmd](args)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
if __name__ == "__main__":
|
|
142
|
+
sys.exit(main())
|