@sellable/install 0.1.300 → 0.1.302

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # Sellable Installer
2
2
 
3
- Installs Sellable MCP for Claude Code and Codex.
3
+ Installs Sellable MCP for Claude Code, Codex, and Hermes.
4
4
 
5
5
  ```bash
6
6
  curl -fsSL "https://app.sellable.dev/api/v2/cli/install" | sh
@@ -16,7 +16,9 @@ Windows users can use PowerShell or Windows Terminal:
16
16
  iwr "https://app.sellable.dev/api/v2/cli/install.ps1" | iex
17
17
  ```
18
18
 
19
- After install, restart Codex Desktop so the Sellable skill appears.
19
+ After install, restart Claude Code, Codex Desktop, or Hermes so Sellable MCP
20
+ tools and skills reload. In an active Hermes session, `/reload-mcp` and
21
+ `/reload-skills` can reload the new config.
20
22
 
21
23
  Verify the runtime tools with:
22
24
 
@@ -32,11 +34,11 @@ sellable create
32
34
  ```
33
35
 
34
36
  Campaign creation, sender send refills, foundation memory, content
35
- capture/ideation, and post drafting run inside Claude Code or Codex, where the
36
- Sellable MCP tools and approval flows are available.
37
+ capture/ideation, and post drafting run inside Claude Code, Codex, or Hermes,
38
+ where the Sellable MCP tools and approval flows are available.
37
39
 
38
40
  Install is auth-free by default. The normal path is first-run login: launch a
39
- Sellable workflow in Claude Code or Codex and the agent handles Sellable
41
+ Sellable workflow in Claude Code, Codex, or Hermes and the agent handles Sellable
40
42
  sign-in with a browser magic-link handoff.
41
43
 
42
44
  The installer uses package stdio MCP by default:
@@ -45,7 +47,7 @@ The installer uses package stdio MCP by default:
45
47
  npm exec --yes --package @sellable/mcp@latest -- sellable-mcp
46
48
  ```
47
49
 
48
- That keeps new Claude Code/Codex MCP starts on the latest stable package. The
50
+ That keeps new Claude Code/Codex/Hermes MCP starts on the latest stable package. The
49
51
  MCP server also checks npm at startup and during `get_auth_status`, caching the
50
52
  result at `~/.sellable/update-check.json` so users are prompted to rerun the
51
53
  latest installer only when an update is actually available.
@@ -81,7 +83,7 @@ Auth is stored once at:
81
83
  ~/.sellable/config.json
82
84
  ```
83
85
 
84
- Claude Code and Codex are configured to launch the same packaged MCP server. The
86
+ Claude Code, Codex, and Hermes are configured to launch the same packaged MCP server. The
85
87
  installer also writes Sellable agent definitions from the packaged `agents/`
86
88
  registry, but normal create-campaign runs use only the Message Drafting
87
89
  background agent (`post-find-leads-message-scout`). Source discovery and
@@ -92,7 +94,7 @@ source-scout agents.
92
94
 
93
95
  ## Names
94
96
 
95
- Use the same public entrypoints in both hosts:
97
+ Use the same public entrypoints across supported hosts:
96
98
 
97
99
  - Claude Code: `/sellable:create-campaign`
98
100
  - Claude Code: `/sellable:create-ab-test`
@@ -110,6 +112,14 @@ Use the same public entrypoints in both hosts:
110
112
  - Codex: `$sellable:create-post`
111
113
  - Codex: `$sellable:refresh-sender-engagement`
112
114
  - Codex: `$sellable:refill-sends`
115
+ - Hermes: `/sellable-create-campaign`
116
+ - Hermes: `/sellable-create-ab-test`
117
+ - Hermes: `/sellable-create-evergreen-campaigns`
118
+ - Hermes: `/sellable-foundation`
119
+ - Hermes: `/sellable-content`
120
+ - Hermes: `/sellable-create-post`
121
+ - Hermes: `/sellable-refresh-sender-engagement`
122
+ - Hermes: `/sellable-refill-sends`
113
123
  - Codex Desktop plugin: `sellable@sellable`
114
124
  - Codex visible skill: `Sellable Create Campaign`
115
125
  - Codex visible skill: `Sellable Create A/B Test`
@@ -125,7 +135,8 @@ Use the same public entrypoints in both hosts:
125
135
 
126
136
  Do not ask users to run `/sellable:create-campaign-v2`,
127
137
  `$sellable:create-campaign-v2`, `$sellable:load-voice`, or
128
- `$sellable:sellable:create-campaign`. `create-campaign-v2` is loaded internally
138
+ `$sellable:sellable:create-campaign`. Do not ask Hermes users to run
139
+ `/sellable:create-campaign`; use `/sellable-create-campaign`. `create-campaign-v2` is loaded internally
129
140
  by the campaign skill. `foundation` is the preferred entrypoint for durable
130
141
  founder/company memory. `content` is the preferred entrypoint for adding
131
142
  transcripts, recurring ideas, and post seeds. `create-post` remains a supported
@@ -140,6 +151,12 @@ mode support by writing `default_mode_request_user_input = true` under
140
151
  `[features]` in `~/.codex/config.toml`. `codex exec` is non-interactive, so it
141
152
  cannot show the structured questionnaire UI.
142
153
 
154
+ Hermes skills use plain chat for setup and approval questions unless a
155
+ Hermes-native approval tool is visible in the current session. Hermes exposes
156
+ Sellable MCP tools as `mcp_sellable_<tool>`, for example
157
+ `mcp_sellable_get_auth_status`, `mcp_sellable_start_cli_login`, and
158
+ `mcp_sellable_wait_for_cli_login`.
159
+
143
160
  For Codex Desktop, the installer also writes a local Sellable plugin bundle into
144
161
  `~/.sellable/codex-marketplace`, includes the Sellable skill entrypoints, and
145
162
  enables it in `~/.codex/config.toml`.
@@ -157,4 +174,4 @@ The installer writes compatibility cache aliases for recent Codex plugin
157
174
  versions so stale Desktop skill links resolve to the current wrapper instead of
158
175
  showing file/version debugging to the user.
159
176
 
160
- If only one host is installed, `--host all` installs the available host and tells you how to add the other one later.
177
+ If only one host is installed, `--host all` installs the available host and tells you how to add the others later.
@@ -15,6 +15,7 @@ import { dirname, join, relative } from "node:path";
15
15
  import { stdout as output } from "node:process";
16
16
  import { createInterface } from "node:readline/promises";
17
17
  import { fileURLToPath } from "node:url";
18
+ import { parseDocument } from "yaml";
18
19
  import {
19
20
  REQUIRED_SELLABLE_MCP_TOOLS,
20
21
  verifySellableMcpRuntime,
@@ -170,7 +171,7 @@ Commands:
170
171
  uninstall Remove Sellable host config and installed artifacts.
171
172
 
172
173
  Options:
173
- --host <host> claude, codex, or all. Default: all
174
+ --host <host> claude, codex, hermes, or all. Default: all
174
175
  --server <mode> package, local, or hosted. Default: package
175
176
  --token <token> Sellable API token. Also reads SELLABLE_TOKEN.
176
177
  --workspace-id <id> Sellable workspace id. Also reads SELLABLE_WORKSPACE_ID.
@@ -190,8 +191,9 @@ Options:
190
191
  Auth:
191
192
  Install is auth-free by default. Sign in happens on the first run of
192
193
  /sellable:create-campaign, /sellable:create-evergreen-campaigns,
193
- /sellable:foundation, /sellable:content, /sellable:create-post, or
194
- /sellable:refresh-sender-engagement, or /sellable:refill-sends in Claude Code or Codex,
194
+ /sellable:foundation, /sellable:content, /sellable:create-post,
195
+ /sellable:refresh-sender-engagement, or /sellable:refill-sends in Claude Code,
196
+ Codex, or Hermes,
195
197
  where the agent walks you through signup or sign-in and stores credentials
196
198
  in ~/.sellable/config.json.
197
199
 
@@ -203,7 +205,7 @@ Auth:
203
205
 
204
206
  function printCreateCommandHint() {
205
207
  printBanner();
206
- console.log(` ${C.bold}Run Sellable from Claude Code or Codex.${C.reset}`);
208
+ console.log(` ${C.bold}Run Sellable from Claude Code, Codex, or Hermes.${C.reset}`);
207
209
  console.log("");
208
210
  console.log(
209
211
  ` ${C.grey}The terminal command installs and verifies Sellable. Campaign creation runs inside your agent session so the workflow can use MCP tools, live state, and approval gates.${C.reset}`
@@ -237,6 +239,18 @@ function printCreateCommandHint() {
237
239
  { label: "Refill", command: "$sellable:refill-sends" },
238
240
  ]);
239
241
  console.log("");
242
+ console.log("");
243
+ printAgentBox("Using Hermes?", "hermes", [
244
+ { label: "Campaign", command: "/sellable-create-campaign" },
245
+ { label: "A/B Test", command: "/sellable-create-ab-test" },
246
+ { label: "Evergreen", command: "/sellable-create-evergreen-campaigns" },
247
+ { label: "Foundation", command: "/sellable-foundation" },
248
+ { label: "Content", command: "/sellable-content" },
249
+ { label: "Post", command: "/sellable-create-post" },
250
+ { label: "Refresh", command: "/sellable-refresh-sender-engagement" },
251
+ { label: "Refill", command: "/sellable-refill-sends" },
252
+ ]);
253
+ console.log("");
240
254
  console.log(` ${"─".repeat(63)}`);
241
255
  console.log(` ${C.grey}If those commands are missing, run:${C.reset}`);
242
256
  console.log("");
@@ -249,7 +263,10 @@ function printCreateCommandHint() {
249
263
 
250
264
  function parseArgs(argv) {
251
265
  const opts = {
252
- host: process.env.SELLABLE_INSTALL_HOST || "all",
266
+ host:
267
+ process.env.SELLABLE_INSTALLER_HOST ||
268
+ process.env.SELLABLE_INSTALL_HOST ||
269
+ "all",
253
270
  server: process.env.SELLABLE_INSTALL_SERVER || "package",
254
271
  token: process.env.SELLABLE_TOKEN || "",
255
272
  workspaceId: process.env.SELLABLE_WORKSPACE_ID || "",
@@ -313,8 +330,8 @@ function parseArgs(argv) {
313
330
  }
314
331
  }
315
332
 
316
- if (!["claude", "codex", "all"].includes(opts.host)) {
317
- throw new Error("--host must be claude, codex, or all");
333
+ if (!["claude", "codex", "hermes", "all"].includes(opts.host)) {
334
+ throw new Error("--host must be claude, codex, hermes, or all");
318
335
  }
319
336
  if (!["package", "local", "hosted"].includes(opts.server)) {
320
337
  throw new Error("--server must be package, local, or hosted");
@@ -1175,6 +1192,29 @@ the Claude Code instruction for customer-facing language and host functions.
1175
1192
 
1176
1193
  Do not tell Claude Code users to run \`$sellable:${commandName}\`, use
1177
1194
  \`request_user_input\`, or restart Codex Desktop. Do not describe this run as Codex.`;
1195
+ }
1196
+ if (host === "hermes") {
1197
+ return `## Installed Host Contract
1198
+
1199
+ This installed skill is running in Hermes Agent. When the shared workflow body
1200
+ or fallback text mentions Claude Code, Codex, or Hermes for internal parity,
1201
+ choose the Hermes instruction for customer-facing language and host functions.
1202
+
1203
+ - Customer-facing command: \`/sellable-${commandName}\`
1204
+ - MCP tool naming: Hermes exposes Sellable tools as \`mcp_sellable_<tool>\`;
1205
+ when shared instructions show \`mcp__sellable__<tool>\`, call the matching
1206
+ \`mcp_sellable_<tool>\` tool instead.
1207
+ - Structured questions: ask plainly in chat unless a Hermes-native approval or
1208
+ question tool is visible in the current session.
1209
+ - Bootstrap host label: \`host: "Hermes"\`
1210
+ - Install/reload blocker label: Hermes install/reload problem
1211
+ - Reload instruction: restart Hermes, or run \`/reload-mcp\` in the active
1212
+ Hermes session after install
1213
+
1214
+ Do not tell Hermes users to run \`$sellable:${commandName}\` or
1215
+ \`/sellable:${commandName}\`, use \`AskUserQuestion\` or \`request_user_input\`,
1216
+ or restart Claude Code or Codex Desktop. Do not describe this run as Claude Code
1217
+ or Codex.`;
1178
1218
  }
1179
1219
  return "";
1180
1220
  }
@@ -2584,6 +2624,256 @@ function codexPluginSkills() {
2584
2624
  ];
2585
2625
  }
2586
2626
 
2627
+ function hermesCommandName(commandName) {
2628
+ return `sellable-${commandName}`;
2629
+ }
2630
+
2631
+ function normalizeHermesSkillMarkdown(markdown, commandName) {
2632
+ const hermesName = hermesCommandName(commandName);
2633
+ let content = String(markdown);
2634
+ if (/^name:\s*.+$/m.test(content)) {
2635
+ content = content.replace(/^name:\s*.+$/m, `name: ${hermesName}`);
2636
+ } else {
2637
+ content = content.replace(/^---\n/m, `---\nname: ${hermesName}\n`);
2638
+ }
2639
+
2640
+ let normalized = content
2641
+ .replace(/mcp__sellable__<tool>/g, "mcp_sellable_<tool>")
2642
+ .replace(/mcp__([A-Za-z0-9_-]+)__<tool>/g, (_, server) => {
2643
+ return `mcp_${String(server).replaceAll("-", "_")}_<tool>`;
2644
+ })
2645
+ .replace(/\bmcp__([A-Za-z0-9_-]+)__([A-Za-z0-9_-]+)\b/g, (_, server, tool) => {
2646
+ return `mcp_${String(server).replaceAll("-", "_")}_${String(tool).replaceAll("-", "_")}`;
2647
+ })
2648
+ .replace(/\bmcp__sellable__/g, "mcp_sellable_")
2649
+ .replace(/\$sellable:/g, "/sellable-")
2650
+ .replace(/\/sellable:/g, "/sellable-")
2651
+ .replace(/Codex install\/reload problem/g, "Hermes install/reload problem")
2652
+ .replace(/Claude Code install\/reload problem/g, "Hermes install/reload problem")
2653
+ .replace(/fully quit and reopen Codex Desktop, then start a new thread/g, "restart Hermes, or run `/reload-mcp` in the active Hermes session")
2654
+ .replace(/fully quit and reopen Claude Code, then start a new session/g, "restart Hermes, or run `/reload-mcp` in the active Hermes session")
2655
+ .replace(/Codex Desktop/g, "Hermes")
2656
+ .replace(/Claude Code\/Codex/g, "Hermes")
2657
+ .replace(/Claude Code or Codex/g, "Hermes")
2658
+ .replace(/Codex or Claude Code/g, "Hermes")
2659
+ .replace(/Claude Code and Codex/g, "Hermes")
2660
+ .replace(/Codex\/Claude/g, "Hermes")
2661
+ .replace(/quick question panel/g, "approval prompt")
2662
+ .replace(/request_user_input/g, "plain chat")
2663
+ .replace(/AskUserQuestion/g, "plain chat");
2664
+
2665
+ normalized = normalized.replace(
2666
+ /Do not tell Hermes users to run[\s\S]*?Do not describe this run as Claude Code\nor Codex\./g,
2667
+ "Do not tell Hermes users to run Codex or Claude command forms, use Codex/Claude structured-question APIs, or restart Codex Desktop or Claude Code. Do not describe this run as Claude Code or Codex."
2668
+ );
2669
+
2670
+ normalized = normalized.replace(
2671
+ /## Structured Questions\n\nUse the host-native structured question gate for intake and approval:[\s\S]*?## Host Runtime Functions/g,
2672
+ `## Structured Questions
2673
+
2674
+ Hermes should ask setup questions directly in chat unless the current Hermes
2675
+ session exposes a native approval or question tool. Use a bounded approval gate
2676
+ only for explicit approval decisions. For open text like LinkedIn URLs, company
2677
+ domains, notes, pasted context, campaign ideas, or feedback, ask in normal chat
2678
+ and wait for the user to paste the value.
2679
+
2680
+ Campaign setup questions are single-choice decisions. Use mutually exclusive
2681
+ options and route blended or custom answers through a short free-text follow-up.
2682
+ Customer-facing language should call this "a couple setup choices" during
2683
+ normal campaign progress.
2684
+
2685
+ ## Host Runtime Functions`
2686
+ );
2687
+
2688
+ normalized = normalized.replace(
2689
+ /## Names To Use\n\nUse these exact public names[\s\S]*?## Structured Questions/g,
2690
+ `## Names To Use
2691
+
2692
+ Use these exact public names for Hermes:
2693
+
2694
+ - Hermes command: \`/${hermesName}\`
2695
+ - Hermes skill directory: \`skills/sellable/${hermesName}/SKILL.md\`
2696
+ - MCP server name: \`sellable\`
2697
+ - Hermes MCP tool prefix: \`mcp_sellable_\`
2698
+ - Internal workflow prompt: \`create-campaign-v2\`
2699
+
2700
+ Do not tell users to run internal subskill names. \`create-campaign-v2\` is only
2701
+ the internal subskill loaded through
2702
+ \`mcp_sellable_get_subskill_prompt({ subskillName: "create-campaign-v2" })\`.
2703
+
2704
+ ## Structured Questions`
2705
+ );
2706
+
2707
+ normalized = normalized.replace(
2708
+ /- `ask_user`:[\s\S]*?(?=\n- `load_subprompt`:)/g,
2709
+ `- \`ask_user\`: ask directly in chat unless a Hermes-native approval or
2710
+ question tool is visible in the current session. Use this for
2711
+ multiple-choice intake, campaign-focus choices, source decisions, and
2712
+ approvals. Campaign setup questions are single-choice only; do not use
2713
+ multi-select or checkbox variants.`
2714
+ );
2715
+
2716
+ normalized = normalized.replace(
2717
+ /- `launch_message_drafting`:[\s\S]*?\n\nIf a required interactive/g,
2718
+ `- \`launch_message_drafting\`: if Hermes exposes a background-agent capability,
2719
+ use the compatible Message Drafting agent. Otherwise run the same message
2720
+ drafting branch inline in the parent session with \`statusSource:
2721
+ "parent-thread-fallback"\`.
2722
+
2723
+ If a required interactive`
2724
+ );
2725
+
2726
+ normalized = normalized.replace(
2727
+ /Do not silently ask (?:Codex|Hermes) intake or approval questions as plain chat[\s\S]*?Plain chat questions are only acceptable in non-interactive `codex exec`\nsmoke\/rehearsal runs because structured user input is unavailable by design\nthere\.\n\n/g,
2728
+ ""
2729
+ );
2730
+
2731
+ normalized = normalized.replace(
2732
+ /## Active Model Metadata\n\nBefore calling `bootstrap_create_campaign`, collect[\s\S]*?Never invent the model or reasoning effort\. Never pass config defaults as active\nmetadata\. If bootstrap returns `modelQuality\.status === "unknown"`, continue\nwithout asking the user to switch models\.\n\n/g,
2733
+ `## Active Model Metadata
2734
+
2735
+ Before calling \`bootstrap_create_campaign\`, pass \`host: "Hermes"\`. If the
2736
+ current Hermes session exposes model and effort metadata for this same turn,
2737
+ pass those exact values with \`modelMetadataSource: "hermes_runtime_metadata"\`.
2738
+ If the current Hermes session context explicitly states both the exact model ID
2739
+ and effort/thinking level, pass them with
2740
+ \`modelMetadataSource: "hermes_session_context"\`. Otherwise omit \`model\`,
2741
+ \`reasoningEffort\`, and \`modelMetadataSource\`.
2742
+
2743
+ Never invent the model or reasoning effort. Never inspect Codex or Claude config
2744
+ files as proof of the active Hermes session. If bootstrap returns
2745
+ \`modelQuality.status === "unknown"\`, continue without asking the user to
2746
+ switch models.
2747
+
2748
+ `
2749
+ );
2750
+
2751
+ normalized = normalized.replace(/not `plain chat`/g, "not internal API names");
2752
+
2753
+ normalized = normalized
2754
+ .replace(/this is a Codex\ninstall\/reload problem/g, "this is a Hermes\ninstall/reload problem")
2755
+ .replace(/Hermes plugin/g, "Hermes skills");
2756
+
2757
+ return normalized;
2758
+ }
2759
+
2760
+ function compactHermesEvergreenSkillMd() {
2761
+ return normalizeHermesSkillMarkdown(
2762
+ stampInstalledHost(`---
2763
+ name: create-evergreen-campaigns
2764
+ description: Create or repair evergreen campaign lanes without launching sends.
2765
+ visibility: public
2766
+ allowed-tools:
2767
+ ${allowedToolsYaml(CREATE_EVERGREEN_CAMPAIGNS_ALLOWED_TOOLS)}
2768
+ ---
2769
+
2770
+ # Sellable Create Evergreen Campaigns
2771
+
2772
+ Use this as the customer-facing entrypoint for the Sellable
2773
+ \`create-evergreen-campaigns\` workflow in Hermes.
2774
+
2775
+ ## Bootstrap
2776
+
2777
+ MCP prompt and command access are required. First call
2778
+ \`mcp__sellable__get_auth_status({})\`. Do not inspect repo files, run shell
2779
+ commands, use \`npm\`, \`node\`, local harness scripts, or read local prompt
2780
+ files to emulate this workflow.
2781
+
2782
+ If the Sellable MCP prompt tools are unavailable, stop and say this is a Hermes
2783
+ install/reload problem. Tell the user to run
2784
+ \`curl -fsSL "https://app.sellable.dev/api/v2/cli/install" | sh\`, then restart
2785
+ Hermes or run \`/reload-mcp\`.
2786
+
2787
+ ## Execute Workflow
2788
+
2789
+ 1. Load the canonical prompt via
2790
+ \`mcp__sellable__get_subskill_prompt({ subskillName: "create-evergreen-campaigns" })\`.
2791
+ If the response has \`hasMore=true\`, continue with \`nextOffset\` until
2792
+ \`hasMore=false\`.
2793
+ 2. Ask for bounded approval in normal chat before mutating if the plan requires
2794
+ approval. Do not call Codex app thread tools or Codex CLI workers from
2795
+ Hermes.
2796
+ 3. Use \`mcp__sellable__setup_evergreen_campaigns({ mode: "plan" })\` for the
2797
+ plan/readback packet, then use the canonical prompt's approved mutation path
2798
+ and return to \`setup_evergreen_campaigns({ mode: "verify" })\` with
2799
+ customer-visible receipts.
2800
+ 4. Do not launch campaigns, schedule sends, archive campaigns, delete tables,
2801
+ or spend paid credits.
2802
+
2803
+ ## MCP Prompt Fallback
2804
+
2805
+ If exact subskill lookup fails, use
2806
+ \`mcp__sellable__search_subskill_prompts({ query: "create-evergreen-campaigns", includePublic: true, includeInternal: true })\`,
2807
+ then retry \`get_subskill_prompt\`.
2808
+ `, "hermes", "create-evergreen-campaigns"),
2809
+ "create-evergreen-campaigns"
2810
+ );
2811
+ }
2812
+
2813
+ function hermesSkillDefinitions() {
2814
+ return [
2815
+ {
2816
+ commandName: "create-campaign",
2817
+ dir: hermesCommandName("create-campaign"),
2818
+ skillMd: normalizeHermesSkillMarkdown(
2819
+ createCampaignSkillMd("hermes"),
2820
+ "create-campaign"
2821
+ ),
2822
+ soulMd: createCampaignSoulMd(),
2823
+ },
2824
+ {
2825
+ commandName: "create-ab-test",
2826
+ dir: hermesCommandName("create-ab-test"),
2827
+ skillMd: normalizeHermesSkillMarkdown(
2828
+ createAbTestSkillMd("hermes"),
2829
+ "create-ab-test"
2830
+ ),
2831
+ },
2832
+ {
2833
+ commandName: "create-evergreen-campaigns",
2834
+ dir: hermesCommandName("create-evergreen-campaigns"),
2835
+ skillMd: compactHermesEvergreenSkillMd(),
2836
+ },
2837
+ {
2838
+ commandName: "foundation",
2839
+ dir: hermesCommandName("foundation"),
2840
+ skillMd: normalizeHermesSkillMarkdown(
2841
+ foundationSkillMd("hermes"),
2842
+ "foundation"
2843
+ ),
2844
+ },
2845
+ {
2846
+ commandName: "content",
2847
+ dir: hermesCommandName("content"),
2848
+ skillMd: normalizeHermesSkillMarkdown(contentSkillMd("hermes"), "content"),
2849
+ },
2850
+ {
2851
+ commandName: "create-post",
2852
+ dir: hermesCommandName("create-post"),
2853
+ skillMd: normalizeHermesSkillMarkdown(
2854
+ createPostSkillMd("hermes"),
2855
+ "create-post"
2856
+ ),
2857
+ },
2858
+ {
2859
+ commandName: "refresh-sender-engagement",
2860
+ dir: hermesCommandName("refresh-sender-engagement"),
2861
+ skillMd: normalizeHermesSkillMarkdown(
2862
+ refreshSenderEngagementSkillMd("hermes"),
2863
+ "refresh-sender-engagement"
2864
+ ),
2865
+ },
2866
+ {
2867
+ commandName: "refill-sends",
2868
+ dir: hermesCommandName("refill-sends"),
2869
+ skillMd: normalizeHermesSkillMarkdown(
2870
+ refillSendsSkillMd("hermes"),
2871
+ "refill-sends"
2872
+ ),
2873
+ },
2874
+ ];
2875
+ }
2876
+
2587
2877
  function installerPackageRoot() {
2588
2878
  return join(dirname(fileURLToPath(import.meta.url)), "..");
2589
2879
  }
@@ -3152,6 +3442,7 @@ exec ${shellQuote(npmCommand)} exec --yes --package ${shellQuote(INSTALL_PACKAGE
3152
3442
  const WATCH_MODE_DRIVER_ENV = {
3153
3443
  claude: "SELLABLE_WATCH_MODE_DRIVER=claude",
3154
3444
  codex: "SELLABLE_WATCH_MODE_DRIVER=codex",
3445
+ hermes: "SELLABLE_WATCH_MODE_DRIVER=hermes",
3155
3446
  };
3156
3447
 
3157
3448
  function withHostedWatchModeDriver(rawUrl, driver) {
@@ -3277,6 +3568,169 @@ function codexPluginMcpServerMatches(content, opts) {
3277
3568
  }
3278
3569
  }
3279
3570
 
3571
+ function hermesHome() {
3572
+ return process.env.HERMES_HOME?.trim() || join(homedir(), ".hermes");
3573
+ }
3574
+
3575
+ function hermesConfigPath() {
3576
+ return join(hermesHome(), "config.yaml");
3577
+ }
3578
+
3579
+ function hermesSkillsRoot() {
3580
+ return join(hermesHome(), "skills", "sellable");
3581
+ }
3582
+
3583
+ function hermesLikelyInstalled() {
3584
+ return (
3585
+ Boolean(process.env.HERMES_HOME?.trim()) ||
3586
+ commandExists("hermes") ||
3587
+ existsSync(hermesConfigPath()) ||
3588
+ existsSync(join(hermesHome(), "skills"))
3589
+ );
3590
+ }
3591
+
3592
+ function parseHermesConfig(raw, configPath = hermesConfigPath()) {
3593
+ const source = raw.trim() ? raw : "{}\n";
3594
+ const doc = parseDocument(source, { prettyErrors: false });
3595
+ if (doc.errors.length > 0) {
3596
+ throw new Error(
3597
+ `Invalid Hermes config YAML at ${configPath}: ${doc.errors[0].message}`
3598
+ );
3599
+ }
3600
+ const value = doc.toJSON() ?? {};
3601
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
3602
+ throw new Error(
3603
+ `Invalid Hermes config YAML at ${configPath}: expected a mapping at the document root`
3604
+ );
3605
+ }
3606
+ return value;
3607
+ }
3608
+
3609
+ function readHermesConfig() {
3610
+ const configPath = hermesConfigPath();
3611
+ const raw = existsSync(configPath) ? readFileSync(configPath, "utf8") : "";
3612
+ return { raw, config: parseHermesConfig(raw, configPath), configPath };
3613
+ }
3614
+
3615
+ function hermesMcpServer(opts) {
3616
+ if (opts.server === "hosted") {
3617
+ return {
3618
+ url: withHostedWatchModeDriver(opts.hostedUrl, "hermes"),
3619
+ enabled: true,
3620
+ env: {
3621
+ SELLABLE_WATCH_MODE_DRIVER: "hermes",
3622
+ },
3623
+ };
3624
+ }
3625
+
3626
+ const [command, args] = mcpCommand(opts);
3627
+ return {
3628
+ command,
3629
+ args,
3630
+ enabled: true,
3631
+ env: {
3632
+ SELLABLE_WATCH_MODE_DRIVER: "hermes",
3633
+ },
3634
+ };
3635
+ }
3636
+
3637
+ function writeHermesConfig(configPath, config, opts) {
3638
+ if (opts.dryRun) return;
3639
+ mkdirSync(dirname(configPath), { recursive: true, mode: 0o700 });
3640
+ const doc = parseDocument("{}\n");
3641
+ doc.contents = config;
3642
+ writeFileSync(configPath, String(doc), { mode: 0o600 });
3643
+ }
3644
+
3645
+ function writeHermesMcpServer(opts) {
3646
+ const { config, configPath } = readHermesConfig();
3647
+ const existingMcpServers =
3648
+ config.mcp_servers &&
3649
+ typeof config.mcp_servers === "object" &&
3650
+ !Array.isArray(config.mcp_servers)
3651
+ ? config.mcp_servers
3652
+ : {};
3653
+ const nextConfig = {
3654
+ ...config,
3655
+ mcp_servers: {
3656
+ ...existingMcpServers,
3657
+ sellable: hermesMcpServer(opts),
3658
+ },
3659
+ };
3660
+ writeHermesConfig(configPath, nextConfig, opts);
3661
+ return configPath;
3662
+ }
3663
+
3664
+ function installHermesSkills(opts) {
3665
+ const root = hermesSkillsRoot();
3666
+ for (const skill of hermesSkillDefinitions()) {
3667
+ const skillRoot = join(root, skill.dir);
3668
+ writeFile(join(skillRoot, "SKILL.md"), skill.skillMd, opts);
3669
+ if (skill.soulMd) {
3670
+ writeFile(join(skillRoot, "SOUL.md"), skill.soulMd, opts);
3671
+ }
3672
+ }
3673
+ return root;
3674
+ }
3675
+
3676
+ function installHermes(opts) {
3677
+ // Parse config before writing skills so invalid user YAML cannot leave a
3678
+ // half-installed Hermes skill tree behind.
3679
+ readHermesConfig();
3680
+ let skillsRoot = null;
3681
+ try {
3682
+ skillsRoot = installHermesSkills(opts);
3683
+ const configPath = writeHermesMcpServer(opts);
3684
+ logVerbose(`${C.grey}+ Hermes config updated: ${configPath}${C.reset}`);
3685
+ return { installed: true, configPath, skillsRoot };
3686
+ } catch (err) {
3687
+ if (skillsRoot && !opts.dryRun) {
3688
+ rmSync(skillsRoot, { recursive: true, force: true });
3689
+ }
3690
+ throw err;
3691
+ }
3692
+ }
3693
+
3694
+ function hermesMcpServerMatches(config, opts) {
3695
+ const server = config?.mcp_servers?.sellable;
3696
+ if (!server || typeof server !== "object") return false;
3697
+ const expected = hermesMcpServer(opts);
3698
+ return JSON.stringify(server) === JSON.stringify(expected);
3699
+ }
3700
+
3701
+ function hermesSkillChecks() {
3702
+ const root = hermesSkillsRoot();
3703
+ const skills = hermesSkillDefinitions();
3704
+ const paths = skills.map((skill) => join(root, skill.dir, "SKILL.md"));
3705
+ const allPresent = paths.every((skillPath) => existsSync(skillPath));
3706
+ const contents = paths
3707
+ .filter((skillPath) => existsSync(skillPath))
3708
+ .map((skillPath) => readFileSync(skillPath, "utf8"));
3709
+ const commandNamesCurrent =
3710
+ contents.length === skills.length &&
3711
+ skills.every((skill, index) =>
3712
+ contents[index]?.includes(`name: ${hermesCommandName(skill.commandName)}`)
3713
+ );
3714
+ const slashCommandsCurrent =
3715
+ contents.length === skills.length &&
3716
+ skills.every((skill, index) =>
3717
+ contents[index]?.includes(`/${hermesCommandName(skill.commandName)}`)
3718
+ );
3719
+ const forbiddenSyntax = contents.some((content) =>
3720
+ /\$sellable:|\/sellable:|mcp__sellable__|request_user_input|AskUserQuestion/.test(
3721
+ content
3722
+ )
3723
+ );
3724
+ return {
3725
+ root,
3726
+ paths,
3727
+ allPresent,
3728
+ commandNamesCurrent,
3729
+ slashCommandsCurrent,
3730
+ forbiddenSyntax,
3731
+ };
3732
+ }
3733
+
3280
3734
  function installClaude(opts) {
3281
3735
  if (!commandExists("claude")) {
3282
3736
  const message =
@@ -3558,6 +4012,13 @@ function warningCheck(ok, label) {
3558
4012
  return { ok, label, required: false };
3559
4013
  }
3560
4014
 
4015
+ function firstRunCommandForHost(host) {
4016
+ if (host === "hermes") return "/sellable-create-campaign";
4017
+ if (host === "codex") return "$sellable:create-campaign";
4018
+ if (host === "claude") return "/sellable:create-campaign";
4019
+ return "/sellable:create-campaign, $sellable:create-campaign, or /sellable-create-campaign";
4020
+ }
4021
+
3561
4022
  async function verify(opts) {
3562
4023
  const startedAt = new Date().toISOString();
3563
4024
  const stored = readStoredAuth();
@@ -3573,7 +4034,7 @@ async function verify(opts) {
3573
4034
  checks.push(
3574
4035
  warningCheck(
3575
4036
  true,
3576
- `Auth: not yet signed in — sign in on first run of /sellable:create-campaign`
4037
+ `Auth: not yet signed in — sign in on first run of ${firstRunCommandForHost(opts.host)}`
3577
4038
  )
3578
4039
  );
3579
4040
  }
@@ -3860,6 +4321,88 @@ async function verify(opts) {
3860
4321
  )
3861
4322
  );
3862
4323
  }
4324
+ if (opts.host === "hermes" || opts.host === "all") {
4325
+ let hermesConfig = null;
4326
+ let hermesConfigError = "";
4327
+ try {
4328
+ hermesConfig = readHermesConfig().config;
4329
+ } catch (err) {
4330
+ hermesConfigError = err instanceof Error ? err.message : String(err);
4331
+ }
4332
+ const npmPresent = opts.server !== "package" || commandExists(packageManagerCommand());
4333
+ const skillChecks = hermesSkillChecks();
4334
+ const configMatches = hermesConfig
4335
+ ? hermesMcpServerMatches(hermesConfig, opts)
4336
+ : false;
4337
+ const hasHermesDriver =
4338
+ hermesConfig?.mcp_servers?.sellable?.env?.SELLABLE_WATCH_MODE_DRIVER ===
4339
+ "hermes";
4340
+
4341
+ const check =
4342
+ opts.host === "hermes" ? requiredCheck : warningCheck;
4343
+ checks.push(
4344
+ check(
4345
+ !hermesConfigError,
4346
+ hermesConfigError || `Hermes config readable: ${hermesConfigPath()}`
4347
+ )
4348
+ );
4349
+ checks.push(
4350
+ check(
4351
+ configMatches,
4352
+ configMatches
4353
+ ? "Hermes Sellable MCP entry canonical"
4354
+ : "Hermes Sellable MCP entry missing or stale"
4355
+ )
4356
+ );
4357
+ checks.push(
4358
+ check(
4359
+ hasHermesDriver,
4360
+ hasHermesDriver
4361
+ ? "Hermes watch mode driver pinned to hermes"
4362
+ : "Hermes watch mode driver missing"
4363
+ )
4364
+ );
4365
+ checks.push(
4366
+ check(
4367
+ skillChecks.allPresent,
4368
+ skillChecks.allPresent
4369
+ ? "Hermes Sellable skills present"
4370
+ : "Hermes Sellable skills missing"
4371
+ )
4372
+ );
4373
+ checks.push(
4374
+ check(
4375
+ skillChecks.commandNamesCurrent,
4376
+ skillChecks.commandNamesCurrent
4377
+ ? "Hermes skill names current"
4378
+ : "Hermes skill names stale"
4379
+ )
4380
+ );
4381
+ checks.push(
4382
+ check(
4383
+ skillChecks.slashCommandsCurrent,
4384
+ skillChecks.slashCommandsCurrent
4385
+ ? "Hermes slash command hints current"
4386
+ : "Hermes slash command hints stale"
4387
+ )
4388
+ );
4389
+ checks.push(
4390
+ check(
4391
+ !skillChecks.forbiddenSyntax,
4392
+ !skillChecks.forbiddenSyntax
4393
+ ? "Hermes skills use Hermes MCP/tool syntax"
4394
+ : "Hermes skills contain Claude/Codex-only syntax"
4395
+ )
4396
+ );
4397
+ checks.push(
4398
+ check(
4399
+ npmPresent,
4400
+ npmPresent
4401
+ ? "Hermes package runtime command present"
4402
+ : `Hermes package runtime command missing: ${packageManagerCommand()}`
4403
+ )
4404
+ );
4405
+ }
3863
4406
 
3864
4407
  let runtimeMcp;
3865
4408
  try {
@@ -4042,7 +4585,7 @@ function printNextSteps(installedHosts, authReused) {
4042
4585
  ` ${C.bold}Almost there — install an agent first.${C.reset}`
4043
4586
  );
4044
4587
  console.log(
4045
- ` ${C.yellow}!${C.reset} No agent CLI found (Claude Code or Codex)`
4588
+ ` ${C.yellow}!${C.reset} No agent CLI/profile found (Claude Code, Codex, or Hermes)`
4046
4589
  );
4047
4590
  console.log("");
4048
4591
  console.log("");
@@ -4086,6 +4629,7 @@ function printNextSteps(installedHosts, authReused) {
4086
4629
 
4087
4630
  const hasClaude = installedHosts.includes("Claude Code");
4088
4631
  const hasCodex = installedHosts.includes("Codex");
4632
+ const hasHermes = installedHosts.includes("Hermes");
4089
4633
 
4090
4634
  // Header
4091
4635
  if (authReused) {
@@ -4101,6 +4645,11 @@ function printNextSteps(installedHosts, authReused) {
4101
4645
  console.log(` ${C.green}✓${C.reset} Skills installed`);
4102
4646
  console.log(` ${C.green}✓${C.reset} Codex custom agents installed`);
4103
4647
  }
4648
+ if (hasHermes) {
4649
+ console.log(
4650
+ ` ${C.green}✓${C.reset} Hermes skills and MCP config installed`
4651
+ );
4652
+ }
4104
4653
  if (hasClaude) {
4105
4654
  console.log(
4106
4655
  ` ${C.green}✓${C.reset} Claude slash commands and custom agents installed`
@@ -4144,6 +4693,19 @@ function printNextSteps(installedHosts, authReused) {
4144
4693
  ]);
4145
4694
  console.log("");
4146
4695
  }
4696
+ if (hasHermes) {
4697
+ printAgentBox("Using Hermes?", "hermes", [
4698
+ { label: "Campaign", command: "/sellable-create-campaign" },
4699
+ { label: "A/B Test", command: "/sellable-create-ab-test" },
4700
+ { label: "Evergreen", command: "/sellable-create-evergreen-campaigns" },
4701
+ { label: "Foundation", command: "/sellable-foundation" },
4702
+ { label: "Content", command: "/sellable-content" },
4703
+ { label: "Post", command: "/sellable-create-post" },
4704
+ { label: "Refresh", command: "/sellable-refresh-sender-engagement" },
4705
+ { label: "Refill", command: "/sellable-refill-sends" },
4706
+ ]);
4707
+ console.log("");
4708
+ }
4147
4709
 
4148
4710
  console.log("");
4149
4711
  console.log(` ${"─".repeat(63)}`);
@@ -4173,7 +4735,7 @@ function removeTomlSection(content, header) {
4173
4735
  return content.replace(pattern, "");
4174
4736
  }
4175
4737
 
4176
- function runUninstall() {
4738
+ function runUninstall(opts = { host: "all" }) {
4177
4739
  console.log("");
4178
4740
  console.log(` ${C.bold}Uninstalling Sellable…${C.reset}`);
4179
4741
  console.log("");
@@ -4184,7 +4746,7 @@ function runUninstall() {
4184
4746
 
4185
4747
  // 1) Surgical removal from Codex config.toml
4186
4748
  const codexConfigPath = join(codexHome(), "config.toml");
4187
- if (existsSync(codexConfigPath)) {
4749
+ if ((opts.host === "codex" || opts.host === "all") && existsSync(codexConfigPath)) {
4188
4750
  try {
4189
4751
  const before = readFileSync(codexConfigPath, "utf8");
4190
4752
  const ts = new Date()
@@ -4221,10 +4783,11 @@ function runUninstall() {
4221
4783
  ` ${C.yellow}!${C.reset} Could not edit Codex config: ${err instanceof Error ? err.message : String(err)}`
4222
4784
  );
4223
4785
  }
4224
- } else {
4786
+ } else if (opts.host === "codex" || opts.host === "all") {
4225
4787
  skipped.push(`Codex config not found at ${codexConfigPath}`);
4226
4788
  }
4227
4789
 
4790
+ if (opts.host === "codex" || opts.host === "all") {
4228
4791
  for (const agent of [...codexCustomAgents(), ...legacyCodexCustomAgents()]) {
4229
4792
  const agentPath = join(codexHome(), "agents", agent.filename);
4230
4793
  if (!existsSync(agentPath)) continue;
@@ -4237,9 +4800,10 @@ function runUninstall() {
4237
4800
  );
4238
4801
  }
4239
4802
  }
4803
+ }
4240
4804
 
4241
4805
  // 2) Claude Code MCP removal
4242
- if (commandExists("claude")) {
4806
+ if ((opts.host === "claude" || opts.host === "all") && commandExists("claude")) {
4243
4807
  const r = spawnSync("claude", ["mcp", "remove", "sellable"], {
4244
4808
  encoding: "utf8",
4245
4809
  });
@@ -4248,10 +4812,11 @@ function runUninstall() {
4248
4812
  } else {
4249
4813
  skipped.push(`Claude Code MCP (none registered or already removed)`);
4250
4814
  }
4251
- } else {
4815
+ } else if (opts.host === "claude" || opts.host === "all") {
4252
4816
  skipped.push(`Claude Code CLI not found`);
4253
4817
  }
4254
4818
 
4819
+ if (opts.host === "claude" || opts.host === "all") {
4255
4820
  for (const agent of [
4256
4821
  ...claudeCustomAgents(),
4257
4822
  ...legacyClaudeCustomAgents(),
@@ -4279,10 +4844,66 @@ function runUninstall() {
4279
4844
  );
4280
4845
  }
4281
4846
  }
4847
+ }
4282
4848
 
4283
- // 3) Surgical removal of Sellable-installed artifacts inside ~/.sellable/
4849
+ // 3) Hermes config and skill removal
4850
+ if (opts.host === "hermes" || opts.host === "all") {
4851
+ const configPath = hermesConfigPath();
4852
+ if (existsSync(configPath)) {
4853
+ try {
4854
+ const before = readFileSync(configPath, "utf8");
4855
+ const ts = new Date()
4856
+ .toISOString()
4857
+ .replace(/[-:]/g, "")
4858
+ .replace(/\..+/, "");
4859
+ const backupPath = `${configPath}.bak-sellable-uninstall-${ts}`;
4860
+ writeFileSync(backupPath, before, { mode: 0o600 });
4861
+ const config = parseHermesConfig(before, configPath);
4862
+ if (
4863
+ config.mcp_servers &&
4864
+ typeof config.mcp_servers === "object" &&
4865
+ !Array.isArray(config.mcp_servers) &&
4866
+ "sellable" in config.mcp_servers
4867
+ ) {
4868
+ const nextMcpServers = { ...config.mcp_servers };
4869
+ delete nextMcpServers.sellable;
4870
+ writeHermesConfig(
4871
+ configPath,
4872
+ { ...config, mcp_servers: nextMcpServers },
4873
+ { dryRun: false }
4874
+ );
4875
+ removed.push(`Hermes config (sellable removed in ${configPath})`);
4876
+ removed.push(` backup: ${backupPath}`);
4877
+ } else {
4878
+ skipped.push(`Hermes config had no sellable MCP server`);
4879
+ }
4880
+ } catch (err) {
4881
+ console.log(
4882
+ ` ${C.yellow}!${C.reset} Could not edit Hermes config: ${err instanceof Error ? err.message : String(err)}`
4883
+ );
4884
+ }
4885
+ } else {
4886
+ skipped.push(`Hermes config not found at ${configPath}`);
4887
+ }
4888
+
4889
+ const skillsRoot = hermesSkillsRoot();
4890
+ if (existsSync(skillsRoot)) {
4891
+ try {
4892
+ rmSync(skillsRoot, { recursive: true, force: true });
4893
+ removed.push(`${skillsRoot}`);
4894
+ } catch (err) {
4895
+ console.log(
4896
+ ` ${C.yellow}!${C.reset} Could not remove ${skillsRoot}: ${err instanceof Error ? err.message : String(err)}`
4897
+ );
4898
+ }
4899
+ } else {
4900
+ skipped.push(`Hermes Sellable skills not found at ${skillsRoot}`);
4901
+ }
4902
+ }
4903
+
4904
+ // 4) Surgical removal of Sellable-installed artifacts inside ~/.sellable/
4284
4905
  const sellableDir = join(homedir(), ".sellable");
4285
- if (existsSync(sellableDir)) {
4906
+ if (opts.host === "all" && existsSync(sellableDir)) {
4286
4907
  const tracked = [
4287
4908
  "config.json",
4288
4909
  "update-check.json",
@@ -4321,13 +4942,13 @@ function runUninstall() {
4321
4942
  // ignore
4322
4943
  }
4323
4944
  }
4324
- } else {
4945
+ } else if (opts.host === "all") {
4325
4946
  skipped.push(`~/.sellable not present`);
4326
4947
  }
4327
4948
 
4328
- // 4) Self-shim
4949
+ // 5) Self-shim
4329
4950
  const shimPath = join(homedir(), ".local", "bin", "sellable");
4330
- if (existsSync(shimPath)) {
4951
+ if (opts.host === "all" && existsSync(shimPath)) {
4331
4952
  try {
4332
4953
  rmSync(shimPath, { force: true });
4333
4954
  removed.push(`${shimPath}`);
@@ -4440,6 +5061,13 @@ async function main() {
4440
5061
  console.log(` Codex: $sellable:create-post`);
4441
5062
  console.log(` Codex: $sellable:refresh-sender-engagement`);
4442
5063
  console.log(` Codex: $sellable:refill-sends`);
5064
+ console.log(` Hermes: /sellable-create-campaign`);
5065
+ console.log(` Hermes: /sellable-create-evergreen-campaigns`);
5066
+ console.log(` Hermes: /sellable-foundation`);
5067
+ console.log(` Hermes: /sellable-content`);
5068
+ console.log(` Hermes: /sellable-create-post`);
5069
+ console.log(` Hermes: /sellable-refresh-sender-engagement`);
5070
+ console.log(` Hermes: /sellable-refill-sends`);
4443
5071
  process.exit(0);
4444
5072
  }
4445
5073
  if (rawArgs[0] === "prefs") {
@@ -4482,7 +5110,8 @@ async function main() {
4482
5110
  process.exit(0);
4483
5111
  }
4484
5112
  if (rawArgs[0] === "uninstall") {
4485
- runUninstall();
5113
+ const opts = parseArgs(rawArgs.slice(1));
5114
+ runUninstall(opts);
4486
5115
  process.exit(0);
4487
5116
  }
4488
5117
  if (rawArgs[0] === "create") {
@@ -4500,7 +5129,7 @@ async function main() {
4500
5129
  if (!opts.verifyOnly) {
4501
5130
  printBanner();
4502
5131
  console.log(
4503
- ` ${C.bold}Connecting Sellable to Claude Code and Codex…${C.reset}`
5132
+ ` ${C.bold}Connecting Sellable to Claude Code, Codex, and Hermes…${C.reset}`
4504
5133
  );
4505
5134
  console.log("");
4506
5135
  } else if (!opts.json) {
@@ -4536,6 +5165,15 @@ async function main() {
4536
5165
  installedHosts.push("Codex");
4537
5166
  }
4538
5167
  }
5168
+ if (
5169
+ opts.host === "hermes" ||
5170
+ (opts.host === "all" && hermesLikelyInstalled())
5171
+ ) {
5172
+ const result = installHermes(opts);
5173
+ if (result.installed) {
5174
+ installedHosts.push("Hermes");
5175
+ }
5176
+ }
4539
5177
  }
4540
5178
 
4541
5179
  if (opts.dryRun) {
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@sellable/install",
3
- "version": "0.1.300",
3
+ "version": "0.1.302",
4
4
  "type": "module",
5
- "description": "One-command installer for Sellable MCP in Claude Code and Codex",
5
+ "description": "One-command installer for Sellable MCP in Claude Code, Codex, and Hermes",
6
6
  "bin": {
7
7
  "sellable": "bin/sellable-install.mjs"
8
8
  },
@@ -18,13 +18,15 @@
18
18
  "README.md"
19
19
  ],
20
20
  "dependencies": {
21
- "@modelcontextprotocol/sdk": "^1.25.2"
21
+ "@modelcontextprotocol/sdk": "^1.25.2",
22
+ "yaml": "^2.8.1"
22
23
  },
23
24
  "keywords": [
24
25
  "sellable",
25
26
  "mcp",
26
27
  "claude-code",
27
28
  "codex",
29
+ "hermes",
28
30
  "linkedin",
29
31
  "outbound"
30
32
  ],
@@ -0,0 +1,19 @@
1
+ ---
2
+ name: refill-sends-evergreen
3
+ description: Deprecated alias for refill-sends-v2.
4
+ visibility: public
5
+ allowed-tools:
6
+ - mcp__sellable__get_subskill_prompt
7
+ ---
8
+
9
+ # Refill Sends Evergreen
10
+
11
+ Superseded by refill-sends-v2. The prompt name now redirects through
12
+ `DEPRECATED_SUBSKILL_PROMPT_REPLACEMENTS`; do not call this deprecated prompt
13
+ directly.
14
+
15
+ Use `refill-sends-v2` instead:
16
+
17
+ ```text
18
+ get_subskill_prompt({ subskillName: "refill-sends-v2" })
19
+ ```
@@ -0,0 +1,156 @@
1
+ ---
2
+ name: refill-sends-v2
3
+ description: Execute refill sends v2 through the fenced evergreen refill loop, with dry-run and resume support.
4
+ visibility: public
5
+ allowed-tools:
6
+ - mcp__sellable__refill_sends_v2
7
+ - mcp__sellable__get_refill_plan_v2
8
+ - mcp__sellable__get_subskill_prompt
9
+ - mcp__sellable__get_subskill_asset
10
+ - mcp__sellable__get_auth_status
11
+ - mcp__sellable__get_active_workspace
12
+ - mcp__sellable__list_workspaces
13
+ - mcp__sellable__get_workspace
14
+ ---
15
+
16
+ # Refill Sends V2
17
+
18
+ Use this command for refill requests such as "refill sends", "fill evergreen
19
+ senders", "load everyone up", "run Damiano", or "show a dry run for this
20
+ workspace".
21
+
22
+ Host command names:
23
+
24
+ - Claude Code: `/sellable:refill-sends-v2`
25
+ - Codex: `$sellable:refill-sends-v2`
26
+
27
+ `refill_sends_v2` is the execution surface. In real-run mode it starts or
28
+ resumes a fenced refill run, reads a fresh packet, executes only the packet's
29
+ named bounded work, verifies the result, and returns either a terminal report, a
30
+ blocked report, or an in-progress resume handle. In dry-run mode it stays
31
+ read-only and writes only a dry-run journal.
32
+
33
+ ## Entry
34
+
35
+ Resolve the explicit `workspaceId` from the request, automation config, or
36
+ active workspace readback. Scheduled or autonomous usage must carry
37
+ `workspaceId` on every tool call. If the workspace is missing or ambiguous, stop
38
+ with `WORKSPACE_REQUIRED`; do not switch the shared active workspace.
39
+
40
+ For a real refill run:
41
+
42
+ ```text
43
+ refill_sends_v2({ workspaceId, intent:"auto", senderIds?, approvalMode? })
44
+ ```
45
+
46
+ For read-only inspection:
47
+
48
+ ```text
49
+ refill_sends_v2({ workspaceId, dryRun:true, intent:"auto", senderIds? })
50
+ ```
51
+
52
+ To resume an in-progress run, pass the handle back exactly:
53
+
54
+ ```text
55
+ refill_sends_v2({ workspaceId, runId, fence })
56
+ ```
57
+
58
+ If a stale handle loses the lease, the tool reports the holder status and the
59
+ approximately 10 minute lockout window. Reinvoke with the current handle or wait
60
+ for lease expiry; never guess a fence.
61
+
62
+ The default `intent:"auto"` inspects `managed_waterfall`,
63
+ `dashboard_evergreen`, and `active_campaign` lane sources. Report the lane
64
+ source and lane chain as proof for every selected sender.
65
+
66
+ If membership blocks a workspace read, report the structured
67
+ `workspace_access` blocker instead of retrying auth or switching workspaces.
68
+
69
+ ## Authority
70
+
71
+ The run may execute only packet-named bounded work:
72
+
73
+ - approve generated messages that are ready for review (`approve_messages`);
74
+ - enrich the next batch of unenriched rows (`enrich_more`);
75
+ - copy leads already found into the campaign table (`reconcile_source_copy`);
76
+ - re-run errored enrichment/message cells (`rerun_errored_cells`);
77
+ - find more leads via this campaign's original source (`add_leads_same_source`)
78
+ only for mechanical same-source continuations;
79
+ - start the paused campaign so ready rows can schedule (`start_campaign`) only
80
+ when the campaign is in the packet's pinned lane chain;
81
+ - refresh paid InMail credit facts during bootstrap and once per scheduler-wait
82
+ entry when the packet requires it.
83
+
84
+ The loop records planned -> did -> outcome before and after every foreign
85
+ mutation. It refuses stale packets by fingerprint and validates packet
86
+ coherence before dispatch. The planner owns repair decisions: if a batch needs
87
+ repair, the next packet names the repair rung; the loop bounds and refuses, it
88
+ does not improvise fixes.
89
+
90
+ Do not create campaigns, switch provider families, lower thresholds, write
91
+ scheduler fields, send messages, archive/delete campaigns, or mutate
92
+ brief/filter/message/sequence/sender config unless a separate exact approval
93
+ packet explicitly authorizes that work. In short: no campaign creation, no
94
+ provider-family switch, no threshold lowering, no scheduler writes, no
95
+ launch/send/archive/delete, and no brief/filter/message/sequence/sender
96
+ mutation.
97
+
98
+ ## Continuations
99
+
100
+ Sales Nav and Prospeo source continuations that are not yolo-safe are blocked
101
+ as executable scoped handoffs. The report will include text shaped like:
102
+
103
+ ```text
104
+ $sellable:create-campaign [campaignId]
105
+ ```
106
+
107
+ That handoff is run in a separate thread only after explicit approval. Scope is
108
+ strict: add leads through this campaign's existing source, keep the same
109
+ saved-search identity, import mode `add` into the same source list, and do
110
+ source work only. It is never a new campaign, provider-family switch, brief
111
+ change, rubric change, message change, sequence change, or sender change.
112
+
113
+ ## Verification
114
+
115
+ The loop verifies whole rows before replanning. It reattaches to an own active
116
+ prep job instead of re-dispatching, waits on foreign prep jobs without adopting
117
+ their outcomes, waits on active enrichment/import work by rereading the packet,
118
+ and treats capped scheduler capacity as complete.
119
+
120
+ Scheduler waits are cross-invocation. Each tool call polls briefly under the
121
+ host guard, heartbeats the run lease, and may return:
122
+
123
+ ```text
124
+ { status:"in_progress", runId, fence, gate, guidance }
125
+ ```
126
+
127
+ Reinvoke with that handle. The approximately five minute scheduler budget is at
128
+ the run level, not one tool call. A window-closed or loaded-awaiting scheduler
129
+ report must include remaining-ready count, exact expected pickup time, and the
130
+ resume handle; never treat bare "awaiting scheduler" copy as a final answer.
131
+
132
+ After the first prep batch, read the conversion-verdict journal line. It is
133
+ rendered from packet facts: `supplyCensus`, `censusReason`, and
134
+ `pipelineDiagnosis.firstFailing`. If the verdict is honest rubric failure, the
135
+ human decision is to update the rubric or change lead source; refill does not
136
+ mutate those automatically.
137
+
138
+ ## Results
139
+
140
+ Terminal reasons are:
141
+
142
+ - `complete`
143
+ - `capped_by_scheduler`
144
+ - `loaded_awaiting_scheduler`
145
+ - `lanes_exhausted`
146
+ - `blocked`
147
+ - `no_refillable_campaigns`
148
+ - `not_an_evergreen_workspace`
149
+
150
+ Report campaign names and sender names first, ids second. Include the terminal
151
+ reason or resume handle, lane source, lane chain, chosen label with token
152
+ secondary, plan revision, journal path, blocked continuation packets, and
153
+ firstFailing checklist when present.
154
+
155
+ Real-run journals live under `~/.sellable/refill/runs` by default and append an
156
+ index line. Dry runs include the dry marker and do not create run records.
@@ -97,8 +97,11 @@ That command helper normalizes arguments and returns the execution contract. In
97
97
  non-yolo mode it does not mutate. In `--yolo`, it may execute exactly one safe
98
98
  bounded primitive from the fresh `target.globalActionQueue[0]`, then reread and
99
99
  return the new target plan; currently safe primitives are paid-credit refresh,
100
- existing-row message preparation, same-source row copy, and read-only wait
101
- rereads. It does not run unbounded approval, lower
100
+ existing-row message preparation, generated-message approval, receipt-proven
101
+ same-source row copy, and read-only wait rereads. Same-source copy/source
102
+ fallback is safe only after receipt-proven exhaustion:
103
+ `hasMoreFrontierRows:false`, zero `approvalCandidates`, no `stuckActiveCells`,
104
+ and no non-terminal `approvedNotDispatched` work. It does not run unbounded approval, lower
102
105
  paid-InMail thresholds, switch source families, create campaigns, launch, send,
103
106
  or write scheduler rows. Continue with the workflow below for route selection,
104
107
  state rereads, approval gating, source import, preparation, and bounded
@@ -198,7 +201,10 @@ Structured planner packet:
198
201
  - `target.senderRefillPlans[]` is the canonical sender-level packet; read and
199
202
  display it before mutation.
200
203
  - Each sender packet includes `campaignRanking.options`, `sourcePlan`,
201
- `nextActions`, and `manualAlternates`.
204
+ `refillReceipt`, `nextActions`, and `manualAlternates`.
205
+ - `refillReceipt` is the public ladder receipt. It carries the selected
206
+ campaign/sender/lane summary, skipped rungs, existing-row frontier proof, and
207
+ any absolute `wait.deadlineAt`.
202
208
  - Preserve these coverage labels exactly: `Need to prepare`, `Goal`,
203
209
  `Already sent`, `Scheduled`, `Ready and waiting to be scheduled`, and
204
210
  `Still need`.
@@ -217,6 +223,13 @@ from the selected source (`selectedLeadListId`, provider, and source
217
223
  fingerprint preserved), then use provider-aligned source-more. A new source or
218
224
  provider switch changes the reply-rate baseline and is a manual alternate, not a
219
225
  `--yolo` side effect.
226
+ Source/copy/fallback requires receipt-proven exhaustion of earlier rungs:
227
+ `existingRowFrontier.hasMoreFrontierRows:false`, zero `approvalCandidates`, no
228
+ fresh active prep, no `stuckActiveCells`, and no non-terminal
229
+ `approvedNotDispatched` rows. Treat anomalies, `stuckActiveCells`, and
230
+ non-terminal `approvedNotDispatched` as diagnose-and-report gates, not
231
+ exhaustion. Terminal `approvedNotDispatched` blockers may be reported, then the
232
+ ladder can proceed.
220
233
 
221
234
  Run-local paid-credit guard: in `--yolo`, the `refill_sends` MCP command
222
235
  automatically maintains a `refreshedPaidInmailSenderIds` set for the current
@@ -266,6 +279,10 @@ interval, until projected coverage fills the target, a concrete non-scheduler
266
279
  blocker appears, or Christian explicitly asks to stop or only receive a status
267
280
  report. Treat `awaiting_scheduler_after_ready_buffer` as an in-progress wait
268
281
  state, not a close-out condition.
282
+ Wait actions are gates, not competing goals. When `wait_for_active_work` or
283
+ `wait_for_scheduler` includes receipt `wait.deadlineAt`, honor that absolute
284
+ deadline; if it is expired on this call, escalate to diagnostics with the
285
+ receipt evidence instead of issuing another blind wait.
269
286
  If paid InMail credit facts are stale or missing and the first target plan
270
287
  contains `refresh_paid_inmail_credits`, do not present that as the operator's
271
288
  next action in `--yolo`. Do not present paid-credit refresh as the next operator