social-autoposter 1.6.71 → 1.6.72
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/index.js +46 -154
- package/mcp/dist/telemetry.js +1 -1
- package/package.json +1 -2
- package/scripts/sentry_init.py +1 -1
- package/skill/lock.sh +3 -4
- package/skill/run-twitter-cycle.sh +12 -16
package/mcp/dist/index.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
// social-autoposter MCP server (X/Twitter rail).
|
|
3
3
|
//
|
|
4
4
|
// Core tools:
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// post_drafts
|
|
5
|
+
// scan_candidates - scan + score X threads, return the top candidates (no draft, no post).
|
|
6
|
+
// submit_drafts - take the replies you drafted and queue them for review (posts nothing).
|
|
7
|
+
// post_drafts - post the drafts the user chose by number from a batch.
|
|
8
8
|
// get_stats - read-only post + engagement stats.
|
|
9
9
|
//
|
|
10
10
|
// THIN wrapper. The pipeline brain (scan, score, drafting prompts, posting)
|
|
@@ -152,7 +152,8 @@ const server = new McpServer({
|
|
|
152
152
|
"ONBOARDING IS A TERMINAL GOAL. When the user asks to set up, install, configure, or onboard " +
|
|
153
153
|
"social-autoposter, keep taking the next safe action until the owned runtime is ready, a " +
|
|
154
154
|
"project is fully configured with seeded search topics, X is connected with its real handle, " +
|
|
155
|
-
"and
|
|
155
|
+
"and the draft path (`scan_candidates` -> draft -> `submit_drafts`) has verified the pipeline " +
|
|
156
|
+
"without posting. Do not ask whether to inspect " +
|
|
156
157
|
"status, install or repair owned dependencies, choose an auto-detected browser profile, connect " +
|
|
157
158
|
"X, scan the profile, research the product website, save conservative inferred fields, seed " +
|
|
158
159
|
"topics, retry a recoverable failure, or run draft-only verification. Do those things. The " +
|
|
@@ -162,25 +163,28 @@ const server = new McpServer({
|
|
|
162
163
|
"when no product can be identified from config, context, the X profile, or public research, " +
|
|
163
164
|
"or pause for an unavoidable interactive login or OS prompt. Never post drafts or enable " +
|
|
164
165
|
"autopilot during setup unless the user explicitly requested that. When setup reaches done — " +
|
|
165
|
-
"runtime ready, a project configured with seeded topics, X connected, and
|
|
166
|
-
"verified — call the `dashboard` tool so the user
|
|
166
|
+
"runtime ready, a project configured with seeded topics, X connected, and the draft path " +
|
|
167
|
+
"(scan_candidates -> draft -> submit_drafts) verified — call the `dashboard` tool so the user " +
|
|
168
|
+
"sees the finished setup rendered, then give " +
|
|
167
169
|
"the completion summary.\n\n" +
|
|
168
170
|
"BE PROACTIVE ABOUT MARKETING MOMENTS. Whenever the user ships, finishes, merges, or releases " +
|
|
169
171
|
"something worth talking about in this session (a new feature, a launch, a long-awaited fix, a " +
|
|
170
|
-
"milestone), don't wait to be asked: offer to tell the world by running
|
|
171
|
-
"
|
|
172
|
+
"milestone), don't wait to be asked: offer to tell the world by running a draft cycle " +
|
|
173
|
+
"(`scan_candidates` -> draft the replies -> `submit_drafts`) about it. A feature nobody hears " +
|
|
174
|
+
"about is wasted distribution. Use judgment, suggest it for " +
|
|
172
175
|
"genuinely shareable moments, not routine refactors, chores, or internal cleanup.\n\n" +
|
|
173
176
|
"STAY CURRENT. At the start of a session, and again right after you help the user ship/merge/" +
|
|
174
177
|
"release, call the `runtime` tool (action:'version'). If `update_available` is true, tell the " +
|
|
175
178
|
"user and offer to run `runtime` with action:'update'. The `project_config` tool's status also " +
|
|
176
179
|
"surfaces `update_available` and an `update_hint`.\n\n" +
|
|
177
|
-
"TYPICAL FLOW: `project_config` (configure OR edit the project, and connect X) -> `
|
|
178
|
-
"(
|
|
180
|
+
"TYPICAL FLOW: `project_config` (configure OR edit the project, and connect X) -> `scan_candidates` " +
|
|
181
|
+
"(get the top threads to reply to) -> draft the replies yourself -> `submit_drafts` (queue them " +
|
|
182
|
+
"for review; nothing posts until the user approves) -> `post_drafts` (post the approved ones) -> " +
|
|
179
183
|
"`get_stats` (see performance). Run `project_config` first; the other tools refuse until a " +
|
|
180
184
|
"project is fully configured. To change anything about a project later, call `project_config` " +
|
|
181
185
|
"again with the project's name and just the changed fields — there is no separate config editor.\n\n" +
|
|
182
186
|
"RENDER THE DASHBOARD AFTER ACTIONS. After any state-changing or results-producing tool call " +
|
|
183
|
-
"(`
|
|
187
|
+
"(`scan_candidates`, `submit_drafts`, `post_drafts`, `get_stats`), end your turn by " +
|
|
184
188
|
"calling the `dashboard` tool so the user sees the updated state visually. Do NOT call " +
|
|
185
189
|
"`dashboard` after pure Q&A, config explanations, or status-only checks that changed nothing.",
|
|
186
190
|
});
|
|
@@ -254,28 +258,28 @@ function blockedReasonMessage(reason) {
|
|
|
254
258
|
"couldn't run. (It DID find and rank threads, it just couldn't draft replies.) This " +
|
|
255
259
|
"CLI uses its own login, separate from Claude Desktop. To fix it, open a terminal and run:\n\n" +
|
|
256
260
|
" claude\n\n" +
|
|
257
|
-
"then `/login` inside it (or run `claude setup-token`). Once it's logged in, run
|
|
261
|
+
"then `/login` inside it (or run `claude setup-token`). Once it's logged in, run scan_candidates again.");
|
|
258
262
|
case "monthly_limit":
|
|
259
263
|
case "daily_limit":
|
|
260
264
|
case "rate_limit_5h":
|
|
261
265
|
return (`The drafting step hit an Anthropic usage limit (${reason}), so no replies were drafted. ` +
|
|
262
|
-
"Wait for the limit to reset, then run
|
|
266
|
+
"Wait for the limit to reset, then run scan_candidates again.");
|
|
263
267
|
case "no_search_topics":
|
|
264
268
|
return ("This project has no search topics yet, so there was nothing to scan. Topics live in the " +
|
|
265
269
|
"DB (project_search_topics) and are seeded from your project's `search_topics` when you " +
|
|
266
270
|
"configure it. Re-run the `project_config` tool for this project with a `search_topics` list " +
|
|
267
271
|
"(comma-separated keywords/phrases your buyers tweet about); it seeds them automatically, then " +
|
|
268
|
-
"run
|
|
272
|
+
"run scan_candidates again.");
|
|
269
273
|
case "topics_api_unreachable":
|
|
270
274
|
return ("Couldn't reach the search-topics service to load this project's topics, so the cycle stopped " +
|
|
271
|
-
"before scanning. This is usually a transient backend/network issue. Try
|
|
275
|
+
"before scanning. This is usually a transient backend/network issue. Try scan_candidates again in a " +
|
|
272
276
|
"moment; if it persists, check connectivity to the autoposter backend.");
|
|
273
277
|
case "credit_balance":
|
|
274
278
|
return ("The drafting step failed because the Anthropic account is out of credits. " +
|
|
275
|
-
"Add credits, then run
|
|
279
|
+
"Add credits, then run scan_candidates again.");
|
|
276
280
|
default:
|
|
277
281
|
return (`The drafting step failed (${reason}) and produced no drafts. ` +
|
|
278
|
-
"Check skill/logs/twitter-cycle-*.log on this machine for details, then run
|
|
282
|
+
"Check skill/logs/twitter-cycle-*.log on this machine for details, then run scan_candidates again.");
|
|
279
283
|
}
|
|
280
284
|
}
|
|
281
285
|
// Turn a raw run-twitter-cycle.sh stdout line into a short, user-facing
|
|
@@ -647,7 +651,7 @@ tool("project_config", {
|
|
|
647
651
|
"Call with status:true (or no name) to list every configured project, its remaining fields, AND " +
|
|
648
652
|
"whether X is connected. Use config, conversation context, profile_scan, and website research " +
|
|
649
653
|
"before asking for fields. Ask only if no product can be identified or an interactive login is " +
|
|
650
|
-
"unavoidable. The
|
|
654
|
+
"unavoidable. The scan_candidates and get_stats tools refuse to run until a project is " +
|
|
651
655
|
"fully set up.",
|
|
652
656
|
inputSchema: {
|
|
653
657
|
status: z.boolean().optional(),
|
|
@@ -811,7 +815,7 @@ tool("project_config", {
|
|
|
811
815
|
next_step: r.connected
|
|
812
816
|
? "X is connected. Next, run project_config action:'profile_scan' to read this account's bio + recent " +
|
|
813
817
|
"posts + replies and draft the project's voice/icp/search_topics in the user's own register " +
|
|
814
|
-
"before saving. Then run
|
|
818
|
+
"before saving. Then run a draft cycle (scan_candidates -> draft -> submit_drafts) once the project is fully set up."
|
|
815
819
|
: r.state === "needs_login"
|
|
816
820
|
? "The user must finish signing in to x.com in the Chrome window that just opened. Tell " +
|
|
817
821
|
"them that single required action, then call project_config action:'connect_x', confirm:true again."
|
|
@@ -918,7 +922,7 @@ tool("project_config", {
|
|
|
918
922
|
(x.connected ? "" : " X is not connected yet either — detect_x_sources, warn about keychain prompts, then run connect_x with confirm:true without a separate permission turn.")
|
|
919
923
|
: projects.every((p) => p.ready)
|
|
920
924
|
? (x.connected
|
|
921
|
-
? "All configured projects are ready and X is connected. Run
|
|
925
|
+
? "All configured projects are ready and X is connected. Run scan_candidates, draft a reply or two, and submit_drafts now to verify end to end without posting. After it verifies, call the `dashboard` tool so the user sees the finished setup."
|
|
922
926
|
: "All configured projects are ready, but X is NOT connected — posting needs a logged-in " +
|
|
923
927
|
"x.com session. Detect sources and run project_config action:'connect_x', confirm:true; do not ask whether to proceed.")
|
|
924
928
|
: "Some projects are missing required fields (see each project's missing_required). Derive them from config, context, profile_scan, and website research, then call project_config again. Ask only if a required field is genuinely unknowable." +
|
|
@@ -960,13 +964,13 @@ tool("project_config", {
|
|
|
960
964
|
topic_count: topicCount,
|
|
961
965
|
});
|
|
962
966
|
seedNote = m
|
|
963
|
-
? ` Seeded ${m[1]} search topic(s) into the DB (new: ${m[2]}, updated: ${m[3]}), so
|
|
964
|
-
: " Seeded search topics into the DB so
|
|
967
|
+
? ` Seeded ${m[1]} search topic(s) into the DB (new: ${m[2]}, updated: ${m[3]}), so scan_candidates has a topic universe to work with.`
|
|
968
|
+
: " Seeded search topics into the DB so scan_candidates has a topic universe to work with.";
|
|
965
969
|
}
|
|
966
970
|
else {
|
|
967
971
|
const tail = (seed.stderr || seed.stdout).trim().split("\n").slice(-1)[0] || "unknown error";
|
|
968
972
|
blockOnboardingMilestone("topics_seeded", "topic_seed_failed", tail, { exit_code: seed.code });
|
|
969
|
-
seedNote = ` (Heads up: couldn't seed search topics into the DB yet — ${tail}.
|
|
973
|
+
seedNote = ` (Heads up: couldn't seed search topics into the DB yet — ${tail}. scan_candidates will tell you clearly if topics are missing.)`;
|
|
970
974
|
}
|
|
971
975
|
// Cold-start QUERY supply: fan the seeded topics out into >=30 real X
|
|
972
976
|
// search queries (project_search_queries) so the deterministic Phase 1
|
|
@@ -1030,7 +1034,7 @@ tool("project_config", {
|
|
|
1030
1034
|
note: (result.ready
|
|
1031
1035
|
? `Project '${result.project}' is fully configured.${seedNote} Next: if X is not connected, ` +
|
|
1032
1036
|
`detect sources, warn about keychain prompts, and call project_config with ` +
|
|
1033
|
-
`action:'connect_x', confirm:true immediately. Once X is connected, run
|
|
1037
|
+
`action:'connect_x', confirm:true immediately. Once X is connected, run scan_candidates -> submit_drafts to` +
|
|
1034
1038
|
`verify without posting. Do not enable autopilot unless explicitly requested.`
|
|
1035
1039
|
: `Saved what you provided for '${result.project}'. Still need: ${result.missing_required.join(", ")}. ` +
|
|
1036
1040
|
`First derive those fields from existing context, profile_scan, and website research, then ` +
|
|
@@ -1042,145 +1046,29 @@ tool("project_config", {
|
|
|
1042
1046
|
return textContent(`Setup failed: ${e.message}`);
|
|
1043
1047
|
}
|
|
1044
1048
|
});
|
|
1045
|
-
// ---- draft_cycle:
|
|
1046
|
-
//
|
|
1047
|
-
//
|
|
1048
|
-
//
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
"to post and which to rewrite, then call `post_drafts` with their decision and the " +
|
|
1054
|
-
"returned batch_id. The table MUST show, per draft: the thread being replied to " +
|
|
1055
|
-
"(thread_text), the draft reply, and the link target (link_url) when present; never " +
|
|
1056
|
-
"drop those columns. Flow: discover -> draft -> review in chat -> post_drafts. " +
|
|
1057
|
-
"After returning the table, call the `dashboard` tool so the user sees the updated state.",
|
|
1058
|
-
inputSchema: {
|
|
1059
|
-
project: z
|
|
1060
|
-
.string()
|
|
1061
|
-
.optional()
|
|
1062
|
-
.describe("Which configured project to draft for. Optional when only one project is set up; required when several are."),
|
|
1063
|
-
},
|
|
1064
|
-
}, async ({ project }, extra) => {
|
|
1065
|
-
const r = resolveProject(project);
|
|
1066
|
-
if (!r.ok)
|
|
1067
|
-
return textContent(r.message);
|
|
1068
|
-
const proj = r.project;
|
|
1069
|
-
recordOnboardingAttempt("draft_verified");
|
|
1070
|
-
// Live progress so the chat doesn't sit on a frozen spinner for minutes.
|
|
1071
|
-
// Two channels, both best-effort (a sink failure must never fail the cycle):
|
|
1072
|
-
// 1. notifications/message — a log line; the host records it (and some
|
|
1073
|
-
// clients show it in a log view). Works with no client opt-in.
|
|
1074
|
-
// 2. notifications/progress — drives the status text under the running
|
|
1075
|
-
// tool. Only valid when the client supplied a progressToken on the
|
|
1076
|
-
// request, so it's guarded on that.
|
|
1077
|
-
const progressToken = extra?._meta?.progressToken;
|
|
1078
|
-
const sendProgress = async (message, step) => {
|
|
1079
|
-
try {
|
|
1080
|
-
await extra.sendNotification({
|
|
1081
|
-
method: "notifications/message",
|
|
1082
|
-
params: { level: "info", logger: "draft_cycle", data: message },
|
|
1083
|
-
});
|
|
1084
|
-
}
|
|
1085
|
-
catch {
|
|
1086
|
-
/* ignore */
|
|
1087
|
-
}
|
|
1088
|
-
if (progressToken !== undefined) {
|
|
1089
|
-
try {
|
|
1090
|
-
await extra.sendNotification({
|
|
1091
|
-
method: "notifications/progress",
|
|
1092
|
-
params: { progressToken, progress: step, message },
|
|
1093
|
-
});
|
|
1094
|
-
}
|
|
1095
|
-
catch {
|
|
1096
|
-
/* ignore */
|
|
1097
|
-
}
|
|
1098
|
-
}
|
|
1099
|
-
};
|
|
1100
|
-
const drafted = await produceDrafts(proj, (message, step) => {
|
|
1101
|
-
void sendProgress(message, step);
|
|
1102
|
-
});
|
|
1103
|
-
if (drafted.blocked || !drafted.batchId) {
|
|
1104
|
-
blockOnboardingMilestone("draft_verified", "draft_cycle_blocked", drafted.blocked ?? "No drafts produced.", { outcome: "blocked" });
|
|
1105
|
-
return textContent(drafted.blocked ?? "No drafts produced.");
|
|
1106
|
-
}
|
|
1107
|
-
const plan = readPlan(drafted.batchId);
|
|
1108
|
-
if (!plan || !(plan.candidates && plan.candidates.length)) {
|
|
1109
|
-
blockOnboardingMilestone("draft_verified", "draft_batch_empty", `No drafts in batch ${drafted.batchId}.`, { outcome: "empty_batch", draft_count: 0 });
|
|
1110
|
-
return textContent(`No drafts in batch ${drafted.batchId}.`);
|
|
1111
|
-
}
|
|
1112
|
-
const count = plan.candidates.length;
|
|
1113
|
-
completeOnboardingMilestone("draft_verified", {
|
|
1114
|
-
outcome: "review_batch",
|
|
1115
|
-
draft_count: count,
|
|
1116
|
-
});
|
|
1117
|
-
// Fire the menu-bar pop-up review (default). The chat-table review below is
|
|
1118
|
-
// unchanged; both surfaces can approve, de-duped by the plan `posted` flag.
|
|
1119
|
-
writeReviewRequest({
|
|
1120
|
-
batch_id: drafted.batchId,
|
|
1121
|
-
project: proj,
|
|
1122
|
-
count,
|
|
1123
|
-
plan_path: planPath(drafted.batchId),
|
|
1124
|
-
created_at: new Date().toISOString(),
|
|
1125
|
-
});
|
|
1126
|
-
const table = renderDraftsTable(plan);
|
|
1127
|
-
const message = `Drafted ${count} ${count === 1 ? "reply" : "replies"} for "${proj}" ` +
|
|
1128
|
-
`(batch ${drafted.batchId}). NOTHING has been posted yet.\n\n` +
|
|
1129
|
-
`${table}\n\n` +
|
|
1130
|
-
`Show this list to the user and ask which to post and which to edit. When you render ` +
|
|
1131
|
-
`the table, ALWAYS include, for every draft: the thread it replies to (in reply to / ` +
|
|
1132
|
-
`thread_text), the draft text, and the link target (link_url) if present. Do NOT drop ` +
|
|
1133
|
-
`the thread or link columns. Note the literal short link is appended at post time and ` +
|
|
1134
|
-
`an A/B gate may omit it, so present link_url as the target, not a guaranteed final URL. ` +
|
|
1135
|
-
`They can reply however is natural, e.g. "post 1, 3 and 5", "edit 2: <new wording>", ` +
|
|
1136
|
-
`"post all", or "skip all". Editing a draft also posts it. Then call the post_drafts ` +
|
|
1137
|
-
`tool with batch_id "${drafted.batchId}" and their decision (post: [numbers], ` +
|
|
1138
|
-
`edits: [{n, text}], or post_all: true). Do not post anything the user didn't ask for.`;
|
|
1139
|
-
return {
|
|
1140
|
-
content: [{ type: "text", text: message }],
|
|
1141
|
-
structuredContent: {
|
|
1142
|
-
batch_id: drafted.batchId,
|
|
1143
|
-
drafted: count,
|
|
1144
|
-
status: "awaiting_decision",
|
|
1145
|
-
onboarding: onboardingSnapshot(),
|
|
1146
|
-
// Include the actual draft text here, not just a count. Some hosts
|
|
1147
|
-
// (e.g. Claude Desktop) surface ONLY structuredContent to the model and
|
|
1148
|
-
// drop the human-readable `content` table — which left the agent saying
|
|
1149
|
-
// "drafted: 2" with no way to show the drafts. Carrying the drafts in
|
|
1150
|
-
// structuredContent makes them available regardless of host behavior.
|
|
1151
|
-
drafts: (plan.candidates || []).map((c, i) => ({
|
|
1152
|
-
n: i + 1,
|
|
1153
|
-
author: c.thread_author,
|
|
1154
|
-
tweet_url: c.candidate_url,
|
|
1155
|
-
// The original tweet being replied to — reviewer context. Hosts that
|
|
1156
|
-
// surface ONLY structuredContent (Claude Desktop, Fazm Code tab) need
|
|
1157
|
-
// this here or the relayed table loses the thread it's responding to.
|
|
1158
|
-
thread_text: c.thread_text,
|
|
1159
|
-
reply_text: c.reply_text,
|
|
1160
|
-
// Target link only. The literal /r/<code> short link is minted at post
|
|
1161
|
-
// time and an A/B gate may omit it; do not pre-mint here.
|
|
1162
|
-
link_url: c.link_url,
|
|
1163
|
-
style: c.engagement_style,
|
|
1164
|
-
language: c.language,
|
|
1165
|
-
})),
|
|
1166
|
-
},
|
|
1167
|
-
};
|
|
1168
|
-
});
|
|
1049
|
+
// ---- draft_cycle: DEPRECATED 2026-06-20 (registration removed) -------------
|
|
1050
|
+
// Replaced by the scan_candidates -> (agent drafts) -> submit_drafts flow, so the
|
|
1051
|
+
// AI drafting now runs in the calling session (on the user's plan) instead of a
|
|
1052
|
+
// spawned `claude -p`. post_drafts (below) still posts the approved subset, and
|
|
1053
|
+
// submit_drafts completes the onboarding "draft_verified" milestone that this
|
|
1054
|
+
// tool used to. The underlying pipeline (run-twitter-cycle.sh) and the
|
|
1055
|
+
// produceDrafts() helper are kept as the source those tools reuse; only the
|
|
1056
|
+
// draft_cycle tool registration was removed here.
|
|
1169
1057
|
// ---- post_drafts: post the user's chosen drafts from a batch ---------------
|
|
1170
1058
|
// Second half of the manual loop. The user reviewed the table from draft_cycle
|
|
1171
1059
|
// and said which numbers to post / edit; this posts exactly those. Editing a
|
|
1172
1060
|
// draft implies posting it. Indices are 1-based, matching the table.
|
|
1173
1061
|
tool("post_drafts", {
|
|
1174
1062
|
title: "Post chosen drafts",
|
|
1175
|
-
description: "Post the drafts the user approved from a
|
|
1176
|
-
"
|
|
1063
|
+
description: "Post the drafts the user approved from a submit_drafts batch. Pass the batch_id from " +
|
|
1064
|
+
"submit_drafts and the user's decision by NUMBER (1-based, matching the table): `post` is " +
|
|
1177
1065
|
"the list of draft numbers to post as drafted; `edits` rewrites a draft's text before " +
|
|
1178
1066
|
"posting it (editing implies posting); `post_all` posts every draft. Only the chosen " +
|
|
1179
1067
|
"drafts post; anything not listed is left unposted. Call this ONLY after the user has " +
|
|
1180
1068
|
"told you which drafts they want. After posting, call the `dashboard` tool so the user " +
|
|
1181
1069
|
"sees the updated state.",
|
|
1182
1070
|
inputSchema: {
|
|
1183
|
-
batch_id: z.string().describe("The batch_id returned by
|
|
1071
|
+
batch_id: z.string().describe("The batch_id returned by submit_drafts."),
|
|
1184
1072
|
post: z
|
|
1185
1073
|
.array(z.number().int().positive())
|
|
1186
1074
|
.optional()
|
|
@@ -1194,7 +1082,7 @@ tool("post_drafts", {
|
|
|
1194
1082
|
}, async ({ batch_id, post, edits, post_all }) => {
|
|
1195
1083
|
const plan = readPlan(batch_id);
|
|
1196
1084
|
if (!plan || !(plan.candidates && plan.candidates.length)) {
|
|
1197
|
-
return textContent(`No drafts found for batch ${batch_id}. Run
|
|
1085
|
+
return textContent(`No drafts found for batch ${batch_id}. Run scan_candidates then submit_drafts again to produce a fresh batch.`);
|
|
1198
1086
|
}
|
|
1199
1087
|
const candidates = plan.candidates;
|
|
1200
1088
|
const total = candidates.length;
|
|
@@ -1847,7 +1735,11 @@ tool("submit_drafts", {
|
|
|
1847
1735
|
}
|
|
1848
1736
|
const finalPlan = readPlan(args.batch_id) ?? plan;
|
|
1849
1737
|
const count = (finalPlan.candidates || []).length;
|
|
1850
|
-
//
|
|
1738
|
+
// Drafts queued = the pipeline verified end-to-end without posting. This is the
|
|
1739
|
+
// onboarding "draft_verified" terminal goal (formerly completed by draft_cycle).
|
|
1740
|
+
if (count > 0)
|
|
1741
|
+
completeOnboardingMilestone("draft_verified", { outcome: "review_batch", draft_count: count });
|
|
1742
|
+
// Surface to the menu-bar review cards (same review plan draft_cycle used).
|
|
1851
1743
|
writeReviewRequest({
|
|
1852
1744
|
batch_id: args.batch_id,
|
|
1853
1745
|
project,
|
|
@@ -1867,7 +1759,7 @@ appTool("dashboard", {
|
|
|
1867
1759
|
"connection, autopilot state, and 7-day stats, with buttons to run a draft cycle, connect X, " +
|
|
1868
1760
|
"and refresh. Use when the user asks to see the dashboard, panel, " +
|
|
1869
1761
|
"status, or controls. ALSO call this at the end of any state-changing or results-producing " +
|
|
1870
|
-
"action (
|
|
1762
|
+
"action (scan_candidates, submit_drafts, post_drafts, get_stats) so the user sees the " +
|
|
1871
1763
|
"updated dashboard. Hosts without UI support get the same data as text.",
|
|
1872
1764
|
inputSchema: {},
|
|
1873
1765
|
// fallback_url is set only when the host can't render the ui:// resource and
|
package/mcp/dist/telemetry.js
CHANGED
|
@@ -13,7 +13,7 @@ import { repoDir, runPython } from "./repo.js";
|
|
|
13
13
|
import { VERSION } from "./version.js";
|
|
14
14
|
// Sentry DSN is a client-side identifier (safe to embed, same posture as Fazm's
|
|
15
15
|
// hardcoded Swift DSN). Overridable via env for dev. Empty -> Sentry disabled.
|
|
16
|
-
const EMBEDDED_DSN = "";
|
|
16
|
+
const EMBEDDED_DSN = "https://4d44ac907262c6545cf8681703528d04@o4507617161314304.ingest.us.sentry.io/4511598804336640";
|
|
17
17
|
const SENTRY_DSN = process.env.SAPS_SENTRY_DSN || EMBEDDED_DSN;
|
|
18
18
|
let sentryReady = false;
|
|
19
19
|
export function initSentry() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "social-autoposter",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.72",
|
|
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"
|
|
@@ -87,7 +87,6 @@
|
|
|
87
87
|
"!scripts/export_kent_handoff.py",
|
|
88
88
|
"!scripts/extract_user_messages_today.py",
|
|
89
89
|
"!scripts/fazm_seo_health.py",
|
|
90
|
-
"!scripts/fetch_twitter_t1.py",
|
|
91
90
|
"!scripts/fix_mdx_light_mode.py",
|
|
92
91
|
"!scripts/fix_style_lengths_20260601.py",
|
|
93
92
|
"!scripts/fix_svg_paragraph_wrap.py",
|
package/scripts/sentry_init.py
CHANGED
|
@@ -15,7 +15,7 @@ import os
|
|
|
15
15
|
|
|
16
16
|
# Client-side DSN: safe to embed, same posture as the Node telemetry and Fazm's
|
|
17
17
|
# hardcoded Swift DSN. Overridable via env for dev. Empty -> Sentry disabled.
|
|
18
|
-
_EMBEDDED_DSN = ""
|
|
18
|
+
_EMBEDDED_DSN = "https://4d44ac907262c6545cf8681703528d04@o4507617161314304.ingest.us.sentry.io/4511598804336640"
|
|
19
19
|
|
|
20
20
|
_initialized = False
|
|
21
21
|
|
package/skill/lock.sh
CHANGED
|
@@ -617,10 +617,9 @@ defer_if_foreign_browser_mcp_active() {
|
|
|
617
617
|
|
|
618
618
|
# Explicit early release. Use this when a long-running script only needs the
|
|
619
619
|
# browser for part of its run (e.g. run-twitter-cycle.sh holds the lock for
|
|
620
|
-
# Phase 1 scrape, releases
|
|
621
|
-
#
|
|
622
|
-
#
|
|
623
|
-
# only sleeping.
|
|
620
|
+
# Phase 1 scrape, releases between Phase 1 and Phase 2b posting, then re-acquires
|
|
621
|
+
# before Phase 2b). Without this, sibling pipelines waiting on the same profile
|
|
622
|
+
# lock block for the full cycle even when the holder is not using the browser.
|
|
624
623
|
release_lock() {
|
|
625
624
|
local name="$1"
|
|
626
625
|
local lock_dir="/tmp/social-autoposter-${name}.lock"
|
|
@@ -12,18 +12,14 @@
|
|
|
12
12
|
# - scrape tweets via twitter-harness, enrich via fxtwitter -> T0 snapshot
|
|
13
13
|
# - store all candidates with batch_id and search_topic
|
|
14
14
|
#
|
|
15
|
-
#
|
|
15
|
+
# No ripen wait (variant D won the A/B/C/D test 2026-05-31): the cycle goes
|
|
16
|
+
# straight from discovery to drafting. There is NO 5-min sleep and NO fxtwitter
|
|
17
|
+
# T1 re-poll anywhere in the Twitter pipeline. delta_score stays at its T0
|
|
18
|
+
# value and is no longer a gate. Do not re-introduce a ripen/sleep step here.
|
|
16
19
|
#
|
|
17
|
-
# Phase 2 (
|
|
18
|
-
# -
|
|
19
|
-
#
|
|
20
|
-
# product-discussion tweets (asking for a tool, venting a pain point) can
|
|
21
|
-
# compete; ranking still favors growth via hybrid sort
|
|
22
|
-
# - Hybrid sort: ORDER BY (delta_score + product_intent_boost) where
|
|
23
|
-
# product_intent_boost is +5 when tweet text matches an intent-signal regex
|
|
24
|
-
# (wish/need/looking for/recommend/alternative/frustrated/etc); raw growth
|
|
25
|
-
# remains the dominant signal, but a slow-burn "anyone know a tool for..."
|
|
26
|
-
# tweet now ranks alongside fast-growing news/drama
|
|
20
|
+
# Phase 2 (immediately after Phase 1):
|
|
21
|
+
# - sort candidates by virality_score DESC (composite predictor stamped at
|
|
22
|
+
# discovery by score_twitter_candidates.py); no delta floor, no T1 re-poll
|
|
27
23
|
# - Claude reads top 25 (raised from 15 so the long tail reaches the model),
|
|
28
24
|
# drops unsuitable, posts every candidate it judges genuinely on-brand
|
|
29
25
|
# (no per-cycle post cap, no per-project quota)
|
|
@@ -412,7 +408,7 @@ python3 "$REPO_DIR/scripts/twitter_batch_phase.py" start "$BATCH_ID" --phase pha
|
|
|
412
408
|
# read from the owner's twitter_batches row:
|
|
413
409
|
# phase0 -> 5 min (just the salvage SQL)
|
|
414
410
|
# phase1 -> 20 min (Claude scan + scrape)
|
|
415
|
-
# phase2a -> 20 min (
|
|
411
|
+
# phase2a -> 20 min (browser-lock handoff window; no ripen wait since 2026-05-31)
|
|
416
412
|
# phase2b-prep -> 45 min (Claude reads threads + drafts; bumped 2026-05-15
|
|
417
413
|
# 15 -> 30 after 17:15 cycle was wrongly salvaged
|
|
418
414
|
# while queued behind 17:30's 42-min lock-hold;
|
|
@@ -1454,16 +1450,16 @@ if [ "$BATCH_COUNT" = "0" ]; then
|
|
|
1454
1450
|
exit 0
|
|
1455
1451
|
fi
|
|
1456
1452
|
|
|
1457
|
-
# Stamp phase2a before releasing the lock so the budget covers the
|
|
1458
|
-
#
|
|
1453
|
+
# Stamp phase2a before releasing the lock so the salvage budget covers the
|
|
1454
|
+
# browser-lock handoff window (phase2a budget = 20 min).
|
|
1459
1455
|
python3 "$REPO_DIR/scripts/twitter_batch_phase.py" advance "$BATCH_ID" --phase phase2a 2>&1 | tee -a "$LOG_FILE" || true
|
|
1460
1456
|
|
|
1461
|
-
# Release the twitter-browser lock
|
|
1457
|
+
# Release the twitter-browser lock between Phase 1 scrape and Phase 2b posting.
|
|
1462
1458
|
# Other pipelines (engage-twitter, dm-outreach-twitter, link-edit-twitter,
|
|
1463
1459
|
# stats.sh) can run their browser steps in this window instead of waiting for us
|
|
1464
1460
|
# to finish. We re-acquire just before Phase 2b posts, blocking up to the
|
|
1465
1461
|
# acquire_lock timeout if another pipeline is mid-run.
|
|
1466
|
-
log "Releasing twitter-browser lock
|
|
1462
|
+
log "Releasing twitter-browser lock between Phase 1 scrape and Phase 2b posting..."
|
|
1467
1463
|
release_lock "twitter-browser" 2>>"$LOG_FILE"
|
|
1468
1464
|
# (2026-06-16) NO `rm -f twitter-browser-lock.json` here. The blind rm was
|
|
1469
1465
|
# ownership-unaware and ran AFTER release_lock, so under a pipeline handoff it
|