@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.
- package/mcp/dist/index.js +99 -68
- package/mcp/dist/panel.html +1 -1
- package/mcp/dist/product-link.html +9 -9
- package/mcp/dist/repo.js +1 -1
- package/mcp/dist/runtime.js +7 -13
- package/mcp/dist/setup.js +2 -2
- package/mcp/dist/telemetry.js +6 -8
- package/mcp/dist/version.js +1 -1
- package/mcp/dist/version.json +2 -2
- package/mcp/install.mjs +2 -2
- package/mcp/manifest.json +1 -1
- package/mcp/menubar/dashboard_server.py +1 -4
- package/mcp/menubar/s4l_card.py +171 -42
- package/mcp/menubar/s4l_menubar.py +80 -28
- package/mcp/menubar/s4l_state.py +20 -2
- package/mcp/package.json +2 -2
- package/mcp/shared/doctor.cjs +0 -2
- package/mcp/shared/onboarding-ledger.cjs +0 -1
- package/mcp-servers/browser-harness/server.py +2 -2
- package/package.json +1 -1
- package/requirements.txt +3 -3
- package/scripts/active_experiments.py +112 -0
- package/scripts/claude_job.py +15 -2
- package/scripts/copy_browser_cookies.py +1 -1
- package/scripts/dev_dashboard_server.py +1 -1
- package/scripts/feedback_digest.py +18 -1
- package/scripts/harness_overlay.py +17 -17
- package/scripts/log_draft.py +1 -1
- package/scripts/log_post.py +3 -3
- package/scripts/memory_snapshot.py +3 -3
- package/scripts/merge_review_queue.py +6 -0
- package/scripts/reap_stale_claude_sessions.py +3 -3
- package/scripts/relay_provider_log.py +186 -0
- package/scripts/salvage_orphaned_prep_results.py +178 -0
- package/scripts/setup_twitter_auth.py +1 -1
- package/scripts/twitter_post_plan.py +88 -13
- package/scripts/watchdog_hung_runs.py +12 -12
- package/skill/engage-linkedin.sh +4 -4
- package/skill/engage-twitter.sh +4 -4
- package/skill/lib/linkedin-backend.sh +1 -1
- package/skill/run-draft-and-publish.sh +59 -10
- package/skill/run-instagram-render.sh +3 -3
- package/skill/run-linkedin.sh +5 -5
- package/skill/run-reddit-threads.sh +5 -5
- package/skill/run-twitter-cycle.sh +91 -50
- package/skill/social-autoposter-update.sh +3 -1
- package/skill/styles.sh +10 -10
- package/skill/topics.sh +8 -8
- package/skill/run-cycle-update-guard.sh +0 -44
- package/skill/run-twitter-cycle-launchd.sh +0 -63
- 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(
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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(), `
|
|
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 = "
|
|
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(
|
|
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 =
|
|
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
|
|
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:
|
|
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
|
-
//
|
|
2986
|
-
//
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
3667
|
+
return path.join(s4lStateDir(), "posting-active.json");
|
|
3692
3668
|
}
|
|
3693
3669
|
function writePostingFlag() {
|
|
3694
3670
|
try {
|
|
3695
|
-
fs.mkdirSync(
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
package/mcp/dist/panel.html
CHANGED
|
@@ -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(`
|