social-autoposter 1.6.68 → 1.6.69
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 +214 -216
- package/mcp/dist/panel.html +55 -102
- package/mcp/dist/setup.js +55 -9
- package/mcp/dist/version.json +2 -2
- package/mcp/manifest.json +4 -12
- package/package.json +1 -1
- package/scripts/harness_overlay.py +97 -6
- package/setup/SKILL.md +11 -11
- package/skill/run-twitter-cycle.sh +20 -2
package/mcp/dist/index.js
CHANGED
|
@@ -34,7 +34,8 @@ const PANEL_URI = "ui://social-autoposter/panel.html";
|
|
|
34
34
|
const TWITTER_AUTOPILOT_LABEL = "com.m13v.social-twitter-cycle";
|
|
35
35
|
const TWITTER_AUTOPILOT_PLIST = path.join(os.homedir(), "Library", "LaunchAgents", `${TWITTER_AUTOPILOT_LABEL}.plist`);
|
|
36
36
|
// Daily self-updater. Enabled alongside autopilot so a hands-free (headless)
|
|
37
|
-
// install keeps itself current — the interactive `
|
|
37
|
+
// install keeps itself current — the interactive `runtime` tool (action:'update')
|
|
38
|
+
// only helps when
|
|
38
39
|
// a human-facing agent session is open, which an autopilot box never has.
|
|
39
40
|
const UPDATER_LABEL = "com.m13v.social-autoposter-update";
|
|
40
41
|
const UPDATER_PLIST = path.join(os.homedir(), "Library", "LaunchAgents", `${UPDATER_LABEL}.plist`);
|
|
@@ -171,13 +172,15 @@ const server = new McpServer({
|
|
|
171
172
|
"about it. A feature nobody hears about is wasted distribution. Use judgment, suggest it for " +
|
|
172
173
|
"genuinely shareable moments, not routine refactors, chores, or internal cleanup.\n\n" +
|
|
173
174
|
"STAY CURRENT. At the start of a session, and again right after you help the user ship/merge/" +
|
|
174
|
-
"release, call the `
|
|
175
|
-
"user and offer to run `
|
|
176
|
-
"`update_available` and an `update_hint`.\n\n" +
|
|
177
|
-
"TYPICAL FLOW: `
|
|
178
|
-
"user approves / edits / skips every draft in a single form) ->
|
|
179
|
-
"on hands-free background posting AND daily auto-updates) ->
|
|
180
|
-
"`
|
|
175
|
+
"release, call the `runtime` tool (action:'version'). If `update_available` is true, tell the " +
|
|
176
|
+
"user and offer to run `runtime` with action:'update'. The `project_config` tool's status also " +
|
|
177
|
+
"surfaces `update_available` and an `update_hint`.\n\n" +
|
|
178
|
+
"TYPICAL FLOW: `project_config` (configure OR edit the project, and connect X) -> `draft_cycle` " +
|
|
179
|
+
"(scan + review a batch; the user approves / edits / skips every draft in a single form) -> " +
|
|
180
|
+
"`autopilot` (enable to also turn on hands-free background posting AND daily auto-updates) -> " +
|
|
181
|
+
"`get_stats` (see performance). Run `project_config` first; the other tools refuse until a " +
|
|
182
|
+
"project is fully configured. To change anything about a project later, call `project_config` " +
|
|
183
|
+
"again with the project's name and just the changed fields — there is no separate config editor.\n\n" +
|
|
181
184
|
"RENDER THE DASHBOARD AFTER ACTIONS. After any state-changing or results-producing tool call " +
|
|
182
185
|
"(`draft_cycle`, `post_drafts`, `autopilot` enable/disable, `get_stats`), end your turn by " +
|
|
183
186
|
"calling the `dashboard` tool so the user sees the updated state visually. Do NOT call " +
|
|
@@ -238,9 +241,10 @@ function blockedReasonMessage(reason) {
|
|
|
238
241
|
"Wait for the limit to reset, then run draft_cycle again.");
|
|
239
242
|
case "no_search_topics":
|
|
240
243
|
return ("This project has no search topics yet, so there was nothing to scan. Topics live in the " +
|
|
241
|
-
"DB (project_search_topics) and are seeded from your project's `search_topics`
|
|
242
|
-
"Re-run the `
|
|
243
|
-
"phrases your buyers tweet about);
|
|
244
|
+
"DB (project_search_topics) and are seeded from your project's `search_topics` when you " +
|
|
245
|
+
"configure it. Re-run the `project_config` tool for this project with a `search_topics` list " +
|
|
246
|
+
"(comma-separated keywords/phrases your buyers tweet about); it seeds them automatically, then " +
|
|
247
|
+
"run draft_cycle again.");
|
|
244
248
|
case "topics_api_unreachable":
|
|
245
249
|
return ("Couldn't reach the search-topics service to load this project's topics, so the cycle stopped " +
|
|
246
250
|
"before scanning. This is usually a transient backend/network issue. Try draft_cycle again in a " +
|
|
@@ -509,10 +513,10 @@ async function postApproved(batchId, plan) {
|
|
|
509
513
|
// This is NOT a tool — the model never auto-calls it. It surfaces in clients
|
|
510
514
|
// that render prompts as slash-commands / starters (e.g. Claude Desktop's "/"
|
|
511
515
|
// menu). When the user picks it, it injects the message below into the chat,
|
|
512
|
-
// which nudges the agent to start the real onboarding via the `
|
|
516
|
+
// which nudges the agent to start the real onboarding via the `project_config` tool.
|
|
513
517
|
// Deliberately a DUMB POINTER: it names no fields and no steps, so it can never
|
|
514
|
-
// drift from REQUIRED_FIELDS / the
|
|
515
|
-
// `
|
|
518
|
+
// drift from REQUIRED_FIELDS / the project_config tool's flow. All real logic stays
|
|
519
|
+
// in `project_config`; this is just a convenience handle to begin.
|
|
516
520
|
server.registerPrompt("getting-started", {
|
|
517
521
|
title: "Set up social-autoposter",
|
|
518
522
|
description: "Start here. Walks you through configuring a product and connecting your X/Twitter " +
|
|
@@ -561,29 +565,36 @@ const WEBSITE_RESEARCH_INSTRUCTIONS = "PRODUCT RESEARCH (do this before saving t
|
|
|
561
565
|
"register, vibe) while keeping every product CLAIM factual to the site. Don't invent " +
|
|
562
566
|
"features, metrics, or guarantees the site doesn't state.\n" +
|
|
563
567
|
"5. Save the best conservative factual draft without adding a confirmation round-trip. Call " +
|
|
564
|
-
"
|
|
568
|
+
"project_config with name + the product fields (plus voice/search_topics from the profile scan). If the " +
|
|
565
569
|
"site is thin or unreachable, use only supported facts and leave optional detail conservative; " +
|
|
566
570
|
"ask the user only if a required field is genuinely unknowable.";
|
|
567
|
-
// ----
|
|
571
|
+
// ---- project_config: per-project config (the "brain": project, website, voice) -----
|
|
568
572
|
// Run this FIRST. The action tools refuse until at least one project is ready.
|
|
569
573
|
// You can set up MULTIPLE products and fill each project's fields INCREMENTALLY
|
|
570
574
|
// across several calls — readiness is derived from config.json, never a stored
|
|
571
575
|
// flag. Call with status:true (or just no name) to list every project this
|
|
572
576
|
// install manages and what each still needs.
|
|
573
|
-
tool("
|
|
574
|
-
title: "
|
|
575
|
-
description: "
|
|
576
|
-
"
|
|
577
|
-
"
|
|
578
|
-
"project
|
|
577
|
+
tool("project_config", {
|
|
578
|
+
title: "Configure or edit a project",
|
|
579
|
+
description: "The ONE tool for a project's whole lifecycle: create it, EDIT it later, and connect its X " +
|
|
580
|
+
"account. There is no separate raw-config editor — every project change goes through here so " +
|
|
581
|
+
"it validates, merges, and re-seeds the search-topic universe the cycle reads. To CHANGE an " +
|
|
582
|
+
"existing project (its website, voice, icp, differentiator, search_topics, guardrails, CTA " +
|
|
583
|
+
"link), call this with that project's `name` and ONLY the fields you want to change; it merges " +
|
|
584
|
+
"onto what's already saved and never clobbers untouched fields. Run it FIRST before any " +
|
|
585
|
+
"drafting or autopilot. A user's request to set up social-autoposter is a request to finish " +
|
|
586
|
+
"the workflow end to end, not to interview them step by step: resume from current status, " +
|
|
587
|
+
"infer discoverable fields, and keep taking safe actions until runtime, project, X connection, " +
|
|
588
|
+
"topic seeding, and draft-only verification are complete.\n" +
|
|
579
589
|
"Two jobs:\n" +
|
|
580
|
-
"1) Configure a project this install posts for: its website, what it does
|
|
581
|
-
"to target (icp), and brand voice. To fill the PRODUCT fields, discover the
|
|
582
|
-
"config, conversation context, the connected X profile, or public research,
|
|
583
|
-
"with your own browser/fetch tools — read 5+ pages (home, pricing, features,
|
|
584
|
-
"blog, FAQ) to learn it deeply, rather than guessing from the name. Set up MULTIPLE
|
|
585
|
-
"(call once per product, identified by name); fill a project's fields
|
|
586
|
-
"several calls — pass whatever you have, it merges and tells you what's
|
|
590
|
+
"1) Configure (or edit) a project this install posts for: its website, what it does " +
|
|
591
|
+
"(description), who to target (icp), and brand voice. To fill the PRODUCT fields, discover the " +
|
|
592
|
+
"product URL from config, conversation context, the connected X profile, or public research, " +
|
|
593
|
+
"then visit it with your own browser/fetch tools — read 5+ pages (home, pricing, features, " +
|
|
594
|
+
"about, docs/blog, FAQ) to learn it deeply, rather than guessing from the name. Set up MULTIPLE " +
|
|
595
|
+
"products (call once per product, identified by name); fill or edit a project's fields " +
|
|
596
|
+
"INCREMENTALLY across several calls — pass whatever you have, it merges and tells you what's " +
|
|
597
|
+
"still missing.\n" +
|
|
587
598
|
"2) Connect X/Twitter (action:'connect_x'): the autoposter posts through its OWN managed Chrome, " +
|
|
588
599
|
"which needs your logged-in x.com session. This imports x.com/twitter.com cookies from your " +
|
|
589
600
|
"everyday browser (Chrome/Arc/Brave/Edge, auto-detected) into that browser — nothing else is " +
|
|
@@ -655,6 +666,17 @@ tool("setup", {
|
|
|
655
666
|
.string()
|
|
656
667
|
.optional()
|
|
657
668
|
.describe("Anything the posts must avoid saying / claiming"),
|
|
669
|
+
fields: z
|
|
670
|
+
.record(z.string(), z.any())
|
|
671
|
+
.optional()
|
|
672
|
+
.describe("Escape hatch to edit ANY other project field the named props above don't cover — e.g. " +
|
|
673
|
+
"weight, platform, voice_relationship, booking_link, qualification, subreddit_bans, " +
|
|
674
|
+
"short_links_host, short_links_live, content_angle, messaging, landing_pages, posthog. " +
|
|
675
|
+
"Pass {name:'<project>', fields:{<key>:<value>, ...}}; each key SHALLOW-merges onto the " +
|
|
676
|
+
"project, REPLACING that key's whole value (read the current value via status:true first if " +
|
|
677
|
+
"you only want to tweak part of a nested object, then pass the full new value). A value of " +
|
|
678
|
+
"null DELETES the key. 'name' is ignored here (can't rename through this path). This is how " +
|
|
679
|
+
"you edit advanced config without any raw whole-file overwrite."),
|
|
658
680
|
},
|
|
659
681
|
}, async (args) => {
|
|
660
682
|
// ---- List import sources (for the panel dropdown) ---------------------
|
|
@@ -707,7 +729,7 @@ tool("setup", {
|
|
|
707
729
|
"Allow (or Always Allow). If you use more than one browser you may see it a couple of times, " +
|
|
708
730
|
"once per browser.",
|
|
709
731
|
how_to_proceed: "If the user explicitly requested setup or connection, relay the say_to_user line as a brief " +
|
|
710
|
-
"heads-up and immediately call
|
|
732
|
+
"heads-up and immediately call project_config again with action:'connect_x', confirm:true; do not wait " +
|
|
711
733
|
"for another yes/no reply. Optionally pass the recommended x_source. If the user only asked " +
|
|
712
734
|
"what connection would do, stop after this preview.",
|
|
713
735
|
});
|
|
@@ -744,12 +766,12 @@ tool("setup", {
|
|
|
744
766
|
: undefined,
|
|
745
767
|
onboarding: onboardingSnapshot(),
|
|
746
768
|
next_step: r.connected
|
|
747
|
-
? "X is connected. Next, run
|
|
769
|
+
? "X is connected. Next, run project_config action:'profile_scan' to read this account's bio + recent " +
|
|
748
770
|
"posts + replies and draft the project's voice/icp/search_topics in the user's own register " +
|
|
749
771
|
"before saving. Then run draft_cycle once the project is fully set up."
|
|
750
772
|
: r.state === "needs_login"
|
|
751
773
|
? "The user must finish signing in to x.com in the Chrome window that just opened. Tell " +
|
|
752
|
-
"them that single required action, then call
|
|
774
|
+
"them that single required action, then call project_config action:'connect_x', confirm:true again."
|
|
753
775
|
: "X is not connected yet. " + summarizeXAuth(r),
|
|
754
776
|
});
|
|
755
777
|
}
|
|
@@ -758,14 +780,14 @@ tool("setup", {
|
|
|
758
780
|
// successful connect_x) to read the user's bio + recent posts + recent
|
|
759
781
|
// replies. Returns the raw corpus plus grounding_instructions; synthesis of
|
|
760
782
|
// voice/icp/topics happens IN THIS CONVERSATION (no nested model), then the
|
|
761
|
-
// agent confirms with the user and calls
|
|
783
|
+
// agent confirms with the user and calls project_config to persist. Read-only.
|
|
762
784
|
if (args.action === "profile_scan") {
|
|
763
785
|
// Handle is auto-detected from the live logged-in session by the scanner.
|
|
764
786
|
recordOnboardingAttempt("profile_scanned");
|
|
765
787
|
const scan = await xScanProfile();
|
|
766
788
|
if (!scan.ok) {
|
|
767
789
|
const hint = scan.state === "browser_not_running" || scan.state === "no_handle"
|
|
768
|
-
? " Run
|
|
790
|
+
? " Run project_config action:'connect_x' (confirm:true) first so the account is connected, then retry profile_scan."
|
|
769
791
|
: "";
|
|
770
792
|
blockOnboardingMilestone("profile_scanned", `profile_${scan.state || "failed"}`, scan.error || "profile scan failed", { state: scan.state || "failed" });
|
|
771
793
|
return jsonContent({
|
|
@@ -838,7 +860,7 @@ tool("setup", {
|
|
|
838
860
|
update_available: ver.update_available,
|
|
839
861
|
update_hint: ver.update_available
|
|
840
862
|
? `A newer version (${ver.latest}) is available — you're on ${ver.installed}. ` +
|
|
841
|
-
`Tell the user and offer to run the \`
|
|
863
|
+
`Tell the user and offer to run the \`runtime\` tool with action:'update' ` +
|
|
842
864
|
`(or \`npx social-autoposter@latest update\`).`
|
|
843
865
|
: undefined,
|
|
844
866
|
required_fields: REQUIRED_FIELDS,
|
|
@@ -847,16 +869,16 @@ tool("setup", {
|
|
|
847
869
|
ready_for_verification: rtReady && configured && x.connected,
|
|
848
870
|
onboarding: onboardingSnapshot(),
|
|
849
871
|
next_step: !rtReady
|
|
850
|
-
? "Runtime is not ready. Call
|
|
872
|
+
? "Runtime is not ready. Call runtime action:'install', poll runtime action:'status' to completion, then continue setup automatically."
|
|
851
873
|
: projects.length === 0
|
|
852
|
-
? "No projects yet. Discover the product from conversation context and the connected X profile; research its website, infer a conservative complete project, and call
|
|
874
|
+
? "No projects yet. Discover the product from conversation context and the connected X profile; research its website, infer a conservative complete project, and call project_config. Ask only if no product can be identified." +
|
|
853
875
|
(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.")
|
|
854
876
|
: projects.every((p) => p.ready)
|
|
855
877
|
? (x.connected
|
|
856
878
|
? "All configured projects are ready and X is connected. Run draft_cycle now to verify end to end without posting. After it verifies, call the `dashboard` tool so the user sees the finished setup."
|
|
857
879
|
: "All configured projects are ready, but X is NOT connected — posting needs a logged-in " +
|
|
858
|
-
"x.com session. Detect sources and run
|
|
859
|
-
: "Some projects are missing required fields (see each project's missing_required). Derive them from config, context, profile_scan, and website research, then call
|
|
880
|
+
"x.com session. Detect sources and run project_config action:'connect_x', confirm:true; do not ask whether to proceed.")
|
|
881
|
+
: "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." +
|
|
860
882
|
(x.connected ? "" : " X is also not connected yet; detect sources and run connect_x with confirm:true."),
|
|
861
883
|
});
|
|
862
884
|
}
|
|
@@ -938,6 +960,17 @@ tool("setup", {
|
|
|
938
960
|
}
|
|
939
961
|
}
|
|
940
962
|
}
|
|
963
|
+
// Surface any advanced (escape-hatch) field edits in the note so the
|
|
964
|
+
// agent can confirm exactly what changed to the user.
|
|
965
|
+
let advancedNote = "";
|
|
966
|
+
if (result.fields_set.length || result.fields_removed.length) {
|
|
967
|
+
const parts = [];
|
|
968
|
+
if (result.fields_set.length)
|
|
969
|
+
parts.push(`set ${result.fields_set.join(", ")}`);
|
|
970
|
+
if (result.fields_removed.length)
|
|
971
|
+
parts.push(`removed ${result.fields_removed.join(", ")}`);
|
|
972
|
+
advancedNote = ` Advanced fields updated: ${parts.join("; ")}.`;
|
|
973
|
+
}
|
|
941
974
|
return jsonContent({
|
|
942
975
|
ok: true,
|
|
943
976
|
project: result.project,
|
|
@@ -947,16 +980,19 @@ tool("setup", {
|
|
|
947
980
|
topics_seeded: topicsSeeded,
|
|
948
981
|
topic_count: topicCount,
|
|
949
982
|
search_queries: searchQueries,
|
|
983
|
+
fields_set: result.fields_set,
|
|
984
|
+
fields_removed: result.fields_removed,
|
|
950
985
|
config_path: configPath(),
|
|
951
986
|
onboarding: onboardingSnapshot(),
|
|
952
|
-
note: result.ready
|
|
987
|
+
note: (result.ready
|
|
953
988
|
? `Project '${result.project}' is fully configured.${seedNote} Next: if X is not connected, ` +
|
|
954
|
-
`detect sources, warn about keychain prompts, and call
|
|
989
|
+
`detect sources, warn about keychain prompts, and call project_config with ` +
|
|
955
990
|
`action:'connect_x', confirm:true immediately. Once X is connected, run draft_cycle to ` +
|
|
956
991
|
`verify without posting. Do not enable autopilot unless explicitly requested.`
|
|
957
992
|
: `Saved what you provided for '${result.project}'. Still need: ${result.missing_required.join(", ")}. ` +
|
|
958
993
|
`First derive those fields from existing context, profile_scan, and website research, then ` +
|
|
959
|
-
`call
|
|
994
|
+
`call project_config again with name='${result.project}'. Ask only if a required field is genuinely unknowable.`) +
|
|
995
|
+
advancedNote,
|
|
960
996
|
});
|
|
961
997
|
}
|
|
962
998
|
catch (e) {
|
|
@@ -1175,7 +1211,7 @@ tool("autopilot", {
|
|
|
1175
1211
|
},
|
|
1176
1212
|
}, async ({ action }) => {
|
|
1177
1213
|
if (action !== "status" && !hasReadyProject()) {
|
|
1178
|
-
return textContent("No project is fully set up yet, so autopilot has nothing to post. Run the `
|
|
1214
|
+
return textContent("No project is fully set up yet, so autopilot has nothing to post. Run the `project_config` tool " +
|
|
1179
1215
|
"first. Note: autopilot runs the background cycle across all configured projects; it is " +
|
|
1180
1216
|
"not scoped to one project.");
|
|
1181
1217
|
}
|
|
@@ -1282,29 +1318,90 @@ tool("get_stats", {
|
|
|
1282
1318
|
}
|
|
1283
1319
|
});
|
|
1284
1320
|
// ---- version: report installed version + deliver updates on demand ---------
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1321
|
+
// ---- runtime: install + version/update + diagnostics ----------------------
|
|
1322
|
+
// ONE plumbing tool for the whole local-runtime lifecycle, action-based like
|
|
1323
|
+
// project_config and autopilot. The pipeline runs Python locally; rather than
|
|
1324
|
+
// depend on the user's system Python (the #1 source of install failures), the
|
|
1325
|
+
// first run provisions a fully OWNED uv runtime: standalone CPython + owned venv
|
|
1326
|
+
// + deps + Chromium. It also reports/installs new releases and runs the Doctor.
|
|
1327
|
+
// Plain (non-UI) so EVERY host can drive it — the panel's Install card and
|
|
1328
|
+
// Update button are just skins that call action:'install' then poll
|
|
1329
|
+
// action:'status'. See runtime.ts for the provisioning + progress contract.
|
|
1330
|
+
//
|
|
1331
|
+
// Actions:
|
|
1332
|
+
// status (default) — is the owned runtime installed? + in-progress step detail
|
|
1333
|
+
// install — start provisioning in the background; poll status to follow
|
|
1334
|
+
// version — installed vs latest published, whether an update is available
|
|
1335
|
+
// update — pull + install the latest release (npx social-autoposter@latest update)
|
|
1336
|
+
// doctor — run structured environment diagnostics (phase: pre_connect|full)
|
|
1337
|
+
// doctor_status — last persisted Doctor result without re-running checks
|
|
1338
|
+
tool("runtime", {
|
|
1339
|
+
title: "Runtime: install, update & diagnostics",
|
|
1340
|
+
description: "The ONE plumbing tool for the autoposter's local runtime lifecycle. action:'status' (default) " +
|
|
1341
|
+
"reports whether the self-contained Python/Chromium runtime is installed and, mid-install, the " +
|
|
1342
|
+
"per-step progress (uv, Python, venv, dependencies, Chromium) — poll it after action:'install'. " +
|
|
1343
|
+
"action:'install' provisions that runtime (a private Python via uv, NOT your system Python, plus " +
|
|
1344
|
+
"deps and Chromium); it runs in the background and returns immediately, is safe to call " +
|
|
1345
|
+
"repeatedly, and is a no-op once installed. action:'version' shows installed vs latest published " +
|
|
1346
|
+
"and whether an update is available; action:'update' pulls and installs the latest release (runs " +
|
|
1347
|
+
"`npx social-autoposter@latest update`, taking effect after the client reconnects/restarts). " +
|
|
1348
|
+
"action:'doctor' runs structured environment diagnostics (phase:'pre_connect' is safe at " +
|
|
1349
|
+
"onboarding start and treats the missing X session/cookies as expected; phase:'full' verifies the " +
|
|
1350
|
+
"completed environment after X is connected); action:'doctor_status' returns the last persisted " +
|
|
1351
|
+
"Doctor result without re-running. Use this the first time the user sets up, when another tool " +
|
|
1352
|
+
"reports the runtime isn't ready, when the user asks what version they're on or to update, or to " +
|
|
1353
|
+
"diagnose a broken environment.",
|
|
1293
1354
|
inputSchema: {
|
|
1294
|
-
action: z
|
|
1355
|
+
action: z
|
|
1356
|
+
.enum(["status", "install", "version", "update", "doctor", "doctor_status"])
|
|
1357
|
+
.optional(),
|
|
1358
|
+
phase: z
|
|
1359
|
+
.enum(["pre_connect", "full"])
|
|
1360
|
+
.optional()
|
|
1361
|
+
.describe("Only for action:'doctor' — which diagnostic phase to run (default pre_connect)."),
|
|
1295
1362
|
},
|
|
1296
|
-
}, async ({ action }) => {
|
|
1363
|
+
}, async ({ action, phase }) => {
|
|
1364
|
+
// ---- install: start provisioning the owned runtime --------------------
|
|
1365
|
+
if (action === "install") {
|
|
1366
|
+
if (runtimeReady()) {
|
|
1367
|
+
completeOnboardingMilestone("runtime_ready");
|
|
1368
|
+
return jsonContent({ already_installed: true, ...runtimeSnapshot() });
|
|
1369
|
+
}
|
|
1370
|
+
recordOnboardingAttempt("runtime_ready");
|
|
1371
|
+
const progress = startProvisioning();
|
|
1372
|
+
return jsonContent({
|
|
1373
|
+
started: true,
|
|
1374
|
+
runtime_ready: false,
|
|
1375
|
+
note: "Runtime install started. Poll runtime action:'status' every ~1.5s for progress.",
|
|
1376
|
+
progress,
|
|
1377
|
+
});
|
|
1378
|
+
}
|
|
1379
|
+
// ---- version: installed vs latest published ---------------------------
|
|
1380
|
+
if (action === "version") {
|
|
1381
|
+
const v = await versionStatus();
|
|
1382
|
+
return jsonContent({
|
|
1383
|
+
installed: v.installed,
|
|
1384
|
+
latest_published: v.latest,
|
|
1385
|
+
update_available: v.update_available,
|
|
1386
|
+
update_command: "npx social-autoposter@latest update",
|
|
1387
|
+
note: v.latest == null
|
|
1388
|
+
? "Could not reach npm to check for a newer version (offline or registry error)."
|
|
1389
|
+
: v.update_available
|
|
1390
|
+
? `A newer version (${v.latest}) is available. Run this tool with action:'update' ` +
|
|
1391
|
+
"to install it, or run `npx social-autoposter@latest update` in a terminal."
|
|
1392
|
+
: "You are on the latest published version.",
|
|
1393
|
+
});
|
|
1394
|
+
}
|
|
1395
|
+
// ---- update: pull + install the latest release ------------------------
|
|
1297
1396
|
if (action === "update") {
|
|
1298
|
-
//
|
|
1299
|
-
//
|
|
1300
|
-
//
|
|
1301
|
-
// non-interactively so it can't stall on a confirm prompt.
|
|
1397
|
+
// Overwrites mcp/dist/ (including this running file — safe; the loaded
|
|
1398
|
+
// process keeps old code) and re-runs install.mjs to re-register the
|
|
1399
|
+
// client config. npx is non-interactive so it can't stall on a confirm.
|
|
1302
1400
|
const before = VERSION;
|
|
1303
1401
|
const res = await run("npx", ["-y", "social-autoposter@latest", "update"], {
|
|
1304
1402
|
timeoutMs: 600_000,
|
|
1305
1403
|
});
|
|
1306
|
-
|
|
1307
|
-
const latest = await latestPublishedVersion();
|
|
1404
|
+
const latest = await latestPublishedVersion(); // bust the cache
|
|
1308
1405
|
return jsonContent({
|
|
1309
1406
|
action: "update",
|
|
1310
1407
|
ran: "npx social-autoposter@latest update",
|
|
@@ -1317,27 +1414,29 @@ tool("version", {
|
|
|
1317
1414
|
output_tail: (res.stdout + "\n" + res.stderr).trim().split("\n").slice(-20).join("\n"),
|
|
1318
1415
|
});
|
|
1319
1416
|
}
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
}
|
|
1417
|
+
// ---- doctor: run structured diagnostics -------------------------------
|
|
1418
|
+
if (action === "doctor") {
|
|
1419
|
+
const selected = phase || "pre_connect";
|
|
1420
|
+
const report = await runDoctorPhase(selected);
|
|
1421
|
+
return jsonContent({ doctor: report, onboarding: onboardingSnapshot() });
|
|
1422
|
+
}
|
|
1423
|
+
// ---- doctor_status: last persisted Doctor result ----------------------
|
|
1424
|
+
if (action === "doctor_status") {
|
|
1425
|
+
return jsonContent({
|
|
1426
|
+
doctor: onboardingLedger()?.doctor?.latest ?? null,
|
|
1427
|
+
onboarding: onboardingSnapshot(),
|
|
1428
|
+
});
|
|
1429
|
+
}
|
|
1430
|
+
// ---- status (default): runtime install snapshot -----------------------
|
|
1431
|
+
const snapshot = runtimeSnapshot();
|
|
1432
|
+
if (snapshot.runtime_ready) {
|
|
1433
|
+
completeOnboardingMilestone("runtime_ready");
|
|
1434
|
+
}
|
|
1435
|
+
else if (snapshot.progress?.done && !snapshot.progress.ok) {
|
|
1436
|
+
blockOnboardingMilestone("runtime_ready", "runtime_install_failed", snapshot.progress.error || "Runtime installation failed", { outcome: "failed" });
|
|
1437
|
+
}
|
|
1438
|
+
return jsonContent({ ...snapshot, onboarding: onboardingSnapshot() });
|
|
1333
1439
|
});
|
|
1334
|
-
// ---- runtime installer ----------------------------------------------------
|
|
1335
|
-
// The pipeline runs Python locally. Rather than depend on the user's system
|
|
1336
|
-
// Python (the #1 source of install failures), the first run provisions a fully
|
|
1337
|
-
// OWNED uv runtime: standalone CPython + owned venv + deps + Chromium. These two
|
|
1338
|
-
// tools drive it. They are plain (non-UI) tools so EVERY host can install — the
|
|
1339
|
-
// panel's Install card is just a skin that calls install_runtime then polls
|
|
1340
|
-
// install_status. See runtime.ts for the provisioning + progress contract.
|
|
1341
1440
|
function runtimeSnapshot() {
|
|
1342
1441
|
const rt = readRuntime();
|
|
1343
1442
|
const progress = readProgress();
|
|
@@ -1350,138 +1449,10 @@ function runtimeSnapshot() {
|
|
|
1350
1449
|
onboarding: onboardingSnapshot(),
|
|
1351
1450
|
};
|
|
1352
1451
|
}
|
|
1353
|
-
tool("install_runtime", {
|
|
1354
|
-
title: "Install the Python runtime",
|
|
1355
|
-
description: "One-time setup that provisions the self-contained runtime the autoposter needs: a private " +
|
|
1356
|
-
"Python (via uv, not your system Python), its dependencies, and the Chromium browser. Runs in " +
|
|
1357
|
-
"the background and returns immediately; poll `install_status` for progress. Safe to call " +
|
|
1358
|
-
"repeatedly; it resumes/repairs and is a no-op once everything is installed. Use this the " +
|
|
1359
|
-
"first time the user sets up, or if other tools report the runtime isn't ready.",
|
|
1360
|
-
inputSchema: {},
|
|
1361
|
-
}, async () => {
|
|
1362
|
-
if (runtimeReady()) {
|
|
1363
|
-
completeOnboardingMilestone("runtime_ready");
|
|
1364
|
-
return jsonContent({ already_installed: true, ...runtimeSnapshot() });
|
|
1365
|
-
}
|
|
1366
|
-
recordOnboardingAttempt("runtime_ready");
|
|
1367
|
-
const progress = startProvisioning();
|
|
1368
|
-
return jsonContent({
|
|
1369
|
-
started: true,
|
|
1370
|
-
runtime_ready: false,
|
|
1371
|
-
note: "Runtime install started. Poll install_status every ~1.5s for progress.",
|
|
1372
|
-
progress,
|
|
1373
|
-
});
|
|
1374
|
-
});
|
|
1375
|
-
tool("install_status", {
|
|
1376
|
-
title: "Runtime install status",
|
|
1377
|
-
description: "Report whether the self-contained Python/Chromium runtime is installed and, if an install is " +
|
|
1378
|
-
"in progress, the per-step progress (uv, Python, venv, dependencies, Chromium). Poll this after " +
|
|
1379
|
-
"install_runtime to follow the install to completion.",
|
|
1380
|
-
inputSchema: {},
|
|
1381
|
-
}, async () => {
|
|
1382
|
-
const snapshot = runtimeSnapshot();
|
|
1383
|
-
if (snapshot.runtime_ready) {
|
|
1384
|
-
completeOnboardingMilestone("runtime_ready");
|
|
1385
|
-
}
|
|
1386
|
-
else if (snapshot.progress?.done && !snapshot.progress.ok) {
|
|
1387
|
-
blockOnboardingMilestone("runtime_ready", "runtime_install_failed", snapshot.progress.error || "Runtime installation failed", { outcome: "failed" });
|
|
1388
|
-
}
|
|
1389
|
-
return jsonContent({ ...snapshot, onboarding: onboardingSnapshot() });
|
|
1390
|
-
});
|
|
1391
|
-
// ---- doctor: structured environment diagnostics --------------------------
|
|
1392
|
-
// Uses the same shared engine as `npx social-autoposter doctor`. Pre-connect
|
|
1393
|
-
// avoids invasive keychain access and classifies not-yet-created X artifacts as
|
|
1394
|
-
// expected; full verifies the completed browser/session/cookie environment.
|
|
1395
|
-
tool("doctor", {
|
|
1396
|
-
title: "Diagnose the onboarding environment",
|
|
1397
|
-
description: "Run the structured social-autoposter Doctor and persist the result in the onboarding " +
|
|
1398
|
-
"ledger. phase:'pre_connect' is safe at onboarding start and treats the missing X session/" +
|
|
1399
|
-
"cookie artifacts as expected. phase:'full' verifies the completed environment after X is " +
|
|
1400
|
-
"connected. action:'status' returns the most recent persisted result without re-running checks.",
|
|
1401
|
-
inputSchema: {
|
|
1402
|
-
action: z.enum(["run", "status"]).optional(),
|
|
1403
|
-
phase: z.enum(["pre_connect", "full"]).optional(),
|
|
1404
|
-
},
|
|
1405
|
-
}, async ({ action, phase, }) => {
|
|
1406
|
-
if (action === "status") {
|
|
1407
|
-
return jsonContent({
|
|
1408
|
-
doctor: onboardingLedger()?.doctor?.latest ?? null,
|
|
1409
|
-
onboarding: onboardingSnapshot(),
|
|
1410
|
-
});
|
|
1411
|
-
}
|
|
1412
|
-
const selected = phase || "pre_connect";
|
|
1413
|
-
const report = await runDoctorPhase(selected);
|
|
1414
|
-
return jsonContent({
|
|
1415
|
-
doctor: report,
|
|
1416
|
-
onboarding: onboardingSnapshot(),
|
|
1417
|
-
});
|
|
1418
|
-
});
|
|
1419
|
-
// ---- config: read / edit the raw config.json ------------------------------
|
|
1420
|
-
// The panel renders the full config and lets the user edit it. Writing is
|
|
1421
|
-
// guarded: the new content must parse as JSON, and we always drop a timestamped
|
|
1422
|
-
// backup next to config.json before overwriting, so a bad paste is recoverable.
|
|
1423
|
-
tool("config", {
|
|
1424
|
-
title: "View or edit config.json",
|
|
1425
|
-
description: "Read or update the autoposter's config.json (the source of truth for every project, the X/" +
|
|
1426
|
-
"Reddit/LinkedIn account handles, topics, and exclusions). action:'get' (default) returns the " +
|
|
1427
|
-
"full raw JSON; action:'save' validates the supplied `content` as JSON, writes a timestamped " +
|
|
1428
|
-
"backup, then overwrites config.json. Use when the user asks to see, edit, or fix their config.",
|
|
1429
|
-
inputSchema: {
|
|
1430
|
-
action: z.enum(["get", "save"]).optional(),
|
|
1431
|
-
content: z.string().optional(),
|
|
1432
|
-
},
|
|
1433
|
-
}, async (args) => {
|
|
1434
|
-
const action = args.action || "get";
|
|
1435
|
-
const cfgPath = configPath();
|
|
1436
|
-
if (action === "get") {
|
|
1437
|
-
try {
|
|
1438
|
-
const content = fs.readFileSync(cfgPath, "utf-8");
|
|
1439
|
-
return jsonContent({ ok: true, path: cfgPath, bytes: content.length, content });
|
|
1440
|
-
}
|
|
1441
|
-
catch (e) {
|
|
1442
|
-
return jsonContent({ ok: false, path: cfgPath, error: String(e?.message || e) });
|
|
1443
|
-
}
|
|
1444
|
-
}
|
|
1445
|
-
// save
|
|
1446
|
-
const content = args.content;
|
|
1447
|
-
if (typeof content !== "string" || content.trim() === "") {
|
|
1448
|
-
return jsonContent({ ok: false, error: "Nothing to save: `content` was empty." });
|
|
1449
|
-
}
|
|
1450
|
-
let parsed;
|
|
1451
|
-
try {
|
|
1452
|
-
parsed = JSON.parse(content);
|
|
1453
|
-
}
|
|
1454
|
-
catch (e) {
|
|
1455
|
-
// Don't write a config that won't parse — every pipeline reads this file.
|
|
1456
|
-
return jsonContent({ ok: false, error: "Invalid JSON, not saved: " + String(e?.message || e) });
|
|
1457
|
-
}
|
|
1458
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1459
|
-
return jsonContent({ ok: false, error: "Top level of config.json must be a JSON object." });
|
|
1460
|
-
}
|
|
1461
|
-
try {
|
|
1462
|
-
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
1463
|
-
const backup = `${cfgPath}.bak-panel-${stamp}`;
|
|
1464
|
-
try {
|
|
1465
|
-
fs.copyFileSync(cfgPath, backup);
|
|
1466
|
-
}
|
|
1467
|
-
catch {
|
|
1468
|
-
/* first-write / missing original is non-fatal */
|
|
1469
|
-
}
|
|
1470
|
-
// Re-serialize the parsed object so what lands on disk is canonical,
|
|
1471
|
-
// 2-space-indented JSON with a trailing newline (matches the Python
|
|
1472
|
-
// writers), regardless of how the user formatted their paste.
|
|
1473
|
-
const out = JSON.stringify(parsed, null, 2) + "\n";
|
|
1474
|
-
fs.writeFileSync(cfgPath, out, "utf-8");
|
|
1475
|
-
return jsonContent({ ok: true, path: cfgPath, bytes: out.length, backup });
|
|
1476
|
-
}
|
|
1477
|
-
catch (e) {
|
|
1478
|
-
return jsonContent({ ok: false, error: "Write failed: " + String(e?.message || e) });
|
|
1479
|
-
}
|
|
1480
|
-
});
|
|
1481
1452
|
// ---- panel: MCP Apps control surface --------------------------------------
|
|
1482
1453
|
// A self-contained HTML view rendered by hosts that support MCP Apps (Claude
|
|
1483
1454
|
// desktop/web, etc.). It duplicates NO pipeline logic: each button calls one of
|
|
1484
|
-
// the tools above (draft_cycle / autopilot /
|
|
1455
|
+
// the tools above (draft_cycle / autopilot / project_config / get_stats) through the host
|
|
1485
1456
|
// and re-reads status. The tool itself returns the first-paint snapshot so the
|
|
1486
1457
|
// view has data the instant it loads.
|
|
1487
1458
|
// Is either launchd job (cycle / daily updater) currently loaded?
|
|
@@ -1641,19 +1612,39 @@ function startLocalPanel() {
|
|
|
1641
1612
|
}
|
|
1642
1613
|
});
|
|
1643
1614
|
srv.on("error", reject);
|
|
1644
|
-
|
|
1615
|
+
// Optional fixed port (SAPS_PANEL_PORT) for deterministic addressing; default
|
|
1616
|
+
// is an OS-assigned ephemeral port.
|
|
1617
|
+
const wantPort = Number(process.env.SAPS_PANEL_PORT) || 0;
|
|
1618
|
+
srv.listen(wantPort, "127.0.0.1", () => {
|
|
1645
1619
|
const addr = srv.address();
|
|
1646
1620
|
const port = typeof addr === "object" && addr ? addr.port : 0;
|
|
1647
1621
|
localPanel = { url: `http://127.0.0.1:${port}/`, server: srv };
|
|
1622
|
+
writePanelUrl(localPanel.url);
|
|
1648
1623
|
resolve(localPanel.url);
|
|
1649
1624
|
});
|
|
1650
1625
|
});
|
|
1651
1626
|
}
|
|
1652
|
-
//
|
|
1653
|
-
//
|
|
1654
|
-
//
|
|
1627
|
+
// Publish the loopback URL to a stable file so an out-of-process reader (the
|
|
1628
|
+
// Claude Code side-panel reverse proxy) can find the ephemeral port without
|
|
1629
|
+
// scraping `lsof`. Best-effort: a write failure never blocks the panel.
|
|
1630
|
+
function writePanelUrl(url) {
|
|
1631
|
+
try {
|
|
1632
|
+
const dir = path.join(process.env.HOME || os.homedir(), ".social-autoposter-mcp");
|
|
1633
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1634
|
+
fs.writeFileSync(path.join(dir, "panel-url"), url, "utf-8");
|
|
1635
|
+
}
|
|
1636
|
+
catch (e) {
|
|
1637
|
+
console.error("[social-autoposter-mcp] writePanelUrl failed:", e?.message || e);
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
// Open a URL in the user's default browser, cross-platform. Opening is OPT-IN:
|
|
1641
|
+
// by default we do NOT pop a browser tab. The dashboard already surfaces in-host
|
|
1642
|
+
// (MCP Apps inline) or via the Claude Code side panel / returned loopback URL, so
|
|
1643
|
+
// auto-opening the OS browser on every dashboard call is unwanted noise. Set
|
|
1644
|
+
// SAPS_PANEL_OPEN_BROWSER=1 to restore the old auto-open behavior. (The URL is
|
|
1645
|
+
// always returned to the caller regardless, so nothing is lost when we don't open.)
|
|
1655
1646
|
async function openInBrowser(url) {
|
|
1656
|
-
if (process.env.
|
|
1647
|
+
if (!process.env.SAPS_PANEL_OPEN_BROWSER)
|
|
1657
1648
|
return;
|
|
1658
1649
|
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
|
|
1659
1650
|
const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
@@ -1699,8 +1690,9 @@ appTool("dashboard", {
|
|
|
1699
1690
|
return { content: [], structuredContent: { snapshot: JSON.stringify(snap) } };
|
|
1700
1691
|
}
|
|
1701
1692
|
// Host CAN'T render inline (Claude Code / Cowork today): serve the identical
|
|
1702
|
-
// panel.html from a loopback HTTP server
|
|
1703
|
-
//
|
|
1693
|
+
// panel.html from a loopback HTTP server. We do NOT auto-open a browser tab
|
|
1694
|
+
// (see openInBrowser — opt-in only); the dashboard is shown in the Claude Code
|
|
1695
|
+
// side panel, and the loopback URL is returned for anyone who wants to open it.
|
|
1704
1696
|
try {
|
|
1705
1697
|
const url = await startLocalPanel();
|
|
1706
1698
|
await openInBrowser(url);
|
|
@@ -1708,7 +1700,7 @@ appTool("dashboard", {
|
|
|
1708
1700
|
content: [{
|
|
1709
1701
|
type: "text",
|
|
1710
1702
|
text: human +
|
|
1711
|
-
`\n\nThis host can't render the dashboard inline
|
|
1703
|
+
`\n\nThis host can't render the dashboard inline. It's available in the side panel; loopback URL: ${url}`,
|
|
1712
1704
|
}],
|
|
1713
1705
|
structuredContent: { snapshot: JSON.stringify(snap), fallback_url: url },
|
|
1714
1706
|
};
|
|
@@ -1804,6 +1796,12 @@ async function main() {
|
|
|
1804
1796
|
const transport = new StdioServerTransport();
|
|
1805
1797
|
await server.connect(transport);
|
|
1806
1798
|
console.error(`[social-autoposter-mcp] connected. v=${VERSION} repo=${repoDir()}`);
|
|
1799
|
+
// Eagerly start the loopback panel server so the Claude Code side panel (and any
|
|
1800
|
+
// reverse proxy in front of it) always has a backend to hit, without waiting for
|
|
1801
|
+
// a first `dashboard` call. Best-effort: a bind failure must never block boot.
|
|
1802
|
+
void startLocalPanel()
|
|
1803
|
+
.then((url) => console.error(`[social-autoposter-mcp] panel loopback ready at ${url}`))
|
|
1804
|
+
.catch((e) => console.error("[social-autoposter-mcp] panel loopback start failed:", e?.message || e));
|
|
1807
1805
|
// Phone home so this .mcpb install is visible in the install-lane digest
|
|
1808
1806
|
// (parity with the npx launchd heartbeat). Once on startup, then every 15m
|
|
1809
1807
|
// while the desktop app keeps the server alive. unref() so it never holds the
|