@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.
- package/README.md +5 -41
- package/agents/registry.json +2 -2
- package/bin/sellable-install.mjs +97 -332
- package/lib/runtime-verify.mjs +31 -2
- package/package.json +2 -3
- package/skill-templates/create-campaign.md +3 -3
- package/skill-templates/create-evergreen-campaigns.md +16 -16
- package/skill-templates/find-leads.md +57 -0
- package/skill-templates/refill-sends-v2.md +6 -6
- package/skill-templates/refill-sends.md +353 -91
- package/HERMES-SLACK-PROFILE-SCOPING.md +0 -169
- package/lib/codex-plugin-selection.mjs +0 -353
package/bin/sellable-install.mjs
CHANGED
|
@@ -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.
|
|
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
|
|
235
|
-
|
|
236
|
-
|
|
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.
|
|
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,
|
|
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,
|
|
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
|
|
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.
|
|
3097
|
-
model_reasoning_effort = ${quoteToml(codex.modelReasoningEffort || "
|
|
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 === "--
|
|
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:
|
|
4166
|
-
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(
|
|
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(
|
|
4355
|
-
for (const [key, value] of Object.entries(
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5793
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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" &&
|