@sellable/install 0.1.325 → 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",
@@ -3554,12 +3569,6 @@ function mcpEnvForHost(host, opts) {
3554
3569
  if (opts.sellableConfigsDir) {
3555
3570
  env.SELLABLE_CONFIGS_DIR = opts.sellableConfigsDir;
3556
3571
  }
3557
- if (opts.lockWorkspaceId) {
3558
- env.SELLABLE_LOCK_WORKSPACE_ID = opts.lockWorkspaceId;
3559
- }
3560
- if (opts.requireWorkspaceLock) {
3561
- env.SELLABLE_REQUIRE_WORKSPACE_LOCK = "1";
3562
- }
3563
3572
  return env;
3564
3573
  }
3565
3574
 
@@ -3844,12 +3853,6 @@ function parseHermesProfileBootstrapArgs(argv) {
3844
3853
  apiUrl: process.env.SELLABLE_API_URL || DEFAULT_API_URL,
3845
3854
  slackBotToken: process.env.SLACK_BOT_TOKEN || "",
3846
3855
  slackAppToken: process.env.SLACK_APP_TOKEN || "",
3847
- slackHomeChannel: process.env.SLACK_HOME_CHANNEL || "",
3848
- slackHomeChannelName: process.env.SLACK_HOME_CHANNEL_NAME || "",
3849
- slackAllowedChannels: process.env.SLACK_ALLOWED_CHANNELS || "",
3850
- slackFreeResponseChannels: process.env.SLACK_FREE_RESPONSE_CHANNELS || "",
3851
- slackAllowedUsers: process.env.SLACK_ALLOWED_USERS || "",
3852
- slackRequireMention: process.env.SLACK_REQUIRE_MENTION || "",
3853
3856
  server: process.env.SELLABLE_INSTALL_SERVER || "package",
3854
3857
  mcpPackage: DEFAULT_SERVER_PACKAGE,
3855
3858
  localCommand: process.env.SELLABLE_MCP_LOCAL_COMMAND || "",
@@ -3858,7 +3861,6 @@ function parseHermesProfileBootstrapArgs(argv) {
3858
3861
  force: false,
3859
3862
  overwriteEnv: false,
3860
3863
  allowDuplicateSlackTokens: false,
3861
- requireWorkspaceLock: false,
3862
3864
  verbose: false,
3863
3865
  };
3864
3866
 
@@ -3883,12 +3885,6 @@ function parseHermesProfileBootstrapArgs(argv) {
3883
3885
  else if (arg === "--api-url") opts.apiUrl = next();
3884
3886
  else if (arg === "--slack-bot-token") opts.slackBotToken = next();
3885
3887
  else if (arg === "--slack-app-token") opts.slackAppToken = next();
3886
- else if (arg === "--slack-home-channel") opts.slackHomeChannel = next();
3887
- else if (arg === "--slack-home-channel-name") opts.slackHomeChannelName = next();
3888
- else if (arg === "--slack-allowed-channels") opts.slackAllowedChannels = next();
3889
- else if (arg === "--slack-free-response-channels") opts.slackFreeResponseChannels = next();
3890
- else if (arg === "--slack-allowed-users") opts.slackAllowedUsers = next();
3891
- else if (arg === "--slack-require-mention") opts.slackRequireMention = next();
3892
3888
  else if (arg === "--server") opts.server = next();
3893
3889
  else if (arg === "--mcp-package") {
3894
3890
  opts.mcpPackage = normalizeWindowsPackageSpec(next());
@@ -3897,9 +3893,7 @@ function parseHermesProfileBootstrapArgs(argv) {
3897
3893
  else if (arg === "--json") opts.json = true;
3898
3894
  else if (arg === "--force") opts.force = true;
3899
3895
  else if (arg === "--overwrite-env") opts.overwriteEnv = true;
3900
- else if (arg === "--require-workspace-lock") {
3901
- opts.requireWorkspaceLock = true;
3902
- } else if (arg === "--allow-duplicate-slack-tokens") {
3896
+ else if (arg === "--allow-duplicate-slack-tokens") {
3903
3897
  opts.allowDuplicateSlackTokens = true;
3904
3898
  } else if (arg === "--verbose") opts.verbose = true;
3905
3899
  else {
@@ -3992,111 +3986,6 @@ function renderEnvFile({ entries, passthrough }) {
3992
3986
  return `${lines.join("\n")}${lines.length ? "\n" : ""}`;
3993
3987
  }
3994
3988
 
3995
- function normalizeSlackChannelIds(value, key) {
3996
- const raw = String(value || "").trim();
3997
- if (!raw) return "";
3998
- const ids = raw
3999
- .split(",")
4000
- .map((item) => item.trim())
4001
- .filter(Boolean);
4002
- if (ids.length === 0) return "";
4003
-
4004
- for (const id of ids) {
4005
- if (
4006
- id === "*" ||
4007
- /^all$/i.test(id) ||
4008
- id.startsWith("#") ||
4009
- /\s/.test(id) ||
4010
- !/^[A-Z][A-Z0-9]{8,}$/.test(id)
4011
- ) {
4012
- throw new Error(
4013
- `${key} must be comma-separated Slack channel IDs, not names, wildcards, or free text. Bad value: ${id}`
4014
- );
4015
- }
4016
- }
4017
- return [...new Set(ids)].join(",");
4018
- }
4019
-
4020
- function normalizeSlackSingleChannelId(value, key) {
4021
- const normalized = normalizeSlackChannelIds(value, key);
4022
- if (normalized.includes(",")) {
4023
- throw new Error(`${key} must be a single Slack channel ID`);
4024
- }
4025
- return normalized;
4026
- }
4027
-
4028
- function normalizeSlackUserIds(value, key) {
4029
- const raw = String(value || "").trim();
4030
- if (!raw) return "";
4031
- const ids = raw
4032
- .split(",")
4033
- .map((item) => item.trim())
4034
- .filter(Boolean);
4035
- if (ids.length === 0) return "";
4036
-
4037
- for (const id of ids) {
4038
- if (!/^[UW][A-Z0-9]{8,}$/.test(id)) {
4039
- throw new Error(`${key} must be comma-separated Slack user IDs. Bad value: ${id}`);
4040
- }
4041
- }
4042
- return [...new Set(ids)].join(",");
4043
- }
4044
-
4045
- function normalizeSlackChannelName(value, key) {
4046
- const raw = String(value || "").trim();
4047
- if (!raw) return "";
4048
- if (raw.startsWith("#") || /[\s,]/.test(raw)) {
4049
- throw new Error(`${key} must be a Slack channel name without #, spaces, or commas`);
4050
- }
4051
- return raw;
4052
- }
4053
-
4054
- function normalizeSlackBoolean(value, key) {
4055
- const raw = String(value || "").trim().toLowerCase();
4056
- if (!raw) return "";
4057
- if (raw !== "true" && raw !== "false") {
4058
- throw new Error(`${key} must be true or false`);
4059
- }
4060
- return raw;
4061
- }
4062
-
4063
- function buildSlackProfileEnv(parsed) {
4064
- const homeChannel = normalizeSlackSingleChannelId(
4065
- parsed.slackHomeChannel,
4066
- "SLACK_HOME_CHANNEL"
4067
- );
4068
- const explicitAllowedChannels = normalizeSlackChannelIds(
4069
- parsed.slackAllowedChannels,
4070
- "SLACK_ALLOWED_CHANNELS"
4071
- );
4072
- const allowedChannels = explicitAllowedChannels || homeChannel;
4073
-
4074
- return {
4075
- SLACK_BOT_TOKEN: parsed.slackBotToken.trim(),
4076
- SLACK_APP_TOKEN: parsed.slackAppToken.trim(),
4077
- SLACK_HOME_CHANNEL: homeChannel,
4078
- SLACK_HOME_CHANNEL_NAME: normalizeSlackChannelName(
4079
- parsed.slackHomeChannelName,
4080
- "SLACK_HOME_CHANNEL_NAME"
4081
- ),
4082
- // Customer Hermes profiles must fail closed to their channel. If the caller
4083
- // supplies only a home channel, we treat that as the allowlist too.
4084
- SLACK_ALLOWED_CHANNELS: allowedChannels,
4085
- SLACK_FREE_RESPONSE_CHANNELS: normalizeSlackChannelIds(
4086
- parsed.slackFreeResponseChannels,
4087
- "SLACK_FREE_RESPONSE_CHANNELS"
4088
- ),
4089
- SLACK_ALLOWED_USERS: normalizeSlackUserIds(
4090
- parsed.slackAllowedUsers,
4091
- "SLACK_ALLOWED_USERS"
4092
- ),
4093
- SLACK_REQUIRE_MENTION: normalizeSlackBoolean(
4094
- parsed.slackRequireMention,
4095
- "SLACK_REQUIRE_MENTION"
4096
- ),
4097
- };
4098
- }
4099
-
4100
3989
  function findDuplicateSlackTokenProfiles(profileRoot, slackTokens) {
4101
3990
  const duplicates = [];
4102
3991
  const profilesRoot = dirname(profileRoot);
@@ -4149,16 +4038,10 @@ function runHermesProfileBootstrap(argv) {
4149
4038
  const sellableConfigsDir = join(profileRoot, "sellable", "configs");
4150
4039
  const envPath = join(profileRoot, ".env");
4151
4040
  const { token, source: tokenSource } = readBootstrapToken(parsed);
4152
- const slackEnv = buildSlackProfileEnv(parsed);
4153
4041
  const slackTokens = {
4154
- SLACK_BOT_TOKEN: slackEnv.SLACK_BOT_TOKEN,
4155
- SLACK_APP_TOKEN: slackEnv.SLACK_APP_TOKEN,
4042
+ SLACK_BOT_TOKEN: parsed.slackBotToken.trim(),
4043
+ SLACK_APP_TOKEN: parsed.slackAppToken.trim(),
4156
4044
  };
4157
- const slackEnvKeys = Object.entries(slackEnv)
4158
- .filter(([, value]) => value)
4159
- .map(([key]) => key);
4160
- const requireWorkspaceLock =
4161
- parsed.requireWorkspaceLock || Boolean(parsed.workspaceId);
4162
4045
  const opts = {
4163
4046
  ...parsed,
4164
4047
  host: "hermes",
@@ -4168,8 +4051,6 @@ function runHermesProfileBootstrap(argv) {
4168
4051
  token,
4169
4052
  tokenSource,
4170
4053
  workspaceId: parsed.workspaceId,
4171
- lockWorkspaceId: parsed.workspaceId,
4172
- requireWorkspaceLock,
4173
4054
  apiUrl: parsed.apiUrl,
4174
4055
  };
4175
4056
 
@@ -4194,17 +4075,6 @@ function runHermesProfileBootstrap(argv) {
4194
4075
  apiUrl: opts.apiUrl,
4195
4076
  activeWorkspaceId: parsed.workspaceId || null,
4196
4077
  activeWorkspaceName: parsed.workspaceName || null,
4197
- workspaceLock: parsed.workspaceId
4198
- ? {
4199
- enabled: true,
4200
- workspaceId: parsed.workspaceId,
4201
- required: requireWorkspaceLock,
4202
- }
4203
- : {
4204
- enabled: false,
4205
- workspaceId: null,
4206
- required: requireWorkspaceLock,
4207
- },
4208
4078
  tokenPresent: Boolean(token),
4209
4079
  tokenSource,
4210
4080
  tokenFingerprint: tokenFingerprint(token),
@@ -4213,29 +4083,11 @@ function runHermesProfileBootstrap(argv) {
4213
4083
  keys: Object.entries(slackTokens)
4214
4084
  .filter(([, value]) => value)
4215
4085
  .map(([key]) => key),
4216
- envKeys: slackEnvKeys,
4217
4086
  fingerprints: Object.fromEntries(
4218
4087
  Object.entries(slackTokens)
4219
4088
  .filter(([, value]) => value)
4220
4089
  .map(([key, value]) => [key, tokenFingerprint(value)])
4221
4090
  ),
4222
- scope: {
4223
- homeChannel: slackEnv.SLACK_HOME_CHANNEL || null,
4224
- homeChannelName: slackEnv.SLACK_HOME_CHANNEL_NAME || null,
4225
- allowedChannels: slackEnv.SLACK_ALLOWED_CHANNELS
4226
- ? slackEnv.SLACK_ALLOWED_CHANNELS.split(",")
4227
- : [],
4228
- freeResponseChannels: slackEnv.SLACK_FREE_RESPONSE_CHANNELS
4229
- ? slackEnv.SLACK_FREE_RESPONSE_CHANNELS.split(",")
4230
- : [],
4231
- allowedUsersCount: slackEnv.SLACK_ALLOWED_USERS
4232
- ? slackEnv.SLACK_ALLOWED_USERS.split(",").length
4233
- : 0,
4234
- requireMention:
4235
- slackEnv.SLACK_REQUIRE_MENTION === ""
4236
- ? null
4237
- : slackEnv.SLACK_REQUIRE_MENTION === "true",
4238
- },
4239
4091
  },
4240
4092
  created: [],
4241
4093
  updated: [],
@@ -4280,7 +4132,7 @@ function runHermesProfileBootstrap(argv) {
4280
4132
  if (existsSync(envPath)) {
4281
4133
  envState = parseEnvLines(readFileSync(envPath, "utf8"));
4282
4134
  }
4283
- for (const [key, value] of Object.entries(slackEnv)) {
4135
+ for (const [key, value] of Object.entries(slackTokens)) {
4284
4136
  if (!value) continue;
4285
4137
  const existing = envState.entries.get(key);
4286
4138
  if (existing && existing !== value && !opts.overwriteEnv) {
@@ -4340,8 +4192,8 @@ function runHermesProfileBootstrap(argv) {
4340
4192
  mkdirSync(sellableConfigsDir, { recursive: true, mode: 0o700 });
4341
4193
  writeHermesProfileSellableConfig(sellableConfigPath, nextAuth, opts);
4342
4194
  installHermes(opts);
4343
- if (Object.values(slackEnv).some(Boolean)) {
4344
- for (const [key, value] of Object.entries(slackEnv)) {
4195
+ if (Object.values(slackTokens).some(Boolean)) {
4196
+ for (const [key, value] of Object.entries(slackTokens)) {
4345
4197
  if (value) envState.entries.set(key, value);
4346
4198
  }
4347
4199
  writeFile(envPath, renderEnvFile(envState), opts, 0o600);
@@ -4356,31 +4208,16 @@ function runHermesProfileBootstrap(argv) {
4356
4208
  hermesConfigPath(opts)
4357
4209
  );
4358
4210
  (skillsExisted ? summary.updated : summary.created).push(hermesSkillsRoot(opts));
4359
- if (Object.values(slackEnv).some(Boolean)) {
4211
+ if (Object.values(slackTokens).some(Boolean)) {
4360
4212
  (envExisted ? summary.updated : summary.created).push(envPath);
4361
4213
  } else {
4362
- summary.skipped.push("Slack env: no Slack tokens or channel scope supplied");
4363
- }
4364
- if (slackEnv.SLACK_HOME_CHANNEL && !parsed.slackAllowedChannels.trim()) {
4365
- summary.warnings.push(
4366
- "Slack scope defaulted SLACK_ALLOWED_CHANNELS to SLACK_HOME_CHANNEL for fail-closed profile listening."
4367
- );
4368
- }
4369
- if (slackEnv.SLACK_BOT_TOKEN && !slackEnv.SLACK_APP_TOKEN) {
4370
- summary.warnings.push(
4371
- "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."
4372
- );
4214
+ summary.skipped.push("Slack env: no Slack tokens supplied");
4373
4215
  }
4374
4216
  if (!token) {
4375
4217
  summary.warnings.push(
4376
4218
  `Manual auth pending. Run: sellable auth set <token> --workspace-id <workspace_id> --sellable-config-path ${sellableConfigPath}`
4377
4219
  );
4378
4220
  }
4379
- if (requireWorkspaceLock && !parsed.workspaceId) {
4380
- summary.warnings.push(
4381
- "Sellable MCP will fail closed until sellable-admin provisions SELLABLE_LOCK_WORKSPACE_ID for this customer profile."
4382
- );
4383
- }
4384
4221
 
4385
4222
  if (opts.json) {
4386
4223
  console.log(JSON.stringify(summary, null, 2));
@@ -5383,7 +5220,8 @@ function printNextSteps(installedHosts, authReused) {
5383
5220
  console.log("");
5384
5221
 
5385
5222
  if (hasClaude) {
5386
- printAgentBox("Using Claude Code?", "claude", [
5223
+ printAgentBox("Using Claude Code?", "claude", [
5224
+ { label: "Find Leads", command: "/sellable:find-leads" },
5387
5225
  { label: "Campaign", command: "/sellable:create-campaign" },
5388
5226
  { label: "Evergreen", command: "/sellable:create-evergreen-campaigns" },
5389
5227
  { label: "Foundation", command: "/sellable:foundation" },
@@ -5396,7 +5234,8 @@ function printNextSteps(installedHosts, authReused) {
5396
5234
  console.log("");
5397
5235
  }
5398
5236
  if (hasCodex) {
5399
- printAgentBox("Using Codex?", "codex", [
5237
+ printAgentBox("Using Codex?", "codex", [
5238
+ { label: "Find Leads", command: "$sellable:find-leads" },
5400
5239
  { label: "Campaign", command: "$sellable:create-campaign" },
5401
5240
  { label: "Evergreen", command: "$sellable:create-evergreen-campaigns" },
5402
5241
  { label: "Foundation", command: "$sellable:foundation" },
@@ -5408,7 +5247,8 @@ function printNextSteps(installedHosts, authReused) {
5408
5247
  console.log("");
5409
5248
  }
5410
5249
  if (hasHermes) {
5411
- printAgentBox("Using Hermes?", "hermes", [
5250
+ printAgentBox("Using Hermes?", "hermes", [
5251
+ { label: "Find Leads", command: "/sellable-find-leads" },
5412
5252
  { label: "Campaign", command: "/sellable-create-campaign" },
5413
5253
  { label: "A/B Test", command: "/sellable-create-ab-test" },
5414
5254
  { label: "Evergreen", command: "/sellable-create-evergreen-campaigns" },
@@ -5778,22 +5618,25 @@ async function main() {
5778
5618
  if (workspaceId) {
5779
5619
  console.log(` activeWorkspaceId: ${workspaceId}`);
5780
5620
  }
5781
- console.log(` Continue in your agent:`);
5782
- 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`);
5783
5624
  console.log(` Claude Code: /sellable:create-evergreen-campaigns`);
5784
5625
  console.log(` Claude Code: /sellable:foundation`);
5785
5626
  console.log(` Claude Code: /sellable:content`);
5786
5627
  console.log(` Claude Code: /sellable:create-post`);
5787
5628
  console.log(` Claude Code: /sellable:refresh-sender-engagement`);
5788
5629
  console.log(` Claude Code: /sellable:refill-sends`);
5789
- console.log(` Codex: $sellable:create-campaign`);
5630
+ console.log(` Codex: $sellable:find-leads`);
5631
+ console.log(` Codex: $sellable:create-campaign`);
5790
5632
  console.log(` Codex: $sellable:create-evergreen-campaigns`);
5791
5633
  console.log(` Codex: $sellable:foundation`);
5792
5634
  console.log(` Codex: $sellable:content`);
5793
5635
  console.log(` Codex: $sellable:create-post`);
5794
5636
  console.log(` Codex: $sellable:refresh-sender-engagement`);
5795
5637
  console.log(` Codex: $sellable:refill-sends`);
5796
- console.log(` Hermes: /sellable-create-campaign`);
5638
+ console.log(` Hermes: /sellable-find-leads`);
5639
+ console.log(` Hermes: /sellable-create-campaign`);
5797
5640
  console.log(` Hermes: /sellable-create-evergreen-campaigns`);
5798
5641
  console.log(` Hermes: /sellable-foundation`);
5799
5642
  console.log(` Hermes: /sellable-content`);
@@ -5850,73 +5693,6 @@ async function main() {
5850
5693
  printCreateCommandHint();
5851
5694
  process.exit(0);
5852
5695
  }
5853
- if (rawArgs[0] === "codex" && rawArgs[1] === "plugin-selection") {
5854
- const command = rawArgs[2];
5855
- const flags = new Map();
5856
- for (let index = 3; index < rawArgs.length; index += 2) {
5857
- const flag = rawArgs[index];
5858
- const value = rawArgs[index + 1];
5859
- if (!flag?.startsWith("--") || !value || value.startsWith("--")) {
5860
- throw new Error(`Invalid Codex plugin-selection option: ${flag ?? "<missing>"}`);
5861
- }
5862
- flags.set(flag, value);
5863
- }
5864
- const required = (name) => {
5865
- const value = flags.get(name);
5866
- if (!value) throw new Error(`Missing value for ${name}`);
5867
- return value;
5868
- };
5869
- const paths = resolveCodexPluginSelectionPaths({ home: homedir() });
5870
- let result;
5871
- if (command === "status") {
5872
- result = readCodexPluginSelection({ paths, stableVersion: flags.get("--stable-version") });
5873
- } else if (command === "snapshot") {
5874
- result = snapshotCodexPluginSelection({
5875
- paths,
5876
- snapshotRoot: required("--snapshot-root"),
5877
- });
5878
- } else if (command === "install-cache") {
5879
- result = installCodexCandidateCache({
5880
- paths,
5881
- sourcePluginRoot: required("--source"),
5882
- version: required("--version"),
5883
- });
5884
- } else if (command === "activate") {
5885
- result = activateCodexCandidateSelection({
5886
- paths,
5887
- snapshotManifestPath: required("--snapshot"),
5888
- expectedSnapshotHash: required("--snapshot-hash"),
5889
- candidateVersion: required("--candidate-version"),
5890
- expectedCandidateHash: required("--candidate-hash"),
5891
- approvalId: required("--approval-id"),
5892
- approvedAt: required("--approved-at"),
5893
- restoreBy: required("--restore-by"),
5894
- now: required("--now"),
5895
- });
5896
- } else if (command === "restore") {
5897
- result = restoreCodexPluginSelection({
5898
- paths,
5899
- snapshotManifestPath: required("--snapshot"),
5900
- expectedSnapshotHash: required("--snapshot-hash"),
5901
- activationPath: required("--activation"),
5902
- now: required("--now"),
5903
- recoveryReason: flags.get("--recovery-reason") || undefined,
5904
- taskEvidence:
5905
- flags.has("--task-id") && flags.has("--resolved-candidate-hash")
5906
- ? {
5907
- taskId: flags.get("--task-id"),
5908
- resolvedCandidateHash: flags.get("--resolved-candidate-hash"),
5909
- }
5910
- : undefined,
5911
- });
5912
- } else {
5913
- throw new Error(
5914
- "Usage: sellable codex plugin-selection snapshot|install-cache|activate|restore|status"
5915
- );
5916
- }
5917
- console.log(JSON.stringify(result, null, 2));
5918
- process.exit(0);
5919
- }
5920
5696
  if (
5921
5697
  rawArgs[0] === "hermes" &&
5922
5698
  rawArgs[1] === "profile" &&