@m13v/s4l 1.6.203-rc.9 → 1.6.203

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.
Files changed (51) hide show
  1. package/mcp/dist/index.js +99 -68
  2. package/mcp/dist/panel.html +1 -1
  3. package/mcp/dist/product-link.html +9 -9
  4. package/mcp/dist/repo.js +1 -1
  5. package/mcp/dist/runtime.js +7 -13
  6. package/mcp/dist/setup.js +2 -2
  7. package/mcp/dist/telemetry.js +6 -8
  8. package/mcp/dist/version.js +1 -1
  9. package/mcp/dist/version.json +2 -2
  10. package/mcp/install.mjs +2 -2
  11. package/mcp/manifest.json +1 -1
  12. package/mcp/menubar/dashboard_server.py +1 -4
  13. package/mcp/menubar/s4l_card.py +171 -42
  14. package/mcp/menubar/s4l_menubar.py +80 -28
  15. package/mcp/menubar/s4l_state.py +20 -2
  16. package/mcp/package.json +2 -2
  17. package/mcp/shared/doctor.cjs +0 -2
  18. package/mcp/shared/onboarding-ledger.cjs +0 -1
  19. package/mcp-servers/browser-harness/server.py +2 -2
  20. package/package.json +1 -1
  21. package/requirements.txt +3 -3
  22. package/scripts/active_experiments.py +112 -0
  23. package/scripts/claude_job.py +15 -2
  24. package/scripts/copy_browser_cookies.py +1 -1
  25. package/scripts/dev_dashboard_server.py +1 -1
  26. package/scripts/feedback_digest.py +18 -1
  27. package/scripts/harness_overlay.py +17 -17
  28. package/scripts/log_draft.py +1 -1
  29. package/scripts/log_post.py +3 -3
  30. package/scripts/memory_snapshot.py +3 -3
  31. package/scripts/merge_review_queue.py +6 -0
  32. package/scripts/reap_stale_claude_sessions.py +3 -3
  33. package/scripts/relay_provider_log.py +186 -0
  34. package/scripts/salvage_orphaned_prep_results.py +178 -0
  35. package/scripts/setup_twitter_auth.py +1 -1
  36. package/scripts/twitter_post_plan.py +88 -13
  37. package/scripts/watchdog_hung_runs.py +12 -12
  38. package/skill/engage-linkedin.sh +4 -4
  39. package/skill/engage-twitter.sh +4 -4
  40. package/skill/lib/linkedin-backend.sh +1 -1
  41. package/skill/run-draft-and-publish.sh +59 -10
  42. package/skill/run-instagram-render.sh +3 -3
  43. package/skill/run-linkedin.sh +5 -5
  44. package/skill/run-reddit-threads.sh +5 -5
  45. package/skill/run-twitter-cycle.sh +91 -50
  46. package/skill/social-autoposter-update.sh +3 -1
  47. package/skill/styles.sh +10 -10
  48. package/skill/topics.sh +8 -8
  49. package/skill/run-cycle-update-guard.sh +0 -44
  50. package/skill/run-twitter-cycle-launchd.sh +0 -63
  51. package/skill/run-twitter-cycle-singleton.sh +0 -62
package/mcp/dist/index.js CHANGED
@@ -156,22 +156,6 @@ function pipelinePath() {
156
156
  function launchdPath() {
157
157
  return [...ownedBinDirs(), LAUNCHD_PATH].join(":");
158
158
  }
159
- // Brand rename 2026-07-03 (SAPS_ -> S4L_): duplicate every S4L_* key under its
160
- // legacy SAPS_* name when emitting env into child processes / plists, so an
161
- // old pipeline-script version still on disk during a partial update keeps
162
- // resolving its env. Never overwrites an explicitly-set legacy key. Remove
163
- // once no pre-rename scripts remain in the field.
164
- function withSapsEnvCompat(env) {
165
- const out = { ...env };
166
- for (const [k, v] of Object.entries(env)) {
167
- if (k.startsWith("S4L_")) {
168
- const legacy = "SAPS_" + k.slice(4);
169
- if (!(legacy in out))
170
- out[legacy] = v;
171
- }
172
- }
173
- return out;
174
- }
175
159
  function plistXml(opts) {
176
160
  const args = opts.programArgs.map((a) => `\t\t<string>${a}</string>`).join("\n");
177
161
  const schedule = opts.keepAlive
@@ -187,10 +171,8 @@ function plistXml(opts) {
187
171
  : "";
188
172
  // Caller-supplied env (e.g. the queue kicker's DRAFT_ONLY / S4L_CLAUDE_PROVIDER).
189
173
  // Rendered after the baked-in vars so a caller can also override S4L_STATE_DIR.
190
- // Dual-named (S4L_* + legacy SAPS_*) so a freshly-written plist still works
191
- // with any pre-rename script version on disk during a partial update.
192
174
  const extraEnv = opts.extraEnv
193
- ? Object.entries(withSapsEnvCompat(opts.extraEnv))
175
+ ? Object.entries(opts.extraEnv)
194
176
  .map(([k, v]) => `\n\t\t<key>${k}</key>\n\t\t<string>${v}</string>`)
195
177
  .join("")
196
178
  : "";
@@ -217,11 +199,7 @@ ${schedule}
217
199
  \t\t<string>${os.homedir()}</string>
218
200
  \t\t<key>S4L_REPO_DIR</key>
219
201
  \t\t<string>${repoDir()}</string>
220
- \t\t<key>SAPS_REPO_DIR</key>
221
- \t\t<string>${repoDir()}</string>
222
202
  \t\t<key>S4L_PYTHON</key>
223
- \t\t<string>${resolvePython()}</string>
224
- \t\t<key>SAPS_PYTHON</key>
225
203
  \t\t<string>${resolvePython()}</string>${chromeEnv}${extraEnv}
226
204
  \t</dict>
227
205
  \t<key>RunAtLoad</key>
@@ -572,7 +550,7 @@ async function ensureOverlayWatch() {
572
550
  try {
573
551
  await run("bash", ["skill/run-overlay-watch.sh"], {
574
552
  timeoutMs: 20_000,
575
- env: withSapsEnvCompat({
553
+ env: ({
576
554
  S4L_PYTHON: resolvePython(),
577
555
  S4L_LOG_DIR: path.join(repoDir(), "skill", "logs"),
578
556
  TWITTER_CDP_URL: process.env.TWITTER_CDP_URL || "http://127.0.0.1:9555",
@@ -655,7 +633,7 @@ async function produceDrafts(project, onProgress) {
655
633
  // (switched in onLine below). Cleared before every return.
656
634
  writeActivity("scanning", "scanning X");
657
635
  const res = await run("bash", ["skill/run-twitter-cycle.sh"], {
658
- env: withSapsEnvCompat(env),
636
+ env: env,
659
637
  timeoutMs: 900_000, // scan+draft can take several minutes
660
638
  // Fan every cycle line out to THREE sinks so progress is never a black box:
661
639
  // 1. draft_cycle-mcp.log — the stable, documented, host-independent file.
@@ -810,6 +788,16 @@ function parsePostCandidateResults(stdout) {
810
788
  upsert(m[1], "skipped", "empty_reply_text");
811
789
  continue;
812
790
  }
791
+ m = /\[post\] candidate (\d+) log_post\.py did not return post_id/.exec(line);
792
+ if (m) {
793
+ // The reply IS live on X; only the posts-row INSERT failed (e.g. the
794
+ // 2026-07-06 draft_prompt_variant validation 400). Mark the card POSTED
795
+ // so the drain never re-attempts a live reply — re-posting slams into
796
+ // X's duplicate guard at best and double-posts at worst. The missing
797
+ // posts row is recoverable from the post-*.log (reply_url + final_text).
798
+ upsert(m[1], "posted", "log_post_no_id");
799
+ continue;
800
+ }
813
801
  m = /\[post\] candidate (\d+) crashed:/.exec(line);
814
802
  if (m)
815
803
  upsert(m[1], "failed", "exception");
@@ -845,7 +833,7 @@ async function ensurePostingHandle() {
845
833
  try {
846
834
  await runPython("scripts/setup_twitter_auth.py", ["resolve-handle"], {
847
835
  timeoutMs: 60_000,
848
- env: withSapsEnvCompat({ S4L_REPO_DIR: repoDir(), PATH: pipelinePath() }),
836
+ env: ({ S4L_REPO_DIR: repoDir(), PATH: pipelinePath() }),
849
837
  });
850
838
  }
851
839
  catch {
@@ -864,7 +852,7 @@ async function ensureTwitterBrowserForPost() {
864
852
  env.BH_CHROME_BIN = chrome;
865
853
  return run("bash", ["-lc", ". skill/lib/twitter-backend.sh && ensure_twitter_browser_for_backend"], {
866
854
  timeoutMs: 90_000,
867
- env: withSapsEnvCompat(env),
855
+ env: env,
868
856
  onLine: (line) => {
869
857
  const t = line.replace(/\s+$/, "");
870
858
  if (t.trim())
@@ -940,7 +928,7 @@ async function postApproved(batchId, plan) {
940
928
  }
941
929
  return await runPython("scripts/twitter_post_plan.py", ["--plan", planPath(approvedBatch)], {
942
930
  timeoutMs: 900_000,
943
- env: withSapsEnvCompat({
931
+ env: ({
944
932
  S4L_SKIP_CAMPAIGN_SUFFIX: "1",
945
933
  // Manual approval is an EXCEPTION to the tail-link A/B. The cron pipeline
946
934
  // runs TWITTER_TAIL_LINK_RATE=0.9 (from .env) so ~10% of autopilot posts
@@ -1202,7 +1190,7 @@ async function seedSearchQueriesForProject(project, rawQueries) {
1202
1190
  };
1203
1191
  }
1204
1192
  try {
1205
- const qfile = path.join(os.tmpdir(), `saps-queries-${project}-${Date.now()}.json`);
1193
+ const qfile = path.join(os.tmpdir(), `s4l-queries-${project}-${Date.now()}.json`);
1206
1194
  fs.writeFileSync(qfile, JSON.stringify({ queries: agentQueries.map((q) => ({ query: q, topic: "" })) }));
1207
1195
  seedInFlight.set(project, Date.now());
1208
1196
  // Fire-and-forget: runPython keeps the output on the repo.ts tee (so the
@@ -2502,7 +2490,7 @@ async function autopilotLoaded() {
2502
2490
  // Claude turn, writes the result back, and stops.
2503
2491
  // ===========================================================================
2504
2492
  const QUEUE_WORKER_PROMPT_VERSION = 7; // v7: universal type-blind worker. ONE task claims `--type any`; per-type execution notes (e.g. the v6 incremental-draft pacing for twitter-prep) moved into claude_job.py TYPE_TO_WORKER_NOTES and ride the prompt sidecar, so the worker prompt never mentions job types. Legacy per-type tasks get this same body on refresh and become interchangeable universal workers.
2505
- const QUEUE_WORKER_PROMPT_MARKER = "saps_queue_worker_prompt_version";
2493
+ const QUEUE_WORKER_PROMPT_MARKER = "s4l_queue_worker_prompt_version";
2506
2494
  // One spec per worker task. queueType MUST match scripts/claude_job.py TAG_TO_TYPE.
2507
2495
  const QUEUE_WORKERS = [
2508
2496
  { taskId: WORKER_TASK_ID, queueType: "any", human: "universal queue" },
@@ -2519,7 +2507,7 @@ function scheduledTaskSkillPath(taskId) {
2519
2507
  // The queue dir the worker reads/writes. MUST equal what the launchd kicker sets
2520
2508
  // (kickerEnv below) and what claude_job.py uses, so both ends meet on one path.
2521
2509
  function queueDir() {
2522
- return path.join(sapsStateDir(), "claude-queue");
2510
+ return path.join(s4lStateDir(), "claude-queue");
2523
2511
  }
2524
2512
  // A draft job left unclaimed in pending/ this long (ms) means no scheduled-task
2525
2513
  // routine is draining the queue — the worker would claim within a minute if it
@@ -2614,7 +2602,7 @@ function queueWorkerCwd() {
2614
2602
  function queueWorkerBody(spec) {
2615
2603
  const py = resolvePython();
2616
2604
  const job = path.join(repoDir(), "scripts", "claude_job.py");
2617
- const sd = sapsStateDir();
2605
+ const sd = s4lStateDir();
2618
2606
  const outDir = queueDir();
2619
2607
  return [
2620
2608
  `You are the S4L queue worker. Run ONE iteration, then STOP.`,
@@ -2774,13 +2762,6 @@ function queueWorkerAllowedTools() {
2774
2762
  "mcp__S4L__project_config",
2775
2763
  "mcp__S4L__get_stats",
2776
2764
  "mcp__S4L__dashboard",
2777
- // Legacy "SAPS" protocol-name namespace (pre-2026-07 brand rename): old
2778
- // registrations still resolve tool ids under it, keep the allow-rules.
2779
- "mcp__SAPS__queue_setup",
2780
- "mcp__SAPS__post_drafts",
2781
- "mcp__SAPS__project_config",
2782
- "mcp__SAPS__get_stats",
2783
- "mcp__SAPS__dashboard",
2784
2765
  ];
2785
2766
  }
2786
2767
  // Merge a list of allow-rules into ~/.claude/settings.json. Returns count added.
@@ -2911,7 +2892,7 @@ function ensureWorkerFolderTrusted() {
2911
2892
  // S4L_COWORK_MCP=0.
2912
2893
  function ensureCoworkMcpRegistered() {
2913
2894
  try {
2914
- if ((process.env.S4L_COWORK_MCP ?? process.env.SAPS_COWORK_MCP) === "0")
2895
+ if ((process.env.S4L_COWORK_MCP) === "0")
2915
2896
  return;
2916
2897
  const home = process.env.HOME || os.homedir();
2917
2898
  const cfgPath = path.join(home, ".claude.json");
@@ -2971,7 +2952,7 @@ function kickerEnv() {
2971
2952
  return {
2972
2953
  DRAFT_ONLY: "1",
2973
2954
  S4L_CLAUDE_PROVIDER: "queue",
2974
- S4L_STATE_DIR: sapsStateDir(),
2955
+ S4L_STATE_DIR: s4lStateDir(),
2975
2956
  TWITTER_PAGE_GEN_RATE: "0",
2976
2957
  // Thread-media context for the drafter (2026-07-06): parity with the
2977
2958
  // retired CLI twitter-cycle plist. Without it the prep step drafts blind to
@@ -2982,12 +2963,8 @@ function kickerEnv() {
2982
2963
  // organic replies); promotion cycles keep the script's own default so
2983
2964
  // draft-only-OFF posts carry the project link per the A/B gate, and card posts
2984
2965
  // still force =1.0 inside post_drafts.
2985
- // Rolling virality bar percentile (2026-07-03): applies to BOTH lanes in
2986
- // the cycle script below-bar picks never become review cards, and in
2987
- // with draft-only OFF never post. p0.95 (not the script's 0.97 default) keeps
2988
- // volume useful while cutting bottom-of-pool drafts; cold start
2989
- // (sample_count < 200) still shows brand-new installs every draft.
2990
- S4L_TWITTER_VIRALITY_PCTILE: "0.95",
2966
+ // Virality bar percentile is NOT set here: it is hardcoded to 0.90 in
2967
+ // skill/run-twitter-cycle.sh (single source of truth, no env dependency).
2991
2968
  };
2992
2969
  }
2993
2970
  async function ensureQueueKickerInstalled() {
@@ -3073,7 +3050,7 @@ async function ensureQueueKickerInstalled() {
3073
3050
  // every later cycle runs the standard 24h + top-1 logic. Best-effort: a
3074
3051
  // failed write just means a standard first cycle.
3075
3052
  try {
3076
- const stateDir = sapsStateDir();
3053
+ const stateDir = s4lStateDir();
3077
3054
  fs.mkdirSync(stateDir, { recursive: true });
3078
3055
  fs.writeFileSync(path.join(stateDir, "first-run-boost.json"), JSON.stringify({ created_at: new Date().toISOString() }) + "\n", "utf-8");
3079
3056
  }
@@ -3205,7 +3182,7 @@ async function ensureStallWatchInstalled() {
3205
3182
  try {
3206
3183
  if (process.platform !== "darwin")
3207
3184
  return { ok: false, detail: "not macOS" };
3208
- if ((process.env.S4L_STALL_WATCH ?? process.env.SAPS_STALL_WATCH) === "0")
3185
+ if ((process.env.S4L_STALL_WATCH) === "0")
3209
3186
  return { ok: false, detail: "disabled (S4L_STALL_WATCH=0)" };
3210
3187
  const logDir = path.join(repoDir(), "skill", "logs");
3211
3188
  try {
@@ -3254,7 +3231,7 @@ async function ensureMemorySnapshotInstalled() {
3254
3231
  try {
3255
3232
  if (process.platform !== "darwin")
3256
3233
  return { ok: false, detail: "not macOS" };
3257
- if ((process.env.S4L_MEMORY_SNAPSHOT ?? process.env.SAPS_MEMORY_SNAPSHOT) === "0")
3234
+ if ((process.env.S4L_MEMORY_SNAPSHOT) === "0")
3258
3235
  return { ok: false, detail: "disabled (S4L_MEMORY_SNAPSHOT=0)" };
3259
3236
  const logDir = path.join(repoDir(), "skill", "logs");
3260
3237
  try {
@@ -3316,7 +3293,7 @@ async function ensureOverlayWatchInstalled() {
3316
3293
  try {
3317
3294
  if (process.platform !== "darwin")
3318
3295
  return { ok: false, detail: "not macOS" };
3319
- if ((process.env.S4L_OVERLAY_WATCH ?? process.env.SAPS_OVERLAY_WATCH) === "0")
3296
+ if ((process.env.S4L_OVERLAY_WATCH) === "0")
3320
3297
  return { ok: false, detail: "disabled (S4L_OVERLAY_WATCH=0)" };
3321
3298
  const logDir = path.join(repoDir(), "skill", "logs");
3322
3299
  try {
@@ -3501,7 +3478,7 @@ async function healTopicsSeeded(snap) {
3501
3478
  // ---- dashboard localhost fallback -----------------------------------------
3502
3479
  // When the connected host doesn't support MCP Apps UI (Claude Code / Cowork
3503
3480
  // today), serve the SAME dist/panel.html from a loopback HTTP server. The page
3504
- // detects it's running over HTTP (window.__SAPS_BRIDGE__) and routes every
3481
+ // detects it's running over HTTP (window.__S4L_BRIDGE__) and routes every
3505
3482
  // app.callServerTool through POST /tool/<name>, which replays the exact captured
3506
3483
  // handler in TOOL_HANDLERS. No pipeline or front-end logic is duplicated.
3507
3484
  // True if the host advertised it can render our ui:// HTML resource inline.
@@ -3521,7 +3498,7 @@ let localPanel = null;
3521
3498
  // minus the postMessage host (there's none over loopback).
3522
3499
  function widgetHtmlForHttp(file) {
3523
3500
  const html = fs.readFileSync(path.join(DIST_DIR, file), "utf-8");
3524
- const inject = `<script>window.__SAPS_BRIDGE__=${JSON.stringify("http")};</script>`;
3501
+ const inject = `<script>window.__S4L_BRIDGE__=${JSON.stringify("http")};</script>`;
3525
3502
  if (html.includes("</head>"))
3526
3503
  return html.replace("</head>", inject + "</head>");
3527
3504
  return inject + html;
@@ -3607,7 +3584,7 @@ function startLocalPanel() {
3607
3584
  srv.on("error", reject);
3608
3585
  // Optional fixed port (S4L_PANEL_PORT) for deterministic addressing; default
3609
3586
  // is an OS-assigned ephemeral port.
3610
- const wantPort = Number(process.env.S4L_PANEL_PORT ?? process.env.SAPS_PANEL_PORT) || 0;
3587
+ const wantPort = Number(process.env.S4L_PANEL_PORT) || 0;
3611
3588
  srv.listen(wantPort, "127.0.0.1", () => {
3612
3589
  const addr = srv.address();
3613
3590
  const port = typeof addr === "object" && addr ? addr.port : 0;
@@ -3635,9 +3612,8 @@ function writePanelUrl(url) {
3635
3612
  }
3636
3613
  }
3637
3614
  // The owned state dir, honoring S4L_STATE_DIR (matches menubar/s4l_state.py).
3638
- function sapsStateDir() {
3615
+ function s4lStateDir() {
3639
3616
  return (process.env.S4L_STATE_DIR ||
3640
- process.env.SAPS_STATE_DIR ||
3641
3617
  path.join(process.env.HOME || os.homedir(), ".social-autoposter-mcp"));
3642
3618
  }
3643
3619
  // Has the user explicitly chosen an engagement mode? mode.json is written by the
@@ -3645,7 +3621,7 @@ function sapsStateDir() {
3645
3621
  // mode_chosen onboarding milestone. (Source of truth: scripts/s4l_mode.py.)
3646
3622
  function modeChosen() {
3647
3623
  try {
3648
- return fs.existsSync(path.join(sapsStateDir(), "mode.json"));
3624
+ return fs.existsSync(path.join(s4lStateDir(), "mode.json"));
3649
3625
  }
3650
3626
  catch {
3651
3627
  return false;
@@ -3657,7 +3633,7 @@ function modeChosen() {
3657
3633
  // legacy {"mode": ...} string; else default personal ON / promotion OFF.
3658
3634
  function currentFlags() {
3659
3635
  try {
3660
- const d = JSON.parse(fs.readFileSync(path.join(sapsStateDir(), "mode.json"), "utf-8"));
3636
+ const d = JSON.parse(fs.readFileSync(path.join(s4lStateDir(), "mode.json"), "utf-8"));
3661
3637
  if ("personal_brand" in d || "promotion" in d) {
3662
3638
  return { personal_brand: !!d.personal_brand, promotion: !!d.promotion };
3663
3639
  }
@@ -3688,19 +3664,30 @@ function currentMode() {
3688
3664
  const POSTING_FLAG_TTL_MS = 45_000;
3689
3665
  let postingFlagHeartbeat = null;
3690
3666
  function postingFlagPath() {
3691
- return path.join(sapsStateDir(), "posting-active.json");
3667
+ return path.join(s4lStateDir(), "posting-active.json");
3692
3668
  }
3693
3669
  function writePostingFlag() {
3694
3670
  try {
3695
- fs.mkdirSync(sapsStateDir(), { recursive: true });
3671
+ fs.mkdirSync(s4lStateDir(), { recursive: true });
3696
3672
  fs.writeFileSync(postingFlagPath(), JSON.stringify({ pid: process.pid, expires_at: Date.now() + POSTING_FLAG_TTL_MS }) + "\n", "utf-8");
3697
3673
  }
3698
- catch {
3699
- /* best effort */
3674
+ catch (e) {
3675
+ // The 2026-06-23 saga flagged "posting-active.json never writes" as open,
3676
+ // and this silent catch is why nobody could tell. Say it, both places.
3677
+ console.error(`[post] posting-active flag write FAILED: ${String(e)}`);
3678
+ logPostEvent(`posting_flag_write_failed err=${String(e)}`);
3700
3679
  }
3701
3680
  }
3702
3681
  function startPostingFlagHeartbeat() {
3703
3682
  writePostingFlag();
3683
+ try {
3684
+ if (!fs.existsSync(postingFlagPath())) {
3685
+ logPostEvent(`posting_flag_missing_after_write path=${postingFlagPath()}`);
3686
+ }
3687
+ }
3688
+ catch {
3689
+ /* best effort */
3690
+ }
3704
3691
  if (postingFlagHeartbeat)
3705
3692
  return;
3706
3693
  // Refresh well within the TTL so a long batch stays flagged, but a dead poster
@@ -3742,7 +3729,7 @@ let _activityLast = null;
3742
3729
  let _activityHb = null;
3743
3730
  function _writeActivityFile(state, label) {
3744
3731
  try {
3745
- const dir = sapsStateDir();
3732
+ const dir = s4lStateDir();
3746
3733
  fs.mkdirSync(dir, { recursive: true });
3747
3734
  fs.writeFileSync(path.join(dir, "activity.json"), JSON.stringify({ state, label, since: new Date().toISOString() }) + "\n", "utf-8");
3748
3735
  }
@@ -3776,7 +3763,7 @@ function clearActivity() {
3776
3763
  _activityHb = null;
3777
3764
  }
3778
3765
  try {
3779
- fs.rmSync(path.join(sapsStateDir(), "activity.json"), { force: true });
3766
+ fs.rmSync(path.join(s4lStateDir(), "activity.json"), { force: true });
3780
3767
  }
3781
3768
  catch {
3782
3769
  /* best effort */
@@ -3789,7 +3776,7 @@ function clearActivity() {
3789
3776
  // Written atomically so a 1s poll never sees a half-written file.
3790
3777
  function persistStatusSummary(snap) {
3791
3778
  try {
3792
- const dir = sapsStateDir();
3779
+ const dir = s4lStateDir();
3793
3780
  fs.mkdirSync(dir, { recursive: true });
3794
3781
  const tmp = path.join(dir, `status-summary.json.${process.pid}.tmp`);
3795
3782
  fs.writeFileSync(tmp, JSON.stringify({ ...snap, written_at: new Date().toISOString() }) + "\n", "utf-8");
@@ -3807,7 +3794,7 @@ function persistStatusSummary(snap) {
3807
3794
  // just means no pop-ups this batch (chat review still works).
3808
3795
  function writeReviewRequest(req) {
3809
3796
  try {
3810
- const dir = sapsStateDir();
3797
+ const dir = s4lStateDir();
3811
3798
  fs.mkdirSync(dir, { recursive: true });
3812
3799
  fs.writeFileSync(path.join(dir, "review-request.json"), JSON.stringify(req, null, 2) + "\n", "utf-8");
3813
3800
  }
@@ -3822,7 +3809,7 @@ function writeReviewRequest(req) {
3822
3809
  // S4L_PANEL_OPEN_BROWSER=1 to restore the old auto-open behavior. (The URL is
3823
3810
  // always returned to the caller regardless, so nothing is lost when we don't open.)
3824
3811
  async function openInBrowser(url) {
3825
- if (!(process.env.S4L_PANEL_OPEN_BROWSER ?? process.env.SAPS_PANEL_OPEN_BROWSER))
3812
+ if (!(process.env.S4L_PANEL_OPEN_BROWSER))
3826
3813
  return;
3827
3814
  const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
3828
3815
  const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
@@ -3904,7 +3891,23 @@ const sleepMs = (ms) => new Promise((r) => setTimeout(r, ms));
3904
3891
  // autopilot tick stacks another on top (the zombie pileup that stale-reclaimed the
3905
3892
  // lock mid-post). SIGKILL can't be trapped. Kill children first so the harness CDP
3906
3893
  // driver lets go of Chrome immediately.
3894
+ // Timestamped on-disk trail for every scan preemption and posting-flag failure.
3895
+ // The console.error lines land in Claude Desktop's per-profile MCP log, which has
3896
+ // proven hard to even LOCATE during incidents (2026-07-06 forensics had to infer
3897
+ // two of six scan kills from bash job-control lines). This file lives next to the
3898
+ // post-*.log dumps so one directory tells the whole posting story. Best-effort.
3899
+ function logPostEvent(msg) {
3900
+ try {
3901
+ const dir = path.join(repoDir(), "skill", "logs");
3902
+ fs.mkdirSync(dir, { recursive: true });
3903
+ fs.appendFileSync(path.join(dir, "post-preempt-events.log"), `${new Date().toISOString()} pid=${process.pid} ${msg}\n`);
3904
+ }
3905
+ catch {
3906
+ /* best effort */
3907
+ }
3908
+ }
3907
3909
  function sigkillScanTree(pid) {
3910
+ logPostEvent(`sigkill_scan_tree target=${pid}`);
3908
3911
  try {
3909
3912
  const out = execFileSync("pgrep", ["-P", String(pid)], { encoding: "utf-8", timeout: 4000 });
3910
3913
  for (const cstr of out.split(/\s+/)) {
@@ -3957,7 +3960,7 @@ function sigkillAllScans() {
3957
3960
  // BETWEEN every card that a parked scan stale-reclaimed (the hijack). Instead we
3958
3961
  // keep the lock and only release it after SHELL_LOCK_GRACE_MS of no posting, so the
3959
3962
  // hold EXPANDS as more cards get approved and there is never a gap between cards.
3960
- const SHELL_LOCK_GRACE_MS = Number(process.env.S4L_POST_LOCK_GRACE_MS ?? process.env.SAPS_POST_LOCK_GRACE_MS) || 60_000;
3963
+ const SHELL_LOCK_GRACE_MS = Number(process.env.S4L_POST_LOCK_GRACE_MS) || 60_000;
3961
3964
  let shellLockReleaseTimer = null;
3962
3965
  // True from the start of a post batch until SHELL_LOCK_GRACE_MS after the last
3963
3966
  // card. The draft-cycle scan checks this and DEFERS launching a scan while it's set —
@@ -3990,6 +3993,7 @@ function preemptScanHoldingBrowser() {
3990
3993
  const pid = shellLockHolderPid();
3991
3994
  if (pid && pidAlive(pid) && pidIsScan(pid)) {
3992
3995
  console.error(`[post] preempting cross-process scan holding the twitter-browser lock (pid ${pid}) — SIGKILL tree`);
3996
+ logPostEvent(`preempt_scan_holding_browser scan_pid=${pid}`);
3993
3997
  sigkillScanTree(pid);
3994
3998
  }
3995
3999
  }
@@ -4034,6 +4038,7 @@ async function acquireShellBrowserLock() {
4034
4038
  rmShellLockDir();
4035
4039
  }
4036
4040
  else if (pidIsScan(pid)) {
4041
+ logPostEvent(`preempt_scan_on_lock_acquire scan_pid=${pid} attempt=${attempt}`);
4037
4042
  sigkillScanTree(pid); // SIGKILL — scans trap SIGTERM and survive it
4038
4043
  await sleepMs(300);
4039
4044
  rmShellLockDir();
@@ -4443,6 +4448,32 @@ async function main() {
4443
4448
  const tr = setInterval(relayTranscripts, 5 * 60_000);
4444
4449
  tr.unref();
4445
4450
  }
4451
+ // Relay the queue producer/consumer log (claude-queue/provider.log) so a
4452
+ // stranded/orphaned draft batch — a producer that died after its worker wrote
4453
+ // the result but before consuming it — is visible in Cloud Logging remotely,
4454
+ // not just on the box (context="queue-provider"). Incremental (byte offset),
4455
+ // self-locking, forward-only baseline. Best-effort; opt out S4L_PROVIDER_LOG_RELAY=0.
4456
+ if ((process.env.S4L_PROVIDER_LOG_RELAY ?? "1") !== "0") {
4457
+ let providerRelayRunning = false;
4458
+ const relayProviderLog = () => {
4459
+ if (providerRelayRunning)
4460
+ return;
4461
+ providerRelayRunning = true;
4462
+ runPython("scripts/relay_provider_log.py", ["--max-lines", "500"], {
4463
+ timeoutMs: 120_000,
4464
+ })
4465
+ .catch((e) => {
4466
+ console.error("[social-autoposter-mcp] provider-log relay failed:", e?.message || e);
4467
+ })
4468
+ .finally(() => {
4469
+ providerRelayRunning = false;
4470
+ });
4471
+ };
4472
+ const prBoot = setTimeout(relayProviderLog, 95_000); // off the boot hot path
4473
+ prBoot.unref();
4474
+ const pr = setInterval(relayProviderLog, 5 * 60_000);
4475
+ pr.unref();
4476
+ }
4446
4477
  // Sync the install's configuration state (config.json, persona corpus, mode,
4447
4478
  // queues, onboarding ledger) to the backend. Hash-gated on the interval, so
4448
4479
  // the recurring tick only POSTs when something actually changed; setup.ts
@@ -70,7 +70,7 @@ Boolean requesting whether a visible border and background is provided by the ho
70
70
  - omitted: host decides border`)});m({method:u("ui/request-display-mode"),params:m({mode:it.describe("The display mode being requested.")})});var xh=m({mode:it.describe("The display mode that was actually set. May differ from requested if not supported.")}).passthrough(),jh=U([u("model"),u("app")]).describe("Tool visibility scope - who can access the tool.");m({resourceUri:d().optional(),visibility:x(jh).optional().describe(`Who can access this tool. Default: ["model", "app"]
71
71
  - "model": Tool visible to and callable by the agent
72
72
  - "app": Tool callable by the app from this server only`),csp:xe().optional(),permissions:xe().optional()});m({mimeTypes:x(d()).optional().describe('Array of supported MIME types for UI resources.\nMust include `"text/html;profile=mcp-app"` for MCP Apps support.')});m({method:u("ui/download-file"),params:m({contents:x(U([wl,Il])).describe("Resource contents to download — embedded (inline data) or linked (host fetches). Uses standard MCP resource types.")})});m({method:u("ui/message"),params:m({role:u("user").describe('Message role, currently only "user" is supported.'),content:x(It).describe("Message content blocks (text, image, etc.).")})});m({method:u("ui/notifications/sandbox-resource-ready"),params:m({html:d().describe("HTML content to load into the inner iframe."),sandbox:d().optional().describe("Optional override for the inner iframe's sandbox attribute."),csp:$o.optional().describe("CSP configuration from resource metadata."),permissions:ko.optional().describe("Sandbox permissions from resource metadata.")})});var Nh=m({method:u("ui/notifications/tool-result"),params:Wn.describe("Standard MCP tool execution result.")}),Tl=m({toolInfo:m({id:bt.optional().describe("JSON-RPC id of the tools/call request."),tool:yo.describe("Tool definition including name, inputSchema, etc.")}).optional().describe("Metadata of the tool call that instantiated this App."),theme:ph.optional().describe("Current color theme preference."),styles:Sh.optional().describe("Style configuration for theming the app."),displayMode:it.optional().describe("How the UI is currently displayed."),availableDisplayModes:x(it).optional().describe("Display modes the host supports."),containerDimensions:U([m({height:O().describe("Fixed container height in pixels.")}),m({maxHeight:U([O(),tt()]).optional().describe("Maximum container height in pixels.")})]).and(U([m({width:O().describe("Fixed container width in pixels.")}),m({maxWidth:U([O(),tt()]).optional().describe("Maximum container width in pixels.")})])).optional().describe(`Container dimensions. Represents the dimensions of the iframe or other
73
- container holding the app. Specify either width or maxWidth, and either height or maxHeight.`),locale:d().optional().describe("User's language and region preference in BCP 47 format."),timeZone:d().optional().describe("User's timezone in IANA format."),userAgent:d().optional().describe("Host application identifier."),platform:U([u("web"),u("desktop"),u("mobile")]).optional().describe("Platform type for responsive design decisions."),deviceCapabilities:m({touch:H().optional().describe("Whether the device supports touch input."),hover:H().optional().describe("Whether the device supports hover interactions.")}).optional().describe("Device input capabilities."),safeAreaInsets:m({top:O().describe("Top safe area inset in pixels."),right:O().describe("Right safe area inset in pixels."),bottom:O().describe("Bottom safe area inset in pixels."),left:O().describe("Left safe area inset in pixels.")}).optional().describe("Mobile safe area boundaries in pixels.")}).passthrough(),Oh=m({method:u("ui/notifications/host-context-changed"),params:Tl.describe("Partial context update containing only changed fields.")});m({method:u("ui/update-model-context"),params:m({content:x(It).optional().describe("Context content blocks (text, image, etc.)."),structuredContent:C(d(),F().describe("Structured content for machine-readable context data.")).optional().describe("Structured content for machine-readable context data.")})});m({method:u("ui/initialize"),params:m({appInfo:Bn.describe("App identification (name and version)."),appCapabilities:zh.describe("Features and capabilities this app provides."),protocolVersion:d().describe("Protocol version this app supports.")})});var Th=m({protocolVersion:d().describe('Negotiated protocol version string (e.g., "2025-11-21").'),hostInfo:Bn.describe("Host application identification and version."),hostCapabilities:Ih.describe("Features and capabilities provided by the host."),hostContext:Tl.describe("Rich context about the host environment.")}).passthrough(),Ph={target:"draft-2020-12"};async function Ko(e,n){let r=e["~standard"];if(r.jsonSchema)return r.jsonSchema[n](Ph);if(r.vendor==="zod"){let{z:o}=await Wl(()=>Promise.resolve().then(()=>bp),void 0,import.meta.url);return o.toJSONSchema(e,{io:n})}throw Error(`Schema (vendor: ${r.vendor}) does not implement Standard JSON Schema (~standard.jsonSchema). Use a library that does (zod v4, ArkType, Valibot) or wrap your schema accordingly.`)}async function Go(e,n,r=""){let o=await e["~standard"].validate(n);if(o.issues){let t=o.issues.map(i=>{var s;let a=(s=i.path)==null?void 0:s.map(c=>typeof c=="object"?c.key:c).join(".");return a?`${a}: ${i.message}`:i.message}).join("; ");throw Error(r+t)}return o.value}function Uh(e){let n=document.documentElement;n.setAttribute("data-theme",e),n.style.colorScheme=e}function Eh(e,n=document.documentElement){for(let[r,o]of Object.entries(e))o!==void 0&&n.style.setProperty(r,o)}function Dh(e){if(document.getElementById("__mcp-host-fonts"))return;let n=document.createElement("style");n.id="__mcp-host-fonts",n.textContent=e,document.head.appendChild(n)}const Ht=class Ht extends uh{constructor(r,o={},t={autoResize:!0}){super(t);D(this,"_appInfo");D(this,"_capabilities");D(this,"options");D(this,"_hostCapabilities");D(this,"_hostInfo");D(this,"_hostContext");D(this,"_registeredTools",{});D(this,"_initializedSent",!1);D(this,"eventSchemas",{toolinput:bh,toolinputpartial:yh,toolresult:Nh,toolcancelled:$h,hostcontextchanged:Oh});D(this,"_everHadListener",new Set);D(this,"_toolHandlersInitialized",!1);D(this,"_onteardown");D(this,"_oncalltool");D(this,"_onlisttools");D(this,"sendOpenLink",this.openLink);this._appInfo=r,this._capabilities=o,this.options=t,t.allowUnsafeEval||X({jitless:!0}),this.setRequestHandler(Vn,i=>(console.log("Received ping:",i.params),{})),this.setEventHandler("hostcontextchanged",void 0)}_assertInitialized(r){var t;if(this._initializedSent)return;let o=`[ext-apps] App.${r}() called before connect() completed the ui/initialize handshake. Await app.connect() before calling this method, or move data loading to an ontoolresult handler.`;if((t=this.options)!=null&&t.strict)throw Error(o);console.warn(`${o}. This will throw in a future release.`)}_assertHandlerTiming(r){var t;if(!Ht.ONE_SHOT_EVENTS.has(r)||this._everHadListener.has(r)||(this._everHadListener.add(r),!this._initializedSent))return;let o=`[ext-apps] "${String(r)}" handler registered after connect() completed the ui/initialize handshake. The host may have already sent this notification. Register handlers before calling app.connect().`;if((t=this.options)!=null&&t.strict)throw Error(o);console.warn(o)}setEventHandler(r,o){o&&this._assertHandlerTiming(r),super.setEventHandler(r,o)}addEventListener(r,o){this._assertHandlerTiming(r),super.addEventListener(r,o)}onEventDispatch(r,o){r==="hostcontextchanged"&&(this._hostContext={...this._hostContext,...o})}registerCapabilities(r){if(this.transport)throw Error("Cannot register capabilities after transport is established");this._capabilities=ch(this._capabilities,r)}registerTool(r,o,t){if(this._registeredTools[r])throw Error(`Tool ${r} is already registered`);let i=this,a=()=>{var p;i._initializedSent&&((p=i._capabilities.tools)!=null&&p.listChanged)&&i.sendToolListChanged()},s=o.inputSchema!==void 0,c={title:o.title,description:o.description,inputSchema:o.inputSchema,outputSchema:o.outputSchema,annotations:o.annotations,_meta:o._meta,enabled:!0,enable(){this.enabled=!0,a()},disable(){this.enabled=!1,a()},update(p){Object.assign(this,p),a()},remove(){i._registeredTools[r]===c&&(delete i._registeredTools[r],a())},handler:async(p,h)=>{if(!c.enabled)throw Error(`Tool ${r} is disabled`);let f;if(s){let _=c.inputSchema,w=_?await Go(_,p??{},`Invalid input for tool ${r}: `):p??{};f=await t(w,h)}else f=await t(h);return c.outputSchema&&!f.isError&&(f.structuredContent=await Go(c.outputSchema,f.structuredContent,`Invalid output for tool ${r}: `)),f}};return this._registeredTools[r]=c,!this._capabilities.tools&&!this.transport&&this.registerCapabilities({tools:{listChanged:!0}}),this.ensureToolHandlersInitialized(),a(),c}ensureToolHandlersInitialized(){this._toolHandlersInitialized||(this._toolHandlersInitialized=!0,this.oncalltool=async(r,o)=>{let t=this._registeredTools[r.name];if(!t)throw Error(`Tool ${r.name} not found`);return t.handler(r.arguments,o)},this.onlisttools=async(r,o)=>({tools:await Promise.all(Object.entries(this._registeredTools).filter(([t,i])=>i.enabled).map(async([t,i])=>{let a={name:t,title:i.title,description:i.description,inputSchema:i.inputSchema?await Ko(i.inputSchema,"input"):{type:"object",properties:{}}};return i.outputSchema&&(a.outputSchema=await Ko(i.outputSchema,"output")),i.annotations&&(a.annotations=i.annotations),i._meta&&(a._meta=i._meta),a}))}))}async sendToolListChanged(r={}){this._assertInitialized("sendToolListChanged"),await this.notification({method:"notifications/tools/list_changed",params:r})}getHostCapabilities(){return this._hostCapabilities}getHostVersion(){return this._hostInfo}getHostContext(){return this._hostContext}get ontoolinput(){return this.getEventHandler("toolinput")}set ontoolinput(r){this.setEventHandler("toolinput",r)}get ontoolinputpartial(){return this.getEventHandler("toolinputpartial")}set ontoolinputpartial(r){this.setEventHandler("toolinputpartial",r)}get ontoolresult(){return this.getEventHandler("toolresult")}set ontoolresult(r){this.setEventHandler("toolresult",r)}get ontoolcancelled(){return this.getEventHandler("toolcancelled")}set ontoolcancelled(r){this.setEventHandler("toolcancelled",r)}get onhostcontextchanged(){return this.getEventHandler("hostcontextchanged")}set onhostcontextchanged(r){this.setEventHandler("hostcontextchanged",r)}get onteardown(){return this._onteardown}set onteardown(r){this.warnIfRequestHandlerReplaced("onteardown",this._onteardown,r),this._onteardown=r,this.replaceRequestHandler(wh,(o,t)=>{if(!this._onteardown)throw Error("No onteardown handler set");return this._onteardown(o.params,t)})}get oncalltool(){return this._oncalltool}set oncalltool(r){this.warnIfRequestHandlerReplaced("oncalltool",this._oncalltool,r),this._oncalltool=r,this.replaceRequestHandler(xl,(o,t)=>{if(!this._oncalltool)throw Error("No oncalltool handler set");return this._oncalltool(o.params,t)})}get onlisttools(){return this._onlisttools}set onlisttools(r){this.warnIfRequestHandlerReplaced("onlisttools",this._onlisttools,r),this._onlisttools=r,this.replaceRequestHandler(zl,(o,t)=>{if(!this._onlisttools)throw Error("No onlisttools handler set");return this._onlisttools(o.params,t)})}assertCapabilityForMethod(r){var o;switch(r){case"sampling/createMessage":if(!((o=this._hostCapabilities)!=null&&o.sampling))throw Error(`Host does not support sampling (required for ${r})`);break}}assertRequestHandlerCapability(r){switch(r){case"tools/call":case"tools/list":if(!this._capabilities.tools)throw Error(`Client does not support tool capability (required for ${r})`);return;case"ping":case"ui/resource-teardown":return;default:throw Error(`No handler for method ${r} registered`)}}assertNotificationCapability(r){}assertTaskCapability(r){throw Error("Tasks are not supported in MCP Apps")}assertTaskHandlerCapability(r){throw Error("Task handlers are not supported in MCP Apps")}async callServerTool(r,o){if(this._assertInitialized("callServerTool"),typeof r=="string")throw Error(`callServerTool() expects an object as its first argument, but received a string ("${r}"). Did you mean: callServerTool({ name: "${r}", arguments: { ... } })?`);return await this.request({method:"tools/call",params:r},Wn,{onprogress:()=>{},resetTimeoutOnProgress:!0,...o})}async readServerResource(r,o){return this._assertInitialized("readServerResource"),await this.request({method:"resources/read",params:r},Sl,o)}async listServerResources(r,o){return this._assertInitialized("listServerResources"),await this.request({method:"resources/list",params:r},kl,o)}async createSamplingMessage(r,o){this._assertInitialized("createSamplingMessage");let t=r.tools?Ol:Nl;return await this.request({method:"sampling/createMessage",params:r},t,o)}sendMessage(r,o){return this._assertInitialized("sendMessage"),this.request({method:"ui/message",params:r},_h,o)}sendLog(r){return this.notification({method:"notifications/message",params:r})}updateModelContext(r,o){return this._assertInitialized("updateModelContext"),this.request({method:"ui/update-model-context",params:r},io,o)}openLink(r,o){return this._assertInitialized("openLink"),this.request({method:"ui/open-link",params:r},gh,o)}downloadFile(r,o){return this._assertInitialized("downloadFile"),this.request({method:"ui/download-file",params:r},vh,o)}requestTeardown(r={}){return this.notification({method:"ui/notifications/request-teardown",params:r})}requestDisplayMode(r,o){return this._assertInitialized("requestDisplayMode"),this.request({method:"ui/request-display-mode",params:r},xh,o)}sendSizeChanged(r){return this.notification({method:"ui/notifications/size-changed",params:r})}setupSizeChangedNotifications(){let r=!1,o=0,t=0,i=()=>{r||(r=!0,requestAnimationFrame(()=>{r=!1;let s=document.documentElement,c=s.style.height;s.style.height="max-content";let p=Math.ceil(s.getBoundingClientRect().height);s.style.height=c;let h=Math.ceil(window.innerWidth);(h!==o||p!==t)&&(o=h,t=p,this.sendSizeChanged({width:h,height:p}))}))};i();let a=new ResizeObserver(i);return a.observe(document.documentElement),a.observe(document.body),()=>a.disconnect()}async connect(r=new mh(window.parent,window.parent),o){var t;if(this.transport)throw Error("App is already connected. Call close() before connecting again.");this._initializedSent=!1,await super.connect(r);try{let i=await this.request({method:"ui/initialize",params:{appCapabilities:this._capabilities,appInfo:this._appInfo,protocolVersion:lh}},Th,o);if(i===void 0)throw Error(`Server sent invalid initialize result: ${i}`);this._hostCapabilities=i.hostCapabilities,this._hostInfo=i.hostInfo,this._hostContext=i.hostContext,await this.notification({method:"ui/notifications/initialized"}),this._initializedSent=!0,(t=this.options)!=null&&t.autoResize&&this.setupSizeChangedNotifications()}catch(i){throw this.close(),i}}};D(Ht,"ONE_SHOT_EVENTS",new Set(["toolinput","toolinputpartial","toolresult","toolcancelled"]));let sr=Ht;function Qo(e){const n=e&&e.structuredContent;if(n&&typeof n=="object"){if(typeof n.snapshot=="string")try{return JSON.parse(n.snapshot)}catch{}return n}const r=(e&&e.content||[]).find(o=>o&&o.type==="text");if(r!=null&&r.text)try{return JSON.parse(r.text)}catch{return{}}return{}}class Rh{constructor(){D(this,"onhostcontextchanged");D(this,"onerror");D(this,"ontoolresult")}async connect(){var n,r;try{const[o,t]=await Promise.all([this.callServerTool({name:"project_config",arguments:{status:!0}}),this.callServerTool({name:"runtime",arguments:{action:"status"}})]),i=Qo(o),a=Qo(t),s=Array.isArray(i.projects)?i.projects:[],c={projects:s,projects_total:s.length,projects_ready:s.filter(p=>p&&p.ready).length,x_connected:!!i.x_connected,x_state:i.x_state||"",x_handle:i.x_handle??null,version:i.mcp_version||"",latest_version:i.latest_version??null,update_available:!!i.update_available,runtime_ready:typeof a.runtime_ready=="boolean"?a.runtime_ready:!0,runtime_provisioning:!!a.provisioning,onboarding:a.onboarding||i.onboarding};(n=this.ontoolresult)==null||n.call(this,{structuredContent:{snapshot:JSON.stringify(c)}})}catch(o){(r=this.onerror)==null||r.call(this,o)}}getHostContext(){}async callServerTool(n){const r=await fetch(`/tool/${encodeURIComponent(n.name)}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n.arguments??{})});if(!r.ok){let o=`HTTP ${r.status}`;try{o=await r.text()||o}catch{}return{isError:!0,content:[{type:"text",text:o}]}}return r.json()}async sendMessage(){return{isError:!0}}}function Ch(){return globalThis.__SAPS_BRIDGE__==="http"?new Rh:new sr({name:"S4L Panel",version:"1.0.0"})}function Pl(e){const n=e.structuredContent;if(n&&typeof n=="object"){if(typeof n.snapshot=="string")try{return JSON.parse(n.snapshot)}catch{}return n}const r=(e.content||[]).find(o=>o.type==="text");if(r!=null&&r.text)try{return JSON.parse(r.text)}catch{return{_raw:r.text}}return{}}const M=e=>document.getElementById(e),Ul=M("ver"),Ge=M("btn-setup"),Lt=M("btn-schedule"),Nt=M("stats-grid"),Gn=M("stats-toggle"),Zh=M("log"),Ah=M("install-card"),Ie=M("setup-summary"),El=M("onboarding-details"),Lh=M("onboarding-steps"),Qn=M("onboarding-blocker"),Yo=M("onboarding-count"),Mh=M("onboarding-bar-fill"),qh=M("live-card"),Hh=M("stats-card"),Xo=M("install-steps"),Ee=M("install-err"),ge=M("btn-install"),Fh=M("menubar-banner"),ea=M("btn-menubar-restart"),So=M("btn-live"),wo=M("btn-live-stop"),ta=M("btn-live-front"),De=M("live-status"),Mt=M("live-img"),Dl=M("switch-personal"),Rl=M("switch-promo"),Jh=M("mode-sub"),Bh=M("settings-card"),Yn=M("settings-toggle"),fe=M("settings-body");let A=null,Xn=!1,Ot=!1,er=!1,Re=!1,We=!1,Te=!1,tr=!1;function W(e){Zh.textContent=e}function Vh(e){switch(e){case"done":return"✓";case"running":return"…";case"error":return"×";default:return"·"}}function Io(e){if(!e||!Array.isArray(e.steps)){Xo.innerHTML="";return}Xo.innerHTML=e.steps.map(n=>{const r=n.detail&&n.status!=="pending"?` <span class="detail">${n.status==="error"?n.detail:""}</span>`:"";return`<li class="${n.status}"><span class="glyph">${Vh(n.status)}</span><span>${n.label}${r}</span></li>`}).join(""),e.error?(Ee.textContent=e.error,Ee.hidden=!1):Ee.hidden=!0}const Wh={environment_checked:"Environment checked",runtime_ready:"Runtime ready",x_connected:"X connected",profile_scanned:"Profile scanned",project_ready:"Project ready",topics_seeded:"Topics seeded",tasks_scheduled:"Tasks scheduled"};function Kh(e){switch(e){case"complete":return"✓";case"in_progress":return"…";case"blocked":return"×";default:return"·"}}function Cl(){El.hidden=!Re,Ie.setAttribute("aria-expanded",String(Re)),Ie.classList.toggle("expanded",Re)}function Zl(e){return e?e.setup_complete!==void 0?!!e.setup_complete:!!e.runtime_ready&&(e.projects_ready||0)>0&&!!e.x_connected:!1}function Gh(e){if(!e||!Array.isArray(e.milestones)){Ie.hidden=!0,El.hidden=!0;return}Ie.hidden=!1;const n=e.milestones.length,r=e.milestones.filter(i=>i.status==="complete").length,o=Zl(A),t=!!e.current_blocker&&!o;Ie.classList.toggle("complete",o),Ie.classList.toggle("blocked",t),Yo.hidden=o,Yo.textContent=t?`${r}/${n} · needs you`:Ot?`${r}/${n} · setting up…`:`${r}/${n}`,Mh.style.width=n>0?`${Math.round(r/n*100)}%`:"0%",Lh.innerHTML=e.milestones.map(i=>{const a=Wh[i.id]||i.id,s=i.attempts>1?` <span class="detail">${i.attempts} attempts</span>`:"";return`<li class="${i.status}"><span class="glyph">${Kh(i.status)}</span><span>${a}${s}</span></li>`}).join(""),e.current_blocker?(Qn.textContent=`Current blocker: ${e.current_blocker.message}`,Qn.hidden=!1,Re=!0):Qn.hidden=!0,Cl()}function qt(){if(!A)return;Gh(A.onboarding),Ul.innerHTML=A.update_available&&A.latest_version?`v${A.version} · <button id="btn-update" class="update-btn">Update to ${A.latest_version}</button>`:`v${A.version}`;const e=!A.runtime_ready;Ah.hidden=!e,Fh.hidden=e||A.menubar_running!==!1;const n=Zl(A);Ge.hidden=n,Ge.disabled=!1,Ge.classList.toggle("primary",!n);const r=n&&(A.schedule_state==="missing"||A.schedule_state==="disabled");Lt.hidden=!r,Lt.classList.toggle("primary",r);const o=A.flags||(A.mode==="promotion"?{personal_brand:!1,promotion:!0}:{personal_brand:!0,promotion:!1}),t=!!o.personal_brand,i=!!o.promotion;Dl.setAttribute("aria-checked",String(t)),Rl.setAttribute("aria-checked",String(i)),Jh.textContent=t&&i?"Both lanes on: cycles split 50/50.":!t&&!i?"No lane on; the cycle falls back to personal brand.":"",qh.hidden=!n,Hh.hidden=!n,Bh.hidden=e}function ye(e){A={...A||{},...e},qt()}function Qh(e){const n=Array.isArray(e.projects)?e.projects:[];return{projects:n,projects_total:n.length,projects_ready:n.filter(r=>r.ready).length,x_connected:!!e.x_connected,x_state:e.x_state||"",x_handle:e.x_handle??null,...e.setup_complete!==void 0?{setup_complete:!!e.setup_complete}:{},version:e.mcp_version||(A==null?void 0:A.version)||"",latest_version:e.latest_version??null,update_available:!!e.update_available,mode:e.mode??(A==null?void 0:A.mode),flags:e.flags??(A==null?void 0:A.flags),onboarding:e.onboarding}}const $e=Ch();function Al(e){var n,r,o;e.theme&&Uh(e.theme),(n=e.styles)!=null&&n.variables&&Eh(e.styles.variables),(o=(r=e.styles)==null?void 0:r.css)!=null&&o.fonts&&Dh(e.styles.css.fonts)}$e.onhostcontextchanged=Al;$e.onerror=e=>console.error(e);$e.ontoolresult=e=>{const n=Pl(e);n&&typeof n.projects_total=="number"&&(ye(n),n.runtime_ready?Ll():n.runtime_provisioning&&zo())};async function oe(e,n={}){const r=await $e.callServerTool({name:e,arguments:n});return Pl(r)}async function Fe(){W("Refreshing…");try{const[e,n]=await Promise.all([oe("project_config",{status:!0}),oe("runtime",{action:"status"}).catch(()=>({}))]);ye({...Qh(e),...typeof n.runtime_ready=="boolean"?{runtime_ready:n.runtime_ready}:{},...typeof n.menubar_running=="boolean"?{menubar_running:n.menubar_running}:{},onboarding:n.onboarding||e.onboarding||(A==null?void 0:A.onboarding)}),A&&!A.runtime_ready&&n.provisioning&&zo(),W(""),Ll()}catch(e){W("Refresh failed: "+((e==null?void 0:e.message)||e))}}async function zo(){if(!Xn){Xn=!0,ge.disabled=!0,ge.textContent="Installing…";try{for(;;){const e=await oe("runtime",{action:"status"}).catch(()=>({}));if(Io(e.progress??null),e.onboarding&&ye({onboarding:e.onboarding}),e.runtime_ready){ye({runtime_ready:!0}),W("Runtime installed; you're ready to set up."),Fe();return}const n=e.progress??null;if(n&&n.done&&!n.ok){ge.disabled=!1,ge.textContent="Retry install",W("Install failed; see the step above, then Retry.");return}await new Promise(r=>setTimeout(r,1500))}}finally{Xn=!1}}}async function Yh(){var r;if(Ot)return;Ot=!0,qt();const e=Date.now(),n=1200*1e3;try{for(;;){const o=await oe("runtime",{action:"status"}).catch(()=>({}));o.progress&&Io(o.progress);const t={};if(typeof o.runtime_ready=="boolean"&&(t.runtime_ready=o.runtime_ready),o.onboarding&&(t.onboarding=o.onboarding),Object.keys(t).length&&ye(t),(r=o.onboarding)!=null&&r.complete){await Fe(),W("Setup complete.");break}if(Date.now()-e>n)break;await new Promise(i=>setTimeout(i,2e3))}}finally{Ot=!1,qt()}}async function Ll(){try{const e=await oe("get_stats",{days:7}),n=Array.isArray(e.projects)?e.projects[0]:null,r=n==null?void 0:n.posts;if(!r){Nt.innerHTML='<div class="muted">No stats yet.</div>';return}const o=[["Posts",r.total??0],["Active",r.active??0],["Views",r.views_period_total??r.views??0],["Replies",r.comments_period_total??r.comments??0],["Clicks",r.post_clicks_period_total??0]];Nt.innerHTML=o.map(([t,i])=>`<div class="stat"><div class="n">${i}</div><div class="l">${t}</div></div>`).join("")}catch(e){Nt.innerHTML=`<div class="muted">Stats unavailable: ${(e==null?void 0:e.message)||e}</div>`}}function Kn(e,n,r){const o=e.textContent;e.disabled=!0,e.textContent=n,r().finally(()=>{e.disabled=!1,e.textContent=o,qt()})}Ge.addEventListener("click",()=>Kn(Ge,"Starting…",async()=>{W("Asking Claude to run setup…");try{const e=await $e.sendMessage({role:"user",content:[{type:"text",text:"Set up S4L plugin end to end now. Inspect and repair the runtime, auto-detect and connect my X session, scan my profile, discover and research my product, then infer and save a complete project with seeded search topics. Keep going without asking me to approve each safe setup step. Ask only if I must interactively sign in or no product can be identified. Keep every reply to me extremely concise: a few short sentences at most, no step-by-step narration or long status walls. If you must ask me something (e.g. the product URL), make it one short question."}]});e!=null&&e.isError?W("The host rejected the setup request — type “set up S4L” in the chat instead."):(W("Setup is running in the chat. It will only stop for an unavoidable login or missing product."),Yh())}catch(e){W("Couldn’t start setup: "+((e==null?void 0:e.message)||e))}}));Lt.addEventListener("click",()=>Kn(Lt,"Setting up…",async()=>{W("Asking Claude to schedule the draft tasks for this account…");try{const e=await $e.sendMessage({role:"user",content:[{type:"text",text:'Set up the S4L draft autopilot schedule for this Claude account. If queue_setup is available, call it; then for s4l-worker call the host tool create_scheduled_task with taskId, cronExpression "* * * * *", and the prompt — read it from ~/.claude/scheduled-tasks/s4l-worker/SKILL.md (already on disk). Do NOT redo my X connection or project setup. Keep replies to me very short.'}]});e!=null&&e.isError?W("The host rejected it — type “set up the draft schedule” in the chat instead."):W("Scheduling is running in the chat. The draft tasks will register for this account.")}catch(e){W("Couldn’t start scheduling: "+((e==null?void 0:e.message)||e))}}));function Ml(e,n){e.addEventListener("click",async()=>{if(!e.disabled){e.disabled=!0,e.setAttribute("aria-checked",String(e.getAttribute("aria-checked")!=="true"));try{const r=await oe("engagement_mode",{action:"toggle",lane:n});r&&r.flags&&ye({flags:r.flags}),await Fe()}catch(r){W("Couldn’t switch lane: "+((r==null?void 0:r.message)||r)),await Fe()}finally{e.disabled=!1}}})}Ml(Dl,"personal_brand");Ml(Rl,"promotion");Ie.addEventListener("click",()=>{Re=!Re,Cl()});Gn.addEventListener("click",()=>{We=!We,Nt.hidden=!We,Gn.setAttribute("aria-expanded",String(We)),Gn.classList.toggle("expanded",We)});Ul.addEventListener("click",e=>{const n=e.target;n&&n.id==="btn-update"&&Xh()});async function Xh(){if(er)return;er=!0;const e=document.getElementById("btn-update");e&&(e.disabled=!0,e.textContent="Updating…"),W("Installing the latest release… this can take a minute.");try{const n=await oe("runtime",{action:"update"});n.ok?(W(`Updated to ${n.latest_published||"the latest version"}. ${n.takes_effect||"Restart the client to apply."}`),e&&(e.textContent="Update installed — restart to apply")):(W("Update failed (exit "+(n.exit_code??"?")+"). Try `npx social-autoposter@latest update` in a terminal."),e&&(e.disabled=!1,e.textContent="Retry update"))}catch(n){W("Update failed: "+((n==null?void 0:n.message)||n)),e&&(e.disabled=!1,e.textContent="Retry update")}finally{er=!1}}ge.addEventListener("click",async()=>{Ee.hidden=!0,ge.disabled=!0,ge.textContent="Starting…",W("Installing the runtime — this is a one-time download (~150MB+).");try{const e=await oe("runtime",{action:"install"});if(e.runtime_ready){ye({runtime_ready:!0}),Fe();return}Io(e.progress??null),zo()}catch(e){ge.disabled=!1,ge.textContent="Retry install",Ee.textContent="Couldn't start install: "+((e==null?void 0:e.message)||e),Ee.hidden=!1}});ea.addEventListener("click",()=>Kn(ea,"Restarting…",async()=>{W("Restarting the S4L menu bar…");try{const e=await oe("restart_menubar");typeof e.menubar_running=="boolean"&&ye({menubar_running:e.menubar_running}),W(e.menubar_running?"Menu bar restarted.":"Couldn’t confirm the menu bar came back"+(e.detail?": "+e.detail:"."))}catch(e){W("Couldn’t restart the menu bar: "+((e==null?void 0:e.message)||e))}}));const eg=["website","description","icp","voice","differentiator","search_topics","get_started_link","content_guardrails"],tg=["description","voice","search_topics","content_angle","content_guardrails"],cr={website:"Website",description:"What it does",icp:"Target audience",voice:"Voice",differentiator:"Differentiator",search_topics:"Search topics",get_started_link:"Get-started link",content_guardrails:"Content guardrails",content_angle:"Content angle"},na=new Set(["website","description","icp","voice","differentiator","search_topics","get_started_link","content_guardrails"]),ng=new Set(["website","get_started_link"]);function Qe(e){const n=e.replace(/[_-]+/g," ").trim();return n?n.charAt(0).toUpperCase()+n.slice(1):e}function ql(e){return!!e&&typeof e=="object"&&!Array.isArray(e)}function ra(e){return Array.isArray(e)&&e.every(n=>typeof n=="string")?{kind:"list",text:e.join(`
73
+ container holding the app. Specify either width or maxWidth, and either height or maxHeight.`),locale:d().optional().describe("User's language and region preference in BCP 47 format."),timeZone:d().optional().describe("User's timezone in IANA format."),userAgent:d().optional().describe("Host application identifier."),platform:U([u("web"),u("desktop"),u("mobile")]).optional().describe("Platform type for responsive design decisions."),deviceCapabilities:m({touch:H().optional().describe("Whether the device supports touch input."),hover:H().optional().describe("Whether the device supports hover interactions.")}).optional().describe("Device input capabilities."),safeAreaInsets:m({top:O().describe("Top safe area inset in pixels."),right:O().describe("Right safe area inset in pixels."),bottom:O().describe("Bottom safe area inset in pixels."),left:O().describe("Left safe area inset in pixels.")}).optional().describe("Mobile safe area boundaries in pixels.")}).passthrough(),Oh=m({method:u("ui/notifications/host-context-changed"),params:Tl.describe("Partial context update containing only changed fields.")});m({method:u("ui/update-model-context"),params:m({content:x(It).optional().describe("Context content blocks (text, image, etc.)."),structuredContent:C(d(),F().describe("Structured content for machine-readable context data.")).optional().describe("Structured content for machine-readable context data.")})});m({method:u("ui/initialize"),params:m({appInfo:Bn.describe("App identification (name and version)."),appCapabilities:zh.describe("Features and capabilities this app provides."),protocolVersion:d().describe("Protocol version this app supports.")})});var Th=m({protocolVersion:d().describe('Negotiated protocol version string (e.g., "2025-11-21").'),hostInfo:Bn.describe("Host application identification and version."),hostCapabilities:Ih.describe("Features and capabilities provided by the host."),hostContext:Tl.describe("Rich context about the host environment.")}).passthrough(),Ph={target:"draft-2020-12"};async function Ko(e,n){let r=e["~standard"];if(r.jsonSchema)return r.jsonSchema[n](Ph);if(r.vendor==="zod"){let{z:o}=await Wl(()=>Promise.resolve().then(()=>bp),void 0,import.meta.url);return o.toJSONSchema(e,{io:n})}throw Error(`Schema (vendor: ${r.vendor}) does not implement Standard JSON Schema (~standard.jsonSchema). Use a library that does (zod v4, ArkType, Valibot) or wrap your schema accordingly.`)}async function Go(e,n,r=""){let o=await e["~standard"].validate(n);if(o.issues){let t=o.issues.map(i=>{var s;let a=(s=i.path)==null?void 0:s.map(c=>typeof c=="object"?c.key:c).join(".");return a?`${a}: ${i.message}`:i.message}).join("; ");throw Error(r+t)}return o.value}function Uh(e){let n=document.documentElement;n.setAttribute("data-theme",e),n.style.colorScheme=e}function Eh(e,n=document.documentElement){for(let[r,o]of Object.entries(e))o!==void 0&&n.style.setProperty(r,o)}function Dh(e){if(document.getElementById("__mcp-host-fonts"))return;let n=document.createElement("style");n.id="__mcp-host-fonts",n.textContent=e,document.head.appendChild(n)}const Ht=class Ht extends uh{constructor(r,o={},t={autoResize:!0}){super(t);D(this,"_appInfo");D(this,"_capabilities");D(this,"options");D(this,"_hostCapabilities");D(this,"_hostInfo");D(this,"_hostContext");D(this,"_registeredTools",{});D(this,"_initializedSent",!1);D(this,"eventSchemas",{toolinput:bh,toolinputpartial:yh,toolresult:Nh,toolcancelled:$h,hostcontextchanged:Oh});D(this,"_everHadListener",new Set);D(this,"_toolHandlersInitialized",!1);D(this,"_onteardown");D(this,"_oncalltool");D(this,"_onlisttools");D(this,"sendOpenLink",this.openLink);this._appInfo=r,this._capabilities=o,this.options=t,t.allowUnsafeEval||X({jitless:!0}),this.setRequestHandler(Vn,i=>(console.log("Received ping:",i.params),{})),this.setEventHandler("hostcontextchanged",void 0)}_assertInitialized(r){var t;if(this._initializedSent)return;let o=`[ext-apps] App.${r}() called before connect() completed the ui/initialize handshake. Await app.connect() before calling this method, or move data loading to an ontoolresult handler.`;if((t=this.options)!=null&&t.strict)throw Error(o);console.warn(`${o}. This will throw in a future release.`)}_assertHandlerTiming(r){var t;if(!Ht.ONE_SHOT_EVENTS.has(r)||this._everHadListener.has(r)||(this._everHadListener.add(r),!this._initializedSent))return;let o=`[ext-apps] "${String(r)}" handler registered after connect() completed the ui/initialize handshake. The host may have already sent this notification. Register handlers before calling app.connect().`;if((t=this.options)!=null&&t.strict)throw Error(o);console.warn(o)}setEventHandler(r,o){o&&this._assertHandlerTiming(r),super.setEventHandler(r,o)}addEventListener(r,o){this._assertHandlerTiming(r),super.addEventListener(r,o)}onEventDispatch(r,o){r==="hostcontextchanged"&&(this._hostContext={...this._hostContext,...o})}registerCapabilities(r){if(this.transport)throw Error("Cannot register capabilities after transport is established");this._capabilities=ch(this._capabilities,r)}registerTool(r,o,t){if(this._registeredTools[r])throw Error(`Tool ${r} is already registered`);let i=this,a=()=>{var p;i._initializedSent&&((p=i._capabilities.tools)!=null&&p.listChanged)&&i.sendToolListChanged()},s=o.inputSchema!==void 0,c={title:o.title,description:o.description,inputSchema:o.inputSchema,outputSchema:o.outputSchema,annotations:o.annotations,_meta:o._meta,enabled:!0,enable(){this.enabled=!0,a()},disable(){this.enabled=!1,a()},update(p){Object.assign(this,p),a()},remove(){i._registeredTools[r]===c&&(delete i._registeredTools[r],a())},handler:async(p,h)=>{if(!c.enabled)throw Error(`Tool ${r} is disabled`);let f;if(s){let _=c.inputSchema,w=_?await Go(_,p??{},`Invalid input for tool ${r}: `):p??{};f=await t(w,h)}else f=await t(h);return c.outputSchema&&!f.isError&&(f.structuredContent=await Go(c.outputSchema,f.structuredContent,`Invalid output for tool ${r}: `)),f}};return this._registeredTools[r]=c,!this._capabilities.tools&&!this.transport&&this.registerCapabilities({tools:{listChanged:!0}}),this.ensureToolHandlersInitialized(),a(),c}ensureToolHandlersInitialized(){this._toolHandlersInitialized||(this._toolHandlersInitialized=!0,this.oncalltool=async(r,o)=>{let t=this._registeredTools[r.name];if(!t)throw Error(`Tool ${r.name} not found`);return t.handler(r.arguments,o)},this.onlisttools=async(r,o)=>({tools:await Promise.all(Object.entries(this._registeredTools).filter(([t,i])=>i.enabled).map(async([t,i])=>{let a={name:t,title:i.title,description:i.description,inputSchema:i.inputSchema?await Ko(i.inputSchema,"input"):{type:"object",properties:{}}};return i.outputSchema&&(a.outputSchema=await Ko(i.outputSchema,"output")),i.annotations&&(a.annotations=i.annotations),i._meta&&(a._meta=i._meta),a}))}))}async sendToolListChanged(r={}){this._assertInitialized("sendToolListChanged"),await this.notification({method:"notifications/tools/list_changed",params:r})}getHostCapabilities(){return this._hostCapabilities}getHostVersion(){return this._hostInfo}getHostContext(){return this._hostContext}get ontoolinput(){return this.getEventHandler("toolinput")}set ontoolinput(r){this.setEventHandler("toolinput",r)}get ontoolinputpartial(){return this.getEventHandler("toolinputpartial")}set ontoolinputpartial(r){this.setEventHandler("toolinputpartial",r)}get ontoolresult(){return this.getEventHandler("toolresult")}set ontoolresult(r){this.setEventHandler("toolresult",r)}get ontoolcancelled(){return this.getEventHandler("toolcancelled")}set ontoolcancelled(r){this.setEventHandler("toolcancelled",r)}get onhostcontextchanged(){return this.getEventHandler("hostcontextchanged")}set onhostcontextchanged(r){this.setEventHandler("hostcontextchanged",r)}get onteardown(){return this._onteardown}set onteardown(r){this.warnIfRequestHandlerReplaced("onteardown",this._onteardown,r),this._onteardown=r,this.replaceRequestHandler(wh,(o,t)=>{if(!this._onteardown)throw Error("No onteardown handler set");return this._onteardown(o.params,t)})}get oncalltool(){return this._oncalltool}set oncalltool(r){this.warnIfRequestHandlerReplaced("oncalltool",this._oncalltool,r),this._oncalltool=r,this.replaceRequestHandler(xl,(o,t)=>{if(!this._oncalltool)throw Error("No oncalltool handler set");return this._oncalltool(o.params,t)})}get onlisttools(){return this._onlisttools}set onlisttools(r){this.warnIfRequestHandlerReplaced("onlisttools",this._onlisttools,r),this._onlisttools=r,this.replaceRequestHandler(zl,(o,t)=>{if(!this._onlisttools)throw Error("No onlisttools handler set");return this._onlisttools(o.params,t)})}assertCapabilityForMethod(r){var o;switch(r){case"sampling/createMessage":if(!((o=this._hostCapabilities)!=null&&o.sampling))throw Error(`Host does not support sampling (required for ${r})`);break}}assertRequestHandlerCapability(r){switch(r){case"tools/call":case"tools/list":if(!this._capabilities.tools)throw Error(`Client does not support tool capability (required for ${r})`);return;case"ping":case"ui/resource-teardown":return;default:throw Error(`No handler for method ${r} registered`)}}assertNotificationCapability(r){}assertTaskCapability(r){throw Error("Tasks are not supported in MCP Apps")}assertTaskHandlerCapability(r){throw Error("Task handlers are not supported in MCP Apps")}async callServerTool(r,o){if(this._assertInitialized("callServerTool"),typeof r=="string")throw Error(`callServerTool() expects an object as its first argument, but received a string ("${r}"). Did you mean: callServerTool({ name: "${r}", arguments: { ... } })?`);return await this.request({method:"tools/call",params:r},Wn,{onprogress:()=>{},resetTimeoutOnProgress:!0,...o})}async readServerResource(r,o){return this._assertInitialized("readServerResource"),await this.request({method:"resources/read",params:r},Sl,o)}async listServerResources(r,o){return this._assertInitialized("listServerResources"),await this.request({method:"resources/list",params:r},kl,o)}async createSamplingMessage(r,o){this._assertInitialized("createSamplingMessage");let t=r.tools?Ol:Nl;return await this.request({method:"sampling/createMessage",params:r},t,o)}sendMessage(r,o){return this._assertInitialized("sendMessage"),this.request({method:"ui/message",params:r},_h,o)}sendLog(r){return this.notification({method:"notifications/message",params:r})}updateModelContext(r,o){return this._assertInitialized("updateModelContext"),this.request({method:"ui/update-model-context",params:r},io,o)}openLink(r,o){return this._assertInitialized("openLink"),this.request({method:"ui/open-link",params:r},gh,o)}downloadFile(r,o){return this._assertInitialized("downloadFile"),this.request({method:"ui/download-file",params:r},vh,o)}requestTeardown(r={}){return this.notification({method:"ui/notifications/request-teardown",params:r})}requestDisplayMode(r,o){return this._assertInitialized("requestDisplayMode"),this.request({method:"ui/request-display-mode",params:r},xh,o)}sendSizeChanged(r){return this.notification({method:"ui/notifications/size-changed",params:r})}setupSizeChangedNotifications(){let r=!1,o=0,t=0,i=()=>{r||(r=!0,requestAnimationFrame(()=>{r=!1;let s=document.documentElement,c=s.style.height;s.style.height="max-content";let p=Math.ceil(s.getBoundingClientRect().height);s.style.height=c;let h=Math.ceil(window.innerWidth);(h!==o||p!==t)&&(o=h,t=p,this.sendSizeChanged({width:h,height:p}))}))};i();let a=new ResizeObserver(i);return a.observe(document.documentElement),a.observe(document.body),()=>a.disconnect()}async connect(r=new mh(window.parent,window.parent),o){var t;if(this.transport)throw Error("App is already connected. Call close() before connecting again.");this._initializedSent=!1,await super.connect(r);try{let i=await this.request({method:"ui/initialize",params:{appCapabilities:this._capabilities,appInfo:this._appInfo,protocolVersion:lh}},Th,o);if(i===void 0)throw Error(`Server sent invalid initialize result: ${i}`);this._hostCapabilities=i.hostCapabilities,this._hostInfo=i.hostInfo,this._hostContext=i.hostContext,await this.notification({method:"ui/notifications/initialized"}),this._initializedSent=!0,(t=this.options)!=null&&t.autoResize&&this.setupSizeChangedNotifications()}catch(i){throw this.close(),i}}};D(Ht,"ONE_SHOT_EVENTS",new Set(["toolinput","toolinputpartial","toolresult","toolcancelled"]));let sr=Ht;function Qo(e){const n=e&&e.structuredContent;if(n&&typeof n=="object"){if(typeof n.snapshot=="string")try{return JSON.parse(n.snapshot)}catch{}return n}const r=(e&&e.content||[]).find(o=>o&&o.type==="text");if(r!=null&&r.text)try{return JSON.parse(r.text)}catch{return{}}return{}}class Rh{constructor(){D(this,"onhostcontextchanged");D(this,"onerror");D(this,"ontoolresult")}async connect(){var n,r;try{const[o,t]=await Promise.all([this.callServerTool({name:"project_config",arguments:{status:!0}}),this.callServerTool({name:"runtime",arguments:{action:"status"}})]),i=Qo(o),a=Qo(t),s=Array.isArray(i.projects)?i.projects:[],c={projects:s,projects_total:s.length,projects_ready:s.filter(p=>p&&p.ready).length,x_connected:!!i.x_connected,x_state:i.x_state||"",x_handle:i.x_handle??null,version:i.mcp_version||"",latest_version:i.latest_version??null,update_available:!!i.update_available,runtime_ready:typeof a.runtime_ready=="boolean"?a.runtime_ready:!0,runtime_provisioning:!!a.provisioning,onboarding:a.onboarding||i.onboarding};(n=this.ontoolresult)==null||n.call(this,{structuredContent:{snapshot:JSON.stringify(c)}})}catch(o){(r=this.onerror)==null||r.call(this,o)}}getHostContext(){}async callServerTool(n){const r=await fetch(`/tool/${encodeURIComponent(n.name)}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n.arguments??{})});if(!r.ok){let o=`HTTP ${r.status}`;try{o=await r.text()||o}catch{}return{isError:!0,content:[{type:"text",text:o}]}}return r.json()}async sendMessage(){return{isError:!0}}}function Ch(){return globalThis.__S4L_BRIDGE__==="http"?new Rh:new sr({name:"S4L Panel",version:"1.0.0"})}function Pl(e){const n=e.structuredContent;if(n&&typeof n=="object"){if(typeof n.snapshot=="string")try{return JSON.parse(n.snapshot)}catch{}return n}const r=(e.content||[]).find(o=>o.type==="text");if(r!=null&&r.text)try{return JSON.parse(r.text)}catch{return{_raw:r.text}}return{}}const M=e=>document.getElementById(e),Ul=M("ver"),Ge=M("btn-setup"),Lt=M("btn-schedule"),Nt=M("stats-grid"),Gn=M("stats-toggle"),Zh=M("log"),Ah=M("install-card"),Ie=M("setup-summary"),El=M("onboarding-details"),Lh=M("onboarding-steps"),Qn=M("onboarding-blocker"),Yo=M("onboarding-count"),Mh=M("onboarding-bar-fill"),qh=M("live-card"),Hh=M("stats-card"),Xo=M("install-steps"),Ee=M("install-err"),ge=M("btn-install"),Fh=M("menubar-banner"),ea=M("btn-menubar-restart"),So=M("btn-live"),wo=M("btn-live-stop"),ta=M("btn-live-front"),De=M("live-status"),Mt=M("live-img"),Dl=M("switch-personal"),Rl=M("switch-promo"),Jh=M("mode-sub"),Bh=M("settings-card"),Yn=M("settings-toggle"),fe=M("settings-body");let A=null,Xn=!1,Ot=!1,er=!1,Re=!1,We=!1,Te=!1,tr=!1;function W(e){Zh.textContent=e}function Vh(e){switch(e){case"done":return"✓";case"running":return"…";case"error":return"×";default:return"·"}}function Io(e){if(!e||!Array.isArray(e.steps)){Xo.innerHTML="";return}Xo.innerHTML=e.steps.map(n=>{const r=n.detail&&n.status!=="pending"?` <span class="detail">${n.status==="error"?n.detail:""}</span>`:"";return`<li class="${n.status}"><span class="glyph">${Vh(n.status)}</span><span>${n.label}${r}</span></li>`}).join(""),e.error?(Ee.textContent=e.error,Ee.hidden=!1):Ee.hidden=!0}const Wh={environment_checked:"Environment checked",runtime_ready:"Runtime ready",x_connected:"X connected",profile_scanned:"Profile scanned",project_ready:"Project ready",topics_seeded:"Topics seeded",tasks_scheduled:"Tasks scheduled"};function Kh(e){switch(e){case"complete":return"✓";case"in_progress":return"…";case"blocked":return"×";default:return"·"}}function Cl(){El.hidden=!Re,Ie.setAttribute("aria-expanded",String(Re)),Ie.classList.toggle("expanded",Re)}function Zl(e){return e?e.setup_complete!==void 0?!!e.setup_complete:!!e.runtime_ready&&(e.projects_ready||0)>0&&!!e.x_connected:!1}function Gh(e){if(!e||!Array.isArray(e.milestones)){Ie.hidden=!0,El.hidden=!0;return}Ie.hidden=!1;const n=e.milestones.length,r=e.milestones.filter(i=>i.status==="complete").length,o=Zl(A),t=!!e.current_blocker&&!o;Ie.classList.toggle("complete",o),Ie.classList.toggle("blocked",t),Yo.hidden=o,Yo.textContent=t?`${r}/${n} · needs you`:Ot?`${r}/${n} · setting up…`:`${r}/${n}`,Mh.style.width=n>0?`${Math.round(r/n*100)}%`:"0%",Lh.innerHTML=e.milestones.map(i=>{const a=Wh[i.id]||i.id,s=i.attempts>1?` <span class="detail">${i.attempts} attempts</span>`:"";return`<li class="${i.status}"><span class="glyph">${Kh(i.status)}</span><span>${a}${s}</span></li>`}).join(""),e.current_blocker?(Qn.textContent=`Current blocker: ${e.current_blocker.message}`,Qn.hidden=!1,Re=!0):Qn.hidden=!0,Cl()}function qt(){if(!A)return;Gh(A.onboarding),Ul.innerHTML=A.update_available&&A.latest_version?`v${A.version} · <button id="btn-update" class="update-btn">Update to ${A.latest_version}</button>`:`v${A.version}`;const e=!A.runtime_ready;Ah.hidden=!e,Fh.hidden=e||A.menubar_running!==!1;const n=Zl(A);Ge.hidden=n,Ge.disabled=!1,Ge.classList.toggle("primary",!n);const r=n&&(A.schedule_state==="missing"||A.schedule_state==="disabled");Lt.hidden=!r,Lt.classList.toggle("primary",r);const o=A.flags||(A.mode==="promotion"?{personal_brand:!1,promotion:!0}:{personal_brand:!0,promotion:!1}),t=!!o.personal_brand,i=!!o.promotion;Dl.setAttribute("aria-checked",String(t)),Rl.setAttribute("aria-checked",String(i)),Jh.textContent=t&&i?"Both lanes on: cycles split 50/50.":!t&&!i?"No lane on; the cycle falls back to personal brand.":"",qh.hidden=!n,Hh.hidden=!n,Bh.hidden=e}function ye(e){A={...A||{},...e},qt()}function Qh(e){const n=Array.isArray(e.projects)?e.projects:[];return{projects:n,projects_total:n.length,projects_ready:n.filter(r=>r.ready).length,x_connected:!!e.x_connected,x_state:e.x_state||"",x_handle:e.x_handle??null,...e.setup_complete!==void 0?{setup_complete:!!e.setup_complete}:{},version:e.mcp_version||(A==null?void 0:A.version)||"",latest_version:e.latest_version??null,update_available:!!e.update_available,mode:e.mode??(A==null?void 0:A.mode),flags:e.flags??(A==null?void 0:A.flags),onboarding:e.onboarding}}const $e=Ch();function Al(e){var n,r,o;e.theme&&Uh(e.theme),(n=e.styles)!=null&&n.variables&&Eh(e.styles.variables),(o=(r=e.styles)==null?void 0:r.css)!=null&&o.fonts&&Dh(e.styles.css.fonts)}$e.onhostcontextchanged=Al;$e.onerror=e=>console.error(e);$e.ontoolresult=e=>{const n=Pl(e);n&&typeof n.projects_total=="number"&&(ye(n),n.runtime_ready?Ll():n.runtime_provisioning&&zo())};async function oe(e,n={}){const r=await $e.callServerTool({name:e,arguments:n});return Pl(r)}async function Fe(){W("Refreshing…");try{const[e,n]=await Promise.all([oe("project_config",{status:!0}),oe("runtime",{action:"status"}).catch(()=>({}))]);ye({...Qh(e),...typeof n.runtime_ready=="boolean"?{runtime_ready:n.runtime_ready}:{},...typeof n.menubar_running=="boolean"?{menubar_running:n.menubar_running}:{},onboarding:n.onboarding||e.onboarding||(A==null?void 0:A.onboarding)}),A&&!A.runtime_ready&&n.provisioning&&zo(),W(""),Ll()}catch(e){W("Refresh failed: "+((e==null?void 0:e.message)||e))}}async function zo(){if(!Xn){Xn=!0,ge.disabled=!0,ge.textContent="Installing…";try{for(;;){const e=await oe("runtime",{action:"status"}).catch(()=>({}));if(Io(e.progress??null),e.onboarding&&ye({onboarding:e.onboarding}),e.runtime_ready){ye({runtime_ready:!0}),W("Runtime installed; you're ready to set up."),Fe();return}const n=e.progress??null;if(n&&n.done&&!n.ok){ge.disabled=!1,ge.textContent="Retry install",W("Install failed; see the step above, then Retry.");return}await new Promise(r=>setTimeout(r,1500))}}finally{Xn=!1}}}async function Yh(){var r;if(Ot)return;Ot=!0,qt();const e=Date.now(),n=1200*1e3;try{for(;;){const o=await oe("runtime",{action:"status"}).catch(()=>({}));o.progress&&Io(o.progress);const t={};if(typeof o.runtime_ready=="boolean"&&(t.runtime_ready=o.runtime_ready),o.onboarding&&(t.onboarding=o.onboarding),Object.keys(t).length&&ye(t),(r=o.onboarding)!=null&&r.complete){await Fe(),W("Setup complete.");break}if(Date.now()-e>n)break;await new Promise(i=>setTimeout(i,2e3))}}finally{Ot=!1,qt()}}async function Ll(){try{const e=await oe("get_stats",{days:7}),n=Array.isArray(e.projects)?e.projects[0]:null,r=n==null?void 0:n.posts;if(!r){Nt.innerHTML='<div class="muted">No stats yet.</div>';return}const o=[["Posts",r.total??0],["Active",r.active??0],["Views",r.views_period_total??r.views??0],["Replies",r.comments_period_total??r.comments??0],["Clicks",r.post_clicks_period_total??0]];Nt.innerHTML=o.map(([t,i])=>`<div class="stat"><div class="n">${i}</div><div class="l">${t}</div></div>`).join("")}catch(e){Nt.innerHTML=`<div class="muted">Stats unavailable: ${(e==null?void 0:e.message)||e}</div>`}}function Kn(e,n,r){const o=e.textContent;e.disabled=!0,e.textContent=n,r().finally(()=>{e.disabled=!1,e.textContent=o,qt()})}Ge.addEventListener("click",()=>Kn(Ge,"Starting…",async()=>{W("Asking Claude to run setup…");try{const e=await $e.sendMessage({role:"user",content:[{type:"text",text:"Set up S4L plugin end to end now. Inspect and repair the runtime, auto-detect and connect my X session, scan my profile, discover and research my product, then infer and save a complete project with seeded search topics. Keep going without asking me to approve each safe setup step. Ask only if I must interactively sign in or no product can be identified. Keep every reply to me extremely concise: a few short sentences at most, no step-by-step narration or long status walls. If you must ask me something (e.g. the product URL), make it one short question."}]});e!=null&&e.isError?W("The host rejected the setup request — type “set up S4L” in the chat instead."):(W("Setup is running in the chat. It will only stop for an unavoidable login or missing product."),Yh())}catch(e){W("Couldn’t start setup: "+((e==null?void 0:e.message)||e))}}));Lt.addEventListener("click",()=>Kn(Lt,"Setting up…",async()=>{W("Asking Claude to schedule the draft tasks for this account…");try{const e=await $e.sendMessage({role:"user",content:[{type:"text",text:'Set up the S4L draft autopilot schedule for this Claude account. If queue_setup is available, call it; then for s4l-worker call the host tool create_scheduled_task with taskId, cronExpression "* * * * *", and the prompt — read it from ~/.claude/scheduled-tasks/s4l-worker/SKILL.md (already on disk). Do NOT redo my X connection or project setup. Keep replies to me very short.'}]});e!=null&&e.isError?W("The host rejected it — type “set up the draft schedule” in the chat instead."):W("Scheduling is running in the chat. The draft tasks will register for this account.")}catch(e){W("Couldn’t start scheduling: "+((e==null?void 0:e.message)||e))}}));function Ml(e,n){e.addEventListener("click",async()=>{if(!e.disabled){e.disabled=!0,e.setAttribute("aria-checked",String(e.getAttribute("aria-checked")!=="true"));try{const r=await oe("engagement_mode",{action:"toggle",lane:n});r&&r.flags&&ye({flags:r.flags}),await Fe()}catch(r){W("Couldn’t switch lane: "+((r==null?void 0:r.message)||r)),await Fe()}finally{e.disabled=!1}}})}Ml(Dl,"personal_brand");Ml(Rl,"promotion");Ie.addEventListener("click",()=>{Re=!Re,Cl()});Gn.addEventListener("click",()=>{We=!We,Nt.hidden=!We,Gn.setAttribute("aria-expanded",String(We)),Gn.classList.toggle("expanded",We)});Ul.addEventListener("click",e=>{const n=e.target;n&&n.id==="btn-update"&&Xh()});async function Xh(){if(er)return;er=!0;const e=document.getElementById("btn-update");e&&(e.disabled=!0,e.textContent="Updating…"),W("Installing the latest release… this can take a minute.");try{const n=await oe("runtime",{action:"update"});n.ok?(W(`Updated to ${n.latest_published||"the latest version"}. ${n.takes_effect||"Restart the client to apply."}`),e&&(e.textContent="Update installed — restart to apply")):(W("Update failed (exit "+(n.exit_code??"?")+"). Try `npx social-autoposter@latest update` in a terminal."),e&&(e.disabled=!1,e.textContent="Retry update"))}catch(n){W("Update failed: "+((n==null?void 0:n.message)||n)),e&&(e.disabled=!1,e.textContent="Retry update")}finally{er=!1}}ge.addEventListener("click",async()=>{Ee.hidden=!0,ge.disabled=!0,ge.textContent="Starting…",W("Installing the runtime — this is a one-time download (~150MB+).");try{const e=await oe("runtime",{action:"install"});if(e.runtime_ready){ye({runtime_ready:!0}),Fe();return}Io(e.progress??null),zo()}catch(e){ge.disabled=!1,ge.textContent="Retry install",Ee.textContent="Couldn't start install: "+((e==null?void 0:e.message)||e),Ee.hidden=!1}});ea.addEventListener("click",()=>Kn(ea,"Restarting…",async()=>{W("Restarting the S4L menu bar…");try{const e=await oe("restart_menubar");typeof e.menubar_running=="boolean"&&ye({menubar_running:e.menubar_running}),W(e.menubar_running?"Menu bar restarted.":"Couldn’t confirm the menu bar came back"+(e.detail?": "+e.detail:"."))}catch(e){W("Couldn’t restart the menu bar: "+((e==null?void 0:e.message)||e))}}));const eg=["website","description","icp","voice","differentiator","search_topics","get_started_link","content_guardrails"],tg=["description","voice","search_topics","content_angle","content_guardrails"],cr={website:"Website",description:"What it does",icp:"Target audience",voice:"Voice",differentiator:"Differentiator",search_topics:"Search topics",get_started_link:"Get-started link",content_guardrails:"Content guardrails",content_angle:"Content angle"},na=new Set(["website","description","icp","voice","differentiator","search_topics","get_started_link","content_guardrails"]),ng=new Set(["website","get_started_link"]);function Qe(e){const n=e.replace(/[_-]+/g," ").trim();return n?n.charAt(0).toUpperCase()+n.slice(1):e}function ql(e){return!!e&&typeof e=="object"&&!Array.isArray(e)}function ra(e){return Array.isArray(e)&&e.every(n=>typeof n=="string")?{kind:"list",text:e.join(`
74
74
  `)}:e==null||typeof e=="string"?{kind:"text",text:String(e??"")}:{kind:"json",text:JSON.stringify(e,null,2)}}function ia(e,n,r){const o=document.createElement("div");o.className="settings-field";const t=document.createElement("label");t.textContent=e+(n.kind==="list"?" (one per line)":""),o.appendChild(t);let i;if(n.kind==="text"&&r)i=document.createElement("input"),i.type="text",i.className="settings-input";else{const a=document.createElement("textarea"),s=n.text?n.text.split(`
75
75
  `).reduce((c,p)=>c+Math.max(1,Math.ceil(p.length/60)),0):1;a.rows=Math.min(10,Math.max(2,s)),a.className="settings-textarea"+(n.kind==="json"?" mono":""),i=a}return i.value=n.text,n.text||(i.placeholder="Not set"),i.dataset.orig=n.text,o.appendChild(i),{wrap:o,el:i}}function rg(e){const n=document.createElement("div");n.className="settings-project";const r=document.createElement("div");r.className="settings-project-head";const o=document.createElement("span");if(o.className="settings-project-name",o.textContent=e.name,r.appendChild(o),e.persona){const f=document.createElement("span");f.className="settings-project-tag",f.textContent="personal brand",r.appendChild(f)}const t=document.createElement("span");t.className="settings-project-state",t.textContent=e.ready?"ready":"missing: "+e.missing_required.join(", "),r.appendChild(t),n.appendChild(r);const i=[],a=e.persona?tg:eg;for(const f of a){const _=e.fields[f];if(ql(_)&&Object.keys(_).length){const z=document.createElement("fieldset");z.className="settings-group";const S=document.createElement("legend");S.textContent=cr[f]||Qe(f),z.appendChild(S);for(const[g,$]of Object.entries(_)){const y=ra($),I=ia(Qe(g),y,!1);z.appendChild(I.wrap),i.push({key:f,sub:g,kind:y.kind,el:I.el,orig:y.text})}n.appendChild(z);continue}const w=f==="search_topics"?{kind:"list",text:Array.isArray(_)?_.map(String).join(`
76
76
  `):String(_??"")}:ra(_),b=ia(cr[f]||Qe(f),w,ng.has(f));n.appendChild(b.wrap),i.push({key:f,kind:w.kind,el:b.el,orig:w.text})}if(e.extra_keys.length){const f=document.createElement("div");f.className="settings-extra",f.textContent="Advanced (edit via chat): "+e.extra_keys.join(", "),n.appendChild(f)}const s=document.createElement("div");s.className="settings-actions";const c=document.createElement("button");c.className="primary",c.textContent="Save changes",c.disabled=!0;const p=document.createElement("span");p.className="settings-status",s.appendChild(c),s.appendChild(p),n.appendChild(s);const h=()=>{c.disabled=!i.some(f=>f.el.value!==f.orig)};for(const f of i)f.el.addEventListener("input",h);return c.addEventListener("click",()=>void ig(e,i,c,p)),n}async function ig(e,n,r,o){const t=n.filter(h=>h.el.value!==h.orig);if(!t.length)return;const i={name:e.name},a={},s=h=>(cr[h.key]||Qe(h.key))+(h.sub?` · ${Qe(h.sub)}`:""),c=[...new Set(t.filter(h=>h.sub!==void 0).map(h=>h.key))];for(const h of c){const f=ql(e.fields[h])?{...e.fields[h]}:{};for(const _ of n.filter(w=>w.key===h&&w.sub!==void 0)){const w=_.el.value;if(_.kind==="list"){const b=w.split(`