@sellable/install 0.1.326 → 0.1.327

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.
@@ -22,14 +22,6 @@ import {
22
22
  REQUIRED_SELLABLE_MCP_TOOLS,
23
23
  verifySellableMcpRuntime,
24
24
  } from "../lib/runtime-verify.mjs";
25
- import {
26
- activateCodexCandidateSelection,
27
- installCodexCandidateCache,
28
- readCodexPluginSelection,
29
- resolveCodexPluginSelectionPaths,
30
- restoreCodexPluginSelection,
31
- snapshotCodexPluginSelection,
32
- } from "../lib/codex-plugin-selection.mjs";
33
25
 
34
26
  const DEFAULT_API_URL = "https://app.sellable.dev";
35
27
  const DEFAULT_SERVER_PACKAGE =
@@ -48,7 +40,7 @@ function getInstallVersion() {
48
40
  }
49
41
  }
50
42
 
51
- const CODEX_PLUGIN_VERSION = "0.1.56";
43
+ const CODEX_PLUGIN_VERSION = "0.1.54";
52
44
  const CODEX_PLUGIN_COMPAT_VERSIONS = [
53
45
  "0.1.8",
54
46
  "0.1.9",
@@ -87,8 +79,6 @@ const CODEX_PLUGIN_COMPAT_VERSIONS = [
87
79
  "0.1.51",
88
80
  "0.1.52",
89
81
  "0.1.53",
90
- "0.1.54",
91
- "0.1.55",
92
82
  ];
93
83
  const RAW_INSTALL_PACKAGE_SPEC =
94
84
  process.env.SELLABLE_INSTALL_PACKAGE_SPEC || "@sellable/install@latest";
@@ -180,8 +170,6 @@ Commands:
180
170
  Save the ask-first prompt-sharing preference.
181
171
  setup claude-permissions --allow-common-setup
182
172
  Add common Sellable Bash permissions to Claude settings.
183
- codex plugin-selection snapshot|install-cache|activate|restore|status
184
- Approval-gated Phase 98 exact-cache selection controls.
185
173
  hermes profile bootstrap Create a profile-local Hermes config, Sellable auth config,
186
174
  configs dir, skills, and optional Slack .env.
187
175
  uninstall Remove Sellable host config and installed artifacts.
@@ -191,8 +179,6 @@ Options:
191
179
  --server <mode> package, local, or hosted. Default: package
192
180
  --token <token> Sellable API token. Also reads SELLABLE_TOKEN.
193
181
  --workspace-id <id> Sellable workspace id. Also reads SELLABLE_WORKSPACE_ID.
194
- --require-workspace-lock For Hermes customer profiles, fail closed unless
195
- SELLABLE_LOCK_WORKSPACE_ID is present in MCP env.
196
182
  --api-url <url> Sellable API URL. Default: ${DEFAULT_API_URL}
197
183
  --sellable-config-path <path>
198
184
  Profile-scoped Sellable config path. Also reads SELLABLE_CONFIG_PATH.
@@ -213,6 +199,7 @@ Options:
213
199
  Auth:
214
200
  Install is auth-free by default. Sign in happens on the first run of
215
201
  /sellable:create-campaign, /sellable:create-evergreen-campaigns,
202
+ /sellable:find-leads,
216
203
  /sellable:foundation, /sellable:content, /sellable:create-post,
217
204
  /sellable:refresh-sender-engagement, or /sellable:refill-sends in Claude Code,
218
205
  Codex, or Hermes,
@@ -231,20 +218,9 @@ Hermes profile bootstrap:
231
218
 
232
219
  Bootstrap writes <profileRoot>/config.yaml, <profileRoot>/sellable/config.json,
233
220
  <profileRoot>/sellable/configs, Sellable Hermes skills, and optional
234
- <profileRoot>/.env Slack token and channel-scope keys. For customer profiles,
235
- pass --slack-home-channel; bootstrap defaults SLACK_ALLOWED_CHANNELS to that
236
- exact channel id unless --slack-allowed-channels is supplied. Use
237
- customer-scoped Sellable credentials and customer-scoped Slack credentials for
238
- customer profiles; shared admin tokens are weaker isolation and should remain
239
- internal-only. Bootstrap writes SELLABLE_REQUIRE_WORKSPACE_LOCK=1 whenever a
240
- workspace id is supplied, so a customer profile fails closed if the workspace
241
- lock is later removed or not yet provisioned.
242
-
243
- Hermes shared Slack dispatcher:
244
- Shared listener setup and route-registry validation are internal Sellable
245
- Admin responsibilities. This customer-facing installer only bootstraps a
246
- profile-local Sellable runtime after Sellable Admin provisions the profile
247
- and dispatcher route.
221
+ <profileRoot>/.env Slack token keys. Use customer-scoped Sellable credentials
222
+ for customer profiles; a shared admin token is weaker isolation and should
223
+ remain internal-only.
248
224
  `;
249
225
  }
250
226
 
@@ -1119,6 +1095,35 @@ const CREATE_CAMPAIGN_ALLOWED_TOOLS = [
1119
1095
  "mcp__sellable__start_campaign",
1120
1096
  ];
1121
1097
 
1098
+ const FIND_LEADS_ALLOWED_TOOLS = [
1099
+ "mcp__sellable__get_auth_status",
1100
+ "mcp__sellable__start_cli_login",
1101
+ "mcp__sellable__wait_for_cli_login",
1102
+ "mcp__sellable__get_active_workspace",
1103
+ "mcp__sellable__list_workspaces",
1104
+ "mcp__sellable__set_active_workspace",
1105
+ "mcp__sellable__get_subskill_prompt",
1106
+ "mcp__sellable__get_subskill_asset",
1107
+ "mcp__sellable__search_subskill_prompts",
1108
+ "mcp__sellable__get_provider_prompt",
1109
+ "mcp__sellable__bootstrap_find_leads",
1110
+ "mcp__sellable__get_find_leads_run",
1111
+ "mcp__sellable__update_find_leads_run",
1112
+ "mcp__sellable__wait_for_find_leads_run",
1113
+ "mcp__sellable__cancel_find_leads",
1114
+ "mcp__sellable__reissue_find_leads_watch_link",
1115
+ "mcp__sellable__preflight_find_leads_provider",
1116
+ "mcp__sellable__lookup_sales_nav_filter",
1117
+ "mcp__sellable__search_sales_nav",
1118
+ "mcp__sellable__search_prospeo",
1119
+ "mcp__sellable__search_signals",
1120
+ "mcp__sellable__select_promising_posts",
1121
+ "mcp__sellable__import_leads",
1122
+ "mcp__sellable__wait_for_lead_list_ready",
1123
+ "mcp__sellable__get_rows_minimal",
1124
+ "mcp__sellable__export_table_csv",
1125
+ ];
1126
+
1122
1127
  const CREATE_EVERGREEN_CAMPAIGNS_ALLOWED_TOOLS = [
1123
1128
  ...CREATE_CAMPAIGN_ALLOWED_TOOLS,
1124
1129
  "mcp__sellable__setup_evergreen_campaigns",
@@ -1672,7 +1677,7 @@ Treat host capabilities as concrete functions, not prose conventions:
1672
1677
  import and before dispatching Message Drafting only.
1673
1678
  - \`launch_message_drafting\`: Claude Code uses \`Task\` with \`subagent_type\`
1674
1679
  \`post-find-leads-message-scout\` when listed; Codex uses the returned
1675
- compatibility agent or a generic \`gpt-5.6-sol\` / \`high\` Message Drafting agent.
1680
+ compatibility agent or a generic \`gpt-5.5\` / \`xhigh\` Message Drafting agent.
1676
1681
 
1677
1682
  If a required interactive question function or MCP loader is missing, stop and
1678
1683
  explain the Sellable install/reload problem. Source and filter work use
@@ -1934,6 +1939,17 @@ then retry \`get_subskill_prompt\`.
1934
1939
  `, host, "create-campaign");
1935
1940
  }
1936
1941
 
1942
+ function findLeadsSkillMd(host = "shared") {
1943
+ const here = dirname(fileURLToPath(import.meta.url));
1944
+ const templatePath = join(here, "..", "skill-templates", "find-leads.md");
1945
+ if (!existsSync(templatePath)) {
1946
+ throw new Error(
1947
+ "Find Leads skill template is missing or stale. Reinstall @sellable/install and retry."
1948
+ );
1949
+ }
1950
+ return stampInstalledHost(readFileSync(templatePath, "utf8"), host, "find-leads");
1951
+ }
1952
+
1937
1953
  function createAbTestSkillMd(host = "shared") {
1938
1954
  try {
1939
1955
  const here = dirname(fileURLToPath(import.meta.url));
@@ -2050,20 +2066,6 @@ then retry \`get_subskill_prompt\`.
2050
2066
  `, host, "create-evergreen-campaigns");
2051
2067
  }
2052
2068
 
2053
- const REFILL_SENDS_FALLBACK_CONTRACT = `<!-- REFILL_CONTRACT_GENERATED:START -->
2054
- contractVersion: 2.0.0
2055
- cacheVersion: refill-contract-v1
2056
- contractHash: e54ba0f80bc534fd70fd8c10c1caab109fe9a91c205cfc423f5f6735cd53fdc1
2057
- scope: workspace_id, target_date, sender_selectors, campaign_selectors, approval_mode, contract_identity, installed_identity
2058
- gates: approval_packet, one_primitive, fresh_reread, normalized_accounting, prep_circuit
2059
- forbidden: direct_send, raw_scheduler_write, campaign_create, campaign_archive_delete, threshold_lowering, unselected_source_mutation, unapproved_campaign_start, sender_config_write
2060
- terminal: projected_full, concrete_blocker, deadline_reached, iteration_limit, operator_stopped
2061
- ready_buffer_exists_is_not_complete
2062
- mcpPackageVersion: installed MCP package spec in host MCP config
2063
- installPackageVersion: ${getInstallVersion()}
2064
- codexPluginVersion: ${CODEX_PLUGIN_VERSION}
2065
- <!-- REFILL_CONTRACT_GENERATED:END -->`;
2066
-
2067
2069
  function refillSendsSkillMd(host = "shared") {
2068
2070
  try {
2069
2071
  const here = dirname(fileURLToPath(import.meta.url));
@@ -2073,10 +2075,7 @@ function refillSendsSkillMd(host = "shared") {
2073
2075
  "skill-templates",
2074
2076
  "refill-sends.md"
2075
2077
  );
2076
- if (
2077
- process.env.SELLABLE_FORCE_REFILL_TEMPLATE_MISSING !== "1" &&
2078
- existsSync(templatePath)
2079
- ) {
2078
+ if (existsSync(templatePath)) {
2080
2079
  return stampInstalledHost(
2081
2080
  addOrReplaceAllowedTools(
2082
2081
  readFileSync(templatePath, "utf8"),
@@ -2099,14 +2098,6 @@ ${allowedToolsYaml(REFILL_SENDS_ALLOWED_TOOLS)}
2099
2098
 
2100
2099
  # Sellable Refill Sends
2101
2100
 
2102
- ${REFILL_SENDS_FALLBACK_CONTRACT}
2103
-
2104
- fallbackSource: embedded
2105
-
2106
- Load \`refill-sends-workflow\`, then \`core/flow.v1.json\`, then
2107
- \`core/contract.v2.json\`, each through the Sellable MCP prompt tools until
2108
- \`hasMore:false\`. Fail closed on unsupported major or stale contract hash.
2109
-
2110
2101
  Use this as the customer-facing entrypoint for the Sellable \`refill-sends\`
2111
2102
  workflow. It supports \`--yolo\` and optional repeated \`--sender <name-or-id>\`
2112
2103
  selectors. It also supports \`--target-date YYYY-MM-DD\` to fill one exact
@@ -2136,9 +2127,9 @@ Desktop, then start a new thread.
2136
2127
 
2137
2128
  1. Call \`mcp__sellable__get_auth_status({})\`.
2138
2129
  2. Resolve the target workspace id from the user request, automation config, or install-time workspace mapping. Pass \`workspaceId\` on every scheduled or \`--yolo\` refill tool call. Missing \`workspaceId\` in scheduled or \`--yolo\` mode is a blocker; return \`WORKSPACE_REQUIRED\` instead of running against shared config state. Do not solve automation workspace uncertainty by changing the shared active workspace.
2139
- 3. Normalize invocation arguments with \`mcp__sellable__refill_sends({ yolo, executionMode, requireWorkspace, workspaceId, senders, senderIds, senderNames, actionTypes, targetDate, untilDate })\` when the host exposes typed MCP tools. Type shape: \`refill_sends({ yolo?: boolean, executionMode?: "manual" | "scheduled" | "yolo", requireWorkspace?: boolean, workspaceId?: string, senders?: string[], senderIds?: string[], senderNames?: string[], actionTypes?: ("send_invite" | "send_inmail_closed")[], horizonSendDays?: number, untilDate?: "YYYY-MM-DD", targetDate?: "YYYY-MM-DD" })\`.
2130
+ 3. Normalize invocation arguments with \`mcp__sellable__refill_sends({ yolo, executionMode, requireWorkspace, workspaceId, senders, senderIds, senderNames, targetDate, untilDate })\` when the host exposes typed MCP tools. Type shape: \`refill_sends({ yolo?: boolean, executionMode?: "manual" | "scheduled" | "yolo", requireWorkspace?: boolean, workspaceId?: string, senders?: string[], senderIds?: string[], senderNames?: string[], horizonSendDays?: number, untilDate?: "YYYY-MM-DD", targetDate?: "YYYY-MM-DD" })\`.
2140
2131
  4. Clover scheduled automation example: \`mcp__sellable__refill_sends({ yolo:false, executionMode:"scheduled", requireWorkspace:true, workspaceId:"cmlq1v8ms0000jx04ang4hi7e" })\`, then \`mcp__sellable__get_refill_target_plan({ intent:"plain", approvalMode:"mark_ready", workspaceId:"cmlq1v8ms0000jx04ang4hi7e" })\`.
2141
- 5. Call \`mcp__sellable__get_refill_target_plan({ workspaceId, senders, senderIds, senderNames, actionTypes, targetDate, untilDate })\` before any mutation. Render \`target.eligibleSenderLedger\`, \`target.senderRefillPlans[]\`, and \`target.globalActionQueue\` as the structured packet. Preserve the labels \`Need to prepare\`, \`Goal\`, \`Already sent\`, \`Scheduled\`, \`Ready and waiting to be scheduled\`, and \`Still need\`. In \`--yolo\`, \`mcp__sellable__refill_sends\` maintains a run-local \`refreshedPaidInmailSenderIds\` set, refreshes stale/missing paid InMail facts for each selected paid-InMail sender at most once, reruns \`get_refill_target_plan\`, and returns \`autoPaidInmailRefresh\` plus the post-refresh \`targetPlan\` before choosing the next prep/source-copy/bounded-approval/read-only wait or exact-date sweep action. Refill ladder: approve generated rows only through the explicit bounded approval gate, process existing same-campaign unenriched/unprepared rows before source work, then same-source copy, then provider-aligned source-more. New source/provider switches change reply-rate baseline and are manual alternates, not \`--yolo\` side effects. Do not present paid-credit refresh as the next operator action after \`refill_sends\` returns that post-refresh plan. For exact-date proof, \`mcp__sellable__get_scheduler_fill_capacity\` is read-only and tells how many cells the scheduler will try to place for a sender/action/date. When the planner returns a request-scoped exact-date scheduler sweep, execute \`mcp__sellable__run_scheduler_sweep({ workspaceId, action:"run", targetDate, requestKey, targetShapeRevision })\` once, capture its receipt, and fully reread before another action. It may schedule eligible workspace cells through existing gates, but it never sends directly or bypasses limits. If the returned target is already complete, stop with the no-op receipt. Only non-exact or no-sweep waits use the read-only scheduler wait/re-read loop until projected coverage fills, a concrete non-scheduler blocker appears, or Christian explicitly stops/statuses the run.
2132
+ 5. Call \`mcp__sellable__get_refill_target_plan({ workspaceId, senders, senderIds, senderNames, targetDate, untilDate })\` before any mutation. Render \`target.eligibleSenderLedger\`, \`target.senderRefillPlans[]\`, and \`target.globalActionQueue\` as the structured packet. Preserve the labels \`Need to prepare\`, \`Goal\`, \`Already sent\`, \`Scheduled\`, \`Ready and waiting to be scheduled\`, and \`Still need\`. In \`--yolo\`, \`mcp__sellable__refill_sends\` maintains a run-local \`refreshedPaidInmailSenderIds\` set, refreshes stale/missing paid InMail facts for each selected paid-InMail sender at most once, reruns \`get_refill_target_plan\`, and returns \`autoPaidInmailRefresh\` plus the post-refresh \`targetPlan\` before choosing the next prep/source-copy/bounded-approval/read-only wait action. Refill ladder: approve generated rows only through the explicit bounded approval gate, process existing same-campaign unenriched/unprepared rows before source work, then same-source copy, then provider-aligned source-more. New source/provider switches change reply-rate baseline and are manual alternates, not \`--yolo\` side effects. Do not present paid-credit refresh as the next operator action after \`refill_sends\` returns that post-refresh plan. For exact-date proof, \`mcp__sellable__get_scheduler_fill_capacity\` is read-only and tells how many cells the scheduler will try to place for a sender/action/date. When ready rows cover the target but scheduled rows do not, \`mcp__sellable__run_scheduler_sweep({ workspaceId, action:"run" })\` may request immediate placement within existing gates and returns a receipt, but it never sends or bypasses limits. If the returned target is already complete, stop with the no-op receipt. If ready rows cover the target but scheduled rows do not, keep the run open in the read-only scheduler wait/re-read loop until projected coverage fills, a concrete non-scheduler blocker appears, or Christian explicitly stops/statuses the run.
2142
2133
  6. Load the canonical prompt and deterministic flow asset via
2143
2134
  \`mcp__sellable__get_subskill_prompt({ subskillName: "refill-sends" })\`,
2144
2135
  then load \`mcp__sellable__get_subskill_prompt({ subskillName: "refill-sends-workflow" })\`,
@@ -2149,7 +2140,8 @@ Desktop, then start a new thread.
2149
2140
  facts are present, refresh exact selected sender facts once, rerun
2150
2141
  \`get_refill_target_plan\`, and only then enter \`wait_for_scheduler\` if
2151
2142
  freshness is clean.
2152
- 7. Follow the canonical prompt exactly. Do not directly send, bypass scheduler gates, or raw-write scheduler fields. Only the exact rendered paused-campaign start and request-scoped scheduler sweep may use their existing product-gated paths; do not launch, archive, or delete unrelated work.
2143
+ 7. Follow the canonical prompt exactly. Do not launch, start, archive, delete,
2144
+ or write scheduler rows as part of refill sends.
2153
2145
 
2154
2146
  ## MCP Prompt Fallback
2155
2147
 
@@ -2670,6 +2662,12 @@ will change or after they approve the settings update.
2670
2662
 
2671
2663
  function codexPluginSkills() {
2672
2664
  return [
2665
+ {
2666
+ dir: "sellable-find-leads",
2667
+ displayName: "Sellable Find Leads",
2668
+ description: "Find prospects and build a live exportable lead list",
2669
+ skillMd: findLeadsSkillMd("codex"),
2670
+ },
2673
2671
  {
2674
2672
  dir: "sellable-create-campaign",
2675
2673
  displayName: "Sellable Create Campaign",
@@ -2910,6 +2908,14 @@ then retry \`get_subskill_prompt\`.
2910
2908
 
2911
2909
  function hermesSkillDefinitions() {
2912
2910
  return [
2911
+ {
2912
+ commandName: "find-leads",
2913
+ dir: hermesCommandName("find-leads"),
2914
+ skillMd: normalizeHermesSkillMarkdown(
2915
+ findLeadsSkillMd("hermes"),
2916
+ "find-leads"
2917
+ ),
2918
+ },
2913
2919
  {
2914
2920
  commandName: "create-campaign",
2915
2921
  dir: hermesCommandName("create-campaign"),
@@ -3093,8 +3099,8 @@ function generateCodexAgentToml(agent) {
3093
3099
  const codex = agent.codex;
3094
3100
  return `name = ${quoteToml(agent.name)}
3095
3101
  description = ${quoteToml(codex.description)}
3096
- model = ${quoteToml(codex.model || "gpt-5.6-sol")}
3097
- model_reasoning_effort = ${quoteToml(codex.modelReasoningEffort || "high")}
3102
+ model = ${quoteToml(codex.model || "gpt-5.5")}
3103
+ model_reasoning_effort = ${quoteToml(codex.modelReasoningEffort || "xhigh")}
3098
3104
  sandbox_mode = ${quoteToml(codex.sandboxMode || "read-only")}
3099
3105
  nickname_candidates = ${tomlArray(codex.nicknameCandidates || [agent.displayName])}
3100
3106
  developer_instructions = ${tomlMultilineString(agent.prompt)}
@@ -3160,6 +3166,15 @@ ${stripFrontmatter(command.skillMd)}
3160
3166
 
3161
3167
  function claudeCommands() {
3162
3168
  return [
3169
+ {
3170
+ name: "find-leads",
3171
+ title: "Find Leads",
3172
+ filename: join("sellable", "find-leads.md"),
3173
+ description: "Find prospects and build a live exportable Sellable lead list.",
3174
+ argumentHint: "[target audience, provider, or lead-list goal]",
3175
+ skillMd: findLeadsSkillMd("claude"),
3176
+ allowedTools: ["AskUserQuestion", ...FIND_LEADS_ALLOWED_TOOLS],
3177
+ },
3163
3178
  {
3164
3179
  name: "create-campaign",
3165
3180
  title: "Create Campaign",
@@ -3544,19 +3559,8 @@ const WATCH_MODE_DRIVER_ENV = {
3544
3559
  };
3545
3560
 
3546
3561
  function mcpEnvForHost(host, opts) {
3547
- const mcpVersionMatch = /^@sellable\/mcp@(.+)$/.exec(opts.mcpPackage || "");
3548
- const mcpVersion = mcpVersionMatch?.[1] || String(opts.mcpPackage || "unknown");
3549
- const pluginVersion = host === "codex" ? CODEX_PLUGIN_VERSION : "not-applicable";
3550
- const cacheIdentity =
3551
- host === "codex"
3552
- ? `codex-plugin:${CODEX_PLUGIN_VERSION}|mcp:${mcpVersion}`
3553
- : `${host}-host|mcp:${mcpVersion}`;
3554
3562
  const env = {
3555
3563
  SELLABLE_WATCH_MODE_DRIVER: host,
3556
- SELLABLE_INSTALL_PACKAGE_VERSION: getInstallVersion(),
3557
- SELLABLE_MCP_PACKAGE_VERSION: mcpVersion,
3558
- SELLABLE_CODEX_PLUGIN_VERSION: pluginVersion,
3559
- SELLABLE_PLUGIN_CACHE_IDENTITY: cacheIdentity,
3560
3564
  };
3561
3565
  if (opts.server === "hosted") return env;
3562
3566
  if (opts.sellableConfigPath) {
@@ -3565,12 +3569,6 @@ function mcpEnvForHost(host, opts) {
3565
3569
  if (opts.sellableConfigsDir) {
3566
3570
  env.SELLABLE_CONFIGS_DIR = opts.sellableConfigsDir;
3567
3571
  }
3568
- if (opts.lockWorkspaceId) {
3569
- env.SELLABLE_LOCK_WORKSPACE_ID = opts.lockWorkspaceId;
3570
- }
3571
- if (opts.requireWorkspaceLock) {
3572
- env.SELLABLE_REQUIRE_WORKSPACE_LOCK = "1";
3573
- }
3574
3572
  return env;
3575
3573
  }
3576
3574
 
@@ -3855,12 +3853,6 @@ function parseHermesProfileBootstrapArgs(argv) {
3855
3853
  apiUrl: process.env.SELLABLE_API_URL || DEFAULT_API_URL,
3856
3854
  slackBotToken: process.env.SLACK_BOT_TOKEN || "",
3857
3855
  slackAppToken: process.env.SLACK_APP_TOKEN || "",
3858
- slackHomeChannel: process.env.SLACK_HOME_CHANNEL || "",
3859
- slackHomeChannelName: process.env.SLACK_HOME_CHANNEL_NAME || "",
3860
- slackAllowedChannels: process.env.SLACK_ALLOWED_CHANNELS || "",
3861
- slackFreeResponseChannels: process.env.SLACK_FREE_RESPONSE_CHANNELS || "",
3862
- slackAllowedUsers: process.env.SLACK_ALLOWED_USERS || "",
3863
- slackRequireMention: process.env.SLACK_REQUIRE_MENTION || "",
3864
3856
  server: process.env.SELLABLE_INSTALL_SERVER || "package",
3865
3857
  mcpPackage: DEFAULT_SERVER_PACKAGE,
3866
3858
  localCommand: process.env.SELLABLE_MCP_LOCAL_COMMAND || "",
@@ -3869,7 +3861,6 @@ function parseHermesProfileBootstrapArgs(argv) {
3869
3861
  force: false,
3870
3862
  overwriteEnv: false,
3871
3863
  allowDuplicateSlackTokens: false,
3872
- requireWorkspaceLock: false,
3873
3864
  verbose: false,
3874
3865
  };
3875
3866
 
@@ -3894,12 +3885,6 @@ function parseHermesProfileBootstrapArgs(argv) {
3894
3885
  else if (arg === "--api-url") opts.apiUrl = next();
3895
3886
  else if (arg === "--slack-bot-token") opts.slackBotToken = next();
3896
3887
  else if (arg === "--slack-app-token") opts.slackAppToken = next();
3897
- else if (arg === "--slack-home-channel") opts.slackHomeChannel = next();
3898
- else if (arg === "--slack-home-channel-name") opts.slackHomeChannelName = next();
3899
- else if (arg === "--slack-allowed-channels") opts.slackAllowedChannels = next();
3900
- else if (arg === "--slack-free-response-channels") opts.slackFreeResponseChannels = next();
3901
- else if (arg === "--slack-allowed-users") opts.slackAllowedUsers = next();
3902
- else if (arg === "--slack-require-mention") opts.slackRequireMention = next();
3903
3888
  else if (arg === "--server") opts.server = next();
3904
3889
  else if (arg === "--mcp-package") {
3905
3890
  opts.mcpPackage = normalizeWindowsPackageSpec(next());
@@ -3908,9 +3893,7 @@ function parseHermesProfileBootstrapArgs(argv) {
3908
3893
  else if (arg === "--json") opts.json = true;
3909
3894
  else if (arg === "--force") opts.force = true;
3910
3895
  else if (arg === "--overwrite-env") opts.overwriteEnv = true;
3911
- else if (arg === "--require-workspace-lock") {
3912
- opts.requireWorkspaceLock = true;
3913
- } else if (arg === "--allow-duplicate-slack-tokens") {
3896
+ else if (arg === "--allow-duplicate-slack-tokens") {
3914
3897
  opts.allowDuplicateSlackTokens = true;
3915
3898
  } else if (arg === "--verbose") opts.verbose = true;
3916
3899
  else {
@@ -4003,111 +3986,6 @@ function renderEnvFile({ entries, passthrough }) {
4003
3986
  return `${lines.join("\n")}${lines.length ? "\n" : ""}`;
4004
3987
  }
4005
3988
 
4006
- function normalizeSlackChannelIds(value, key) {
4007
- const raw = String(value || "").trim();
4008
- if (!raw) return "";
4009
- const ids = raw
4010
- .split(",")
4011
- .map((item) => item.trim())
4012
- .filter(Boolean);
4013
- if (ids.length === 0) return "";
4014
-
4015
- for (const id of ids) {
4016
- if (
4017
- id === "*" ||
4018
- /^all$/i.test(id) ||
4019
- id.startsWith("#") ||
4020
- /\s/.test(id) ||
4021
- !/^[A-Z][A-Z0-9]{8,}$/.test(id)
4022
- ) {
4023
- throw new Error(
4024
- `${key} must be comma-separated Slack channel IDs, not names, wildcards, or free text. Bad value: ${id}`
4025
- );
4026
- }
4027
- }
4028
- return [...new Set(ids)].join(",");
4029
- }
4030
-
4031
- function normalizeSlackSingleChannelId(value, key) {
4032
- const normalized = normalizeSlackChannelIds(value, key);
4033
- if (normalized.includes(",")) {
4034
- throw new Error(`${key} must be a single Slack channel ID`);
4035
- }
4036
- return normalized;
4037
- }
4038
-
4039
- function normalizeSlackUserIds(value, key) {
4040
- const raw = String(value || "").trim();
4041
- if (!raw) return "";
4042
- const ids = raw
4043
- .split(",")
4044
- .map((item) => item.trim())
4045
- .filter(Boolean);
4046
- if (ids.length === 0) return "";
4047
-
4048
- for (const id of ids) {
4049
- if (!/^[UW][A-Z0-9]{8,}$/.test(id)) {
4050
- throw new Error(`${key} must be comma-separated Slack user IDs. Bad value: ${id}`);
4051
- }
4052
- }
4053
- return [...new Set(ids)].join(",");
4054
- }
4055
-
4056
- function normalizeSlackChannelName(value, key) {
4057
- const raw = String(value || "").trim();
4058
- if (!raw) return "";
4059
- if (raw.startsWith("#") || /[\s,]/.test(raw)) {
4060
- throw new Error(`${key} must be a Slack channel name without #, spaces, or commas`);
4061
- }
4062
- return raw;
4063
- }
4064
-
4065
- function normalizeSlackBoolean(value, key) {
4066
- const raw = String(value || "").trim().toLowerCase();
4067
- if (!raw) return "";
4068
- if (raw !== "true" && raw !== "false") {
4069
- throw new Error(`${key} must be true or false`);
4070
- }
4071
- return raw;
4072
- }
4073
-
4074
- function buildSlackProfileEnv(parsed) {
4075
- const homeChannel = normalizeSlackSingleChannelId(
4076
- parsed.slackHomeChannel,
4077
- "SLACK_HOME_CHANNEL"
4078
- );
4079
- const explicitAllowedChannels = normalizeSlackChannelIds(
4080
- parsed.slackAllowedChannels,
4081
- "SLACK_ALLOWED_CHANNELS"
4082
- );
4083
- const allowedChannels = explicitAllowedChannels || homeChannel;
4084
-
4085
- return {
4086
- SLACK_BOT_TOKEN: parsed.slackBotToken.trim(),
4087
- SLACK_APP_TOKEN: parsed.slackAppToken.trim(),
4088
- SLACK_HOME_CHANNEL: homeChannel,
4089
- SLACK_HOME_CHANNEL_NAME: normalizeSlackChannelName(
4090
- parsed.slackHomeChannelName,
4091
- "SLACK_HOME_CHANNEL_NAME"
4092
- ),
4093
- // Customer Hermes profiles must fail closed to their channel. If the caller
4094
- // supplies only a home channel, we treat that as the allowlist too.
4095
- SLACK_ALLOWED_CHANNELS: allowedChannels,
4096
- SLACK_FREE_RESPONSE_CHANNELS: normalizeSlackChannelIds(
4097
- parsed.slackFreeResponseChannels,
4098
- "SLACK_FREE_RESPONSE_CHANNELS"
4099
- ),
4100
- SLACK_ALLOWED_USERS: normalizeSlackUserIds(
4101
- parsed.slackAllowedUsers,
4102
- "SLACK_ALLOWED_USERS"
4103
- ),
4104
- SLACK_REQUIRE_MENTION: normalizeSlackBoolean(
4105
- parsed.slackRequireMention,
4106
- "SLACK_REQUIRE_MENTION"
4107
- ),
4108
- };
4109
- }
4110
-
4111
3989
  function findDuplicateSlackTokenProfiles(profileRoot, slackTokens) {
4112
3990
  const duplicates = [];
4113
3991
  const profilesRoot = dirname(profileRoot);
@@ -4160,16 +4038,10 @@ function runHermesProfileBootstrap(argv) {
4160
4038
  const sellableConfigsDir = join(profileRoot, "sellable", "configs");
4161
4039
  const envPath = join(profileRoot, ".env");
4162
4040
  const { token, source: tokenSource } = readBootstrapToken(parsed);
4163
- const slackEnv = buildSlackProfileEnv(parsed);
4164
4041
  const slackTokens = {
4165
- SLACK_BOT_TOKEN: slackEnv.SLACK_BOT_TOKEN,
4166
- SLACK_APP_TOKEN: slackEnv.SLACK_APP_TOKEN,
4042
+ SLACK_BOT_TOKEN: parsed.slackBotToken.trim(),
4043
+ SLACK_APP_TOKEN: parsed.slackAppToken.trim(),
4167
4044
  };
4168
- const slackEnvKeys = Object.entries(slackEnv)
4169
- .filter(([, value]) => value)
4170
- .map(([key]) => key);
4171
- const requireWorkspaceLock =
4172
- parsed.requireWorkspaceLock || Boolean(parsed.workspaceId);
4173
4045
  const opts = {
4174
4046
  ...parsed,
4175
4047
  host: "hermes",
@@ -4179,8 +4051,6 @@ function runHermesProfileBootstrap(argv) {
4179
4051
  token,
4180
4052
  tokenSource,
4181
4053
  workspaceId: parsed.workspaceId,
4182
- lockWorkspaceId: parsed.workspaceId,
4183
- requireWorkspaceLock,
4184
4054
  apiUrl: parsed.apiUrl,
4185
4055
  };
4186
4056
 
@@ -4205,17 +4075,6 @@ function runHermesProfileBootstrap(argv) {
4205
4075
  apiUrl: opts.apiUrl,
4206
4076
  activeWorkspaceId: parsed.workspaceId || null,
4207
4077
  activeWorkspaceName: parsed.workspaceName || null,
4208
- workspaceLock: parsed.workspaceId
4209
- ? {
4210
- enabled: true,
4211
- workspaceId: parsed.workspaceId,
4212
- required: requireWorkspaceLock,
4213
- }
4214
- : {
4215
- enabled: false,
4216
- workspaceId: null,
4217
- required: requireWorkspaceLock,
4218
- },
4219
4078
  tokenPresent: Boolean(token),
4220
4079
  tokenSource,
4221
4080
  tokenFingerprint: tokenFingerprint(token),
@@ -4224,29 +4083,11 @@ function runHermesProfileBootstrap(argv) {
4224
4083
  keys: Object.entries(slackTokens)
4225
4084
  .filter(([, value]) => value)
4226
4085
  .map(([key]) => key),
4227
- envKeys: slackEnvKeys,
4228
4086
  fingerprints: Object.fromEntries(
4229
4087
  Object.entries(slackTokens)
4230
4088
  .filter(([, value]) => value)
4231
4089
  .map(([key, value]) => [key, tokenFingerprint(value)])
4232
4090
  ),
4233
- scope: {
4234
- homeChannel: slackEnv.SLACK_HOME_CHANNEL || null,
4235
- homeChannelName: slackEnv.SLACK_HOME_CHANNEL_NAME || null,
4236
- allowedChannels: slackEnv.SLACK_ALLOWED_CHANNELS
4237
- ? slackEnv.SLACK_ALLOWED_CHANNELS.split(",")
4238
- : [],
4239
- freeResponseChannels: slackEnv.SLACK_FREE_RESPONSE_CHANNELS
4240
- ? slackEnv.SLACK_FREE_RESPONSE_CHANNELS.split(",")
4241
- : [],
4242
- allowedUsersCount: slackEnv.SLACK_ALLOWED_USERS
4243
- ? slackEnv.SLACK_ALLOWED_USERS.split(",").length
4244
- : 0,
4245
- requireMention:
4246
- slackEnv.SLACK_REQUIRE_MENTION === ""
4247
- ? null
4248
- : slackEnv.SLACK_REQUIRE_MENTION === "true",
4249
- },
4250
4091
  },
4251
4092
  created: [],
4252
4093
  updated: [],
@@ -4291,7 +4132,7 @@ function runHermesProfileBootstrap(argv) {
4291
4132
  if (existsSync(envPath)) {
4292
4133
  envState = parseEnvLines(readFileSync(envPath, "utf8"));
4293
4134
  }
4294
- for (const [key, value] of Object.entries(slackEnv)) {
4135
+ for (const [key, value] of Object.entries(slackTokens)) {
4295
4136
  if (!value) continue;
4296
4137
  const existing = envState.entries.get(key);
4297
4138
  if (existing && existing !== value && !opts.overwriteEnv) {
@@ -4351,8 +4192,8 @@ function runHermesProfileBootstrap(argv) {
4351
4192
  mkdirSync(sellableConfigsDir, { recursive: true, mode: 0o700 });
4352
4193
  writeHermesProfileSellableConfig(sellableConfigPath, nextAuth, opts);
4353
4194
  installHermes(opts);
4354
- if (Object.values(slackEnv).some(Boolean)) {
4355
- for (const [key, value] of Object.entries(slackEnv)) {
4195
+ if (Object.values(slackTokens).some(Boolean)) {
4196
+ for (const [key, value] of Object.entries(slackTokens)) {
4356
4197
  if (value) envState.entries.set(key, value);
4357
4198
  }
4358
4199
  writeFile(envPath, renderEnvFile(envState), opts, 0o600);
@@ -4367,31 +4208,16 @@ function runHermesProfileBootstrap(argv) {
4367
4208
  hermesConfigPath(opts)
4368
4209
  );
4369
4210
  (skillsExisted ? summary.updated : summary.created).push(hermesSkillsRoot(opts));
4370
- if (Object.values(slackEnv).some(Boolean)) {
4211
+ if (Object.values(slackTokens).some(Boolean)) {
4371
4212
  (envExisted ? summary.updated : summary.created).push(envPath);
4372
4213
  } else {
4373
- summary.skipped.push("Slack env: no Slack tokens or channel scope supplied");
4374
- }
4375
- if (slackEnv.SLACK_HOME_CHANNEL && !parsed.slackAllowedChannels.trim()) {
4376
- summary.warnings.push(
4377
- "Slack scope defaulted SLACK_ALLOWED_CHANNELS to SLACK_HOME_CHANNEL for fail-closed profile listening."
4378
- );
4379
- }
4380
- if (slackEnv.SLACK_BOT_TOKEN && !slackEnv.SLACK_APP_TOKEN) {
4381
- summary.warnings.push(
4382
- "Slack bot token is present without SLACK_APP_TOKEN; outbound Slack sends may work, but native Hermes Socket Mode listening still requires an xapp app-level token with connections:write."
4383
- );
4214
+ summary.skipped.push("Slack env: no Slack tokens supplied");
4384
4215
  }
4385
4216
  if (!token) {
4386
4217
  summary.warnings.push(
4387
4218
  `Manual auth pending. Run: sellable auth set <token> --workspace-id <workspace_id> --sellable-config-path ${sellableConfigPath}`
4388
4219
  );
4389
4220
  }
4390
- if (requireWorkspaceLock && !parsed.workspaceId) {
4391
- summary.warnings.push(
4392
- "Sellable MCP will fail closed until sellable-admin provisions SELLABLE_LOCK_WORKSPACE_ID for this customer profile."
4393
- );
4394
- }
4395
4221
 
4396
4222
  if (opts.json) {
4397
4223
  console.log(JSON.stringify(summary, null, 2));
@@ -5394,7 +5220,8 @@ function printNextSteps(installedHosts, authReused) {
5394
5220
  console.log("");
5395
5221
 
5396
5222
  if (hasClaude) {
5397
- printAgentBox("Using Claude Code?", "claude", [
5223
+ printAgentBox("Using Claude Code?", "claude", [
5224
+ { label: "Find Leads", command: "/sellable:find-leads" },
5398
5225
  { label: "Campaign", command: "/sellable:create-campaign" },
5399
5226
  { label: "Evergreen", command: "/sellable:create-evergreen-campaigns" },
5400
5227
  { label: "Foundation", command: "/sellable:foundation" },
@@ -5407,7 +5234,8 @@ function printNextSteps(installedHosts, authReused) {
5407
5234
  console.log("");
5408
5235
  }
5409
5236
  if (hasCodex) {
5410
- printAgentBox("Using Codex?", "codex", [
5237
+ printAgentBox("Using Codex?", "codex", [
5238
+ { label: "Find Leads", command: "$sellable:find-leads" },
5411
5239
  { label: "Campaign", command: "$sellable:create-campaign" },
5412
5240
  { label: "Evergreen", command: "$sellable:create-evergreen-campaigns" },
5413
5241
  { label: "Foundation", command: "$sellable:foundation" },
@@ -5419,7 +5247,8 @@ function printNextSteps(installedHosts, authReused) {
5419
5247
  console.log("");
5420
5248
  }
5421
5249
  if (hasHermes) {
5422
- printAgentBox("Using Hermes?", "hermes", [
5250
+ printAgentBox("Using Hermes?", "hermes", [
5251
+ { label: "Find Leads", command: "/sellable-find-leads" },
5423
5252
  { label: "Campaign", command: "/sellable-create-campaign" },
5424
5253
  { label: "A/B Test", command: "/sellable-create-ab-test" },
5425
5254
  { label: "Evergreen", command: "/sellable-create-evergreen-campaigns" },
@@ -5789,22 +5618,25 @@ async function main() {
5789
5618
  if (workspaceId) {
5790
5619
  console.log(` activeWorkspaceId: ${workspaceId}`);
5791
5620
  }
5792
- console.log(` Continue in your agent:`);
5793
- console.log(` Claude Code: /sellable:create-campaign`);
5621
+ console.log(` Continue in your agent:`);
5622
+ console.log(` Claude Code: /sellable:find-leads`);
5623
+ console.log(` Claude Code: /sellable:create-campaign`);
5794
5624
  console.log(` Claude Code: /sellable:create-evergreen-campaigns`);
5795
5625
  console.log(` Claude Code: /sellable:foundation`);
5796
5626
  console.log(` Claude Code: /sellable:content`);
5797
5627
  console.log(` Claude Code: /sellable:create-post`);
5798
5628
  console.log(` Claude Code: /sellable:refresh-sender-engagement`);
5799
5629
  console.log(` Claude Code: /sellable:refill-sends`);
5800
- console.log(` Codex: $sellable:create-campaign`);
5630
+ console.log(` Codex: $sellable:find-leads`);
5631
+ console.log(` Codex: $sellable:create-campaign`);
5801
5632
  console.log(` Codex: $sellable:create-evergreen-campaigns`);
5802
5633
  console.log(` Codex: $sellable:foundation`);
5803
5634
  console.log(` Codex: $sellable:content`);
5804
5635
  console.log(` Codex: $sellable:create-post`);
5805
5636
  console.log(` Codex: $sellable:refresh-sender-engagement`);
5806
5637
  console.log(` Codex: $sellable:refill-sends`);
5807
- console.log(` Hermes: /sellable-create-campaign`);
5638
+ console.log(` Hermes: /sellable-find-leads`);
5639
+ console.log(` Hermes: /sellable-create-campaign`);
5808
5640
  console.log(` Hermes: /sellable-create-evergreen-campaigns`);
5809
5641
  console.log(` Hermes: /sellable-foundation`);
5810
5642
  console.log(` Hermes: /sellable-content`);
@@ -5861,73 +5693,6 @@ async function main() {
5861
5693
  printCreateCommandHint();
5862
5694
  process.exit(0);
5863
5695
  }
5864
- if (rawArgs[0] === "codex" && rawArgs[1] === "plugin-selection") {
5865
- const command = rawArgs[2];
5866
- const flags = new Map();
5867
- for (let index = 3; index < rawArgs.length; index += 2) {
5868
- const flag = rawArgs[index];
5869
- const value = rawArgs[index + 1];
5870
- if (!flag?.startsWith("--") || !value || value.startsWith("--")) {
5871
- throw new Error(`Invalid Codex plugin-selection option: ${flag ?? "<missing>"}`);
5872
- }
5873
- flags.set(flag, value);
5874
- }
5875
- const required = (name) => {
5876
- const value = flags.get(name);
5877
- if (!value) throw new Error(`Missing value for ${name}`);
5878
- return value;
5879
- };
5880
- const paths = resolveCodexPluginSelectionPaths({ home: homedir() });
5881
- let result;
5882
- if (command === "status") {
5883
- result = readCodexPluginSelection({ paths, stableVersion: flags.get("--stable-version") });
5884
- } else if (command === "snapshot") {
5885
- result = snapshotCodexPluginSelection({
5886
- paths,
5887
- snapshotRoot: required("--snapshot-root"),
5888
- });
5889
- } else if (command === "install-cache") {
5890
- result = installCodexCandidateCache({
5891
- paths,
5892
- sourcePluginRoot: required("--source"),
5893
- version: required("--version"),
5894
- });
5895
- } else if (command === "activate") {
5896
- result = activateCodexCandidateSelection({
5897
- paths,
5898
- snapshotManifestPath: required("--snapshot"),
5899
- expectedSnapshotHash: required("--snapshot-hash"),
5900
- candidateVersion: required("--candidate-version"),
5901
- expectedCandidateHash: required("--candidate-hash"),
5902
- approvalId: required("--approval-id"),
5903
- approvedAt: required("--approved-at"),
5904
- restoreBy: required("--restore-by"),
5905
- now: required("--now"),
5906
- });
5907
- } else if (command === "restore") {
5908
- result = restoreCodexPluginSelection({
5909
- paths,
5910
- snapshotManifestPath: required("--snapshot"),
5911
- expectedSnapshotHash: required("--snapshot-hash"),
5912
- activationPath: required("--activation"),
5913
- now: required("--now"),
5914
- recoveryReason: flags.get("--recovery-reason") || undefined,
5915
- taskEvidence:
5916
- flags.has("--task-id") && flags.has("--resolved-candidate-hash")
5917
- ? {
5918
- taskId: flags.get("--task-id"),
5919
- resolvedCandidateHash: flags.get("--resolved-candidate-hash"),
5920
- }
5921
- : undefined,
5922
- });
5923
- } else {
5924
- throw new Error(
5925
- "Usage: sellable codex plugin-selection snapshot|install-cache|activate|restore|status"
5926
- );
5927
- }
5928
- console.log(JSON.stringify(result, null, 2));
5929
- process.exit(0);
5930
- }
5931
5696
  if (
5932
5697
  rawArgs[0] === "hermes" &&
5933
5698
  rawArgs[1] === "profile" &&