@m13v/s4l 1.7.1-rc.21 → 1.7.1-rc.23
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/mcp/dist/version.json +2 -2
- package/mcp/manifest.json +1 -1
- package/mcp/menubar/s4l_card.py +108 -28
- package/mcp/menubar/s4l_menubar.py +58 -21
- package/mcp/package.json +1 -1
- package/package.json +2 -1
- package/scripts/merge_review_queue.py +42 -16
- package/scripts/scan_pii.py +9 -1
package/mcp/dist/version.json
CHANGED
package/mcp/manifest.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"dxt_version": "0.1",
|
|
3
3
|
"name": "social-autoposter",
|
|
4
4
|
"display_name": "S4L",
|
|
5
|
-
"version": "1.7.1-rc.
|
|
5
|
+
"version": "1.7.1-rc.23",
|
|
6
6
|
"description": "Draft, review, approve, and autopilot X/Twitter posts.",
|
|
7
7
|
"long_description": "## **⚠️ The disclaimer above is generic Claude boilerplate.** Anthropic shows the same warning on every plugin regardless of what it does; any plugin has the same level of access as any app you download from the internet.\n\nS4L is an open source product developed by Mediar.ai Incorporated, a VC-backed San Francisco-based startup.\n\nTo get started:\n\n1\\. Copy this prompt: **Set me up on S4L plugin end to end**\n\n2\\. Quit with CMD+Q, reopen Claude, paste into a new chat.\n\nWhat happens next:\n\n* About every 5 minutes S4L scans X for posts that match your topics and drafts replies in your voice.\n* Drafts show up as review cards, usually the first within a few minutes. Nothing is posted automatically; you approve each one.\n* Posting autopilot stays off until you explicitly turn it on.",
|
|
8
8
|
"author": {
|
package/mcp/menubar/s4l_card.py
CHANGED
|
@@ -102,6 +102,7 @@ from AppKit import (
|
|
|
102
102
|
NSEventModifierFlagShift,
|
|
103
103
|
NSEventModifierFlagDeviceIndependentFlagsMask,
|
|
104
104
|
NSViewWidthSizable,
|
|
105
|
+
NSColorSpace,
|
|
105
106
|
)
|
|
106
107
|
|
|
107
108
|
# Styling extras that may be missing on older AppKit; every consumer degrades
|
|
@@ -499,6 +500,22 @@ def _fill_color():
|
|
|
499
500
|
return NSColor.labelColor().colorWithAlphaComponent_(0.06)
|
|
500
501
|
|
|
501
502
|
|
|
503
|
+
def _solid(color):
|
|
504
|
+
"""Bake a dynamic/semantic NSColor (textBackgroundColor, controlAccentColor,
|
|
505
|
+
etc.) down to concrete sRGB components. The card sits inside an
|
|
506
|
+
NSVisualEffectView (see _frosted); dynamic system colors drawn in that
|
|
507
|
+
vibrant context render partially see-through against whatever is behind
|
|
508
|
+
the window instead of the flat opaque/solid color they look like in a
|
|
509
|
+
normal window (2026-07-08 feedback: the selection ring and its "opaque"
|
|
510
|
+
backing both still blended into a dark desktop behind the card). Once
|
|
511
|
+
converted to a plain sRGB color it is no longer a vibrancy-aware dynamic
|
|
512
|
+
color, so CALayer draws it as flat, fully opaque pixels."""
|
|
513
|
+
try:
|
|
514
|
+
return color.colorUsingColorSpace_(NSColorSpace.sRGBColorSpace()) or color
|
|
515
|
+
except Exception:
|
|
516
|
+
return color
|
|
517
|
+
|
|
518
|
+
|
|
502
519
|
def _round_rect(view, *, border=True):
|
|
503
520
|
"""Rounded-rect skin: 8px corners, optional hairline border. Returns True
|
|
504
521
|
on success so callers can restore their square-bezel fallback when the
|
|
@@ -619,7 +636,7 @@ class _ReviewController(NSObject):
|
|
|
619
636
|
# Two-draft cards (2026-07-08 redesign, no-recommendation pass same
|
|
620
637
|
# day): both drafts render at once as separate editable boxes;
|
|
621
638
|
# `_selected_draft` (0=a, 1=b) is "whichever box the reviewer is
|
|
622
|
-
# currently in", driven by
|
|
639
|
+
# currently in", driven by caret movement (see textViewDidChangeSelection_ below),
|
|
623
640
|
# not a button. None = not yet chosen this card, _render() defaults
|
|
624
641
|
# it to slot 0 (Draft A) — the model never picks a favorite, so
|
|
625
642
|
# there's no recommendation to default to instead. Reset to None on
|
|
@@ -1076,9 +1093,9 @@ class _ReviewController(NSObject):
|
|
|
1076
1093
|
# day): show BOTH drafts at once, stacked, each independently
|
|
1077
1094
|
# editable, rather than a toggle that swaps one field's content. No
|
|
1078
1095
|
# buttons: selection is "whichever box the reviewer is currently in",
|
|
1079
|
-
# shown via that box
|
|
1080
|
-
# hairline, mirroring a standard focus ring) and
|
|
1081
|
-
#
|
|
1096
|
+
# shown via a dedicated outline view wrapping that box (an accent
|
|
1097
|
+
# outline vs a plain hairline, mirroring a standard focus ring) and
|
|
1098
|
+
# updated live by textViewDidChangeSelection_ below. The model never picks a favorite
|
|
1082
1099
|
# (removed 2026-07-08 per user: no ask-the-model-to-recommend), so
|
|
1083
1100
|
# Draft A (slot 0) is simply the fixed default until the reviewer
|
|
1084
1101
|
# clicks into Draft B. Absent/short (reused stale draft, legacy plan)
|
|
@@ -1089,7 +1106,7 @@ class _ReviewController(NSObject):
|
|
|
1089
1106
|
if dual:
|
|
1090
1107
|
self._selected_draft = sel_idx
|
|
1091
1108
|
|
|
1092
|
-
edit_top = H - 172 -
|
|
1109
|
+
edit_top = H - 172 - 3
|
|
1093
1110
|
link = d.get("link_url")
|
|
1094
1111
|
|
|
1095
1112
|
# Tail link baked at draft time is normally already in each draft's
|
|
@@ -1102,10 +1119,15 @@ class _ReviewController(NSObject):
|
|
|
1102
1119
|
|
|
1103
1120
|
self._draft_textviews = {}
|
|
1104
1121
|
self._draft_scrolls = {}
|
|
1122
|
+
self._draft_outlines = {}
|
|
1105
1123
|
if dual:
|
|
1106
1124
|
avail_h = edit_top - M
|
|
1107
1125
|
gap = 5
|
|
1108
|
-
|
|
1126
|
+
# Reserved only for foreign-language cards, which show a muted
|
|
1127
|
+
# "EN:" line above each box; English drafts (the common case)
|
|
1128
|
+
# have nothing to put there, so reserving it unconditionally used
|
|
1129
|
+
# to leave a dead 15px gap above every box for no reason.
|
|
1130
|
+
label_h = 15 if is_foreign else 0
|
|
1109
1131
|
unit = (avail_h - gap) / 2.0
|
|
1110
1132
|
box_h = unit - label_h
|
|
1111
1133
|
for slot in (0, 1):
|
|
@@ -1126,14 +1148,42 @@ class _ReviewController(NSObject):
|
|
|
1126
1148
|
truncates=True,
|
|
1127
1149
|
)
|
|
1128
1150
|
)
|
|
1151
|
+
# The selection ring can't be drawn on the scroll view's own
|
|
1152
|
+
# layer: NSScrollView's opaque clip/document view fully paints
|
|
1153
|
+
# over its parent layer's border, so setBorderWidth/Color on
|
|
1154
|
+
# scroll.layer() is accepted (no error) but never visibly
|
|
1155
|
+
# renders, at any width or color (verified empirically). A
|
|
1156
|
+
# separate outline view, sized to the box and holding the
|
|
1157
|
+
# scroll view inset a few px inside it, keeps the ring
|
|
1158
|
+
# unobstructed since nothing opaque reaches its edge.
|
|
1159
|
+
# The inset gap between this wrapper's edge (where the ring is
|
|
1160
|
+
# drawn) and the scroll view's own background otherwise sits
|
|
1161
|
+
# directly on the translucent frosted panel behind it, so a
|
|
1162
|
+
# plain hairline there read as barely visible (2026-07-08
|
|
1163
|
+
# feedback: "outline not obvious, background is mostly
|
|
1164
|
+
# transparent"). _update_draft_borders (called once the
|
|
1165
|
+
# content view is installed, below) backs the whole wrapper a
|
|
1166
|
+
# solid color so box + ring read as one opaque card; no need
|
|
1167
|
+
# to pre-seed it here since that call always follows.
|
|
1168
|
+
outline_frame = NSMakeRect(M, box_y, W - 2 * M, box_h)
|
|
1169
|
+
outline = NSView.alloc().initWithFrame_(outline_frame)
|
|
1170
|
+
_round_rect(outline)
|
|
1171
|
+
inset = 4
|
|
1129
1172
|
scroll, tv = _editable_scroll(
|
|
1130
|
-
NSMakeRect(
|
|
1173
|
+
NSMakeRect(
|
|
1174
|
+
inset,
|
|
1175
|
+
inset,
|
|
1176
|
+
outline_frame.size.width - 2 * inset,
|
|
1177
|
+
outline_frame.size.height - 2 * inset,
|
|
1178
|
+
),
|
|
1179
|
+
_compose(draft.get("text")),
|
|
1131
1180
|
)
|
|
1132
1181
|
tv.setDelegate_(self)
|
|
1133
|
-
|
|
1182
|
+
outline.addSubview_(scroll)
|
|
1183
|
+
content.addSubview_(outline)
|
|
1134
1184
|
self._draft_scrolls[slot] = scroll
|
|
1185
|
+
self._draft_outlines[slot] = outline
|
|
1135
1186
|
self._draft_textviews[slot] = tv
|
|
1136
|
-
self._update_draft_borders()
|
|
1137
1187
|
tv = self._draft_textviews[sel_idx]
|
|
1138
1188
|
else:
|
|
1139
1189
|
reply = d.get("reply_text") or ""
|
|
@@ -1161,6 +1211,13 @@ class _ReviewController(NSObject):
|
|
|
1161
1211
|
self._textview = tv
|
|
1162
1212
|
|
|
1163
1213
|
self._panel.setContentView_(_frosted(content))
|
|
1214
|
+
if dual:
|
|
1215
|
+
# Layer border properties set before a view is attached to its
|
|
1216
|
+
# eventual window get silently dropped when AppKit backs the view
|
|
1217
|
+
# for real on attach, so the accent outline must be (re)applied
|
|
1218
|
+
# only after setContentView_ installs the view tree, not during
|
|
1219
|
+
# construction above.
|
|
1220
|
+
self._update_draft_borders()
|
|
1164
1221
|
# Counter lives in the native title bar, not inside the content, with
|
|
1165
1222
|
# the product name so a stray card is identifiable at a glance.
|
|
1166
1223
|
self._panel.setTitle_(
|
|
@@ -1342,14 +1399,18 @@ class _ReviewController(NSObject):
|
|
|
1342
1399
|
pass
|
|
1343
1400
|
return True
|
|
1344
1401
|
|
|
1345
|
-
# NSTextView
|
|
1346
|
-
#
|
|
1347
|
-
# keystroke
|
|
1348
|
-
#
|
|
1349
|
-
#
|
|
1350
|
-
#
|
|
1351
|
-
#
|
|
1352
|
-
|
|
1402
|
+
# NSTextView delegate: fires the instant the caret/selection moves inside
|
|
1403
|
+
# a text view, which includes a plain click-to-place-cursor with no
|
|
1404
|
+
# keystroke (unlike NSTextDidBeginEditingNotification/textDidBeginEditing_,
|
|
1405
|
+
# which only fires once an actual edit starts and does NOT fire from
|
|
1406
|
+
# clicking into a box to merely place the cursor, verified empirically —
|
|
1407
|
+
# that gap was the "clicking the other draft doesn't select it" bug).
|
|
1408
|
+
# Two-draft cards use this as the ONLY selection mechanism: whichever
|
|
1409
|
+
# draft box the reviewer is in IS the selected one. Only the two draft
|
|
1410
|
+
# boxes set self as delegate for this notification (the read-only thread
|
|
1411
|
+
# quote never fires it), so no candidate_id lookup is needed, just a slot
|
|
1412
|
+
# match against self._draft_textviews.
|
|
1413
|
+
def textViewDidChangeSelection_(self, notification):
|
|
1353
1414
|
try:
|
|
1354
1415
|
tv = notification.object()
|
|
1355
1416
|
except Exception:
|
|
@@ -1363,23 +1424,42 @@ class _ReviewController(NSObject):
|
|
|
1363
1424
|
|
|
1364
1425
|
@objc.python_method
|
|
1365
1426
|
def _update_draft_borders(self):
|
|
1366
|
-
"""Redraw the two draft boxes'
|
|
1367
|
-
in-progress edit/caret in either box is never disturbed): the
|
|
1368
|
-
selected box gets a
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1427
|
+
"""Redraw the two draft boxes' selection ring in place (no re-render,
|
|
1428
|
+
so an in-progress edit/caret in either box is never disturbed): the
|
|
1429
|
+
selected box's outline view gets a thick, solid black outline
|
|
1430
|
+
(deliberately NOT the user's system accent color — on a Graphite
|
|
1431
|
+
accent it renders as plain gray and is indistinguishable from chrome;
|
|
1432
|
+
2026-07-08 feedback wanted "a stronger color", then specifically
|
|
1433
|
+
black), the other a plain hairline. Every color here is baked via
|
|
1434
|
+
_solid() first: drawn as-is, dynamic system colors render partially
|
|
1435
|
+
see-through against whatever is behind the card's frosted/vibrant
|
|
1436
|
+
panel, which was why an earlier pass still looked washed out over a
|
|
1437
|
+
dark desktop. Applied to the dedicated outline wrapper, not the
|
|
1438
|
+
scroll view itself; see the comment at its construction in _render
|
|
1439
|
+
for why."""
|
|
1440
|
+
selected_color = NSColor.blackColor()
|
|
1441
|
+
for slot, outline in (self._draft_outlines or {}).items():
|
|
1373
1442
|
try:
|
|
1374
|
-
layer =
|
|
1443
|
+
layer = outline.layer()
|
|
1375
1444
|
if layer is None:
|
|
1376
1445
|
continue
|
|
1377
1446
|
if slot == self._selected_draft:
|
|
1378
|
-
layer.setBorderWidth_(
|
|
1379
|
-
layer.setBorderColor_(
|
|
1447
|
+
layer.setBorderWidth_(3.0)
|
|
1448
|
+
layer.setBorderColor_(selected_color.CGColor())
|
|
1449
|
+
# A ring alone read as barely-there against the frosted
|
|
1450
|
+
# panel. The margin between this wrapper's edge and the
|
|
1451
|
+
# inset scroll view is otherwise plain background, so
|
|
1452
|
+
# tinting it toward black turns that margin into a visible
|
|
1453
|
+
# halo, not just a hairline — obvious at a glance, not
|
|
1454
|
+
# just on close inspection.
|
|
1455
|
+
tint = _solid(
|
|
1456
|
+
NSColor.textBackgroundColor()
|
|
1457
|
+
).blendedColorWithFraction_ofColor_(0.30, selected_color)
|
|
1458
|
+
layer.setBackgroundColor_((tint or selected_color).CGColor())
|
|
1380
1459
|
else:
|
|
1381
1460
|
layer.setBorderWidth_(1.0)
|
|
1382
|
-
layer.setBorderColor_(NSColor.separatorColor().CGColor())
|
|
1461
|
+
layer.setBorderColor_(_solid(NSColor.separatorColor()).CGColor())
|
|
1462
|
+
layer.setBackgroundColor_(_solid(NSColor.textBackgroundColor()).CGColor())
|
|
1383
1463
|
except Exception:
|
|
1384
1464
|
pass
|
|
1385
1465
|
|
|
@@ -2338,14 +2338,26 @@ class S4LMenuBar(rumps.App):
|
|
|
2338
2338
|
# has been "drafting" past DRAFT_STUCK_SECONDS the worker keeps getting
|
|
2339
2339
|
# killed mid-run (or never claims) and nothing is draining — flip to ⚠
|
|
2340
2340
|
# instead of leaving the reassuring "drafting (8m)" spinner up. Skip when a
|
|
2341
|
-
# more specific cause (rate limit) already owns the reason.
|
|
2342
|
-
#
|
|
2343
|
-
#
|
|
2344
|
-
#
|
|
2345
|
-
#
|
|
2346
|
-
#
|
|
2347
|
-
#
|
|
2348
|
-
|
|
2341
|
+
# more specific cause (rate limit) already owns the reason.
|
|
2342
|
+
#
|
|
2343
|
+
# Gated on schedule_state in ("ok", "stalled"), NOT missing/disabled: when
|
|
2344
|
+
# the schedule is missing/disabled (e.g. orphaned by an account switch),
|
|
2345
|
+
# the producer ALSO sits "drafting" forever, and without excluding those two
|
|
2346
|
+
# draft_stuck shadowed the missing/disabled branch in _build_menu — the user
|
|
2347
|
+
# saw "worker keeps getting killed" with NO Re-arm button instead of "Draft
|
|
2348
|
+
# tasks aren't scheduled on this account" + the one-click fix (Karol,
|
|
2349
|
+
# 2026-07-06). "stalled" is deliberately included (2026-07-08): that branch's
|
|
2350
|
+
# only offered fix is "Restart Claude Desktop", but a job that has sat this
|
|
2351
|
+
# long — claimed-and-hung, OR never claimed at all (see the ⧖ prefix check
|
|
2352
|
+
# below) — means a restart was either already tried and didn't help, or
|
|
2353
|
+
# isn't the right first move; Diagnose is the more useful single action once
|
|
2354
|
+
# we have DRAFT_STUCK_SECONDS of direct evidence from the queue itself,
|
|
2355
|
+
# which is a more reliable signal than the host's lastRunAt staleness.
|
|
2356
|
+
if (
|
|
2357
|
+
setup_complete
|
|
2358
|
+
and schedule_state in ("ok", "stalled")
|
|
2359
|
+
and self._stall_reason_info[0] != "rate_limited"
|
|
2360
|
+
):
|
|
2349
2361
|
_act = st.read_activity()
|
|
2350
2362
|
if (
|
|
2351
2363
|
_act
|
|
@@ -2398,6 +2410,19 @@ class S4LMenuBar(rumps.App):
|
|
|
2398
2410
|
"Drafts can’t run — this Claude account hit its rate limit. "
|
|
2399
2411
|
+ (self._stall_reason_info[1] or "Wait for the limit to reset or switch account."),
|
|
2400
2412
|
)
|
|
2413
|
+
elif self._stall_reason_info[0] == "draft_stuck":
|
|
2414
|
+
# Previously unhandled here: fell through to the final `else`
|
|
2415
|
+
# below and told the user to click "Set up draft schedule" — a
|
|
2416
|
+
# button that doesn't exist in the draft_stuck menu (only
|
|
2417
|
+
# Diagnose does). Fixed 2026-07-08 so the notification always
|
|
2418
|
+
# matches the one button the menu actually shows.
|
|
2419
|
+
_unclaimed = "⧖" in (self._stall_reason_info[1] or "")
|
|
2420
|
+
self._notify(
|
|
2421
|
+
"S4L drafts not completing",
|
|
2422
|
+
("No worker is claiming draft jobs. " if _unclaimed else
|
|
2423
|
+
"A worker claimed a draft job and never finished it. ")
|
|
2424
|
+
+ "Open the S4L menu → “Diagnose & fix in Claude…”.",
|
|
2425
|
+
)
|
|
2401
2426
|
elif schedule_state == "disabled":
|
|
2402
2427
|
self._notify(
|
|
2403
2428
|
"S4L draft tasks disabled",
|
|
@@ -2937,21 +2962,37 @@ class S4LMenuBar(rumps.App):
|
|
|
2937
2962
|
# When the schedule IS firing (ok), attention is False and nothing shows here
|
|
2938
2963
|
# — a firing autopilot reads as healthy even if no draft has drained yet.
|
|
2939
2964
|
if attention:
|
|
2965
|
+
# Exactly ONE clickable action per ⚠ scenario, always — never stack a
|
|
2966
|
+
# specific fix next to the universal "Diagnose & fix" escape hatch.
|
|
2967
|
+
# Each branch below picks whichever single action is most relevant:
|
|
2968
|
+
# a known mechanical fix when one exists, or Diagnose when it doesn't
|
|
2969
|
+
# (2026-07-08, replacing the old "specific fix + Diagnose" pattern that
|
|
2970
|
+
# showed 2-3 buttons at once and confused which one to click).
|
|
2940
2971
|
if self._stall_reason_info[0] == "rate_limited":
|
|
2941
2972
|
# Routines fire but every run dies on a Claude rate limit (429).
|
|
2942
|
-
# Re-arm can't fix this
|
|
2973
|
+
# Re-arm/restart can't fix this — Diagnose is the one relevant action.
|
|
2943
2974
|
items.append(self._label("⚠ Claude rate-limited — drafts can’t run"))
|
|
2944
2975
|
items.append(self._label(
|
|
2945
2976
|
" " + (self._stall_reason_info[1] or "wait for reset or switch account")
|
|
2946
2977
|
))
|
|
2978
|
+
items.append(rumps.MenuItem("Diagnose & fix in Claude…", callback=self._diagnose_fix))
|
|
2947
2979
|
elif self._stall_reason_info[0] == "draft_stuck":
|
|
2948
|
-
# Routines fire and the producer keeps narrating "drafting" but
|
|
2949
|
-
#
|
|
2950
|
-
#
|
|
2951
|
-
|
|
2980
|
+
# Routines fire and the producer keeps narrating "drafting" but
|
|
2981
|
+
# nothing is finishing. Two different root causes share this one
|
|
2982
|
+
# reason code, and the label already tells us which: a '⧖' prefix
|
|
2983
|
+
# (see _label_elapsed_secs) means the job has sat in the pending
|
|
2984
|
+
# queue this whole time with no worker ever claiming it; no '⧖'
|
|
2985
|
+
# means something DID claim it and then hung or died mid-run.
|
|
2986
|
+
# Re-arm/restart can't fix either case — Diagnose is the one action.
|
|
2987
|
+
_unclaimed = "⧖" in (self._stall_reason_info[1] or "")
|
|
2988
|
+
items.append(self._label(
|
|
2989
|
+
"⚠ No worker is claiming draft jobs" if _unclaimed
|
|
2990
|
+
else "⚠ Draft not completing — worker keeps getting killed"
|
|
2991
|
+
))
|
|
2952
2992
|
items.append(self._label(
|
|
2953
2993
|
" " + (self._stall_reason_info[1] or "drafting") + " — no result yet"
|
|
2954
2994
|
))
|
|
2995
|
+
items.append(rumps.MenuItem("Diagnose & fix in Claude…", callback=self._diagnose_fix))
|
|
2955
2996
|
elif schedule_state == "disabled":
|
|
2956
2997
|
items.append(self._label("⚠ Draft tasks are scheduled but disabled"))
|
|
2957
2998
|
items.append(rumps.MenuItem("Set up draft schedule for this account", callback=self._rearm))
|
|
@@ -2959,19 +3000,15 @@ class S4LMenuBar(rumps.App):
|
|
|
2959
3000
|
# Task registered + enabled but the host stopped launching it: the
|
|
2960
3001
|
# Claude Desktop warm-session wedge (finished worker sessions never
|
|
2961
3002
|
# exit; the overlap guard skips every fire) or an account-switch
|
|
2962
|
-
# orphan.
|
|
2963
|
-
#
|
|
2964
|
-
#
|
|
3003
|
+
# orphan. _restart_claude_fix_work already runs the registry
|
|
3004
|
+
# self-heal (ensure-worker/orphan repair) while Claude is down,
|
|
3005
|
+
# before relaunching — so it covers both known causes in one click,
|
|
3006
|
+
# making it the one relevant action here (Karol, 2026-07-06).
|
|
2965
3007
|
items.append(self._label("⚠ Drafts stopped — Claude’s scheduler is stuck"))
|
|
2966
3008
|
items.append(rumps.MenuItem("Restart Claude Desktop to fix", callback=self._restart_claude_fix))
|
|
2967
|
-
items.append(rumps.MenuItem("Set up draft schedule for this account", callback=self._rearm))
|
|
2968
3009
|
else:
|
|
2969
3010
|
items.append(self._label("⚠ Draft tasks aren’t scheduled on this account"))
|
|
2970
3011
|
items.append(rumps.MenuItem("Set up draft schedule for this account", callback=self._rearm))
|
|
2971
|
-
# Universal escape hatch for EVERY persistent ⚠ (the draft_stuck and
|
|
2972
|
-
# rate_limited branches previously dead-ended with labels only): hand
|
|
2973
|
-
# Claude a diagnose-and-heal prompt that also reports back to us.
|
|
2974
|
-
items.append(rumps.MenuItem("Diagnose & fix in Claude…", callback=self._diagnose_fix))
|
|
2975
3012
|
items.append(rumps.separator)
|
|
2976
3013
|
|
|
2977
3014
|
if not runtime_ready:
|
package/mcp/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@m13v/s4l-mcp",
|
|
3
|
-
"version": "1.7.1-rc.
|
|
3
|
+
"version": "1.7.1-rc.23",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Desktop MCP client for social-autoposter (X/Twitter rail): manual draft/review/approve loop, autopilot control, and stats. Thin wrapper over the existing pipeline scripts.",
|
|
6
6
|
"license": "MIT",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@m13v/s4l",
|
|
3
|
-
"version": "1.7.1-rc.
|
|
3
|
+
"version": "1.7.1-rc.23",
|
|
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",
|
|
@@ -155,6 +155,7 @@
|
|
|
155
155
|
"node": ">=16"
|
|
156
156
|
},
|
|
157
157
|
"dependencies": {
|
|
158
|
+
"@sentry/node": "^10.64.0",
|
|
158
159
|
"firebase-admin": "^13.8.0",
|
|
159
160
|
"pg": "^8.20.0",
|
|
160
161
|
"ws": "^8.0.0"
|
|
@@ -262,22 +262,48 @@ def main() -> int:
|
|
|
262
262
|
plan_obj = {"candidates": merged}
|
|
263
263
|
if plan_created_at:
|
|
264
264
|
plan_obj["created_at"] = plan_created_at
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
#
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
265
|
+
|
|
266
|
+
# This is the actual delivery: if anything below throws, the cycle's drafts
|
|
267
|
+
# were computed but never reached the store the menu bar reads, and the
|
|
268
|
+
# wrapper (run-draft-and-publish.sh) captures this process's whole stdout+
|
|
269
|
+
# stderr with `|| true`, so a crash here previously vanished into a local
|
|
270
|
+
# log nothing central reads — the exact blind spot that cost the 2026-07-08
|
|
271
|
+
# Karol investigation its root cause. Report it like any other handled
|
|
272
|
+
# pipeline failure (see twitter_post_plan.py's post-failure capture).
|
|
273
|
+
try:
|
|
274
|
+
_atomic_write(dst, plan_obj)
|
|
275
|
+
ensure_store_symlink()
|
|
276
|
+
|
|
277
|
+
# Refresh the review-request marker the menu bar polls (count = pending, not posted).
|
|
278
|
+
pending = len([c for c in merged if not c.get("posted")])
|
|
279
|
+
project = ns.project or batch.get("project") or (new_cands[0].get("matched_project") if new_cands else None)
|
|
280
|
+
_atomic_write(
|
|
281
|
+
review_request_path(),
|
|
282
|
+
{
|
|
283
|
+
"batch_id": REVIEW_QUEUE_ID,
|
|
284
|
+
"project": project,
|
|
285
|
+
"count": pending,
|
|
286
|
+
"plan_path": dst,
|
|
287
|
+
"created_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
|
288
|
+
},
|
|
289
|
+
)
|
|
290
|
+
except Exception as e:
|
|
291
|
+
print(f"[merge_review_queue] delivery failed (drafts NOT merged into cards): {e}", file=sys.stderr)
|
|
292
|
+
try:
|
|
293
|
+
import sentry_init
|
|
294
|
+
|
|
295
|
+
sentry_init.init()
|
|
296
|
+
sentry_init.capture_message(
|
|
297
|
+
f"merge_review_queue delivery failed: {e}",
|
|
298
|
+
level="error",
|
|
299
|
+
tags={"component": "merge_review_queue", "added": str(added)},
|
|
300
|
+
extra={"plan_src": src},
|
|
301
|
+
)
|
|
302
|
+
sentry_init.flush(2.0)
|
|
303
|
+
except Exception:
|
|
304
|
+
pass
|
|
305
|
+
return 1
|
|
306
|
+
|
|
281
307
|
print(
|
|
282
308
|
f"[merge_review_queue] merged {added} new draft(s) into {REVIEW_QUEUE_ID} "
|
|
283
309
|
f"({pending} pending total) from {os.path.basename(src)}",
|
package/scripts/scan_pii.py
CHANGED
|
@@ -56,6 +56,14 @@ SECRET_PATTERNS = [
|
|
|
56
56
|
# Absolute home path leak. Placeholder forms (/Users/<you>, /Users/USERNAME) pass.
|
|
57
57
|
HOME_PATH_RE = re.compile(r"/Users/(?!<|USER|USERNAME|you\b|me\b|name\b)[a-z0-9._-]{2,}", re.I)
|
|
58
58
|
|
|
59
|
+
|
|
60
|
+
def _home_path_exempt(path: str) -> bool:
|
|
61
|
+
"""launchd .plist files legitimately require absolute paths — launchd does not
|
|
62
|
+
expand ~ or $HOME in ProgramArguments/StandardOutPath/etc. The repo already
|
|
63
|
+
tracks 70+ of them, so exempt this file class from the absolute-home-path rule.
|
|
64
|
+
Secrets, PII, and image checks still apply."""
|
|
65
|
+
return path.startswith("launchd/") and path.endswith(".plist")
|
|
66
|
+
|
|
59
67
|
IMAGE_EXT = {".png", ".jpg", ".jpeg", ".gif", ".webp", ".heic", ".bmp", ".tiff"}
|
|
60
68
|
|
|
61
69
|
|
|
@@ -146,7 +154,7 @@ def scan(paths: list[str], staged: bool) -> tuple[list[str], list[str]]:
|
|
|
146
154
|
if deny_re and deny_re.search(text):
|
|
147
155
|
m = deny_re.search(text)
|
|
148
156
|
hard.append(f"{path}:{lineno}: PII denylist match ('{m.group(0)}')")
|
|
149
|
-
if HOME_PATH_RE.search(text):
|
|
157
|
+
if HOME_PATH_RE.search(text) and not _home_path_exempt(path):
|
|
150
158
|
m = HOME_PATH_RE.search(text)
|
|
151
159
|
(hard if staged else soft).append(
|
|
152
160
|
f"{path}:{lineno}: absolute home path ('{m.group(0)}...')"
|