@sellable/install 0.1.327 → 0.1.328
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/HERMES-SLACK-PROFILE-SCOPING.md +169 -0
- package/README.md +41 -5
- package/agents/registry.json +2 -2
- package/bin/sellable-install.mjs +385 -112
- package/lib/codex-plugin-selection.mjs +442 -0
- package/lib/runtime-verify.mjs +2 -31
- package/package.json +3 -2
- package/skill-templates/create-campaign.md +3 -3
- package/skill-templates/create-evergreen-campaigns.md +16 -16
- package/skill-templates/refill-sends-v2.md +6 -6
- package/skill-templates/refill-sends.md +91 -353
- package/skill-templates/find-leads.md +0 -57
package/bin/sellable-install.mjs
CHANGED
|
@@ -22,6 +22,14 @@ 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";
|
|
25
33
|
|
|
26
34
|
const DEFAULT_API_URL = "https://app.sellable.dev";
|
|
27
35
|
const DEFAULT_SERVER_PACKAGE =
|
|
@@ -40,7 +48,7 @@ function getInstallVersion() {
|
|
|
40
48
|
}
|
|
41
49
|
}
|
|
42
50
|
|
|
43
|
-
const CODEX_PLUGIN_VERSION = "0.1.
|
|
51
|
+
const CODEX_PLUGIN_VERSION = "0.1.58";
|
|
44
52
|
const CODEX_PLUGIN_COMPAT_VERSIONS = [
|
|
45
53
|
"0.1.8",
|
|
46
54
|
"0.1.9",
|
|
@@ -79,6 +87,8 @@ const CODEX_PLUGIN_COMPAT_VERSIONS = [
|
|
|
79
87
|
"0.1.51",
|
|
80
88
|
"0.1.52",
|
|
81
89
|
"0.1.53",
|
|
90
|
+
"0.1.54",
|
|
91
|
+
"0.1.55",
|
|
82
92
|
];
|
|
83
93
|
const RAW_INSTALL_PACKAGE_SPEC =
|
|
84
94
|
process.env.SELLABLE_INSTALL_PACKAGE_SPEC || "@sellable/install@latest";
|
|
@@ -170,6 +180,8 @@ Commands:
|
|
|
170
180
|
Save the ask-first prompt-sharing preference.
|
|
171
181
|
setup claude-permissions --allow-common-setup
|
|
172
182
|
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.
|
|
173
185
|
hermes profile bootstrap Create a profile-local Hermes config, Sellable auth config,
|
|
174
186
|
configs dir, skills, and optional Slack .env.
|
|
175
187
|
uninstall Remove Sellable host config and installed artifacts.
|
|
@@ -179,6 +191,8 @@ Options:
|
|
|
179
191
|
--server <mode> package, local, or hosted. Default: package
|
|
180
192
|
--token <token> Sellable API token. Also reads SELLABLE_TOKEN.
|
|
181
193
|
--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.
|
|
182
196
|
--api-url <url> Sellable API URL. Default: ${DEFAULT_API_URL}
|
|
183
197
|
--sellable-config-path <path>
|
|
184
198
|
Profile-scoped Sellable config path. Also reads SELLABLE_CONFIG_PATH.
|
|
@@ -199,7 +213,6 @@ Options:
|
|
|
199
213
|
Auth:
|
|
200
214
|
Install is auth-free by default. Sign in happens on the first run of
|
|
201
215
|
/sellable:create-campaign, /sellable:create-evergreen-campaigns,
|
|
202
|
-
/sellable:find-leads,
|
|
203
216
|
/sellable:foundation, /sellable:content, /sellable:create-post,
|
|
204
217
|
/sellable:refresh-sender-engagement, or /sellable:refill-sends in Claude Code,
|
|
205
218
|
Codex, or Hermes,
|
|
@@ -218,9 +231,20 @@ Hermes profile bootstrap:
|
|
|
218
231
|
|
|
219
232
|
Bootstrap writes <profileRoot>/config.yaml, <profileRoot>/sellable/config.json,
|
|
220
233
|
<profileRoot>/sellable/configs, Sellable Hermes skills, and optional
|
|
221
|
-
<profileRoot>/.env Slack token keys.
|
|
222
|
-
|
|
223
|
-
|
|
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.
|
|
224
248
|
`;
|
|
225
249
|
}
|
|
226
250
|
|
|
@@ -1095,35 +1119,6 @@ const CREATE_CAMPAIGN_ALLOWED_TOOLS = [
|
|
|
1095
1119
|
"mcp__sellable__start_campaign",
|
|
1096
1120
|
];
|
|
1097
1121
|
|
|
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
|
-
|
|
1127
1122
|
const CREATE_EVERGREEN_CAMPAIGNS_ALLOWED_TOOLS = [
|
|
1128
1123
|
...CREATE_CAMPAIGN_ALLOWED_TOOLS,
|
|
1129
1124
|
"mcp__sellable__setup_evergreen_campaigns",
|
|
@@ -1677,7 +1672,7 @@ Treat host capabilities as concrete functions, not prose conventions:
|
|
|
1677
1672
|
import and before dispatching Message Drafting only.
|
|
1678
1673
|
- \`launch_message_drafting\`: Claude Code uses \`Task\` with \`subagent_type\`
|
|
1679
1674
|
\`post-find-leads-message-scout\` when listed; Codex uses the returned
|
|
1680
|
-
compatibility agent or a generic \`gpt-5.
|
|
1675
|
+
compatibility agent or a generic \`gpt-5.6-sol\` / \`high\` Message Drafting agent.
|
|
1681
1676
|
|
|
1682
1677
|
If a required interactive question function or MCP loader is missing, stop and
|
|
1683
1678
|
explain the Sellable install/reload problem. Source and filter work use
|
|
@@ -1939,17 +1934,6 @@ then retry \`get_subskill_prompt\`.
|
|
|
1939
1934
|
`, host, "create-campaign");
|
|
1940
1935
|
}
|
|
1941
1936
|
|
|
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
|
-
|
|
1953
1937
|
function createAbTestSkillMd(host = "shared") {
|
|
1954
1938
|
try {
|
|
1955
1939
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
@@ -2066,7 +2050,55 @@ then retry \`get_subskill_prompt\`.
|
|
|
2066
2050
|
`, host, "create-evergreen-campaigns");
|
|
2067
2051
|
}
|
|
2068
2052
|
|
|
2069
|
-
|
|
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
|
+
function refillReleaseIdentityBlock(host, opts = {}) {
|
|
2068
|
+
const match = /^@sellable\/mcp@(.+)$/.exec(opts.mcpPackage || "");
|
|
2069
|
+
const mcpVersion = match?.[1] || String(opts.mcpPackage || "unknown");
|
|
2070
|
+
const pluginVersion = host === "codex" ? CODEX_PLUGIN_VERSION : "not-applicable";
|
|
2071
|
+
const cacheIdentity =
|
|
2072
|
+
host === "codex"
|
|
2073
|
+
? `codex-plugin:${pluginVersion}|mcp:${mcpVersion}`
|
|
2074
|
+
: `${host}-host|mcp:${mcpVersion}`;
|
|
2075
|
+
return `## Installed release identity
|
|
2076
|
+
|
|
2077
|
+
expectedMcpPackageVersion: ${mcpVersion}
|
|
2078
|
+
expectedInstallPackageVersion: ${getInstallVersion()}
|
|
2079
|
+
expectedCodexPluginVersion: ${pluginVersion}
|
|
2080
|
+
expectedPluginCacheIdentity: ${cacheIdentity}
|
|
2081
|
+
|
|
2082
|
+
Immediately after \`get_auth_status\`, require
|
|
2083
|
+
\`update.mcp.currentVersion\` to equal \`${mcpVersion}\`. If it is missing or
|
|
2084
|
+
different, stop with \`installed_mcp_identity_mismatch\` before any product mutation.
|
|
2085
|
+
Do not call \`get_refill_target_plan\`, \`refill_sends\`, or another mutating
|
|
2086
|
+
workflow tool. Explain that Codex is still using a stale Sellable MCP runtime,
|
|
2087
|
+
then fully quit and reopen Codex Desktop and start a new thread.`;
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
function stampRefillReleaseIdentity(markdown, host, opts) {
|
|
2091
|
+
const marker = markdown.includes("## Bootstrap and execute")
|
|
2092
|
+
? "## Bootstrap and execute"
|
|
2093
|
+
: "## Bootstrap";
|
|
2094
|
+
if (!markdown.includes(marker)) return markdown;
|
|
2095
|
+
return markdown.replace(
|
|
2096
|
+
marker,
|
|
2097
|
+
`${refillReleaseIdentityBlock(host, opts)}\n\n${marker}`
|
|
2098
|
+
);
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2101
|
+
function refillSendsSkillMd(host = "shared", opts = {}) {
|
|
2070
2102
|
try {
|
|
2071
2103
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
2072
2104
|
const templatePath = join(
|
|
@@ -2075,11 +2107,18 @@ function refillSendsSkillMd(host = "shared") {
|
|
|
2075
2107
|
"skill-templates",
|
|
2076
2108
|
"refill-sends.md"
|
|
2077
2109
|
);
|
|
2078
|
-
if (
|
|
2110
|
+
if (
|
|
2111
|
+
process.env.SELLABLE_FORCE_REFILL_TEMPLATE_MISSING !== "1" &&
|
|
2112
|
+
existsSync(templatePath)
|
|
2113
|
+
) {
|
|
2079
2114
|
return stampInstalledHost(
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2115
|
+
stampRefillReleaseIdentity(
|
|
2116
|
+
addOrReplaceAllowedTools(
|
|
2117
|
+
readFileSync(templatePath, "utf8"),
|
|
2118
|
+
REFILL_SENDS_ALLOWED_TOOLS
|
|
2119
|
+
),
|
|
2120
|
+
host,
|
|
2121
|
+
opts
|
|
2083
2122
|
),
|
|
2084
2123
|
host,
|
|
2085
2124
|
"refill-sends"
|
|
@@ -2088,7 +2127,7 @@ function refillSendsSkillMd(host = "shared") {
|
|
|
2088
2127
|
} catch {
|
|
2089
2128
|
// fall through to hardcoded fallback below
|
|
2090
2129
|
}
|
|
2091
|
-
return stampInstalledHost(`---
|
|
2130
|
+
return stampInstalledHost(stampRefillReleaseIdentity(`---
|
|
2092
2131
|
name: refill-sends
|
|
2093
2132
|
description: Refill Sellable sender sends through the approval-gated refill workflow.
|
|
2094
2133
|
visibility: public
|
|
@@ -2098,6 +2137,14 @@ ${allowedToolsYaml(REFILL_SENDS_ALLOWED_TOOLS)}
|
|
|
2098
2137
|
|
|
2099
2138
|
# Sellable Refill Sends
|
|
2100
2139
|
|
|
2140
|
+
${REFILL_SENDS_FALLBACK_CONTRACT}
|
|
2141
|
+
|
|
2142
|
+
fallbackSource: embedded
|
|
2143
|
+
|
|
2144
|
+
Load \`refill-sends-workflow\`, then \`core/flow.v1.json\`, then
|
|
2145
|
+
\`core/contract.v2.json\`, each through the Sellable MCP prompt tools until
|
|
2146
|
+
\`hasMore:false\`. Fail closed on unsupported major or stale contract hash.
|
|
2147
|
+
|
|
2101
2148
|
Use this as the customer-facing entrypoint for the Sellable \`refill-sends\`
|
|
2102
2149
|
workflow. It supports \`--yolo\` and optional repeated \`--sender <name-or-id>\`
|
|
2103
2150
|
selectors. It also supports \`--target-date YYYY-MM-DD\` to fill one exact
|
|
@@ -2127,9 +2174,9 @@ Desktop, then start a new thread.
|
|
|
2127
2174
|
|
|
2128
2175
|
1. Call \`mcp__sellable__get_auth_status({})\`.
|
|
2129
2176
|
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.
|
|
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" })\`.
|
|
2177
|
+
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" })\`.
|
|
2131
2178
|
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" })\`.
|
|
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
|
|
2179
|
+
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.
|
|
2133
2180
|
6. Load the canonical prompt and deterministic flow asset via
|
|
2134
2181
|
\`mcp__sellable__get_subskill_prompt({ subskillName: "refill-sends" })\`,
|
|
2135
2182
|
then load \`mcp__sellable__get_subskill_prompt({ subskillName: "refill-sends-workflow" })\`,
|
|
@@ -2140,15 +2187,14 @@ Desktop, then start a new thread.
|
|
|
2140
2187
|
facts are present, refresh exact selected sender facts once, rerun
|
|
2141
2188
|
\`get_refill_target_plan\`, and only then enter \`wait_for_scheduler\` if
|
|
2142
2189
|
freshness is clean.
|
|
2143
|
-
7. Follow the canonical prompt exactly. Do not
|
|
2144
|
-
or write scheduler rows as part of refill sends.
|
|
2190
|
+
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.
|
|
2145
2191
|
|
|
2146
2192
|
## MCP Prompt Fallback
|
|
2147
2193
|
|
|
2148
2194
|
If exact subskill lookup fails, use
|
|
2149
2195
|
\`mcp__sellable__search_subskill_prompts({ query: "refill-sends", includePublic: true, includeInternal: true })\`,
|
|
2150
2196
|
then retry \`get_subskill_prompt\`.
|
|
2151
|
-
`, host, "refill-sends");
|
|
2197
|
+
`, host, opts), host, "refill-sends");
|
|
2152
2198
|
}
|
|
2153
2199
|
|
|
2154
2200
|
function refreshSenderEngagementSkillMd(host = "shared") {
|
|
@@ -2660,14 +2706,8 @@ will change or after they approve the settings update.
|
|
|
2660
2706
|
`;
|
|
2661
2707
|
}
|
|
2662
2708
|
|
|
2663
|
-
function codexPluginSkills() {
|
|
2709
|
+
function codexPluginSkills(opts = {}) {
|
|
2664
2710
|
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
|
-
},
|
|
2671
2711
|
{
|
|
2672
2712
|
dir: "sellable-create-campaign",
|
|
2673
2713
|
displayName: "Sellable Create Campaign",
|
|
@@ -2715,7 +2755,7 @@ function codexPluginSkills() {
|
|
|
2715
2755
|
dir: "sellable-refill-sends",
|
|
2716
2756
|
displayName: "Sellable Refill Sends",
|
|
2717
2757
|
description: "Refill sender sends with approval-gated campaign selection",
|
|
2718
|
-
skillMd: refillSendsSkillMd("codex"),
|
|
2758
|
+
skillMd: refillSendsSkillMd("codex", opts),
|
|
2719
2759
|
},
|
|
2720
2760
|
];
|
|
2721
2761
|
}
|
|
@@ -2906,16 +2946,8 @@ then retry \`get_subskill_prompt\`.
|
|
|
2906
2946
|
);
|
|
2907
2947
|
}
|
|
2908
2948
|
|
|
2909
|
-
function hermesSkillDefinitions() {
|
|
2949
|
+
function hermesSkillDefinitions(opts = {}) {
|
|
2910
2950
|
return [
|
|
2911
|
-
{
|
|
2912
|
-
commandName: "find-leads",
|
|
2913
|
-
dir: hermesCommandName("find-leads"),
|
|
2914
|
-
skillMd: normalizeHermesSkillMarkdown(
|
|
2915
|
-
findLeadsSkillMd("hermes"),
|
|
2916
|
-
"find-leads"
|
|
2917
|
-
),
|
|
2918
|
-
},
|
|
2919
2951
|
{
|
|
2920
2952
|
commandName: "create-campaign",
|
|
2921
2953
|
dir: hermesCommandName("create-campaign"),
|
|
@@ -2971,7 +3003,7 @@ function hermesSkillDefinitions() {
|
|
|
2971
3003
|
commandName: "refill-sends",
|
|
2972
3004
|
dir: hermesCommandName("refill-sends"),
|
|
2973
3005
|
skillMd: normalizeHermesSkillMarkdown(
|
|
2974
|
-
refillSendsSkillMd("hermes"),
|
|
3006
|
+
refillSendsSkillMd("hermes", opts),
|
|
2975
3007
|
"refill-sends"
|
|
2976
3008
|
),
|
|
2977
3009
|
},
|
|
@@ -3099,8 +3131,8 @@ function generateCodexAgentToml(agent) {
|
|
|
3099
3131
|
const codex = agent.codex;
|
|
3100
3132
|
return `name = ${quoteToml(agent.name)}
|
|
3101
3133
|
description = ${quoteToml(codex.description)}
|
|
3102
|
-
model = ${quoteToml(codex.model || "gpt-5.
|
|
3103
|
-
model_reasoning_effort = ${quoteToml(codex.modelReasoningEffort || "
|
|
3134
|
+
model = ${quoteToml(codex.model || "gpt-5.6-sol")}
|
|
3135
|
+
model_reasoning_effort = ${quoteToml(codex.modelReasoningEffort || "high")}
|
|
3104
3136
|
sandbox_mode = ${quoteToml(codex.sandboxMode || "read-only")}
|
|
3105
3137
|
nickname_candidates = ${tomlArray(codex.nicknameCandidates || [agent.displayName])}
|
|
3106
3138
|
developer_instructions = ${tomlMultilineString(agent.prompt)}
|
|
@@ -3164,17 +3196,8 @@ ${stripFrontmatter(command.skillMd)}
|
|
|
3164
3196
|
`;
|
|
3165
3197
|
}
|
|
3166
3198
|
|
|
3167
|
-
function claudeCommands() {
|
|
3199
|
+
function claudeCommands(opts = {}) {
|
|
3168
3200
|
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
|
-
},
|
|
3178
3201
|
{
|
|
3179
3202
|
name: "create-campaign",
|
|
3180
3203
|
title: "Create Campaign",
|
|
@@ -3229,7 +3252,7 @@ function claudeCommands() {
|
|
|
3229
3252
|
description:
|
|
3230
3253
|
"Refill Sellable sender sends through the approval-gated refill workflow.",
|
|
3231
3254
|
argumentHint: "[--yolo] [--sender name-or-id ...]",
|
|
3232
|
-
skillMd: refillSendsSkillMd("claude"),
|
|
3255
|
+
skillMd: refillSendsSkillMd("claude", opts),
|
|
3233
3256
|
allowedTools: ["AskUserQuestion", ...REFILL_SENDS_ALLOWED_TOOLS],
|
|
3234
3257
|
},
|
|
3235
3258
|
{
|
|
@@ -3254,7 +3277,7 @@ function claudeCommands() {
|
|
|
3254
3277
|
}
|
|
3255
3278
|
|
|
3256
3279
|
function writeCodexPluginSkills(pluginRoot, opts) {
|
|
3257
|
-
const skills = codexPluginSkills();
|
|
3280
|
+
const skills = codexPluginSkills(opts);
|
|
3258
3281
|
const currentSkillDirs = new Set(skills.map((skill) => skill.dir));
|
|
3259
3282
|
const skillsRoot = join(pluginRoot, "skills");
|
|
3260
3283
|
|
|
@@ -3318,7 +3341,7 @@ function writeClaudeCustomAgents(opts) {
|
|
|
3318
3341
|
|
|
3319
3342
|
function writeClaudeCommands(opts) {
|
|
3320
3343
|
const home = claudeHome();
|
|
3321
|
-
for (const command of claudeCommands()) {
|
|
3344
|
+
for (const command of claudeCommands(opts)) {
|
|
3322
3345
|
writeFile(join(home, "commands", command.filename), command.content, opts);
|
|
3323
3346
|
}
|
|
3324
3347
|
}
|
|
@@ -3559,8 +3582,19 @@ const WATCH_MODE_DRIVER_ENV = {
|
|
|
3559
3582
|
};
|
|
3560
3583
|
|
|
3561
3584
|
function mcpEnvForHost(host, opts) {
|
|
3585
|
+
const mcpVersionMatch = /^@sellable\/mcp@(.+)$/.exec(opts.mcpPackage || "");
|
|
3586
|
+
const mcpVersion = mcpVersionMatch?.[1] || String(opts.mcpPackage || "unknown");
|
|
3587
|
+
const pluginVersion = host === "codex" ? CODEX_PLUGIN_VERSION : "not-applicable";
|
|
3588
|
+
const cacheIdentity =
|
|
3589
|
+
host === "codex"
|
|
3590
|
+
? `codex-plugin:${CODEX_PLUGIN_VERSION}|mcp:${mcpVersion}`
|
|
3591
|
+
: `${host}-host|mcp:${mcpVersion}`;
|
|
3562
3592
|
const env = {
|
|
3563
3593
|
SELLABLE_WATCH_MODE_DRIVER: host,
|
|
3594
|
+
SELLABLE_INSTALL_PACKAGE_VERSION: getInstallVersion(),
|
|
3595
|
+
SELLABLE_MCP_PACKAGE_VERSION: mcpVersion,
|
|
3596
|
+
SELLABLE_CODEX_PLUGIN_VERSION: pluginVersion,
|
|
3597
|
+
SELLABLE_PLUGIN_CACHE_IDENTITY: cacheIdentity,
|
|
3564
3598
|
};
|
|
3565
3599
|
if (opts.server === "hosted") return env;
|
|
3566
3600
|
if (opts.sellableConfigPath) {
|
|
@@ -3569,6 +3603,12 @@ function mcpEnvForHost(host, opts) {
|
|
|
3569
3603
|
if (opts.sellableConfigsDir) {
|
|
3570
3604
|
env.SELLABLE_CONFIGS_DIR = opts.sellableConfigsDir;
|
|
3571
3605
|
}
|
|
3606
|
+
if (opts.lockWorkspaceId) {
|
|
3607
|
+
env.SELLABLE_LOCK_WORKSPACE_ID = opts.lockWorkspaceId;
|
|
3608
|
+
}
|
|
3609
|
+
if (opts.requireWorkspaceLock) {
|
|
3610
|
+
env.SELLABLE_REQUIRE_WORKSPACE_LOCK = "1";
|
|
3611
|
+
}
|
|
3572
3612
|
return env;
|
|
3573
3613
|
}
|
|
3574
3614
|
|
|
@@ -3813,7 +3853,7 @@ function writeHermesMcpServer(opts) {
|
|
|
3813
3853
|
|
|
3814
3854
|
function installHermesSkills(opts) {
|
|
3815
3855
|
const root = hermesSkillsRoot(opts);
|
|
3816
|
-
for (const skill of hermesSkillDefinitions()) {
|
|
3856
|
+
for (const skill of hermesSkillDefinitions(opts)) {
|
|
3817
3857
|
const skillRoot = join(root, skill.dir);
|
|
3818
3858
|
writeFile(join(skillRoot, "SKILL.md"), skill.skillMd, opts);
|
|
3819
3859
|
if (skill.soulMd) {
|
|
@@ -3853,6 +3893,12 @@ function parseHermesProfileBootstrapArgs(argv) {
|
|
|
3853
3893
|
apiUrl: process.env.SELLABLE_API_URL || DEFAULT_API_URL,
|
|
3854
3894
|
slackBotToken: process.env.SLACK_BOT_TOKEN || "",
|
|
3855
3895
|
slackAppToken: process.env.SLACK_APP_TOKEN || "",
|
|
3896
|
+
slackHomeChannel: process.env.SLACK_HOME_CHANNEL || "",
|
|
3897
|
+
slackHomeChannelName: process.env.SLACK_HOME_CHANNEL_NAME || "",
|
|
3898
|
+
slackAllowedChannels: process.env.SLACK_ALLOWED_CHANNELS || "",
|
|
3899
|
+
slackFreeResponseChannels: process.env.SLACK_FREE_RESPONSE_CHANNELS || "",
|
|
3900
|
+
slackAllowedUsers: process.env.SLACK_ALLOWED_USERS || "",
|
|
3901
|
+
slackRequireMention: process.env.SLACK_REQUIRE_MENTION || "",
|
|
3856
3902
|
server: process.env.SELLABLE_INSTALL_SERVER || "package",
|
|
3857
3903
|
mcpPackage: DEFAULT_SERVER_PACKAGE,
|
|
3858
3904
|
localCommand: process.env.SELLABLE_MCP_LOCAL_COMMAND || "",
|
|
@@ -3861,6 +3907,7 @@ function parseHermesProfileBootstrapArgs(argv) {
|
|
|
3861
3907
|
force: false,
|
|
3862
3908
|
overwriteEnv: false,
|
|
3863
3909
|
allowDuplicateSlackTokens: false,
|
|
3910
|
+
requireWorkspaceLock: false,
|
|
3864
3911
|
verbose: false,
|
|
3865
3912
|
};
|
|
3866
3913
|
|
|
@@ -3885,6 +3932,12 @@ function parseHermesProfileBootstrapArgs(argv) {
|
|
|
3885
3932
|
else if (arg === "--api-url") opts.apiUrl = next();
|
|
3886
3933
|
else if (arg === "--slack-bot-token") opts.slackBotToken = next();
|
|
3887
3934
|
else if (arg === "--slack-app-token") opts.slackAppToken = next();
|
|
3935
|
+
else if (arg === "--slack-home-channel") opts.slackHomeChannel = next();
|
|
3936
|
+
else if (arg === "--slack-home-channel-name") opts.slackHomeChannelName = next();
|
|
3937
|
+
else if (arg === "--slack-allowed-channels") opts.slackAllowedChannels = next();
|
|
3938
|
+
else if (arg === "--slack-free-response-channels") opts.slackFreeResponseChannels = next();
|
|
3939
|
+
else if (arg === "--slack-allowed-users") opts.slackAllowedUsers = next();
|
|
3940
|
+
else if (arg === "--slack-require-mention") opts.slackRequireMention = next();
|
|
3888
3941
|
else if (arg === "--server") opts.server = next();
|
|
3889
3942
|
else if (arg === "--mcp-package") {
|
|
3890
3943
|
opts.mcpPackage = normalizeWindowsPackageSpec(next());
|
|
@@ -3893,7 +3946,9 @@ function parseHermesProfileBootstrapArgs(argv) {
|
|
|
3893
3946
|
else if (arg === "--json") opts.json = true;
|
|
3894
3947
|
else if (arg === "--force") opts.force = true;
|
|
3895
3948
|
else if (arg === "--overwrite-env") opts.overwriteEnv = true;
|
|
3896
|
-
else if (arg === "--
|
|
3949
|
+
else if (arg === "--require-workspace-lock") {
|
|
3950
|
+
opts.requireWorkspaceLock = true;
|
|
3951
|
+
} else if (arg === "--allow-duplicate-slack-tokens") {
|
|
3897
3952
|
opts.allowDuplicateSlackTokens = true;
|
|
3898
3953
|
} else if (arg === "--verbose") opts.verbose = true;
|
|
3899
3954
|
else {
|
|
@@ -3986,6 +4041,111 @@ function renderEnvFile({ entries, passthrough }) {
|
|
|
3986
4041
|
return `${lines.join("\n")}${lines.length ? "\n" : ""}`;
|
|
3987
4042
|
}
|
|
3988
4043
|
|
|
4044
|
+
function normalizeSlackChannelIds(value, key) {
|
|
4045
|
+
const raw = String(value || "").trim();
|
|
4046
|
+
if (!raw) return "";
|
|
4047
|
+
const ids = raw
|
|
4048
|
+
.split(",")
|
|
4049
|
+
.map((item) => item.trim())
|
|
4050
|
+
.filter(Boolean);
|
|
4051
|
+
if (ids.length === 0) return "";
|
|
4052
|
+
|
|
4053
|
+
for (const id of ids) {
|
|
4054
|
+
if (
|
|
4055
|
+
id === "*" ||
|
|
4056
|
+
/^all$/i.test(id) ||
|
|
4057
|
+
id.startsWith("#") ||
|
|
4058
|
+
/\s/.test(id) ||
|
|
4059
|
+
!/^[A-Z][A-Z0-9]{8,}$/.test(id)
|
|
4060
|
+
) {
|
|
4061
|
+
throw new Error(
|
|
4062
|
+
`${key} must be comma-separated Slack channel IDs, not names, wildcards, or free text. Bad value: ${id}`
|
|
4063
|
+
);
|
|
4064
|
+
}
|
|
4065
|
+
}
|
|
4066
|
+
return [...new Set(ids)].join(",");
|
|
4067
|
+
}
|
|
4068
|
+
|
|
4069
|
+
function normalizeSlackSingleChannelId(value, key) {
|
|
4070
|
+
const normalized = normalizeSlackChannelIds(value, key);
|
|
4071
|
+
if (normalized.includes(",")) {
|
|
4072
|
+
throw new Error(`${key} must be a single Slack channel ID`);
|
|
4073
|
+
}
|
|
4074
|
+
return normalized;
|
|
4075
|
+
}
|
|
4076
|
+
|
|
4077
|
+
function normalizeSlackUserIds(value, key) {
|
|
4078
|
+
const raw = String(value || "").trim();
|
|
4079
|
+
if (!raw) return "";
|
|
4080
|
+
const ids = raw
|
|
4081
|
+
.split(",")
|
|
4082
|
+
.map((item) => item.trim())
|
|
4083
|
+
.filter(Boolean);
|
|
4084
|
+
if (ids.length === 0) return "";
|
|
4085
|
+
|
|
4086
|
+
for (const id of ids) {
|
|
4087
|
+
if (!/^[UW][A-Z0-9]{8,}$/.test(id)) {
|
|
4088
|
+
throw new Error(`${key} must be comma-separated Slack user IDs. Bad value: ${id}`);
|
|
4089
|
+
}
|
|
4090
|
+
}
|
|
4091
|
+
return [...new Set(ids)].join(",");
|
|
4092
|
+
}
|
|
4093
|
+
|
|
4094
|
+
function normalizeSlackChannelName(value, key) {
|
|
4095
|
+
const raw = String(value || "").trim();
|
|
4096
|
+
if (!raw) return "";
|
|
4097
|
+
if (raw.startsWith("#") || /[\s,]/.test(raw)) {
|
|
4098
|
+
throw new Error(`${key} must be a Slack channel name without #, spaces, or commas`);
|
|
4099
|
+
}
|
|
4100
|
+
return raw;
|
|
4101
|
+
}
|
|
4102
|
+
|
|
4103
|
+
function normalizeSlackBoolean(value, key) {
|
|
4104
|
+
const raw = String(value || "").trim().toLowerCase();
|
|
4105
|
+
if (!raw) return "";
|
|
4106
|
+
if (raw !== "true" && raw !== "false") {
|
|
4107
|
+
throw new Error(`${key} must be true or false`);
|
|
4108
|
+
}
|
|
4109
|
+
return raw;
|
|
4110
|
+
}
|
|
4111
|
+
|
|
4112
|
+
function buildSlackProfileEnv(parsed) {
|
|
4113
|
+
const homeChannel = normalizeSlackSingleChannelId(
|
|
4114
|
+
parsed.slackHomeChannel,
|
|
4115
|
+
"SLACK_HOME_CHANNEL"
|
|
4116
|
+
);
|
|
4117
|
+
const explicitAllowedChannels = normalizeSlackChannelIds(
|
|
4118
|
+
parsed.slackAllowedChannels,
|
|
4119
|
+
"SLACK_ALLOWED_CHANNELS"
|
|
4120
|
+
);
|
|
4121
|
+
const allowedChannels = explicitAllowedChannels || homeChannel;
|
|
4122
|
+
|
|
4123
|
+
return {
|
|
4124
|
+
SLACK_BOT_TOKEN: parsed.slackBotToken.trim(),
|
|
4125
|
+
SLACK_APP_TOKEN: parsed.slackAppToken.trim(),
|
|
4126
|
+
SLACK_HOME_CHANNEL: homeChannel,
|
|
4127
|
+
SLACK_HOME_CHANNEL_NAME: normalizeSlackChannelName(
|
|
4128
|
+
parsed.slackHomeChannelName,
|
|
4129
|
+
"SLACK_HOME_CHANNEL_NAME"
|
|
4130
|
+
),
|
|
4131
|
+
// Customer Hermes profiles must fail closed to their channel. If the caller
|
|
4132
|
+
// supplies only a home channel, we treat that as the allowlist too.
|
|
4133
|
+
SLACK_ALLOWED_CHANNELS: allowedChannels,
|
|
4134
|
+
SLACK_FREE_RESPONSE_CHANNELS: normalizeSlackChannelIds(
|
|
4135
|
+
parsed.slackFreeResponseChannels,
|
|
4136
|
+
"SLACK_FREE_RESPONSE_CHANNELS"
|
|
4137
|
+
),
|
|
4138
|
+
SLACK_ALLOWED_USERS: normalizeSlackUserIds(
|
|
4139
|
+
parsed.slackAllowedUsers,
|
|
4140
|
+
"SLACK_ALLOWED_USERS"
|
|
4141
|
+
),
|
|
4142
|
+
SLACK_REQUIRE_MENTION: normalizeSlackBoolean(
|
|
4143
|
+
parsed.slackRequireMention,
|
|
4144
|
+
"SLACK_REQUIRE_MENTION"
|
|
4145
|
+
),
|
|
4146
|
+
};
|
|
4147
|
+
}
|
|
4148
|
+
|
|
3989
4149
|
function findDuplicateSlackTokenProfiles(profileRoot, slackTokens) {
|
|
3990
4150
|
const duplicates = [];
|
|
3991
4151
|
const profilesRoot = dirname(profileRoot);
|
|
@@ -4038,10 +4198,16 @@ function runHermesProfileBootstrap(argv) {
|
|
|
4038
4198
|
const sellableConfigsDir = join(profileRoot, "sellable", "configs");
|
|
4039
4199
|
const envPath = join(profileRoot, ".env");
|
|
4040
4200
|
const { token, source: tokenSource } = readBootstrapToken(parsed);
|
|
4201
|
+
const slackEnv = buildSlackProfileEnv(parsed);
|
|
4041
4202
|
const slackTokens = {
|
|
4042
|
-
SLACK_BOT_TOKEN:
|
|
4043
|
-
SLACK_APP_TOKEN:
|
|
4203
|
+
SLACK_BOT_TOKEN: slackEnv.SLACK_BOT_TOKEN,
|
|
4204
|
+
SLACK_APP_TOKEN: slackEnv.SLACK_APP_TOKEN,
|
|
4044
4205
|
};
|
|
4206
|
+
const slackEnvKeys = Object.entries(slackEnv)
|
|
4207
|
+
.filter(([, value]) => value)
|
|
4208
|
+
.map(([key]) => key);
|
|
4209
|
+
const requireWorkspaceLock =
|
|
4210
|
+
parsed.requireWorkspaceLock || Boolean(parsed.workspaceId);
|
|
4045
4211
|
const opts = {
|
|
4046
4212
|
...parsed,
|
|
4047
4213
|
host: "hermes",
|
|
@@ -4051,6 +4217,8 @@ function runHermesProfileBootstrap(argv) {
|
|
|
4051
4217
|
token,
|
|
4052
4218
|
tokenSource,
|
|
4053
4219
|
workspaceId: parsed.workspaceId,
|
|
4220
|
+
lockWorkspaceId: parsed.workspaceId,
|
|
4221
|
+
requireWorkspaceLock,
|
|
4054
4222
|
apiUrl: parsed.apiUrl,
|
|
4055
4223
|
};
|
|
4056
4224
|
|
|
@@ -4075,6 +4243,17 @@ function runHermesProfileBootstrap(argv) {
|
|
|
4075
4243
|
apiUrl: opts.apiUrl,
|
|
4076
4244
|
activeWorkspaceId: parsed.workspaceId || null,
|
|
4077
4245
|
activeWorkspaceName: parsed.workspaceName || null,
|
|
4246
|
+
workspaceLock: parsed.workspaceId
|
|
4247
|
+
? {
|
|
4248
|
+
enabled: true,
|
|
4249
|
+
workspaceId: parsed.workspaceId,
|
|
4250
|
+
required: requireWorkspaceLock,
|
|
4251
|
+
}
|
|
4252
|
+
: {
|
|
4253
|
+
enabled: false,
|
|
4254
|
+
workspaceId: null,
|
|
4255
|
+
required: requireWorkspaceLock,
|
|
4256
|
+
},
|
|
4078
4257
|
tokenPresent: Boolean(token),
|
|
4079
4258
|
tokenSource,
|
|
4080
4259
|
tokenFingerprint: tokenFingerprint(token),
|
|
@@ -4083,11 +4262,29 @@ function runHermesProfileBootstrap(argv) {
|
|
|
4083
4262
|
keys: Object.entries(slackTokens)
|
|
4084
4263
|
.filter(([, value]) => value)
|
|
4085
4264
|
.map(([key]) => key),
|
|
4265
|
+
envKeys: slackEnvKeys,
|
|
4086
4266
|
fingerprints: Object.fromEntries(
|
|
4087
4267
|
Object.entries(slackTokens)
|
|
4088
4268
|
.filter(([, value]) => value)
|
|
4089
4269
|
.map(([key, value]) => [key, tokenFingerprint(value)])
|
|
4090
4270
|
),
|
|
4271
|
+
scope: {
|
|
4272
|
+
homeChannel: slackEnv.SLACK_HOME_CHANNEL || null,
|
|
4273
|
+
homeChannelName: slackEnv.SLACK_HOME_CHANNEL_NAME || null,
|
|
4274
|
+
allowedChannels: slackEnv.SLACK_ALLOWED_CHANNELS
|
|
4275
|
+
? slackEnv.SLACK_ALLOWED_CHANNELS.split(",")
|
|
4276
|
+
: [],
|
|
4277
|
+
freeResponseChannels: slackEnv.SLACK_FREE_RESPONSE_CHANNELS
|
|
4278
|
+
? slackEnv.SLACK_FREE_RESPONSE_CHANNELS.split(",")
|
|
4279
|
+
: [],
|
|
4280
|
+
allowedUsersCount: slackEnv.SLACK_ALLOWED_USERS
|
|
4281
|
+
? slackEnv.SLACK_ALLOWED_USERS.split(",").length
|
|
4282
|
+
: 0,
|
|
4283
|
+
requireMention:
|
|
4284
|
+
slackEnv.SLACK_REQUIRE_MENTION === ""
|
|
4285
|
+
? null
|
|
4286
|
+
: slackEnv.SLACK_REQUIRE_MENTION === "true",
|
|
4287
|
+
},
|
|
4091
4288
|
},
|
|
4092
4289
|
created: [],
|
|
4093
4290
|
updated: [],
|
|
@@ -4132,7 +4329,7 @@ function runHermesProfileBootstrap(argv) {
|
|
|
4132
4329
|
if (existsSync(envPath)) {
|
|
4133
4330
|
envState = parseEnvLines(readFileSync(envPath, "utf8"));
|
|
4134
4331
|
}
|
|
4135
|
-
for (const [key, value] of Object.entries(
|
|
4332
|
+
for (const [key, value] of Object.entries(slackEnv)) {
|
|
4136
4333
|
if (!value) continue;
|
|
4137
4334
|
const existing = envState.entries.get(key);
|
|
4138
4335
|
if (existing && existing !== value && !opts.overwriteEnv) {
|
|
@@ -4192,8 +4389,8 @@ function runHermesProfileBootstrap(argv) {
|
|
|
4192
4389
|
mkdirSync(sellableConfigsDir, { recursive: true, mode: 0o700 });
|
|
4193
4390
|
writeHermesProfileSellableConfig(sellableConfigPath, nextAuth, opts);
|
|
4194
4391
|
installHermes(opts);
|
|
4195
|
-
if (Object.values(
|
|
4196
|
-
for (const [key, value] of Object.entries(
|
|
4392
|
+
if (Object.values(slackEnv).some(Boolean)) {
|
|
4393
|
+
for (const [key, value] of Object.entries(slackEnv)) {
|
|
4197
4394
|
if (value) envState.entries.set(key, value);
|
|
4198
4395
|
}
|
|
4199
4396
|
writeFile(envPath, renderEnvFile(envState), opts, 0o600);
|
|
@@ -4208,16 +4405,31 @@ function runHermesProfileBootstrap(argv) {
|
|
|
4208
4405
|
hermesConfigPath(opts)
|
|
4209
4406
|
);
|
|
4210
4407
|
(skillsExisted ? summary.updated : summary.created).push(hermesSkillsRoot(opts));
|
|
4211
|
-
if (Object.values(
|
|
4408
|
+
if (Object.values(slackEnv).some(Boolean)) {
|
|
4212
4409
|
(envExisted ? summary.updated : summary.created).push(envPath);
|
|
4213
4410
|
} else {
|
|
4214
|
-
summary.skipped.push("Slack env: no Slack tokens supplied");
|
|
4411
|
+
summary.skipped.push("Slack env: no Slack tokens or channel scope supplied");
|
|
4412
|
+
}
|
|
4413
|
+
if (slackEnv.SLACK_HOME_CHANNEL && !parsed.slackAllowedChannels.trim()) {
|
|
4414
|
+
summary.warnings.push(
|
|
4415
|
+
"Slack scope defaulted SLACK_ALLOWED_CHANNELS to SLACK_HOME_CHANNEL for fail-closed profile listening."
|
|
4416
|
+
);
|
|
4417
|
+
}
|
|
4418
|
+
if (slackEnv.SLACK_BOT_TOKEN && !slackEnv.SLACK_APP_TOKEN) {
|
|
4419
|
+
summary.warnings.push(
|
|
4420
|
+
"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."
|
|
4421
|
+
);
|
|
4215
4422
|
}
|
|
4216
4423
|
if (!token) {
|
|
4217
4424
|
summary.warnings.push(
|
|
4218
4425
|
`Manual auth pending. Run: sellable auth set <token> --workspace-id <workspace_id> --sellable-config-path ${sellableConfigPath}`
|
|
4219
4426
|
);
|
|
4220
4427
|
}
|
|
4428
|
+
if (requireWorkspaceLock && !parsed.workspaceId) {
|
|
4429
|
+
summary.warnings.push(
|
|
4430
|
+
"Sellable MCP will fail closed until sellable-admin provisions SELLABLE_LOCK_WORKSPACE_ID for this customer profile."
|
|
4431
|
+
);
|
|
4432
|
+
}
|
|
4221
4433
|
|
|
4222
4434
|
if (opts.json) {
|
|
4223
4435
|
console.log(JSON.stringify(summary, null, 2));
|
|
@@ -5220,8 +5432,7 @@ function printNextSteps(installedHosts, authReused) {
|
|
|
5220
5432
|
console.log("");
|
|
5221
5433
|
|
|
5222
5434
|
if (hasClaude) {
|
|
5223
|
-
|
|
5224
|
-
{ label: "Find Leads", command: "/sellable:find-leads" },
|
|
5435
|
+
printAgentBox("Using Claude Code?", "claude", [
|
|
5225
5436
|
{ label: "Campaign", command: "/sellable:create-campaign" },
|
|
5226
5437
|
{ label: "Evergreen", command: "/sellable:create-evergreen-campaigns" },
|
|
5227
5438
|
{ label: "Foundation", command: "/sellable:foundation" },
|
|
@@ -5234,8 +5445,7 @@ function printNextSteps(installedHosts, authReused) {
|
|
|
5234
5445
|
console.log("");
|
|
5235
5446
|
}
|
|
5236
5447
|
if (hasCodex) {
|
|
5237
|
-
|
|
5238
|
-
{ label: "Find Leads", command: "$sellable:find-leads" },
|
|
5448
|
+
printAgentBox("Using Codex?", "codex", [
|
|
5239
5449
|
{ label: "Campaign", command: "$sellable:create-campaign" },
|
|
5240
5450
|
{ label: "Evergreen", command: "$sellable:create-evergreen-campaigns" },
|
|
5241
5451
|
{ label: "Foundation", command: "$sellable:foundation" },
|
|
@@ -5247,8 +5457,7 @@ function printNextSteps(installedHosts, authReused) {
|
|
|
5247
5457
|
console.log("");
|
|
5248
5458
|
}
|
|
5249
5459
|
if (hasHermes) {
|
|
5250
|
-
|
|
5251
|
-
{ label: "Find Leads", command: "/sellable-find-leads" },
|
|
5460
|
+
printAgentBox("Using Hermes?", "hermes", [
|
|
5252
5461
|
{ label: "Campaign", command: "/sellable-create-campaign" },
|
|
5253
5462
|
{ label: "A/B Test", command: "/sellable-create-ab-test" },
|
|
5254
5463
|
{ label: "Evergreen", command: "/sellable-create-evergreen-campaigns" },
|
|
@@ -5618,25 +5827,22 @@ async function main() {
|
|
|
5618
5827
|
if (workspaceId) {
|
|
5619
5828
|
console.log(` activeWorkspaceId: ${workspaceId}`);
|
|
5620
5829
|
}
|
|
5621
|
-
|
|
5622
|
-
|
|
5623
|
-
console.log(` Claude Code: /sellable:create-campaign`);
|
|
5830
|
+
console.log(` Continue in your agent:`);
|
|
5831
|
+
console.log(` Claude Code: /sellable:create-campaign`);
|
|
5624
5832
|
console.log(` Claude Code: /sellable:create-evergreen-campaigns`);
|
|
5625
5833
|
console.log(` Claude Code: /sellable:foundation`);
|
|
5626
5834
|
console.log(` Claude Code: /sellable:content`);
|
|
5627
5835
|
console.log(` Claude Code: /sellable:create-post`);
|
|
5628
5836
|
console.log(` Claude Code: /sellable:refresh-sender-engagement`);
|
|
5629
5837
|
console.log(` Claude Code: /sellable:refill-sends`);
|
|
5630
|
-
|
|
5631
|
-
console.log(` Codex: $sellable:create-campaign`);
|
|
5838
|
+
console.log(` Codex: $sellable:create-campaign`);
|
|
5632
5839
|
console.log(` Codex: $sellable:create-evergreen-campaigns`);
|
|
5633
5840
|
console.log(` Codex: $sellable:foundation`);
|
|
5634
5841
|
console.log(` Codex: $sellable:content`);
|
|
5635
5842
|
console.log(` Codex: $sellable:create-post`);
|
|
5636
5843
|
console.log(` Codex: $sellable:refresh-sender-engagement`);
|
|
5637
5844
|
console.log(` Codex: $sellable:refill-sends`);
|
|
5638
|
-
|
|
5639
|
-
console.log(` Hermes: /sellable-create-campaign`);
|
|
5845
|
+
console.log(` Hermes: /sellable-create-campaign`);
|
|
5640
5846
|
console.log(` Hermes: /sellable-create-evergreen-campaigns`);
|
|
5641
5847
|
console.log(` Hermes: /sellable-foundation`);
|
|
5642
5848
|
console.log(` Hermes: /sellable-content`);
|
|
@@ -5693,6 +5899,73 @@ async function main() {
|
|
|
5693
5899
|
printCreateCommandHint();
|
|
5694
5900
|
process.exit(0);
|
|
5695
5901
|
}
|
|
5902
|
+
if (rawArgs[0] === "codex" && rawArgs[1] === "plugin-selection") {
|
|
5903
|
+
const command = rawArgs[2];
|
|
5904
|
+
const flags = new Map();
|
|
5905
|
+
for (let index = 3; index < rawArgs.length; index += 2) {
|
|
5906
|
+
const flag = rawArgs[index];
|
|
5907
|
+
const value = rawArgs[index + 1];
|
|
5908
|
+
if (!flag?.startsWith("--") || !value || value.startsWith("--")) {
|
|
5909
|
+
throw new Error(`Invalid Codex plugin-selection option: ${flag ?? "<missing>"}`);
|
|
5910
|
+
}
|
|
5911
|
+
flags.set(flag, value);
|
|
5912
|
+
}
|
|
5913
|
+
const required = (name) => {
|
|
5914
|
+
const value = flags.get(name);
|
|
5915
|
+
if (!value) throw new Error(`Missing value for ${name}`);
|
|
5916
|
+
return value;
|
|
5917
|
+
};
|
|
5918
|
+
const paths = resolveCodexPluginSelectionPaths({ home: homedir() });
|
|
5919
|
+
let result;
|
|
5920
|
+
if (command === "status") {
|
|
5921
|
+
result = readCodexPluginSelection({ paths, stableVersion: flags.get("--stable-version") });
|
|
5922
|
+
} else if (command === "snapshot") {
|
|
5923
|
+
result = snapshotCodexPluginSelection({
|
|
5924
|
+
paths,
|
|
5925
|
+
snapshotRoot: required("--snapshot-root"),
|
|
5926
|
+
});
|
|
5927
|
+
} else if (command === "install-cache") {
|
|
5928
|
+
result = installCodexCandidateCache({
|
|
5929
|
+
paths,
|
|
5930
|
+
sourcePluginRoot: required("--source"),
|
|
5931
|
+
version: required("--version"),
|
|
5932
|
+
});
|
|
5933
|
+
} else if (command === "activate") {
|
|
5934
|
+
result = activateCodexCandidateSelection({
|
|
5935
|
+
paths,
|
|
5936
|
+
snapshotManifestPath: required("--snapshot"),
|
|
5937
|
+
expectedSnapshotHash: required("--snapshot-hash"),
|
|
5938
|
+
candidateVersion: required("--candidate-version"),
|
|
5939
|
+
expectedCandidateHash: required("--candidate-hash"),
|
|
5940
|
+
approvalId: required("--approval-id"),
|
|
5941
|
+
approvedAt: required("--approved-at"),
|
|
5942
|
+
restoreBy: required("--restore-by"),
|
|
5943
|
+
now: required("--now"),
|
|
5944
|
+
});
|
|
5945
|
+
} else if (command === "restore") {
|
|
5946
|
+
result = restoreCodexPluginSelection({
|
|
5947
|
+
paths,
|
|
5948
|
+
snapshotManifestPath: required("--snapshot"),
|
|
5949
|
+
expectedSnapshotHash: required("--snapshot-hash"),
|
|
5950
|
+
activationPath: required("--activation"),
|
|
5951
|
+
now: required("--now"),
|
|
5952
|
+
recoveryReason: flags.get("--recovery-reason") || undefined,
|
|
5953
|
+
taskEvidence:
|
|
5954
|
+
flags.has("--task-id") && flags.has("--resolved-candidate-hash")
|
|
5955
|
+
? {
|
|
5956
|
+
taskId: flags.get("--task-id"),
|
|
5957
|
+
resolvedCandidateHash: flags.get("--resolved-candidate-hash"),
|
|
5958
|
+
}
|
|
5959
|
+
: undefined,
|
|
5960
|
+
});
|
|
5961
|
+
} else {
|
|
5962
|
+
throw new Error(
|
|
5963
|
+
"Usage: sellable codex plugin-selection snapshot|install-cache|activate|restore|status"
|
|
5964
|
+
);
|
|
5965
|
+
}
|
|
5966
|
+
console.log(JSON.stringify(result, null, 2));
|
|
5967
|
+
process.exit(0);
|
|
5968
|
+
}
|
|
5696
5969
|
if (
|
|
5697
5970
|
rawArgs[0] === "hermes" &&
|
|
5698
5971
|
rawArgs[1] === "profile" &&
|