@m13v/s4l 1.6.197-rc.7
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/README.md +143 -0
- package/SKILL.md +342 -0
- package/bin/cli.js +980 -0
- package/bin/cookie-helper.js +315 -0
- package/bin/platform.js +59 -0
- package/bin/scheduler/index.js +12 -0
- package/bin/scheduler/launchd.js +518 -0
- package/browser-agent-configs/all-agents-mcp.json +68 -0
- package/browser-agent-configs/linkedin-agent-mcp.json +16 -0
- package/browser-agent-configs/linkedin-agent.json +17 -0
- package/browser-agent-configs/linkedin-harness-mcp.json +21 -0
- package/browser-agent-configs/reddit-agent-mcp.json +16 -0
- package/browser-agent-configs/reddit-agent.json +17 -0
- package/browser-agent-configs/twitter-harness-mcp.json +18 -0
- package/config.example.json +45 -0
- package/mcp/dist/index.js +4212 -0
- package/mcp/dist/onboarding.js +200 -0
- package/mcp/dist/panel.html +176 -0
- package/mcp/dist/product-link.html +102 -0
- package/mcp/dist/repo.js +222 -0
- package/mcp/dist/runtime.js +1079 -0
- package/mcp/dist/screencast.js +323 -0
- package/mcp/dist/setup.js +545 -0
- package/mcp/dist/telemetry.js +306 -0
- package/mcp/dist/twitterAuth.js +138 -0
- package/mcp/dist/version.js +271 -0
- package/mcp/dist/version.json +4 -0
- package/mcp/install-runtime.mjs +70 -0
- package/mcp/install.mjs +169 -0
- package/mcp/manifest.json +80 -0
- package/mcp/menubar/dashboard_server.py +213 -0
- package/mcp/menubar/s4l_card.py +1314 -0
- package/mcp/menubar/s4l_log_relay.py +179 -0
- package/mcp/menubar/s4l_menubar.py +2439 -0
- package/mcp/menubar/s4l_state.py +891 -0
- package/mcp/package.json +34 -0
- package/mcp/shared/doctor.cjs +437 -0
- package/mcp/shared/onboarding-ledger.cjs +324 -0
- package/mcp-servers/browser-harness/server.py +968 -0
- package/package.json +160 -0
- package/requirements.txt +20 -0
- package/scripts/_compute_allowlist.py +58 -0
- package/scripts/_db_update.py +20 -0
- package/scripts/_filt.py +9 -0
- package/scripts/_li_notif_match.py +76 -0
- package/scripts/_li_notif_orchestrate.py +126 -0
- package/scripts/_lock_preempt_test.py +60 -0
- package/scripts/_run_icp_precheck.py +57 -0
- package/scripts/a16z_pearx_calendar_reminders.py +99 -0
- package/scripts/account_resolver.py +141 -0
- package/scripts/active_campaigns.py +114 -0
- package/scripts/active_users.py +190 -0
- package/scripts/amplitude_24h_signups.py +468 -0
- package/scripts/amplitude_signups.py +177 -0
- package/scripts/apply_onboarding_selections.py +131 -0
- package/scripts/audience_pages.py +243 -0
- package/scripts/audit_helper.py +120 -0
- package/scripts/author_history_block.py +353 -0
- package/scripts/autopilot_stall_watch.py +284 -0
- package/scripts/backfill_twitter_attempts_topic.py +81 -0
- package/scripts/backfill_twitter_log_post_no_id.py +322 -0
- package/scripts/bench_dashboard.sh +138 -0
- package/scripts/bh_send.py +39 -0
- package/scripts/build_persona.py +409 -0
- package/scripts/bulk_icp.py +18 -0
- package/scripts/campaign_bump.py +51 -0
- package/scripts/capture_thread_media.py +288 -0
- package/scripts/check_browser_lock_health.sh +81 -0
- package/scripts/check_external_pool_depth.py +253 -0
- package/scripts/check_unread_web_chats.py +28 -0
- package/scripts/claim_web_chat.py +47 -0
- package/scripts/classify_run_error.py +158 -0
- package/scripts/claude_job.py +988 -0
- package/scripts/clean_stale_singleton.sh +56 -0
- package/scripts/cleanup_harness_tabs.py +68 -0
- package/scripts/copy_browser_cookies.py +454 -0
- package/scripts/counterparty_history.py +350 -0
- package/scripts/db.py +57 -0
- package/scripts/discover_claude_profiles.py +120 -0
- package/scripts/discover_linkedin_candidates.py +984 -0
- package/scripts/dm_conversation.py +682 -0
- package/scripts/dm_db_update.py +69 -0
- package/scripts/dm_engage_helper.py +161 -0
- package/scripts/dm_outreach_helper.py +147 -0
- package/scripts/dm_outreach_twitter_helper.py +129 -0
- package/scripts/dm_send_log.py +106 -0
- package/scripts/dm_short_links.py +1084 -0
- package/scripts/dump_web_chat_history.py +47 -0
- package/scripts/engage_github.py +640 -0
- package/scripts/engage_reddit.py +1235 -0
- package/scripts/engage_twitter_helper.py +301 -0
- package/scripts/engagement_styles.py +1787 -0
- package/scripts/enrich_twitter_candidates.py +82 -0
- package/scripts/feedback_digest.py +448 -0
- package/scripts/fetch_prospect_profile.py +312 -0
- package/scripts/fetch_twitter_t1.py +134 -0
- package/scripts/find_threads.py +530 -0
- package/scripts/follow_gate_log.py +59 -0
- package/scripts/funnel_per_day.py +194 -0
- package/scripts/generate_daily_human_style.py +494 -0
- package/scripts/generation_trace.py +173 -0
- package/scripts/get_run_cost.py +107 -0
- package/scripts/github_engage_helper.py +93 -0
- package/scripts/github_tools.py +509 -0
- package/scripts/harness_overlay.py +556 -0
- package/scripts/harvest_twitter_following.py +243 -0
- package/scripts/heartbeat.sh +70 -0
- package/scripts/history_context.py +284 -0
- package/scripts/http_api.py +206 -0
- package/scripts/human_dm_replies_helper.py +169 -0
- package/scripts/identity.py +302 -0
- package/scripts/ig_batch_creator.sh +93 -0
- package/scripts/ig_post_type_picker.py +243 -0
- package/scripts/ig_scrape_transcribe.sh +91 -0
- package/scripts/ingest_human_dm_replies.py +271 -0
- package/scripts/ingest_web_chat_replies.py +229 -0
- package/scripts/install_fleet.py +187 -0
- package/scripts/invent_mcp_server.py +350 -0
- package/scripts/invent_topics.py +1462 -0
- package/scripts/learned_preferences.py +263 -0
- package/scripts/li_discovery.py +161 -0
- package/scripts/link_edit_helper.py +142 -0
- package/scripts/link_tail.py +592 -0
- package/scripts/linkedin_api.py +561 -0
- package/scripts/linkedin_browser.py +730 -0
- package/scripts/linkedin_cooldown.py +128 -0
- package/scripts/linkedin_exclusions.py +234 -0
- package/scripts/linkedin_killswitch.py +1333 -0
- package/scripts/linkedin_search_topic_schema.py +49 -0
- package/scripts/linkedin_unipile.py +658 -0
- package/scripts/linkedin_url.py +228 -0
- package/scripts/log_claude_session.py +636 -0
- package/scripts/log_draft.py +143 -0
- package/scripts/log_linkedin_search_attempts.py +126 -0
- package/scripts/log_post.py +651 -0
- package/scripts/log_run.py +364 -0
- package/scripts/log_thread_media.py +108 -0
- package/scripts/log_twitter_search_attempts.py +150 -0
- package/scripts/log_twitter_skips.py +211 -0
- package/scripts/lookup_post.py +78 -0
- package/scripts/mark_web_chat_processed.py +32 -0
- package/scripts/mcp_lock_proxy.py +370 -0
- package/scripts/memory_snapshot.py +972 -0
- package/scripts/merge_review_queue.py +215 -0
- package/scripts/mint_external_pool.py +182 -0
- package/scripts/mint_kent_pool.py +249 -0
- package/scripts/moltbook_post.py +320 -0
- package/scripts/moltbook_tools.py +159 -0
- package/scripts/pending_threads.py +188 -0
- package/scripts/pick_ig_account.py +177 -0
- package/scripts/pick_project.py +208 -0
- package/scripts/pick_search_topic.py +771 -0
- package/scripts/pick_thread_target.py +279 -0
- package/scripts/pick_twitter_thread_target.py +202 -0
- package/scripts/podlog_fetch_batch.sh +32 -0
- package/scripts/post_github.py +1311 -0
- package/scripts/post_reddit.py +2668 -0
- package/scripts/precompute_dashboard_stats.py +204 -0
- package/scripts/preflight.sh +297 -0
- package/scripts/progress.py +88 -0
- package/scripts/project_excludes.py +353 -0
- package/scripts/project_slugs.py +91 -0
- package/scripts/project_stats.py +241 -0
- package/scripts/project_stats_json.py +1563 -0
- package/scripts/project_topics.py +192 -0
- package/scripts/qualified_query_bank.py +436 -0
- package/scripts/reap_stale_claude_sessions.py +867 -0
- package/scripts/reddit_browser.py +2549 -0
- package/scripts/reddit_browser_fetch.py +141 -0
- package/scripts/reddit_browser_lock.py +593 -0
- package/scripts/reddit_chat_sync.py +710 -0
- package/scripts/reddit_query_bank.py +200 -0
- package/scripts/reddit_threads_helper.py +151 -0
- package/scripts/reddit_tools.py +956 -0
- package/scripts/refresh_instagram_tokens.py +280 -0
- package/scripts/release-mcpb.sh +497 -0
- package/scripts/reply_db.py +334 -0
- package/scripts/reply_insert.py +98 -0
- package/scripts/reply_risk_digest.py +761 -0
- package/scripts/reset-test-machine.sh +602 -0
- package/scripts/restore_twitter_session.py +177 -0
- package/scripts/ripen_reddit_plan.py +478 -0
- package/scripts/run_claude.sh +433 -0
- package/scripts/run_moltbook_cycle.py +555 -0
- package/scripts/s4l_box_update.sh +226 -0
- package/scripts/s4l_channel.py +103 -0
- package/scripts/s4l_ctl.sh +75 -0
- package/scripts/s4l_env.py +47 -0
- package/scripts/saps_activity.py +126 -0
- package/scripts/saps_mode.py +328 -0
- package/scripts/scan_dm_candidates.py +580 -0
- package/scripts/scan_github_replies.py +168 -0
- package/scripts/scan_instagram_comments.py +481 -0
- package/scripts/scan_moltbook_replies.py +252 -0
- package/scripts/scan_pii.py +190 -0
- package/scripts/scan_reddit_replies.py +377 -0
- package/scripts/scan_twitter_mentions_browser.py +327 -0
- package/scripts/scan_twitter_thread_followups.py +299 -0
- package/scripts/scan_x_profile.py +384 -0
- package/scripts/schedule_state.py +202 -0
- package/scripts/scheduled_tasks_snapshot.py +123 -0
- package/scripts/score_linkedin_candidates.py +419 -0
- package/scripts/score_twitter_candidates.py +718 -0
- package/scripts/scrape_linkedin_comment_stats.py +1755 -0
- package/scripts/scrape_linkedin_stats_browser.py +52 -0
- package/scripts/scrape_reddit_views.py +365 -0
- package/scripts/seed_search_queries.py +453 -0
- package/scripts/seed_search_topics.py +127 -0
- package/scripts/send_web_chat_reply.py +130 -0
- package/scripts/sentry_init.py +128 -0
- package/scripts/setup_twitter_auth.py +1320 -0
- package/scripts/snapshot.py +583 -0
- package/scripts/stats.py +2702 -0
- package/scripts/stats_helper.py +52 -0
- package/scripts/strike_alert.py +783 -0
- package/scripts/sweep_post_link_clicks.py +107 -0
- package/scripts/sync_ig_to_posts.py +147 -0
- package/scripts/test_browser_lock.py +189 -0
- package/scripts/test_installation_api.sh +52 -0
- package/scripts/test_percard_posting.py +142 -0
- package/scripts/top_dud_linkedin_queries.py +71 -0
- package/scripts/top_dud_reddit_queries.py +67 -0
- package/scripts/top_dud_twitter_queries.py +71 -0
- package/scripts/top_dud_twitter_topics.py +102 -0
- package/scripts/top_linkedin_queries.py +55 -0
- package/scripts/top_omitted_reddit_topics.py +91 -0
- package/scripts/top_performers.py +588 -0
- package/scripts/top_search_topics.py +180 -0
- package/scripts/top_twitter_queries.py +190 -0
- package/scripts/twitter_access_check.py +382 -0
- package/scripts/twitter_account.py +41 -0
- package/scripts/twitter_batch_phase.py +126 -0
- package/scripts/twitter_browser.py +2804 -0
- package/scripts/twitter_cookie_mirror.py +130 -0
- package/scripts/twitter_cycle_helper.py +310 -0
- package/scripts/twitter_gen_links.py +287 -0
- package/scripts/twitter_post_plan.py +1188 -0
- package/scripts/twitter_scan.py +324 -0
- package/scripts/twitter_supply_signal.py +57 -0
- package/scripts/twitter_threads_helper.py +152 -0
- package/scripts/unclaim_web_chat.py +29 -0
- package/scripts/update_instagram_stats.py +261 -0
- package/scripts/update_linkedin_stats_from_feed.py +328 -0
- package/scripts/version.py +72 -0
- package/scripts/watchdog_hung_runs.py +343 -0
- package/scripts/write_generation_trace.py +73 -0
- package/setup/SKILL.md +277 -0
- package/skill/amplitude-24h-signups.sh +38 -0
- package/skill/archive-old-logs.sh +40 -0
- package/skill/audit-dm-staleness.sh +42 -0
- package/skill/audit-linkedin.sh +14 -0
- package/skill/audit-moltbook.sh +4 -0
- package/skill/audit-reddit-resurrect.sh +67 -0
- package/skill/audit-reddit.sh +4 -0
- package/skill/audit-twitter.sh +4 -0
- package/skill/audit.sh +287 -0
- package/skill/backfill-twitter-attempts-topic.sh +19 -0
- package/skill/backfill-twitter-ghost-posts.sh +24 -0
- package/skill/check-external-pool-depth.sh +7 -0
- package/skill/check-web-chats.sh +203 -0
- package/skill/dm-outreach-linkedin.sh +250 -0
- package/skill/dm-outreach-reddit.sh +274 -0
- package/skill/dm-outreach-twitter.sh +265 -0
- package/skill/engage-dm-replies-linkedin.sh +4 -0
- package/skill/engage-dm-replies-reddit.sh +4 -0
- package/skill/engage-dm-replies-twitter.sh +4 -0
- package/skill/engage-dm-replies.sh +1597 -0
- package/skill/engage-linkedin.sh +581 -0
- package/skill/engage-moltbook.sh +36 -0
- package/skill/engage-reddit.sh +146 -0
- package/skill/engage-twitter.sh +467 -0
- package/skill/github-engage.sh +176 -0
- package/skill/ingest-web-chat-replies.sh +38 -0
- package/skill/invent-supply-test.sh +100 -0
- package/skill/invent-topics.sh +50 -0
- package/skill/lib/linkedin-backend.sh +364 -0
- package/skill/lib/platform.sh +48 -0
- package/skill/lib/reddit-backend.sh +234 -0
- package/skill/lib/twitter-backend.sh +314 -0
- package/skill/link-edit-github.sh +136 -0
- package/skill/link-edit-moltbook.sh +117 -0
- package/skill/link-edit-reddit.sh +201 -0
- package/skill/linkedin-presence.sh +182 -0
- package/skill/linkedin-recovery.sh +282 -0
- package/skill/lock.sh +647 -0
- package/skill/memory-snapshot.sh +39 -0
- package/skill/precompute-stats.sh +35 -0
- package/skill/prewarm-funnel.sh +104 -0
- package/skill/refresh-instagram-tokens.sh +57 -0
- package/skill/refresh-twitter-following.sh +52 -0
- package/skill/reply-risk-digest.sh +31 -0
- package/skill/run-cycle-update-guard.sh +44 -0
- package/skill/run-draft-and-publish.sh +123 -0
- package/skill/run-generate-daily-style.sh +50 -0
- package/skill/run-github-launchd.sh +62 -0
- package/skill/run-github.sh +102 -0
- package/skill/run-instagram-daily.sh +149 -0
- package/skill/run-instagram-render.sh +875 -0
- package/skill/run-linkedin-launchd.sh +81 -0
- package/skill/run-linkedin-unipile.sh +130 -0
- package/skill/run-linkedin.sh +1593 -0
- package/skill/run-moltbook-launchd.sh +61 -0
- package/skill/run-moltbook.sh +38 -0
- package/skill/run-overlay-watch.sh +100 -0
- package/skill/run-reddit-search-launchd.sh +64 -0
- package/skill/run-reddit-search.sh +505 -0
- package/skill/run-reddit-threads-double.sh +32 -0
- package/skill/run-reddit-threads.sh +847 -0
- package/skill/run-scan-moltbook-replies.sh +57 -0
- package/skill/run-twitter-cycle-launchd.sh +63 -0
- package/skill/run-twitter-cycle-singleton.sh +62 -0
- package/skill/run-twitter-cycle.sh +2408 -0
- package/skill/run-twitter-threads.sh +592 -0
- package/skill/scan-instagram-replies.sh +61 -0
- package/skill/scan-twitter-followups.sh +57 -0
- package/skill/social-autoposter-update.sh +66 -0
- package/skill/stats-instagram.sh +72 -0
- package/skill/stats-linkedin.sh +271 -0
- package/skill/stats-moltbook.sh +4 -0
- package/skill/stats-reddit.sh +4 -0
- package/skill/stats-twitter.sh +4 -0
- package/skill/stats.sh +521 -0
- package/skill/strike-alert.sh +18 -0
- package/skill/styles.sh +87 -0
- package/skill/sweep-link-clicks.sh +40 -0
- package/skill/topics.sh +51 -0
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
// Setup / multi-project config for the social-autoposter MCP.
|
|
2
|
+
//
|
|
3
|
+
// Source of truth = config.json projects[] (the pipeline reads it). Readiness is
|
|
4
|
+
// DERIVED per-project from whether its required fields are present, never stored
|
|
5
|
+
// as a boolean (so the saved config and the reported status can't disagree).
|
|
6
|
+
//
|
|
7
|
+
// The only thing persisted outside config.json is a small scoping list: which
|
|
8
|
+
// project names were set up via THIS install. That list exists so (a) multi-
|
|
9
|
+
// project disambiguation works and (b) we don't surface unrelated projects that
|
|
10
|
+
// happen to live in config.json. It is NOT the source of truth for readiness.
|
|
11
|
+
import fs from "node:fs";
|
|
12
|
+
import os from "node:os";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
import { repoDir } from "./repo.js";
|
|
15
|
+
// Per-install scoping list lives outside the repo so it survives repo updates.
|
|
16
|
+
const STATE_DIR = process.env.S4L_STATE_DIR || process.env.SAPS_STATE_DIR || path.join(os.homedir(), ".social-autoposter-mcp");
|
|
17
|
+
const STATE_PATH = path.join(STATE_DIR, "setup-state.json");
|
|
18
|
+
// The pipeline reads projects[] from config.json. Override for tests / custom
|
|
19
|
+
// installs; defaults to the (dynamically resolved) repo's config.json. Resolved
|
|
20
|
+
// per call, not a load-time const, because a bare .mcpb install materializes the
|
|
21
|
+
// repo after boot and setup must write config.json into THAT repo.
|
|
22
|
+
export function configPath() {
|
|
23
|
+
return process.env.S4L_CONFIG_PATH || process.env.SAPS_CONFIG_PATH || path.join(repoDir(), "config.json");
|
|
24
|
+
}
|
|
25
|
+
// Fields the X drafting prompts genuinely consume. Required ones must all be
|
|
26
|
+
// present before a project is "ready"; recommended ones improve draft quality.
|
|
27
|
+
export const REQUIRED_FIELDS = [
|
|
28
|
+
"name",
|
|
29
|
+
"website",
|
|
30
|
+
"description",
|
|
31
|
+
"icp",
|
|
32
|
+
"voice",
|
|
33
|
+
// search_topics is required: the cycle's topic picker reads the DB universe
|
|
34
|
+
// (project_search_topics) seeded FROM these on setup. With zero topics the
|
|
35
|
+
// picker raises and the whole draft cycle silently returns nothing, so a
|
|
36
|
+
// project is NOT ready until it has at least one topic to seed. (2026-06-02)
|
|
37
|
+
"search_topics",
|
|
38
|
+
];
|
|
39
|
+
// Required fields for a personal-brand PERSONA project (persona:true). A persona
|
|
40
|
+
// is a person's voice, NOT a product: it has no `website` and no target `icp` by
|
|
41
|
+
// design. Validating a persona against the product REQUIRED_FIELDS therefore ALWAYS
|
|
42
|
+
// reports it "missing website + icp" -> personaReady() could never be true, which
|
|
43
|
+
// silently defeated the persona-aware kicker gate (a personal-brand-only setup then
|
|
44
|
+
// never scheduled the autopilot and drafted nothing). So a persona is "ready" once
|
|
45
|
+
// it has the fields the cycle actually consumes: a name, a voice, and at least one
|
|
46
|
+
// seedable search topic. (2026-06-30, completing the 1.6.173 persona path)
|
|
47
|
+
export const PERSONA_REQUIRED_FIELDS = [
|
|
48
|
+
"name",
|
|
49
|
+
"description",
|
|
50
|
+
"voice",
|
|
51
|
+
"search_topics",
|
|
52
|
+
];
|
|
53
|
+
export const RECOMMENDED_FIELDS = [
|
|
54
|
+
"differentiator",
|
|
55
|
+
"get_started_link",
|
|
56
|
+
"content_guardrails",
|
|
57
|
+
];
|
|
58
|
+
function readScope() {
|
|
59
|
+
try {
|
|
60
|
+
if (!fs.existsSync(STATE_PATH))
|
|
61
|
+
return { projects: [] };
|
|
62
|
+
const s = JSON.parse(fs.readFileSync(STATE_PATH, "utf-8"));
|
|
63
|
+
// New shape.
|
|
64
|
+
if (Array.isArray(s.projects)) {
|
|
65
|
+
return { projects: s.projects, last_project: s.last_project };
|
|
66
|
+
}
|
|
67
|
+
// Migrate old single-project shape { configured, project }.
|
|
68
|
+
if (typeof s.project === "string") {
|
|
69
|
+
return { projects: [s.project], last_project: s.project };
|
|
70
|
+
}
|
|
71
|
+
return { projects: [] };
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return { projects: [] };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function writeScope(s) {
|
|
78
|
+
fs.mkdirSync(STATE_DIR, { recursive: true });
|
|
79
|
+
fs.writeFileSync(STATE_PATH, JSON.stringify(s, null, 2) + "\n", "utf-8");
|
|
80
|
+
}
|
|
81
|
+
export function managedProjects() {
|
|
82
|
+
return readScope().projects;
|
|
83
|
+
}
|
|
84
|
+
export function recordManagedProject(name) {
|
|
85
|
+
const s = readScope();
|
|
86
|
+
if (!s.projects.includes(name))
|
|
87
|
+
s.projects.push(name);
|
|
88
|
+
s.last_project = name;
|
|
89
|
+
writeScope(s);
|
|
90
|
+
}
|
|
91
|
+
function readConfig() {
|
|
92
|
+
const cfgPath = configPath();
|
|
93
|
+
if (!fs.existsSync(cfgPath))
|
|
94
|
+
return { projects: [] };
|
|
95
|
+
const raw = fs.readFileSync(cfgPath, "utf-8").trim();
|
|
96
|
+
if (!raw)
|
|
97
|
+
return { projects: [] };
|
|
98
|
+
return JSON.parse(raw);
|
|
99
|
+
}
|
|
100
|
+
export function projectExists(name) {
|
|
101
|
+
try {
|
|
102
|
+
return (readConfig().projects || []).some((p) => p.name === name);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Strip stray JSON-array syntax that leaks into a single topic when a
|
|
109
|
+
// stringified array was split on commas (leading "[", trailing "]", and the
|
|
110
|
+
// surrounding quotes on each element). Without this, topics like '["AI video
|
|
111
|
+
// generation"' and '"ex-Google"]' get seeded verbatim and poison every search
|
|
112
|
+
// query with literal [, ] and " characters (Karol, 2026-06-30).
|
|
113
|
+
function stripTopicSyntax(x) {
|
|
114
|
+
let s = String(x).trim();
|
|
115
|
+
s = s.replace(/^\[+/, "").replace(/\]+$/, "").trim();
|
|
116
|
+
s = s.replace(/^["']+/, "").replace(/["']+$/, "").trim();
|
|
117
|
+
return s;
|
|
118
|
+
}
|
|
119
|
+
// Normalize a list field the model may pass as a real array, a stringified
|
|
120
|
+
// JSON array, or a comma/newline-delimited string, into a clean string[].
|
|
121
|
+
// Used for BOTH search_topics and search_queries: the model frequently passes
|
|
122
|
+
// a stringified JSON array for either, and the naive comma-split baked [, ] and
|
|
123
|
+
// " into each element (Karol, 2026-06-30 — corrupted topics AND a silently
|
|
124
|
+
// skipped query seed). Returns undefined only for nullish input.
|
|
125
|
+
export function normalizeStringList(t) {
|
|
126
|
+
if (t == null)
|
|
127
|
+
return undefined;
|
|
128
|
+
let raw;
|
|
129
|
+
if (Array.isArray(t)) {
|
|
130
|
+
raw = t;
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
const s = String(t).trim();
|
|
134
|
+
let parsed = null;
|
|
135
|
+
if (s.startsWith("[")) {
|
|
136
|
+
try {
|
|
137
|
+
parsed = JSON.parse(s);
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
parsed = null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
raw = Array.isArray(parsed) ? parsed : s.split(/[,\n]/);
|
|
144
|
+
}
|
|
145
|
+
return raw.map((x) => stripTopicSyntax(String(x))).filter(Boolean);
|
|
146
|
+
}
|
|
147
|
+
function normalizeTopics(t) {
|
|
148
|
+
return normalizeStringList(t);
|
|
149
|
+
}
|
|
150
|
+
// The fields the user actually supplies via setup. Only fields that are present
|
|
151
|
+
// AND non-empty get written, so an incremental call merges just what it carries
|
|
152
|
+
// and never blanks an existing field or clobbers weight/platform/links/github.
|
|
153
|
+
// name is always included (it's the match key).
|
|
154
|
+
function userFields(input) {
|
|
155
|
+
const fields = {};
|
|
156
|
+
const setStr = (k) => {
|
|
157
|
+
const v = input[k];
|
|
158
|
+
if (v != null && String(v).trim() !== "")
|
|
159
|
+
fields[k] = v;
|
|
160
|
+
};
|
|
161
|
+
if (input.name != null)
|
|
162
|
+
fields.name = input.name;
|
|
163
|
+
setStr("website");
|
|
164
|
+
setStr("description");
|
|
165
|
+
setStr("icp");
|
|
166
|
+
setStr("voice");
|
|
167
|
+
setStr("differentiator");
|
|
168
|
+
const topics = normalizeTopics(input.search_topics);
|
|
169
|
+
if (topics && topics.length)
|
|
170
|
+
fields.search_topics = topics;
|
|
171
|
+
setStr("get_started_link");
|
|
172
|
+
setStr("content_guardrails");
|
|
173
|
+
return fields;
|
|
174
|
+
}
|
|
175
|
+
// Apply the generic `fields` escape hatch onto a project object IN PLACE.
|
|
176
|
+
// Shallow per-key: each key replaces that key's whole value; a value of null
|
|
177
|
+
// (or undefined) DELETES the key. Returns the list of keys touched so callers
|
|
178
|
+
// can report exactly what changed. `name` is protected — it's the match key and
|
|
179
|
+
// renaming via this path would orphan the entry, so it's ignored here.
|
|
180
|
+
export function applyExtraFields(target, fields) {
|
|
181
|
+
const set = [];
|
|
182
|
+
const removed = [];
|
|
183
|
+
if (!fields || typeof fields !== "object")
|
|
184
|
+
return { set, removed };
|
|
185
|
+
for (const [k, v] of Object.entries(fields)) {
|
|
186
|
+
if (k === "name")
|
|
187
|
+
continue; // never rename through the escape hatch
|
|
188
|
+
if (v === null || v === undefined) {
|
|
189
|
+
if (k in target) {
|
|
190
|
+
delete target[k];
|
|
191
|
+
removed.push(k);
|
|
192
|
+
}
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
target[k] = v;
|
|
196
|
+
set.push(k);
|
|
197
|
+
}
|
|
198
|
+
return { set, removed };
|
|
199
|
+
}
|
|
200
|
+
// A brand-new project entry: user fields plus the defaults a fresh X-rail
|
|
201
|
+
// project needs. Applied ONLY on create, never on update.
|
|
202
|
+
export function buildProjectEntry(input) {
|
|
203
|
+
const entry = {
|
|
204
|
+
weight: 10,
|
|
205
|
+
platform: "twitter",
|
|
206
|
+
voice_relationship: "first_party",
|
|
207
|
+
// A freshly onboarded customer has NOT shipped the @m13v/seo-components
|
|
208
|
+
// /r/[code] resolver on their own domain, so a wrapped short link minted
|
|
209
|
+
// against project.website would 404 ("this link doesn't exist"). Default
|
|
210
|
+
// new projects to route /r/<code> through the social-autoposter-owned
|
|
211
|
+
// resolver at https://s4l.ai instead: short_links_live=false makes
|
|
212
|
+
// _project_short_links_host / getProjectWrapperHost fall back to
|
|
213
|
+
// DEFAULT_FALLBACK_HOST. The customer flips this to true (or removes it)
|
|
214
|
+
// only AFTER they ship their own /r/[code] handler. See the "URL wrapping"
|
|
215
|
+
// section in CLAUDE.md.
|
|
216
|
+
short_links_live: false,
|
|
217
|
+
...userFields(input),
|
|
218
|
+
};
|
|
219
|
+
// Generic fields can override the defaults above (e.g. platform/weight) and
|
|
220
|
+
// set any advanced field at creation time too.
|
|
221
|
+
applyExtraFields(entry, input.fields);
|
|
222
|
+
return entry;
|
|
223
|
+
}
|
|
224
|
+
// Upsert the project into config.json projects[] (match by name), incrementally
|
|
225
|
+
// merging only the supplied fields, and record it as managed by this install.
|
|
226
|
+
// Backs up config.json first. Does NOT require all fields — readiness is checked
|
|
227
|
+
// separately, so a project can be filled out over several calls.
|
|
228
|
+
export function applySetup(input) {
|
|
229
|
+
const cfg = readConfig();
|
|
230
|
+
cfg.projects = cfg.projects || [];
|
|
231
|
+
const idx = cfg.projects.findIndex((p) => p.name === input.name);
|
|
232
|
+
let created;
|
|
233
|
+
let fields_set = [];
|
|
234
|
+
let fields_removed = [];
|
|
235
|
+
if (idx >= 0) {
|
|
236
|
+
// Update: merge ONLY supplied modeled fields; keep every other existing
|
|
237
|
+
// field. Then apply the generic `fields` escape hatch (set/delete any key).
|
|
238
|
+
const merged = { ...cfg.projects[idx], ...userFields(input) };
|
|
239
|
+
const r = applyExtraFields(merged, input.fields);
|
|
240
|
+
fields_set = r.set;
|
|
241
|
+
fields_removed = r.removed;
|
|
242
|
+
cfg.projects[idx] = merged;
|
|
243
|
+
created = false;
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
const entry = buildProjectEntry(input);
|
|
247
|
+
// Report which advanced keys the create call set via the escape hatch.
|
|
248
|
+
fields_set = Object.keys(input.fields ?? {}).filter((k) => k !== "name" && input.fields[k] != null);
|
|
249
|
+
cfg.projects.push(entry);
|
|
250
|
+
created = true;
|
|
251
|
+
}
|
|
252
|
+
const cfgPath = configPath();
|
|
253
|
+
if (fs.existsSync(cfgPath)) {
|
|
254
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
255
|
+
fs.copyFileSync(cfgPath, `${cfgPath}.bak-${stamp}`);
|
|
256
|
+
}
|
|
257
|
+
fs.mkdirSync(path.dirname(cfgPath), { recursive: true });
|
|
258
|
+
fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2) + "\n", "utf-8");
|
|
259
|
+
recordManagedProject(input.name);
|
|
260
|
+
const missing = missingForProject(input.name) ?? [];
|
|
261
|
+
return {
|
|
262
|
+
project: input.name,
|
|
263
|
+
created,
|
|
264
|
+
ready: missing.length === 0,
|
|
265
|
+
missing_required: missing,
|
|
266
|
+
fields_set,
|
|
267
|
+
fields_removed,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
// ---------------------------------------------------------------------------
|
|
271
|
+
// Personal-brand PERSONA project (2026-06-26).
|
|
272
|
+
//
|
|
273
|
+
// The persona is a project the autopilot can draft for in personal_brand mode:
|
|
274
|
+
// link-free organic engagement in the user's own voice (the revived 2026-02
|
|
275
|
+
// flow). It is NOT a product, so it is deliberately kept OUT of the managed-
|
|
276
|
+
// products scope list (no recordManagedProject) — that keeps it off the product-
|
|
277
|
+
// readiness counts and the "all projects ready" gate. It is identified by
|
|
278
|
+
// `persona: true`, runs with `enabled: false` (so the normal weighted pick never
|
|
279
|
+
// touches it) and is force-selected via S4L_FORCE_PROJECT only when the mode
|
|
280
|
+
// toggle is on. Keep these defaults in lockstep with scripts/saps_mode.py and the
|
|
281
|
+
// hand-authored PersonalBrand entry in config.json.
|
|
282
|
+
export const PERSONA_DEFAULT_NAME = "PersonalBrand";
|
|
283
|
+
const PERSONA_DEFAULTS = {
|
|
284
|
+
persona: true,
|
|
285
|
+
enabled: false,
|
|
286
|
+
weight: 10, // for the FUTURE personal/promo percentage blend; ignored while enabled:false
|
|
287
|
+
voice_relationship: "first_party",
|
|
288
|
+
description: "The user's own personal brand. Not a product. Goal is pure organic " +
|
|
289
|
+
"engagement that grows the user's authority and following by adding genuine " +
|
|
290
|
+
"value to conversations they have real experience with. No company, no " +
|
|
291
|
+
"signup, no pitch.",
|
|
292
|
+
content_angle: "Ground every reply in the user's actual first-hand experience. Only engage " +
|
|
293
|
+
"a thread when there is a concrete, specific angle from that lived " +
|
|
294
|
+
"experience; otherwise skip it.",
|
|
295
|
+
voice: {
|
|
296
|
+
tone: "write like you're texting a coworker. lowercase is fine, sentence " +
|
|
297
|
+
"fragments are fine. first person and specific. reply to high-signal " +
|
|
298
|
+
"comments, not just OP. match the thread's energy and length (1-2 " +
|
|
299
|
+
"sentences is ideal).",
|
|
300
|
+
never: [
|
|
301
|
+
"self-promotion, links, or feature lists",
|
|
302
|
+
"mentioning a product or company unless it directly solves OP's problem",
|
|
303
|
+
"opening with 'Makes sense', 'The nuance here is', or 'What everyone here is describing'",
|
|
304
|
+
"sounding like a blog post, a thought-leader, or an AI",
|
|
305
|
+
"generic advice with no specific personal angle",
|
|
306
|
+
"em dashes or en dashes",
|
|
307
|
+
],
|
|
308
|
+
},
|
|
309
|
+
content_guardrails: {
|
|
310
|
+
summary: "This is personal-brand growth, not marketing. Follow a 60/30/10 mix: " +
|
|
311
|
+
"~60% humor (self-deprecating dev stories, funny bugs, relatable pain), " +
|
|
312
|
+
"~30% inspirational (cool technical wins, 'look what's possible'), ~10% " +
|
|
313
|
+
"light personal mention only when it genuinely fits. NEVER attach a link " +
|
|
314
|
+
"or a CTA. NEVER list features or name a product to sell it. Only reply " +
|
|
315
|
+
"when the user has a real, specific angle from their own work; if the " +
|
|
316
|
+
"thread doesn't connect to something they've actually done, skip it. Be a " +
|
|
317
|
+
"value-adding peer, not a promoter.",
|
|
318
|
+
},
|
|
319
|
+
search_topics: [
|
|
320
|
+
"AI agents",
|
|
321
|
+
"Claude Code",
|
|
322
|
+
"coding agents",
|
|
323
|
+
"AI coding tools",
|
|
324
|
+
"LLM developer workflow",
|
|
325
|
+
"prompt engineering",
|
|
326
|
+
"MCP servers",
|
|
327
|
+
"macOS automation",
|
|
328
|
+
"browser automation",
|
|
329
|
+
"desktop app development",
|
|
330
|
+
"building in public",
|
|
331
|
+
"indie hacking",
|
|
332
|
+
"shipping solo",
|
|
333
|
+
"API costs",
|
|
334
|
+
"developer productivity",
|
|
335
|
+
],
|
|
336
|
+
};
|
|
337
|
+
// The persona project entry (the one with persona:true), or null if none yet.
|
|
338
|
+
export function findPersonaProject() {
|
|
339
|
+
try {
|
|
340
|
+
const proj = (readConfig().projects || []).find((p) => p.persona === true);
|
|
341
|
+
return proj ? { name: String(proj.name) } : null;
|
|
342
|
+
}
|
|
343
|
+
catch {
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
// Create the persona project if it doesn't exist; otherwise merge ONLY the
|
|
348
|
+
// supplied grounding fields (from the profile scan) onto the existing entry,
|
|
349
|
+
// never touching persona/enabled/weight. Writes config.json (backup + full
|
|
350
|
+
// rewrite, same as applySetup). Deliberately does NOT recordManagedProject.
|
|
351
|
+
export function ensurePersonaProject(grounding) {
|
|
352
|
+
const cfg = readConfig();
|
|
353
|
+
cfg.projects = cfg.projects || [];
|
|
354
|
+
const g = {};
|
|
355
|
+
if (grounding) {
|
|
356
|
+
if (grounding.description && String(grounding.description).trim())
|
|
357
|
+
g.description = grounding.description;
|
|
358
|
+
if (grounding.content_angle && String(grounding.content_angle).trim())
|
|
359
|
+
g.content_angle = grounding.content_angle;
|
|
360
|
+
if (grounding.voice && typeof grounding.voice === "object")
|
|
361
|
+
g.voice = grounding.voice;
|
|
362
|
+
const topics = normalizeTopics(grounding.search_topics);
|
|
363
|
+
if (topics && topics.length)
|
|
364
|
+
g.search_topics = topics;
|
|
365
|
+
}
|
|
366
|
+
const existing = cfg.projects.find((p) => p.persona === true);
|
|
367
|
+
let name;
|
|
368
|
+
let created;
|
|
369
|
+
if (existing) {
|
|
370
|
+
Object.assign(existing, g); // merge only provided grounding; keep persona flags
|
|
371
|
+
name = String(existing.name);
|
|
372
|
+
created = false;
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
cfg.projects.push({ name: PERSONA_DEFAULT_NAME, ...PERSONA_DEFAULTS, ...g });
|
|
376
|
+
name = PERSONA_DEFAULT_NAME;
|
|
377
|
+
created = true;
|
|
378
|
+
}
|
|
379
|
+
const cfgPath = configPath();
|
|
380
|
+
if (fs.existsSync(cfgPath)) {
|
|
381
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
382
|
+
fs.copyFileSync(cfgPath, `${cfgPath}.bak-${stamp}`);
|
|
383
|
+
}
|
|
384
|
+
fs.mkdirSync(path.dirname(cfgPath), { recursive: true });
|
|
385
|
+
fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2) + "\n", "utf-8");
|
|
386
|
+
// Raw dictation transcript -> persona_corpus.txt sidecar (NOT config.json).
|
|
387
|
+
// Mirrors scripts/build_persona.py cmd_apply: config.json is inlined into many
|
|
388
|
+
// prompts (ALL_PROJECTS_JSON), so a multi-KB corpus there would bloat every
|
|
389
|
+
// cycle's token bill. The persona lane reads this file only when it drafts.
|
|
390
|
+
// Best-effort; a write failure must never block persona provisioning.
|
|
391
|
+
if (grounding && typeof grounding.content_corpus === "string" && grounding.content_corpus.trim()) {
|
|
392
|
+
try {
|
|
393
|
+
const corpusPath = path.join(path.dirname(cfgPath), "persona_corpus.txt");
|
|
394
|
+
fs.writeFileSync(corpusPath, grounding.content_corpus.trim().slice(0, 8000) + "\n", "utf-8");
|
|
395
|
+
}
|
|
396
|
+
catch {
|
|
397
|
+
// ignore: corpus is grounding fuel, not required for a working persona.
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return { name, created };
|
|
401
|
+
}
|
|
402
|
+
// Heal installs that onboarded BEFORE short_links_live defaulted to false.
|
|
403
|
+
// Such a project has neither short_links_host nor an explicit short_links_live,
|
|
404
|
+
// so the wrapper host resolves to project.website — but the customer never
|
|
405
|
+
// shipped a /r/[code] resolver there, so every minted short link 404s. Set
|
|
406
|
+
// short_links_live=false (route /r/<code> through s4l.ai) for those projects.
|
|
407
|
+
//
|
|
408
|
+
// Scoped to projects this install actually manages, so a hand-maintained dev
|
|
409
|
+
// config with branded-resolver projects isn't rewritten. Idempotent: a project
|
|
410
|
+
// that already has either short-link field set is left untouched (someone made
|
|
411
|
+
// a deliberate choice). Best-effort; never throws.
|
|
412
|
+
export function ensureShortLinksDefault() {
|
|
413
|
+
const healed = [];
|
|
414
|
+
try {
|
|
415
|
+
const cfg = readConfig();
|
|
416
|
+
const projects = cfg.projects || [];
|
|
417
|
+
if (!projects.length)
|
|
418
|
+
return { healed };
|
|
419
|
+
const managed = new Set(managedProjects());
|
|
420
|
+
let changed = false;
|
|
421
|
+
for (const p of projects) {
|
|
422
|
+
const name = typeof p.name === "string" ? p.name : "";
|
|
423
|
+
if (!name || !managed.has(name))
|
|
424
|
+
continue;
|
|
425
|
+
const hasHost = typeof p.short_links_host === "string" && p.short_links_host.trim() !== "";
|
|
426
|
+
const hasLiveFlag = p.short_links_live === true || p.short_links_live === false;
|
|
427
|
+
if (hasHost || hasLiveFlag)
|
|
428
|
+
continue; // deliberate config: leave it.
|
|
429
|
+
p.short_links_live = false;
|
|
430
|
+
healed.push(name);
|
|
431
|
+
changed = true;
|
|
432
|
+
}
|
|
433
|
+
if (changed) {
|
|
434
|
+
const cfgPath = configPath();
|
|
435
|
+
if (fs.existsSync(cfgPath)) {
|
|
436
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
437
|
+
fs.copyFileSync(cfgPath, `${cfgPath}.bak-${stamp}`);
|
|
438
|
+
}
|
|
439
|
+
fs.mkdirSync(path.dirname(cfgPath), { recursive: true });
|
|
440
|
+
fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2) + "\n", "utf-8");
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
catch {
|
|
444
|
+
// best-effort heal; a failure here must never block boot.
|
|
445
|
+
}
|
|
446
|
+
return { healed };
|
|
447
|
+
}
|
|
448
|
+
// ---------------------------------------------------------------------------
|
|
449
|
+
// Readiness (derived from config.json, never stored).
|
|
450
|
+
// ---------------------------------------------------------------------------
|
|
451
|
+
// Which required fields are missing on the persisted project in config.json.
|
|
452
|
+
// Returns null when the named project can't be found/read.
|
|
453
|
+
export function missingForProject(name, fields = REQUIRED_FIELDS) {
|
|
454
|
+
if (!name)
|
|
455
|
+
return null;
|
|
456
|
+
try {
|
|
457
|
+
const proj = (readConfig().projects || []).find((p) => p.name === name);
|
|
458
|
+
if (!proj)
|
|
459
|
+
return null;
|
|
460
|
+
return fields.filter((f) => {
|
|
461
|
+
const v = proj[f];
|
|
462
|
+
if (v == null)
|
|
463
|
+
return true;
|
|
464
|
+
if (typeof v === "string")
|
|
465
|
+
return v.trim() === "";
|
|
466
|
+
if (Array.isArray(v))
|
|
467
|
+
return v.length === 0;
|
|
468
|
+
if (typeof v === "object")
|
|
469
|
+
return Object.keys(v).length === 0;
|
|
470
|
+
return false;
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
catch {
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
export function projectStatus(name) {
|
|
478
|
+
const missing = missingForProject(name);
|
|
479
|
+
if (missing === null) {
|
|
480
|
+
return { name, in_config: false, ready: false, missing_required: [...REQUIRED_FIELDS] };
|
|
481
|
+
}
|
|
482
|
+
return { name, in_config: true, ready: missing.length === 0, missing_required: missing };
|
|
483
|
+
}
|
|
484
|
+
// Status of every project this install manages.
|
|
485
|
+
export function listManagedProjectStatus() {
|
|
486
|
+
return managedProjects().map(projectStatus);
|
|
487
|
+
}
|
|
488
|
+
export function hasReadyProject() {
|
|
489
|
+
return listManagedProjectStatus().some((s) => s.ready);
|
|
490
|
+
}
|
|
491
|
+
// The personal-brand persona project (persona:true) is intentionally kept OUT of
|
|
492
|
+
// the managed-products scope, so listManagedProjectStatus()/hasReadyProject() never
|
|
493
|
+
// count it. But in personal_brand mode the cycle DOES draft for it (force-selected
|
|
494
|
+
// via S4L_FORCE_PROJECT). Without this helper a personal-brand-only ("self promo")
|
|
495
|
+
// setup looks like "no project configured" everywhere — the autopilot kicker never
|
|
496
|
+
// installs and the doctor can't see it. Reports whether the persona exists AND is
|
|
497
|
+
// fully configured (has every required field, incl. seeded topics). (2026-06-30)
|
|
498
|
+
export function personaReady() {
|
|
499
|
+
const persona = findPersonaProject();
|
|
500
|
+
if (!persona)
|
|
501
|
+
return false;
|
|
502
|
+
// Validate against PERSONA_REQUIRED_FIELDS, NOT the product REQUIRED_FIELDS: a
|
|
503
|
+
// persona has no website/icp by design, so the product set would never pass.
|
|
504
|
+
const missing = missingForProject(persona.name, PERSONA_REQUIRED_FIELDS);
|
|
505
|
+
return missing !== null && missing.length === 0;
|
|
506
|
+
}
|
|
507
|
+
const SETUP_REQUIRED_MESSAGE = "No project is set up yet. Run the `project_config` tool first: collect from the user their website " +
|
|
508
|
+
"URL, what the product does (description), who to target (icp), and brand voice/tone, then " +
|
|
509
|
+
"call project_config with a short name plus those fields. You can set up multiple products; each is " +
|
|
510
|
+
"configured independently and you fill the fields incrementally.";
|
|
511
|
+
export function resolveProject(requested) {
|
|
512
|
+
if (requested) {
|
|
513
|
+
const st = projectStatus(requested);
|
|
514
|
+
if (!st.in_config) {
|
|
515
|
+
return {
|
|
516
|
+
ok: false,
|
|
517
|
+
message: `Project '${requested}' isn't set up yet. Run project_config with name='${requested}' plus its ` +
|
|
518
|
+
`website, description, icp, and voice.`,
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
if (!st.ready) {
|
|
522
|
+
return {
|
|
523
|
+
ok: false,
|
|
524
|
+
message: `Project '${requested}' still needs: ${st.missing_required.join(", ")}. Ask the user ` +
|
|
525
|
+
`for those and call project_config again with name='${requested}'.`,
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
return { ok: true, project: requested };
|
|
529
|
+
}
|
|
530
|
+
const statuses = listManagedProjectStatus();
|
|
531
|
+
const ready = statuses.filter((s) => s.ready).map((s) => s.name);
|
|
532
|
+
if (ready.length === 1)
|
|
533
|
+
return { ok: true, project: ready[0] };
|
|
534
|
+
if (ready.length > 1) {
|
|
535
|
+
return {
|
|
536
|
+
ok: false,
|
|
537
|
+
message: `Multiple projects are set up (${ready.join(", ")}). Tell me which one to use (pass the project name).`,
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
const partial = statuses.filter((s) => !s.ready).map((s) => s.name);
|
|
541
|
+
if (partial.length) {
|
|
542
|
+
return { ok: false, message: `No project is fully set up yet. Finish setup for: ${partial.join(", ")}.` };
|
|
543
|
+
}
|
|
544
|
+
return { ok: false, message: SETUP_REQUIRED_MESSAGE };
|
|
545
|
+
}
|