@sellable/install 0.1.303 → 0.1.305
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 +101 -10
- package/bin/sellable-install.mjs +1202 -56
- package/lib/runtime-verify.mjs +5 -0
- package/package.json +5 -3
- package/skill-templates/refill-sends-v2.md +11 -0
- package/skill-templates/refill-sends.md +23 -4
package/bin/sellable-install.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
2
3
|
import { spawnSync } from "node:child_process";
|
|
3
4
|
import {
|
|
5
|
+
chmodSync,
|
|
4
6
|
cpSync,
|
|
5
7
|
existsSync,
|
|
6
8
|
mkdirSync,
|
|
@@ -11,10 +13,11 @@ import {
|
|
|
11
13
|
writeFileSync,
|
|
12
14
|
} from "node:fs";
|
|
13
15
|
import { homedir } from "node:os";
|
|
14
|
-
import { dirname, join, relative } from "node:path";
|
|
16
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
15
17
|
import { stdout as output } from "node:process";
|
|
16
18
|
import { createInterface } from "node:readline/promises";
|
|
17
19
|
import { fileURLToPath } from "node:url";
|
|
20
|
+
import { parseDocument } from "yaml";
|
|
18
21
|
import {
|
|
19
22
|
REQUIRED_SELLABLE_MCP_TOOLS,
|
|
20
23
|
verifySellableMcpRuntime,
|
|
@@ -167,14 +170,20 @@ Commands:
|
|
|
167
170
|
Save the ask-first prompt-sharing preference.
|
|
168
171
|
setup claude-permissions --allow-common-setup
|
|
169
172
|
Add common Sellable Bash permissions to Claude settings.
|
|
173
|
+
hermes profile bootstrap Create a profile-local Hermes config, Sellable auth config,
|
|
174
|
+
configs dir, skills, and optional Slack .env.
|
|
170
175
|
uninstall Remove Sellable host config and installed artifacts.
|
|
171
176
|
|
|
172
177
|
Options:
|
|
173
|
-
--host <host> claude, codex, or all. Default: all
|
|
178
|
+
--host <host> claude, codex, hermes, or all. Default: all
|
|
174
179
|
--server <mode> package, local, or hosted. Default: package
|
|
175
180
|
--token <token> Sellable API token. Also reads SELLABLE_TOKEN.
|
|
176
181
|
--workspace-id <id> Sellable workspace id. Also reads SELLABLE_WORKSPACE_ID.
|
|
177
182
|
--api-url <url> Sellable API URL. Default: ${DEFAULT_API_URL}
|
|
183
|
+
--sellable-config-path <path>
|
|
184
|
+
Profile-scoped Sellable config path. Also reads SELLABLE_CONFIG_PATH.
|
|
185
|
+
--sellable-configs-dir <path>
|
|
186
|
+
Profile-scoped Sellable configs/memory dir. Also reads SELLABLE_CONFIGS_DIR.
|
|
178
187
|
--mcp-package <pkg> MCP server package. Default: ${DEFAULT_SERVER_PACKAGE}
|
|
179
188
|
--local-command <cmd> Local MCP command for --server local.
|
|
180
189
|
--hosted-url <url> Hosted MCP URL for --server hosted.
|
|
@@ -190,20 +199,33 @@ Options:
|
|
|
190
199
|
Auth:
|
|
191
200
|
Install is auth-free by default. Sign in happens on the first run of
|
|
192
201
|
/sellable:create-campaign, /sellable:create-evergreen-campaigns,
|
|
193
|
-
/sellable:foundation, /sellable:content, /sellable:create-post,
|
|
194
|
-
/sellable:refresh-sender-engagement, or /sellable:refill-sends in Claude Code
|
|
202
|
+
/sellable:foundation, /sellable:content, /sellable:create-post,
|
|
203
|
+
/sellable:refresh-sender-engagement, or /sellable:refill-sends in Claude Code,
|
|
204
|
+
Codex, or Hermes,
|
|
195
205
|
where the agent walks you through signup or sign-in and stores credentials
|
|
196
206
|
in ~/.sellable/config.json.
|
|
197
207
|
|
|
198
208
|
For CI/scripted installs, pass --token + --workspace-id or set
|
|
199
209
|
SELLABLE_TOKEN + SELLABLE_WORKSPACE_ID and the installer will write the
|
|
200
|
-
config file directly without prompting.
|
|
210
|
+
config file directly without prompting. For Hermes customer/admin profiles,
|
|
211
|
+
also pass --sellable-config-path or set SELLABLE_CONFIG_PATH so auth and MCP
|
|
212
|
+
runtime use the same profile-local config file.
|
|
213
|
+
|
|
214
|
+
Hermes profile bootstrap:
|
|
215
|
+
sellable hermes profile bootstrap --profile acme --profiles-root /srv/hermes/profiles --workspace-id ws_acme --token-file /run/secrets/sellable-token --json
|
|
216
|
+
sellable hermes profile bootstrap --profile-root "$HOME/.hermes/profiles/acme" --workspace-id ws_acme --json
|
|
217
|
+
|
|
218
|
+
Bootstrap writes <profileRoot>/config.yaml, <profileRoot>/sellable/config.json,
|
|
219
|
+
<profileRoot>/sellable/configs, Sellable Hermes skills, and optional
|
|
220
|
+
<profileRoot>/.env Slack token keys. Use customer-scoped Sellable credentials
|
|
221
|
+
for customer profiles; a shared admin token is weaker isolation and should
|
|
222
|
+
remain internal-only.
|
|
201
223
|
`;
|
|
202
224
|
}
|
|
203
225
|
|
|
204
226
|
function printCreateCommandHint() {
|
|
205
227
|
printBanner();
|
|
206
|
-
console.log(` ${C.bold}Run Sellable from Claude Code or
|
|
228
|
+
console.log(` ${C.bold}Run Sellable from Claude Code, Codex, or Hermes.${C.reset}`);
|
|
207
229
|
console.log("");
|
|
208
230
|
console.log(
|
|
209
231
|
` ${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 +259,18 @@ function printCreateCommandHint() {
|
|
|
237
259
|
{ label: "Refill", command: "$sellable:refill-sends" },
|
|
238
260
|
]);
|
|
239
261
|
console.log("");
|
|
262
|
+
console.log("");
|
|
263
|
+
printAgentBox("Using Hermes?", "hermes", [
|
|
264
|
+
{ label: "Campaign", command: "/sellable-create-campaign" },
|
|
265
|
+
{ label: "A/B Test", command: "/sellable-create-ab-test" },
|
|
266
|
+
{ label: "Evergreen", command: "/sellable-create-evergreen-campaigns" },
|
|
267
|
+
{ label: "Foundation", command: "/sellable-foundation" },
|
|
268
|
+
{ label: "Content", command: "/sellable-content" },
|
|
269
|
+
{ label: "Post", command: "/sellable-create-post" },
|
|
270
|
+
{ label: "Refresh", command: "/sellable-refresh-sender-engagement" },
|
|
271
|
+
{ label: "Refill", command: "/sellable-refill-sends" },
|
|
272
|
+
]);
|
|
273
|
+
console.log("");
|
|
240
274
|
console.log(` ${"─".repeat(63)}`);
|
|
241
275
|
console.log(` ${C.grey}If those commands are missing, run:${C.reset}`);
|
|
242
276
|
console.log("");
|
|
@@ -249,11 +283,16 @@ function printCreateCommandHint() {
|
|
|
249
283
|
|
|
250
284
|
function parseArgs(argv) {
|
|
251
285
|
const opts = {
|
|
252
|
-
host:
|
|
286
|
+
host:
|
|
287
|
+
process.env.SELLABLE_INSTALLER_HOST ||
|
|
288
|
+
process.env.SELLABLE_INSTALL_HOST ||
|
|
289
|
+
"all",
|
|
253
290
|
server: process.env.SELLABLE_INSTALL_SERVER || "package",
|
|
254
291
|
token: process.env.SELLABLE_TOKEN || "",
|
|
255
292
|
workspaceId: process.env.SELLABLE_WORKSPACE_ID || "",
|
|
256
293
|
apiUrl: process.env.SELLABLE_API_URL || DEFAULT_API_URL,
|
|
294
|
+
sellableConfigPath: process.env.SELLABLE_CONFIG_PATH || "",
|
|
295
|
+
sellableConfigsDir: process.env.SELLABLE_CONFIGS_DIR || "",
|
|
257
296
|
mcpPackage: DEFAULT_SERVER_PACKAGE,
|
|
258
297
|
localCommand: process.env.SELLABLE_MCP_LOCAL_COMMAND || "",
|
|
259
298
|
hostedUrl: process.env.SELLABLE_MCP_HOSTED_URL || "",
|
|
@@ -288,6 +327,10 @@ function parseArgs(argv) {
|
|
|
288
327
|
opts.workspaceId = next();
|
|
289
328
|
} else if (arg === "--api-url") {
|
|
290
329
|
opts.apiUrl = next();
|
|
330
|
+
} else if (arg === "--sellable-config-path") {
|
|
331
|
+
opts.sellableConfigPath = next();
|
|
332
|
+
} else if (arg === "--sellable-configs-dir") {
|
|
333
|
+
opts.sellableConfigsDir = next();
|
|
291
334
|
} else if (arg === "--mcp-package") {
|
|
292
335
|
opts.mcpPackage = normalizeWindowsPackageSpec(next());
|
|
293
336
|
} else if (arg === "--local-command") {
|
|
@@ -313,12 +356,17 @@ function parseArgs(argv) {
|
|
|
313
356
|
}
|
|
314
357
|
}
|
|
315
358
|
|
|
316
|
-
if (!["claude", "codex", "all"].includes(opts.host)) {
|
|
317
|
-
throw new Error("--host must be claude, codex, or all");
|
|
359
|
+
if (!["claude", "codex", "hermes", "all"].includes(opts.host)) {
|
|
360
|
+
throw new Error("--host must be claude, codex, hermes, or all");
|
|
318
361
|
}
|
|
319
362
|
if (!["package", "local", "hosted"].includes(opts.server)) {
|
|
320
363
|
throw new Error("--server must be package, local, or hosted");
|
|
321
364
|
}
|
|
365
|
+
opts.sellableConfigPath = normalizeOptionalPath(opts.sellableConfigPath);
|
|
366
|
+
opts.sellableConfigsDir = normalizeOptionalPath(opts.sellableConfigsDir);
|
|
367
|
+
if (opts.sellableConfigPath && !opts.sellableConfigsDir) {
|
|
368
|
+
opts.sellableConfigsDir = join(dirname(opts.sellableConfigPath), "configs");
|
|
369
|
+
}
|
|
322
370
|
|
|
323
371
|
return opts;
|
|
324
372
|
}
|
|
@@ -576,8 +624,17 @@ function getMcpVersion() {
|
|
|
576
624
|
return "latest";
|
|
577
625
|
}
|
|
578
626
|
|
|
579
|
-
function
|
|
580
|
-
|
|
627
|
+
function normalizeOptionalPath(value) {
|
|
628
|
+
const trimmed = String(value || "").trim();
|
|
629
|
+
return trimmed ? resolve(trimmed) : "";
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function authPath(opts = {}) {
|
|
633
|
+
return (
|
|
634
|
+
normalizeOptionalPath(opts.sellableConfigPath) ||
|
|
635
|
+
normalizeOptionalPath(process.env.SELLABLE_CONFIG_PATH) ||
|
|
636
|
+
join(homedir(), ".sellable", "config.json")
|
|
637
|
+
);
|
|
581
638
|
}
|
|
582
639
|
|
|
583
640
|
function normalizeStoredAuth(raw) {
|
|
@@ -598,15 +655,15 @@ function normalizeStoredAuth(raw) {
|
|
|
598
655
|
};
|
|
599
656
|
}
|
|
600
657
|
|
|
601
|
-
function readStoredAuth() {
|
|
602
|
-
const raw = readExisting(authPath());
|
|
658
|
+
function readStoredAuth(opts = {}) {
|
|
659
|
+
const raw = readExisting(authPath(opts));
|
|
603
660
|
return raw ? normalizeStoredAuth(raw) : null;
|
|
604
661
|
}
|
|
605
662
|
|
|
606
663
|
async function loadAuthIfPresent(opts) {
|
|
607
664
|
if (opts.token && opts.workspaceId) return opts;
|
|
608
665
|
|
|
609
|
-
const stored = readStoredAuth();
|
|
666
|
+
const stored = readStoredAuth(opts);
|
|
610
667
|
if (stored?.token && stored?.workspaceId) {
|
|
611
668
|
opts.token ||= stored.token;
|
|
612
669
|
opts.workspaceId ||= stored.workspaceId;
|
|
@@ -670,7 +727,7 @@ function mergeAuthConfig(raw, auth) {
|
|
|
670
727
|
Array.isArray(envConfig)
|
|
671
728
|
) {
|
|
672
729
|
throw new Error(
|
|
673
|
-
`Unknown active environment '${activeEnv}' in
|
|
730
|
+
`Unknown active environment '${activeEnv}' in configured Sellable auth path`
|
|
674
731
|
);
|
|
675
732
|
}
|
|
676
733
|
return {
|
|
@@ -691,8 +748,14 @@ function mergeAuthConfig(raw, auth) {
|
|
|
691
748
|
};
|
|
692
749
|
}
|
|
693
750
|
|
|
694
|
-
function writeAuthSetConfig({
|
|
695
|
-
|
|
751
|
+
function writeAuthSetConfig({
|
|
752
|
+
token,
|
|
753
|
+
workspaceId,
|
|
754
|
+
apiUrl,
|
|
755
|
+
dryRun,
|
|
756
|
+
sellableConfigPath,
|
|
757
|
+
}) {
|
|
758
|
+
const configPath = authPath({ sellableConfigPath });
|
|
696
759
|
const raw = readExisting(configPath) || {};
|
|
697
760
|
const config = mergeAuthConfig(raw, {
|
|
698
761
|
token,
|
|
@@ -843,6 +906,7 @@ function writeFile(path, content, opts, mode = 0o644) {
|
|
|
843
906
|
if (opts.dryRun) return;
|
|
844
907
|
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
845
908
|
writeFileSync(path, content, { mode });
|
|
909
|
+
chmodSync(path, mode);
|
|
846
910
|
}
|
|
847
911
|
|
|
848
912
|
function ensureSymlink(target, linkPath, opts) {
|
|
@@ -935,9 +999,7 @@ function codexPluginMcp(opts) {
|
|
|
935
999
|
type: "stdio",
|
|
936
1000
|
command,
|
|
937
1001
|
args,
|
|
938
|
-
env:
|
|
939
|
-
SELLABLE_WATCH_MODE_DRIVER: "codex",
|
|
940
|
-
},
|
|
1002
|
+
env: mcpEnvForHost("codex", opts),
|
|
941
1003
|
},
|
|
942
1004
|
},
|
|
943
1005
|
};
|
|
@@ -950,9 +1012,7 @@ function codexPluginMcp(opts) {
|
|
|
950
1012
|
type: "stdio",
|
|
951
1013
|
command,
|
|
952
1014
|
args,
|
|
953
|
-
env:
|
|
954
|
-
SELLABLE_WATCH_MODE_DRIVER: "codex",
|
|
955
|
-
},
|
|
1015
|
+
env: mcpEnvForHost("codex", opts),
|
|
956
1016
|
},
|
|
957
1017
|
},
|
|
958
1018
|
};
|
|
@@ -1062,6 +1122,7 @@ const REFILL_SENDS_ALLOWED_TOOLS = [
|
|
|
1062
1122
|
"mcp__sellable__get_subskill_asset",
|
|
1063
1123
|
"mcp__sellable__search_subskill_prompts",
|
|
1064
1124
|
"mcp__sellable__get_scheduler_fill_capacity",
|
|
1125
|
+
"mcp__sellable__run_scheduler_sweep",
|
|
1065
1126
|
"mcp__sellable__refresh_paid_inmail_credits",
|
|
1066
1127
|
"mcp__sellable__list_senders",
|
|
1067
1128
|
"mcp__sellable__get_sender_routing",
|
|
@@ -1175,6 +1236,29 @@ the Claude Code instruction for customer-facing language and host functions.
|
|
|
1175
1236
|
|
|
1176
1237
|
Do not tell Claude Code users to run \`$sellable:${commandName}\`, use
|
|
1177
1238
|
\`request_user_input\`, or restart Codex Desktop. Do not describe this run as Codex.`;
|
|
1239
|
+
}
|
|
1240
|
+
if (host === "hermes") {
|
|
1241
|
+
return `## Installed Host Contract
|
|
1242
|
+
|
|
1243
|
+
This installed skill is running in Hermes Agent. When the shared workflow body
|
|
1244
|
+
or fallback text mentions Claude Code, Codex, or Hermes for internal parity,
|
|
1245
|
+
choose the Hermes instruction for customer-facing language and host functions.
|
|
1246
|
+
|
|
1247
|
+
- Customer-facing command: \`/sellable-${commandName}\`
|
|
1248
|
+
- MCP tool naming: Hermes exposes Sellable tools as \`mcp_sellable_<tool>\`;
|
|
1249
|
+
when shared instructions show \`mcp__sellable__<tool>\`, call the matching
|
|
1250
|
+
\`mcp_sellable_<tool>\` tool instead.
|
|
1251
|
+
- Structured questions: ask plainly in chat unless a Hermes-native approval or
|
|
1252
|
+
question tool is visible in the current session.
|
|
1253
|
+
- Bootstrap host label: \`host: "Hermes"\`
|
|
1254
|
+
- Install/reload blocker label: Hermes install/reload problem
|
|
1255
|
+
- Reload instruction: restart Hermes, or run \`/reload-mcp\` in the active
|
|
1256
|
+
Hermes session after install
|
|
1257
|
+
|
|
1258
|
+
Do not tell Hermes users to run \`$sellable:${commandName}\` or
|
|
1259
|
+
\`/sellable:${commandName}\`, use \`AskUserQuestion\` or \`request_user_input\`,
|
|
1260
|
+
or restart Claude Code or Codex Desktop. Do not describe this run as Claude Code
|
|
1261
|
+
or Codex.`;
|
|
1178
1262
|
}
|
|
1179
1263
|
return "";
|
|
1180
1264
|
}
|
|
@@ -2005,7 +2089,7 @@ Desktop, then start a new thread.
|
|
|
2005
2089
|
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.
|
|
2006
2090
|
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" })\`.
|
|
2007
2091
|
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" })\`.
|
|
2008
|
-
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. 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.
|
|
2092
|
+
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.
|
|
2009
2093
|
6. Load the canonical prompt and deterministic flow asset via
|
|
2010
2094
|
\`mcp__sellable__get_subskill_prompt({ subskillName: "refill-sends" })\`,
|
|
2011
2095
|
then load \`mcp__sellable__get_subskill_prompt({ subskillName: "refill-sends-workflow" })\`,
|
|
@@ -2584,6 +2668,256 @@ function codexPluginSkills() {
|
|
|
2584
2668
|
];
|
|
2585
2669
|
}
|
|
2586
2670
|
|
|
2671
|
+
function hermesCommandName(commandName) {
|
|
2672
|
+
return `sellable-${commandName}`;
|
|
2673
|
+
}
|
|
2674
|
+
|
|
2675
|
+
function normalizeHermesSkillMarkdown(markdown, commandName) {
|
|
2676
|
+
const hermesName = hermesCommandName(commandName);
|
|
2677
|
+
let content = String(markdown);
|
|
2678
|
+
if (/^name:\s*.+$/m.test(content)) {
|
|
2679
|
+
content = content.replace(/^name:\s*.+$/m, `name: ${hermesName}`);
|
|
2680
|
+
} else {
|
|
2681
|
+
content = content.replace(/^---\n/m, `---\nname: ${hermesName}\n`);
|
|
2682
|
+
}
|
|
2683
|
+
|
|
2684
|
+
let normalized = content
|
|
2685
|
+
.replace(/mcp__sellable__<tool>/g, "mcp_sellable_<tool>")
|
|
2686
|
+
.replace(/mcp__([A-Za-z0-9_-]+)__<tool>/g, (_, server) => {
|
|
2687
|
+
return `mcp_${String(server).replaceAll("-", "_")}_<tool>`;
|
|
2688
|
+
})
|
|
2689
|
+
.replace(/\bmcp__([A-Za-z0-9_-]+)__([A-Za-z0-9_-]+)\b/g, (_, server, tool) => {
|
|
2690
|
+
return `mcp_${String(server).replaceAll("-", "_")}_${String(tool).replaceAll("-", "_")}`;
|
|
2691
|
+
})
|
|
2692
|
+
.replace(/\bmcp__sellable__/g, "mcp_sellable_")
|
|
2693
|
+
.replace(/\$sellable:/g, "/sellable-")
|
|
2694
|
+
.replace(/\/sellable:/g, "/sellable-")
|
|
2695
|
+
.replace(/Codex install\/reload problem/g, "Hermes install/reload problem")
|
|
2696
|
+
.replace(/Claude Code install\/reload problem/g, "Hermes install/reload problem")
|
|
2697
|
+
.replace(/fully quit and reopen Codex Desktop, then start a new thread/g, "restart Hermes, or run `/reload-mcp` in the active Hermes session")
|
|
2698
|
+
.replace(/fully quit and reopen Claude Code, then start a new session/g, "restart Hermes, or run `/reload-mcp` in the active Hermes session")
|
|
2699
|
+
.replace(/Codex Desktop/g, "Hermes")
|
|
2700
|
+
.replace(/Claude Code\/Codex/g, "Hermes")
|
|
2701
|
+
.replace(/Claude Code or Codex/g, "Hermes")
|
|
2702
|
+
.replace(/Codex or Claude Code/g, "Hermes")
|
|
2703
|
+
.replace(/Claude Code and Codex/g, "Hermes")
|
|
2704
|
+
.replace(/Codex\/Claude/g, "Hermes")
|
|
2705
|
+
.replace(/quick question panel/g, "approval prompt")
|
|
2706
|
+
.replace(/request_user_input/g, "plain chat")
|
|
2707
|
+
.replace(/AskUserQuestion/g, "plain chat");
|
|
2708
|
+
|
|
2709
|
+
normalized = normalized.replace(
|
|
2710
|
+
/Do not tell Hermes users to run[\s\S]*?Do not describe this run as Claude Code\nor Codex\./g,
|
|
2711
|
+
"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."
|
|
2712
|
+
);
|
|
2713
|
+
|
|
2714
|
+
normalized = normalized.replace(
|
|
2715
|
+
/## Structured Questions\n\nUse the host-native structured question gate for intake and approval:[\s\S]*?## Host Runtime Functions/g,
|
|
2716
|
+
`## Structured Questions
|
|
2717
|
+
|
|
2718
|
+
Hermes should ask setup questions directly in chat unless the current Hermes
|
|
2719
|
+
session exposes a native approval or question tool. Use a bounded approval gate
|
|
2720
|
+
only for explicit approval decisions. For open text like LinkedIn URLs, company
|
|
2721
|
+
domains, notes, pasted context, campaign ideas, or feedback, ask in normal chat
|
|
2722
|
+
and wait for the user to paste the value.
|
|
2723
|
+
|
|
2724
|
+
Campaign setup questions are single-choice decisions. Use mutually exclusive
|
|
2725
|
+
options and route blended or custom answers through a short free-text follow-up.
|
|
2726
|
+
Customer-facing language should call this "a couple setup choices" during
|
|
2727
|
+
normal campaign progress.
|
|
2728
|
+
|
|
2729
|
+
## Host Runtime Functions`
|
|
2730
|
+
);
|
|
2731
|
+
|
|
2732
|
+
normalized = normalized.replace(
|
|
2733
|
+
/## Names To Use\n\nUse these exact public names[\s\S]*?## Structured Questions/g,
|
|
2734
|
+
`## Names To Use
|
|
2735
|
+
|
|
2736
|
+
Use these exact public names for Hermes:
|
|
2737
|
+
|
|
2738
|
+
- Hermes command: \`/${hermesName}\`
|
|
2739
|
+
- Hermes skill directory: \`skills/sellable/${hermesName}/SKILL.md\`
|
|
2740
|
+
- MCP server name: \`sellable\`
|
|
2741
|
+
- Hermes MCP tool prefix: \`mcp_sellable_\`
|
|
2742
|
+
- Internal workflow prompt: \`create-campaign-v2\`
|
|
2743
|
+
|
|
2744
|
+
Do not tell users to run internal subskill names. \`create-campaign-v2\` is only
|
|
2745
|
+
the internal subskill loaded through
|
|
2746
|
+
\`mcp_sellable_get_subskill_prompt({ subskillName: "create-campaign-v2" })\`.
|
|
2747
|
+
|
|
2748
|
+
## Structured Questions`
|
|
2749
|
+
);
|
|
2750
|
+
|
|
2751
|
+
normalized = normalized.replace(
|
|
2752
|
+
/- `ask_user`:[\s\S]*?(?=\n- `load_subprompt`:)/g,
|
|
2753
|
+
`- \`ask_user\`: ask directly in chat unless a Hermes-native approval or
|
|
2754
|
+
question tool is visible in the current session. Use this for
|
|
2755
|
+
multiple-choice intake, campaign-focus choices, source decisions, and
|
|
2756
|
+
approvals. Campaign setup questions are single-choice only; do not use
|
|
2757
|
+
multi-select or checkbox variants.`
|
|
2758
|
+
);
|
|
2759
|
+
|
|
2760
|
+
normalized = normalized.replace(
|
|
2761
|
+
/- `launch_message_drafting`:[\s\S]*?\n\nIf a required interactive/g,
|
|
2762
|
+
`- \`launch_message_drafting\`: if Hermes exposes a background-agent capability,
|
|
2763
|
+
use the compatible Message Drafting agent. Otherwise run the same message
|
|
2764
|
+
drafting branch inline in the parent session with \`statusSource:
|
|
2765
|
+
"parent-thread-fallback"\`.
|
|
2766
|
+
|
|
2767
|
+
If a required interactive`
|
|
2768
|
+
);
|
|
2769
|
+
|
|
2770
|
+
normalized = normalized.replace(
|
|
2771
|
+
/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,
|
|
2772
|
+
""
|
|
2773
|
+
);
|
|
2774
|
+
|
|
2775
|
+
normalized = normalized.replace(
|
|
2776
|
+
/## 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,
|
|
2777
|
+
`## Active Model Metadata
|
|
2778
|
+
|
|
2779
|
+
Before calling \`bootstrap_create_campaign\`, pass \`host: "Hermes"\`. If the
|
|
2780
|
+
current Hermes session exposes model and effort metadata for this same turn,
|
|
2781
|
+
pass those exact values with \`modelMetadataSource: "hermes_runtime_metadata"\`.
|
|
2782
|
+
If the current Hermes session context explicitly states both the exact model ID
|
|
2783
|
+
and effort/thinking level, pass them with
|
|
2784
|
+
\`modelMetadataSource: "hermes_session_context"\`. Otherwise omit \`model\`,
|
|
2785
|
+
\`reasoningEffort\`, and \`modelMetadataSource\`.
|
|
2786
|
+
|
|
2787
|
+
Never invent the model or reasoning effort. Never inspect Codex or Claude config
|
|
2788
|
+
files as proof of the active Hermes session. If bootstrap returns
|
|
2789
|
+
\`modelQuality.status === "unknown"\`, continue without asking the user to
|
|
2790
|
+
switch models.
|
|
2791
|
+
|
|
2792
|
+
`
|
|
2793
|
+
);
|
|
2794
|
+
|
|
2795
|
+
normalized = normalized.replace(/not `plain chat`/g, "not internal API names");
|
|
2796
|
+
|
|
2797
|
+
normalized = normalized
|
|
2798
|
+
.replace(/this is a Codex\ninstall\/reload problem/g, "this is a Hermes\ninstall/reload problem")
|
|
2799
|
+
.replace(/Hermes plugin/g, "Hermes skills");
|
|
2800
|
+
|
|
2801
|
+
return normalized;
|
|
2802
|
+
}
|
|
2803
|
+
|
|
2804
|
+
function compactHermesEvergreenSkillMd() {
|
|
2805
|
+
return normalizeHermesSkillMarkdown(
|
|
2806
|
+
stampInstalledHost(`---
|
|
2807
|
+
name: create-evergreen-campaigns
|
|
2808
|
+
description: Create or repair evergreen campaign lanes without launching sends.
|
|
2809
|
+
visibility: public
|
|
2810
|
+
allowed-tools:
|
|
2811
|
+
${allowedToolsYaml(CREATE_EVERGREEN_CAMPAIGNS_ALLOWED_TOOLS)}
|
|
2812
|
+
---
|
|
2813
|
+
|
|
2814
|
+
# Sellable Create Evergreen Campaigns
|
|
2815
|
+
|
|
2816
|
+
Use this as the customer-facing entrypoint for the Sellable
|
|
2817
|
+
\`create-evergreen-campaigns\` workflow in Hermes.
|
|
2818
|
+
|
|
2819
|
+
## Bootstrap
|
|
2820
|
+
|
|
2821
|
+
MCP prompt and command access are required. First call
|
|
2822
|
+
\`mcp__sellable__get_auth_status({})\`. Do not inspect repo files, run shell
|
|
2823
|
+
commands, use \`npm\`, \`node\`, local harness scripts, or read local prompt
|
|
2824
|
+
files to emulate this workflow.
|
|
2825
|
+
|
|
2826
|
+
If the Sellable MCP prompt tools are unavailable, stop and say this is a Hermes
|
|
2827
|
+
install/reload problem. Tell the user to run
|
|
2828
|
+
\`curl -fsSL "https://app.sellable.dev/api/v2/cli/install" | sh\`, then restart
|
|
2829
|
+
Hermes or run \`/reload-mcp\`.
|
|
2830
|
+
|
|
2831
|
+
## Execute Workflow
|
|
2832
|
+
|
|
2833
|
+
1. Load the canonical prompt via
|
|
2834
|
+
\`mcp__sellable__get_subskill_prompt({ subskillName: "create-evergreen-campaigns" })\`.
|
|
2835
|
+
If the response has \`hasMore=true\`, continue with \`nextOffset\` until
|
|
2836
|
+
\`hasMore=false\`.
|
|
2837
|
+
2. Ask for bounded approval in normal chat before mutating if the plan requires
|
|
2838
|
+
approval. Do not call Codex app thread tools or Codex CLI workers from
|
|
2839
|
+
Hermes.
|
|
2840
|
+
3. Use \`mcp__sellable__setup_evergreen_campaigns({ mode: "plan" })\` for the
|
|
2841
|
+
plan/readback packet, then use the canonical prompt's approved mutation path
|
|
2842
|
+
and return to \`setup_evergreen_campaigns({ mode: "verify" })\` with
|
|
2843
|
+
customer-visible receipts.
|
|
2844
|
+
4. Do not launch campaigns, schedule sends, archive campaigns, delete tables,
|
|
2845
|
+
or spend paid credits.
|
|
2846
|
+
|
|
2847
|
+
## MCP Prompt Fallback
|
|
2848
|
+
|
|
2849
|
+
If exact subskill lookup fails, use
|
|
2850
|
+
\`mcp__sellable__search_subskill_prompts({ query: "create-evergreen-campaigns", includePublic: true, includeInternal: true })\`,
|
|
2851
|
+
then retry \`get_subskill_prompt\`.
|
|
2852
|
+
`, "hermes", "create-evergreen-campaigns"),
|
|
2853
|
+
"create-evergreen-campaigns"
|
|
2854
|
+
);
|
|
2855
|
+
}
|
|
2856
|
+
|
|
2857
|
+
function hermesSkillDefinitions() {
|
|
2858
|
+
return [
|
|
2859
|
+
{
|
|
2860
|
+
commandName: "create-campaign",
|
|
2861
|
+
dir: hermesCommandName("create-campaign"),
|
|
2862
|
+
skillMd: normalizeHermesSkillMarkdown(
|
|
2863
|
+
createCampaignSkillMd("hermes"),
|
|
2864
|
+
"create-campaign"
|
|
2865
|
+
),
|
|
2866
|
+
soulMd: createCampaignSoulMd(),
|
|
2867
|
+
},
|
|
2868
|
+
{
|
|
2869
|
+
commandName: "create-ab-test",
|
|
2870
|
+
dir: hermesCommandName("create-ab-test"),
|
|
2871
|
+
skillMd: normalizeHermesSkillMarkdown(
|
|
2872
|
+
createAbTestSkillMd("hermes"),
|
|
2873
|
+
"create-ab-test"
|
|
2874
|
+
),
|
|
2875
|
+
},
|
|
2876
|
+
{
|
|
2877
|
+
commandName: "create-evergreen-campaigns",
|
|
2878
|
+
dir: hermesCommandName("create-evergreen-campaigns"),
|
|
2879
|
+
skillMd: compactHermesEvergreenSkillMd(),
|
|
2880
|
+
},
|
|
2881
|
+
{
|
|
2882
|
+
commandName: "foundation",
|
|
2883
|
+
dir: hermesCommandName("foundation"),
|
|
2884
|
+
skillMd: normalizeHermesSkillMarkdown(
|
|
2885
|
+
foundationSkillMd("hermes"),
|
|
2886
|
+
"foundation"
|
|
2887
|
+
),
|
|
2888
|
+
},
|
|
2889
|
+
{
|
|
2890
|
+
commandName: "content",
|
|
2891
|
+
dir: hermesCommandName("content"),
|
|
2892
|
+
skillMd: normalizeHermesSkillMarkdown(contentSkillMd("hermes"), "content"),
|
|
2893
|
+
},
|
|
2894
|
+
{
|
|
2895
|
+
commandName: "create-post",
|
|
2896
|
+
dir: hermesCommandName("create-post"),
|
|
2897
|
+
skillMd: normalizeHermesSkillMarkdown(
|
|
2898
|
+
createPostSkillMd("hermes"),
|
|
2899
|
+
"create-post"
|
|
2900
|
+
),
|
|
2901
|
+
},
|
|
2902
|
+
{
|
|
2903
|
+
commandName: "refresh-sender-engagement",
|
|
2904
|
+
dir: hermesCommandName("refresh-sender-engagement"),
|
|
2905
|
+
skillMd: normalizeHermesSkillMarkdown(
|
|
2906
|
+
refreshSenderEngagementSkillMd("hermes"),
|
|
2907
|
+
"refresh-sender-engagement"
|
|
2908
|
+
),
|
|
2909
|
+
},
|
|
2910
|
+
{
|
|
2911
|
+
commandName: "refill-sends",
|
|
2912
|
+
dir: hermesCommandName("refill-sends"),
|
|
2913
|
+
skillMd: normalizeHermesSkillMarkdown(
|
|
2914
|
+
refillSendsSkillMd("hermes"),
|
|
2915
|
+
"refill-sends"
|
|
2916
|
+
),
|
|
2917
|
+
},
|
|
2918
|
+
];
|
|
2919
|
+
}
|
|
2920
|
+
|
|
2587
2921
|
function installerPackageRoot() {
|
|
2588
2922
|
return join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
2589
2923
|
}
|
|
@@ -3095,7 +3429,7 @@ function writeAuth(opts) {
|
|
|
3095
3429
|
activeWorkspaceId: opts.workspaceId,
|
|
3096
3430
|
apiUrl: opts.apiUrl,
|
|
3097
3431
|
};
|
|
3098
|
-
writeJson(authPath(), config, opts);
|
|
3432
|
+
writeJson(authPath(opts), config, opts);
|
|
3099
3433
|
return { written: true, reused: false };
|
|
3100
3434
|
}
|
|
3101
3435
|
|
|
@@ -3152,8 +3486,43 @@ exec ${shellQuote(npmCommand)} exec --yes --package ${shellQuote(INSTALL_PACKAGE
|
|
|
3152
3486
|
const WATCH_MODE_DRIVER_ENV = {
|
|
3153
3487
|
claude: "SELLABLE_WATCH_MODE_DRIVER=claude",
|
|
3154
3488
|
codex: "SELLABLE_WATCH_MODE_DRIVER=codex",
|
|
3489
|
+
hermes: "SELLABLE_WATCH_MODE_DRIVER=hermes",
|
|
3155
3490
|
};
|
|
3156
3491
|
|
|
3492
|
+
function mcpEnvForHost(host, opts) {
|
|
3493
|
+
const env = {
|
|
3494
|
+
SELLABLE_WATCH_MODE_DRIVER: host,
|
|
3495
|
+
};
|
|
3496
|
+
if (opts.server === "hosted") return env;
|
|
3497
|
+
if (opts.sellableConfigPath) {
|
|
3498
|
+
env.SELLABLE_CONFIG_PATH = opts.sellableConfigPath;
|
|
3499
|
+
}
|
|
3500
|
+
if (opts.sellableConfigsDir) {
|
|
3501
|
+
env.SELLABLE_CONFIGS_DIR = opts.sellableConfigsDir;
|
|
3502
|
+
}
|
|
3503
|
+
return env;
|
|
3504
|
+
}
|
|
3505
|
+
|
|
3506
|
+
function mcpEnvVarArgs(host, opts) {
|
|
3507
|
+
return Object.entries(mcpEnvForHost(host, opts)).map(
|
|
3508
|
+
([key, value]) => `${key}=${value}`
|
|
3509
|
+
);
|
|
3510
|
+
}
|
|
3511
|
+
|
|
3512
|
+
function tomlEnvTable(host, opts) {
|
|
3513
|
+
return Object.entries(mcpEnvForHost(host, opts))
|
|
3514
|
+
.map(([key, value]) => `${key} = ${quoteToml(value)}`)
|
|
3515
|
+
.join("\n");
|
|
3516
|
+
}
|
|
3517
|
+
|
|
3518
|
+
function runtimeMcpEnvForVerify(opts) {
|
|
3519
|
+
const host =
|
|
3520
|
+
opts.host === "claude" || opts.host === "codex" || opts.host === "hermes"
|
|
3521
|
+
? opts.host
|
|
3522
|
+
: "codex";
|
|
3523
|
+
return mcpEnvForHost(host, opts);
|
|
3524
|
+
}
|
|
3525
|
+
|
|
3157
3526
|
function withHostedWatchModeDriver(rawUrl, driver) {
|
|
3158
3527
|
if (!WATCH_MODE_DRIVER_ENV[driver]) {
|
|
3159
3528
|
throw new Error(`Unknown watch mode driver: ${driver}`);
|
|
@@ -3207,8 +3576,7 @@ function codexMcpAddArgs(opts) {
|
|
|
3207
3576
|
"mcp",
|
|
3208
3577
|
"add",
|
|
3209
3578
|
"sellable",
|
|
3210
|
-
"--env",
|
|
3211
|
-
WATCH_MODE_DRIVER_ENV.codex,
|
|
3579
|
+
...mcpEnvVarArgs("codex", opts).flatMap((entry) => ["--env", entry]),
|
|
3212
3580
|
"--",
|
|
3213
3581
|
command,
|
|
3214
3582
|
...args,
|
|
@@ -3239,7 +3607,7 @@ args = ${tomlArray(args)}`
|
|
|
3239
3607
|
content,
|
|
3240
3608
|
"mcp_servers.sellable.env",
|
|
3241
3609
|
`[mcp_servers.sellable.env]
|
|
3242
|
-
|
|
3610
|
+
${tomlEnvTable("codex", opts)}`
|
|
3243
3611
|
);
|
|
3244
3612
|
return content;
|
|
3245
3613
|
}
|
|
@@ -3262,7 +3630,9 @@ function codexMcpServerMatches(content, opts) {
|
|
|
3262
3630
|
return (
|
|
3263
3631
|
server.includes(`command = ${quoteToml(command)}`) &&
|
|
3264
3632
|
server.includes(`args = ${tomlArray(args)}`) &&
|
|
3265
|
-
|
|
3633
|
+
Object.entries(mcpEnvForHost("codex", opts)).every(([key, value]) =>
|
|
3634
|
+
env.includes(`${key} = ${quoteToml(value)}`)
|
|
3635
|
+
)
|
|
3266
3636
|
);
|
|
3267
3637
|
}
|
|
3268
3638
|
|
|
@@ -3277,6 +3647,568 @@ function codexPluginMcpServerMatches(content, opts) {
|
|
|
3277
3647
|
}
|
|
3278
3648
|
}
|
|
3279
3649
|
|
|
3650
|
+
function hermesHome(opts = {}) {
|
|
3651
|
+
return (
|
|
3652
|
+
opts.hermesHome ||
|
|
3653
|
+
process.env.HERMES_HOME?.trim() ||
|
|
3654
|
+
join(homedir(), ".hermes")
|
|
3655
|
+
);
|
|
3656
|
+
}
|
|
3657
|
+
|
|
3658
|
+
function hermesConfigPath(opts = {}) {
|
|
3659
|
+
return join(hermesHome(opts), "config.yaml");
|
|
3660
|
+
}
|
|
3661
|
+
|
|
3662
|
+
function hermesSkillsRoot(opts = {}) {
|
|
3663
|
+
return join(hermesHome(opts), "skills", "sellable");
|
|
3664
|
+
}
|
|
3665
|
+
|
|
3666
|
+
function hermesLikelyInstalled() {
|
|
3667
|
+
return (
|
|
3668
|
+
Boolean(process.env.HERMES_HOME?.trim()) ||
|
|
3669
|
+
commandExists("hermes") ||
|
|
3670
|
+
existsSync(hermesConfigPath()) ||
|
|
3671
|
+
existsSync(join(hermesHome(), "skills"))
|
|
3672
|
+
);
|
|
3673
|
+
}
|
|
3674
|
+
|
|
3675
|
+
function parseHermesConfig(raw, configPath = hermesConfigPath()) {
|
|
3676
|
+
const source = raw.trim() ? raw : "{}\n";
|
|
3677
|
+
const doc = parseDocument(source, { prettyErrors: false });
|
|
3678
|
+
if (doc.errors.length > 0) {
|
|
3679
|
+
throw new Error(
|
|
3680
|
+
`Invalid Hermes config YAML at ${configPath}: ${doc.errors[0].message}`
|
|
3681
|
+
);
|
|
3682
|
+
}
|
|
3683
|
+
const value = doc.toJSON() ?? {};
|
|
3684
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
3685
|
+
throw new Error(
|
|
3686
|
+
`Invalid Hermes config YAML at ${configPath}: expected a mapping at the document root`
|
|
3687
|
+
);
|
|
3688
|
+
}
|
|
3689
|
+
return value;
|
|
3690
|
+
}
|
|
3691
|
+
|
|
3692
|
+
function readHermesConfig(opts = {}) {
|
|
3693
|
+
const configPath = hermesConfigPath(opts);
|
|
3694
|
+
const raw = existsSync(configPath) ? readFileSync(configPath, "utf8") : "";
|
|
3695
|
+
return { raw, config: parseHermesConfig(raw, configPath), configPath };
|
|
3696
|
+
}
|
|
3697
|
+
|
|
3698
|
+
function hermesMcpServer(opts) {
|
|
3699
|
+
if (opts.server === "hosted") {
|
|
3700
|
+
return {
|
|
3701
|
+
url: withHostedWatchModeDriver(opts.hostedUrl, "hermes"),
|
|
3702
|
+
enabled: true,
|
|
3703
|
+
env: {
|
|
3704
|
+
SELLABLE_WATCH_MODE_DRIVER: "hermes",
|
|
3705
|
+
},
|
|
3706
|
+
};
|
|
3707
|
+
}
|
|
3708
|
+
|
|
3709
|
+
const [command, args] = mcpCommand(opts);
|
|
3710
|
+
return {
|
|
3711
|
+
command,
|
|
3712
|
+
args,
|
|
3713
|
+
enabled: true,
|
|
3714
|
+
env: mcpEnvForHost("hermes", opts),
|
|
3715
|
+
};
|
|
3716
|
+
}
|
|
3717
|
+
|
|
3718
|
+
function writeHermesConfig(configPath, config, opts) {
|
|
3719
|
+
if (opts.dryRun) return;
|
|
3720
|
+
mkdirSync(dirname(configPath), { recursive: true, mode: 0o700 });
|
|
3721
|
+
const doc = parseDocument("{}\n");
|
|
3722
|
+
doc.contents = config;
|
|
3723
|
+
writeFileSync(configPath, String(doc), { mode: 0o600 });
|
|
3724
|
+
}
|
|
3725
|
+
|
|
3726
|
+
function writeHermesMcpServer(opts) {
|
|
3727
|
+
const { config, configPath } = readHermesConfig(opts);
|
|
3728
|
+
const existingMcpServers =
|
|
3729
|
+
config.mcp_servers &&
|
|
3730
|
+
typeof config.mcp_servers === "object" &&
|
|
3731
|
+
!Array.isArray(config.mcp_servers)
|
|
3732
|
+
? config.mcp_servers
|
|
3733
|
+
: {};
|
|
3734
|
+
const nextConfig = {
|
|
3735
|
+
...config,
|
|
3736
|
+
mcp_servers: {
|
|
3737
|
+
...existingMcpServers,
|
|
3738
|
+
sellable: hermesMcpServer(opts),
|
|
3739
|
+
},
|
|
3740
|
+
};
|
|
3741
|
+
writeHermesConfig(configPath, nextConfig, opts);
|
|
3742
|
+
return configPath;
|
|
3743
|
+
}
|
|
3744
|
+
|
|
3745
|
+
function installHermesSkills(opts) {
|
|
3746
|
+
const root = hermesSkillsRoot(opts);
|
|
3747
|
+
for (const skill of hermesSkillDefinitions()) {
|
|
3748
|
+
const skillRoot = join(root, skill.dir);
|
|
3749
|
+
writeFile(join(skillRoot, "SKILL.md"), skill.skillMd, opts);
|
|
3750
|
+
if (skill.soulMd) {
|
|
3751
|
+
writeFile(join(skillRoot, "SOUL.md"), skill.soulMd, opts);
|
|
3752
|
+
}
|
|
3753
|
+
}
|
|
3754
|
+
return root;
|
|
3755
|
+
}
|
|
3756
|
+
|
|
3757
|
+
function installHermes(opts) {
|
|
3758
|
+
// Parse config before writing skills so invalid user YAML cannot leave a
|
|
3759
|
+
// half-installed Hermes skill tree behind.
|
|
3760
|
+
readHermesConfig(opts);
|
|
3761
|
+
let skillsRoot = null;
|
|
3762
|
+
try {
|
|
3763
|
+
skillsRoot = installHermesSkills(opts);
|
|
3764
|
+
const configPath = writeHermesMcpServer(opts);
|
|
3765
|
+
logVerbose(`${C.grey}+ Hermes config updated: ${configPath}${C.reset}`);
|
|
3766
|
+
return { installed: true, configPath, skillsRoot };
|
|
3767
|
+
} catch (err) {
|
|
3768
|
+
if (skillsRoot && !opts.dryRun) {
|
|
3769
|
+
rmSync(skillsRoot, { recursive: true, force: true });
|
|
3770
|
+
}
|
|
3771
|
+
throw err;
|
|
3772
|
+
}
|
|
3773
|
+
}
|
|
3774
|
+
|
|
3775
|
+
function parseHermesProfileBootstrapArgs(argv) {
|
|
3776
|
+
const opts = {
|
|
3777
|
+
profile: "",
|
|
3778
|
+
profileRoot: "",
|
|
3779
|
+
profilesRoot: "",
|
|
3780
|
+
workspaceId: "",
|
|
3781
|
+
workspaceName: "",
|
|
3782
|
+
token: "",
|
|
3783
|
+
tokenFile: "",
|
|
3784
|
+
apiUrl: process.env.SELLABLE_API_URL || DEFAULT_API_URL,
|
|
3785
|
+
slackBotToken: process.env.SLACK_BOT_TOKEN || "",
|
|
3786
|
+
slackAppToken: process.env.SLACK_APP_TOKEN || "",
|
|
3787
|
+
server: process.env.SELLABLE_INSTALL_SERVER || "package",
|
|
3788
|
+
mcpPackage: DEFAULT_SERVER_PACKAGE,
|
|
3789
|
+
localCommand: process.env.SELLABLE_MCP_LOCAL_COMMAND || "",
|
|
3790
|
+
dryRun: false,
|
|
3791
|
+
json: false,
|
|
3792
|
+
force: false,
|
|
3793
|
+
overwriteEnv: false,
|
|
3794
|
+
allowDuplicateSlackTokens: false,
|
|
3795
|
+
verbose: false,
|
|
3796
|
+
};
|
|
3797
|
+
|
|
3798
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
3799
|
+
const arg = argv[i];
|
|
3800
|
+
const next = () => {
|
|
3801
|
+
const value = argv[i + 1];
|
|
3802
|
+
if (!value || value.startsWith("--")) {
|
|
3803
|
+
throw new Error(`Missing value for ${arg}`);
|
|
3804
|
+
}
|
|
3805
|
+
i += 1;
|
|
3806
|
+
return value;
|
|
3807
|
+
};
|
|
3808
|
+
|
|
3809
|
+
if (arg === "--profile") opts.profile = next();
|
|
3810
|
+
else if (arg === "--profile-root") opts.profileRoot = next();
|
|
3811
|
+
else if (arg === "--profiles-root") opts.profilesRoot = next();
|
|
3812
|
+
else if (arg === "--workspace-id") opts.workspaceId = next();
|
|
3813
|
+
else if (arg === "--workspace-name") opts.workspaceName = next();
|
|
3814
|
+
else if (arg === "--token") opts.token = next();
|
|
3815
|
+
else if (arg === "--token-file") opts.tokenFile = next();
|
|
3816
|
+
else if (arg === "--api-url") opts.apiUrl = next();
|
|
3817
|
+
else if (arg === "--slack-bot-token") opts.slackBotToken = next();
|
|
3818
|
+
else if (arg === "--slack-app-token") opts.slackAppToken = next();
|
|
3819
|
+
else if (arg === "--server") opts.server = next();
|
|
3820
|
+
else if (arg === "--mcp-package") {
|
|
3821
|
+
opts.mcpPackage = normalizeWindowsPackageSpec(next());
|
|
3822
|
+
} else if (arg === "--local-command") opts.localCommand = next();
|
|
3823
|
+
else if (arg === "--dry-run") opts.dryRun = true;
|
|
3824
|
+
else if (arg === "--json") opts.json = true;
|
|
3825
|
+
else if (arg === "--force") opts.force = true;
|
|
3826
|
+
else if (arg === "--overwrite-env") opts.overwriteEnv = true;
|
|
3827
|
+
else if (arg === "--allow-duplicate-slack-tokens") {
|
|
3828
|
+
opts.allowDuplicateSlackTokens = true;
|
|
3829
|
+
} else if (arg === "--verbose") opts.verbose = true;
|
|
3830
|
+
else {
|
|
3831
|
+
throw new Error(`Unknown hermes profile bootstrap option: ${arg}`);
|
|
3832
|
+
}
|
|
3833
|
+
}
|
|
3834
|
+
|
|
3835
|
+
if (!["package", "local"].includes(opts.server)) {
|
|
3836
|
+
throw new Error("hermes profile bootstrap supports --server package or local");
|
|
3837
|
+
}
|
|
3838
|
+
return opts;
|
|
3839
|
+
}
|
|
3840
|
+
|
|
3841
|
+
function assertSafeProfileName(profile) {
|
|
3842
|
+
if (!profile) return;
|
|
3843
|
+
if (profile.includes("/") || profile.includes("\\") || profile === "." || profile === "..") {
|
|
3844
|
+
throw new Error("--profile must be a profile name, not a path");
|
|
3845
|
+
}
|
|
3846
|
+
}
|
|
3847
|
+
|
|
3848
|
+
function resolveHermesProfileRoot(opts) {
|
|
3849
|
+
if (opts.profileRoot) {
|
|
3850
|
+
return normalizeOptionalPath(opts.profileRoot);
|
|
3851
|
+
}
|
|
3852
|
+
|
|
3853
|
+
if (opts.profilesRoot || opts.profile) {
|
|
3854
|
+
if (!opts.profilesRoot || !opts.profile) {
|
|
3855
|
+
throw new Error("--profiles-root requires --profile for Hermes bootstrap");
|
|
3856
|
+
}
|
|
3857
|
+
assertSafeProfileName(opts.profile);
|
|
3858
|
+
return join(normalizeOptionalPath(opts.profilesRoot), opts.profile);
|
|
3859
|
+
}
|
|
3860
|
+
|
|
3861
|
+
const envHermesHome = process.env.HERMES_HOME?.trim();
|
|
3862
|
+
if (envHermesHome) {
|
|
3863
|
+
return resolve(envHermesHome);
|
|
3864
|
+
}
|
|
3865
|
+
|
|
3866
|
+
throw new Error(
|
|
3867
|
+
"Missing Hermes profile root. Pass --profile-root, or --profiles-root with --profile, or set HERMES_HOME."
|
|
3868
|
+
);
|
|
3869
|
+
}
|
|
3870
|
+
|
|
3871
|
+
function readBootstrapToken(opts) {
|
|
3872
|
+
if (opts.tokenFile) {
|
|
3873
|
+
const tokenPath = normalizeOptionalPath(opts.tokenFile);
|
|
3874
|
+
if (!existsSync(tokenPath)) {
|
|
3875
|
+
throw new Error(`Token file not found: ${tokenPath}`);
|
|
3876
|
+
}
|
|
3877
|
+
return {
|
|
3878
|
+
token: readFileSync(tokenPath, "utf8").trim(),
|
|
3879
|
+
source: "token-file",
|
|
3880
|
+
};
|
|
3881
|
+
}
|
|
3882
|
+
|
|
3883
|
+
const envToken = process.env.SELLABLE_API_TOKEN || process.env.SELLABLE_TOKEN || "";
|
|
3884
|
+
if (envToken) return { token: envToken.trim(), source: "env" };
|
|
3885
|
+
if (opts.token) return { token: opts.token.trim(), source: "flag" };
|
|
3886
|
+
return { token: "", source: "none" };
|
|
3887
|
+
}
|
|
3888
|
+
|
|
3889
|
+
function tokenFingerprint(value) {
|
|
3890
|
+
if (!value) return null;
|
|
3891
|
+
return createHash("sha256").update(value).digest("hex").slice(0, 16);
|
|
3892
|
+
}
|
|
3893
|
+
|
|
3894
|
+
function parseEnvLines(raw) {
|
|
3895
|
+
const entries = new Map();
|
|
3896
|
+
const passthrough = [];
|
|
3897
|
+
for (const line of String(raw || "").split(/\r?\n/)) {
|
|
3898
|
+
if (!line) continue;
|
|
3899
|
+
const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
|
|
3900
|
+
if (!match) {
|
|
3901
|
+
passthrough.push(line);
|
|
3902
|
+
continue;
|
|
3903
|
+
}
|
|
3904
|
+
entries.set(match[1], match[2]);
|
|
3905
|
+
}
|
|
3906
|
+
return { entries, passthrough };
|
|
3907
|
+
}
|
|
3908
|
+
|
|
3909
|
+
function renderEnvFile({ entries, passthrough }) {
|
|
3910
|
+
const lines = [...passthrough];
|
|
3911
|
+
for (const [key, value] of entries.entries()) {
|
|
3912
|
+
if (String(value).includes("\n")) {
|
|
3913
|
+
throw new Error(`Invalid newline in ${key}`);
|
|
3914
|
+
}
|
|
3915
|
+
lines.push(`${key}=${value}`);
|
|
3916
|
+
}
|
|
3917
|
+
return `${lines.join("\n")}${lines.length ? "\n" : ""}`;
|
|
3918
|
+
}
|
|
3919
|
+
|
|
3920
|
+
function findDuplicateSlackTokenProfiles(profileRoot, slackTokens) {
|
|
3921
|
+
const duplicates = [];
|
|
3922
|
+
const profilesRoot = dirname(profileRoot);
|
|
3923
|
+
if (!existsSync(profilesRoot)) return duplicates;
|
|
3924
|
+
|
|
3925
|
+
const requested = Object.entries(slackTokens)
|
|
3926
|
+
.filter(([, value]) => value)
|
|
3927
|
+
.map(([key, value]) => [key, tokenFingerprint(value), value]);
|
|
3928
|
+
if (requested.length === 0) return duplicates;
|
|
3929
|
+
|
|
3930
|
+
for (const name of readdirSync(profilesRoot)) {
|
|
3931
|
+
const siblingRoot = join(profilesRoot, name);
|
|
3932
|
+
if (resolve(siblingRoot) === resolve(profileRoot)) continue;
|
|
3933
|
+
const envPath = join(siblingRoot, ".env");
|
|
3934
|
+
if (!existsSync(envPath)) continue;
|
|
3935
|
+
const existing = parseEnvLines(readFileSync(envPath, "utf8")).entries;
|
|
3936
|
+
for (const [key, fingerprint, rawValue] of requested) {
|
|
3937
|
+
if (existing.get(key) === rawValue) {
|
|
3938
|
+
duplicates.push({ key, fingerprint, profileRoot: siblingRoot });
|
|
3939
|
+
}
|
|
3940
|
+
}
|
|
3941
|
+
}
|
|
3942
|
+
return duplicates;
|
|
3943
|
+
}
|
|
3944
|
+
|
|
3945
|
+
function readJsonFile(path) {
|
|
3946
|
+
if (!existsSync(path)) return {};
|
|
3947
|
+
try {
|
|
3948
|
+
const raw = JSON.parse(readFileSync(path, "utf8"));
|
|
3949
|
+
return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
|
|
3950
|
+
} catch (err) {
|
|
3951
|
+
throw new Error(
|
|
3952
|
+
`Invalid JSON in ${path}: ${err instanceof Error ? err.message : String(err)}`
|
|
3953
|
+
);
|
|
3954
|
+
}
|
|
3955
|
+
}
|
|
3956
|
+
|
|
3957
|
+
function writeHermesProfileSellableConfig(configPath, raw, opts) {
|
|
3958
|
+
writeJson(configPath, raw, opts);
|
|
3959
|
+
}
|
|
3960
|
+
|
|
3961
|
+
function runHermesProfileBootstrap(argv) {
|
|
3962
|
+
const parsed = parseHermesProfileBootstrapArgs(argv);
|
|
3963
|
+
const previousVerbose = VERBOSE;
|
|
3964
|
+
VERBOSE = Boolean(parsed.verbose);
|
|
3965
|
+
try {
|
|
3966
|
+
const profileRoot = resolveHermesProfileRoot(parsed);
|
|
3967
|
+
const profile = parsed.profile || basenameSafe(profileRoot);
|
|
3968
|
+
const sellableConfigPath = join(profileRoot, "sellable", "config.json");
|
|
3969
|
+
const sellableConfigsDir = join(profileRoot, "sellable", "configs");
|
|
3970
|
+
const envPath = join(profileRoot, ".env");
|
|
3971
|
+
const { token, source: tokenSource } = readBootstrapToken(parsed);
|
|
3972
|
+
const slackTokens = {
|
|
3973
|
+
SLACK_BOT_TOKEN: parsed.slackBotToken.trim(),
|
|
3974
|
+
SLACK_APP_TOKEN: parsed.slackAppToken.trim(),
|
|
3975
|
+
};
|
|
3976
|
+
const opts = {
|
|
3977
|
+
...parsed,
|
|
3978
|
+
host: "hermes",
|
|
3979
|
+
hermesHome: profileRoot,
|
|
3980
|
+
sellableConfigPath,
|
|
3981
|
+
sellableConfigsDir,
|
|
3982
|
+
token,
|
|
3983
|
+
tokenSource,
|
|
3984
|
+
workspaceId: parsed.workspaceId,
|
|
3985
|
+
apiUrl: parsed.apiUrl,
|
|
3986
|
+
};
|
|
3987
|
+
|
|
3988
|
+
if (token && !parsed.workspaceId) {
|
|
3989
|
+
throw new Error("--workspace-id is required when a Sellable token is supplied");
|
|
3990
|
+
}
|
|
3991
|
+
|
|
3992
|
+
const summary = {
|
|
3993
|
+
ok: true,
|
|
3994
|
+
command: "hermes profile bootstrap",
|
|
3995
|
+
profile,
|
|
3996
|
+
dryRun: opts.dryRun,
|
|
3997
|
+
paths: {
|
|
3998
|
+
profileRoot,
|
|
3999
|
+
hermesConfigPath: hermesConfigPath(opts),
|
|
4000
|
+
envPath,
|
|
4001
|
+
sellableConfigPath,
|
|
4002
|
+
sellableConfigsDir,
|
|
4003
|
+
skillsRoot: hermesSkillsRoot(opts),
|
|
4004
|
+
},
|
|
4005
|
+
auth: {
|
|
4006
|
+
apiUrl: opts.apiUrl,
|
|
4007
|
+
activeWorkspaceId: parsed.workspaceId || null,
|
|
4008
|
+
activeWorkspaceName: parsed.workspaceName || null,
|
|
4009
|
+
tokenPresent: Boolean(token),
|
|
4010
|
+
tokenSource,
|
|
4011
|
+
tokenFingerprint: tokenFingerprint(token),
|
|
4012
|
+
},
|
|
4013
|
+
slack: {
|
|
4014
|
+
keys: Object.entries(slackTokens)
|
|
4015
|
+
.filter(([, value]) => value)
|
|
4016
|
+
.map(([key]) => key),
|
|
4017
|
+
fingerprints: Object.fromEntries(
|
|
4018
|
+
Object.entries(slackTokens)
|
|
4019
|
+
.filter(([, value]) => value)
|
|
4020
|
+
.map(([key, value]) => [key, tokenFingerprint(value)])
|
|
4021
|
+
),
|
|
4022
|
+
},
|
|
4023
|
+
created: [],
|
|
4024
|
+
updated: [],
|
|
4025
|
+
preserved: [],
|
|
4026
|
+
skipped: [],
|
|
4027
|
+
warnings: [],
|
|
4028
|
+
};
|
|
4029
|
+
|
|
4030
|
+
let hermesConfig = {};
|
|
4031
|
+
try {
|
|
4032
|
+
hermesConfig = readHermesConfig(opts).config;
|
|
4033
|
+
} catch (err) {
|
|
4034
|
+
throw err;
|
|
4035
|
+
}
|
|
4036
|
+
const existingSellablePath =
|
|
4037
|
+
hermesConfig?.mcp_servers?.sellable?.env?.SELLABLE_CONFIG_PATH || "";
|
|
4038
|
+
if (
|
|
4039
|
+
existingSellablePath &&
|
|
4040
|
+
resolve(existingSellablePath) !== sellableConfigPath &&
|
|
4041
|
+
!opts.force
|
|
4042
|
+
) {
|
|
4043
|
+
throw new Error(
|
|
4044
|
+
`Existing Hermes sellable MCP points at ${existingSellablePath}; pass --force to replace it with ${sellableConfigPath}.`
|
|
4045
|
+
);
|
|
4046
|
+
}
|
|
4047
|
+
|
|
4048
|
+
const existingAuth = readJsonFile(sellableConfigPath);
|
|
4049
|
+
const existingWorkspaceId =
|
|
4050
|
+
existingAuth.activeWorkspaceId || existingAuth.workspaceId || "";
|
|
4051
|
+
if (
|
|
4052
|
+
existingWorkspaceId &&
|
|
4053
|
+
parsed.workspaceId &&
|
|
4054
|
+
existingWorkspaceId !== parsed.workspaceId &&
|
|
4055
|
+
!opts.force
|
|
4056
|
+
) {
|
|
4057
|
+
throw new Error(
|
|
4058
|
+
`Existing Sellable profile config uses workspace ${existingWorkspaceId}; pass --force to replace it with ${parsed.workspaceId}.`
|
|
4059
|
+
);
|
|
4060
|
+
}
|
|
4061
|
+
|
|
4062
|
+
let envState = { entries: new Map(), passthrough: [] };
|
|
4063
|
+
if (existsSync(envPath)) {
|
|
4064
|
+
envState = parseEnvLines(readFileSync(envPath, "utf8"));
|
|
4065
|
+
}
|
|
4066
|
+
for (const [key, value] of Object.entries(slackTokens)) {
|
|
4067
|
+
if (!value) continue;
|
|
4068
|
+
const existing = envState.entries.get(key);
|
|
4069
|
+
if (existing && existing !== value && !opts.overwriteEnv) {
|
|
4070
|
+
throw new Error(
|
|
4071
|
+
`${envPath} already has ${key}; pass --overwrite-env to replace it.`
|
|
4072
|
+
);
|
|
4073
|
+
}
|
|
4074
|
+
}
|
|
4075
|
+
if (!opts.allowDuplicateSlackTokens) {
|
|
4076
|
+
const duplicates = findDuplicateSlackTokenProfiles(profileRoot, slackTokens);
|
|
4077
|
+
if (duplicates.length > 0) {
|
|
4078
|
+
const rendered = duplicates
|
|
4079
|
+
.map((dup) => `${dup.key} fingerprint ${dup.fingerprint} in ${dup.profileRoot}`)
|
|
4080
|
+
.join("; ");
|
|
4081
|
+
throw new Error(
|
|
4082
|
+
`Duplicate Slack token detected across Hermes profiles: ${rendered}. Pass --allow-duplicate-slack-tokens to allow this.`
|
|
4083
|
+
);
|
|
4084
|
+
}
|
|
4085
|
+
}
|
|
4086
|
+
|
|
4087
|
+
const hermesConfigExisted = existsSync(hermesConfigPath(opts));
|
|
4088
|
+
const skillsExisted = existsSync(hermesSkillsRoot(opts));
|
|
4089
|
+
const authExisted = existsSync(sellableConfigPath);
|
|
4090
|
+
const configsDirExisted = existsSync(sellableConfigsDir);
|
|
4091
|
+
const envExisted = existsSync(envPath);
|
|
4092
|
+
|
|
4093
|
+
const nextAuth = {
|
|
4094
|
+
...existingAuth,
|
|
4095
|
+
apiUrl: opts.apiUrl,
|
|
4096
|
+
bootstrap: {
|
|
4097
|
+
...(existingAuth.bootstrap &&
|
|
4098
|
+
typeof existingAuth.bootstrap === "object" &&
|
|
4099
|
+
!Array.isArray(existingAuth.bootstrap)
|
|
4100
|
+
? existingAuth.bootstrap
|
|
4101
|
+
: {}),
|
|
4102
|
+
profile,
|
|
4103
|
+
status: token ? "ready" : "pending_auth",
|
|
4104
|
+
updatedAt: new Date().toISOString(),
|
|
4105
|
+
},
|
|
4106
|
+
};
|
|
4107
|
+
if (parsed.workspaceId) {
|
|
4108
|
+
nextAuth.activeWorkspaceId = parsed.workspaceId;
|
|
4109
|
+
nextAuth.workspaceId = parsed.workspaceId;
|
|
4110
|
+
}
|
|
4111
|
+
if (parsed.workspaceName) {
|
|
4112
|
+
nextAuth.activeWorkspaceName = parsed.workspaceName;
|
|
4113
|
+
nextAuth.workspaceName = parsed.workspaceName;
|
|
4114
|
+
}
|
|
4115
|
+
if (token) {
|
|
4116
|
+
nextAuth.token = token;
|
|
4117
|
+
} else if (!existingAuth.token) {
|
|
4118
|
+
delete nextAuth.token;
|
|
4119
|
+
}
|
|
4120
|
+
|
|
4121
|
+
if (!opts.dryRun) {
|
|
4122
|
+
mkdirSync(profileRoot, { recursive: true, mode: 0o700 });
|
|
4123
|
+
mkdirSync(sellableConfigsDir, { recursive: true, mode: 0o700 });
|
|
4124
|
+
writeHermesProfileSellableConfig(sellableConfigPath, nextAuth, opts);
|
|
4125
|
+
installHermes(opts);
|
|
4126
|
+
if (Object.values(slackTokens).some(Boolean)) {
|
|
4127
|
+
for (const [key, value] of Object.entries(slackTokens)) {
|
|
4128
|
+
if (value) envState.entries.set(key, value);
|
|
4129
|
+
}
|
|
4130
|
+
writeFile(envPath, renderEnvFile(envState), opts, 0o600);
|
|
4131
|
+
}
|
|
4132
|
+
}
|
|
4133
|
+
|
|
4134
|
+
(configsDirExisted ? summary.preserved : summary.created).push(
|
|
4135
|
+
sellableConfigsDir
|
|
4136
|
+
);
|
|
4137
|
+
(authExisted ? summary.updated : summary.created).push(sellableConfigPath);
|
|
4138
|
+
(hermesConfigExisted ? summary.updated : summary.created).push(
|
|
4139
|
+
hermesConfigPath(opts)
|
|
4140
|
+
);
|
|
4141
|
+
(skillsExisted ? summary.updated : summary.created).push(hermesSkillsRoot(opts));
|
|
4142
|
+
if (Object.values(slackTokens).some(Boolean)) {
|
|
4143
|
+
(envExisted ? summary.updated : summary.created).push(envPath);
|
|
4144
|
+
} else {
|
|
4145
|
+
summary.skipped.push("Slack env: no Slack tokens supplied");
|
|
4146
|
+
}
|
|
4147
|
+
if (!token) {
|
|
4148
|
+
summary.warnings.push(
|
|
4149
|
+
`Manual auth pending. Run: sellable auth set <token> --workspace-id <workspace_id> --sellable-config-path ${sellableConfigPath}`
|
|
4150
|
+
);
|
|
4151
|
+
}
|
|
4152
|
+
|
|
4153
|
+
if (opts.json) {
|
|
4154
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
4155
|
+
} else {
|
|
4156
|
+
logMilestone(`Hermes profile bootstrapped at ${profileRoot}`);
|
|
4157
|
+
logStep(`Sellable config: ${sellableConfigPath}`);
|
|
4158
|
+
logStep(`Sellable configs dir: ${sellableConfigsDir}`);
|
|
4159
|
+
if (!token) logWarn(summary.warnings[0]);
|
|
4160
|
+
}
|
|
4161
|
+
return summary;
|
|
4162
|
+
} finally {
|
|
4163
|
+
VERBOSE = previousVerbose;
|
|
4164
|
+
}
|
|
4165
|
+
}
|
|
4166
|
+
|
|
4167
|
+
function basenameSafe(pathValue) {
|
|
4168
|
+
const normalized = resolve(pathValue);
|
|
4169
|
+
return normalized.split(/[\\/]/).filter(Boolean).pop() || "hermes-profile";
|
|
4170
|
+
}
|
|
4171
|
+
|
|
4172
|
+
function hermesMcpServerMatches(config, opts) {
|
|
4173
|
+
const server = config?.mcp_servers?.sellable;
|
|
4174
|
+
if (!server || typeof server !== "object") return false;
|
|
4175
|
+
const expected = hermesMcpServer(opts);
|
|
4176
|
+
return JSON.stringify(server) === JSON.stringify(expected);
|
|
4177
|
+
}
|
|
4178
|
+
|
|
4179
|
+
function hermesSkillChecks() {
|
|
4180
|
+
const root = hermesSkillsRoot();
|
|
4181
|
+
const skills = hermesSkillDefinitions();
|
|
4182
|
+
const paths = skills.map((skill) => join(root, skill.dir, "SKILL.md"));
|
|
4183
|
+
const allPresent = paths.every((skillPath) => existsSync(skillPath));
|
|
4184
|
+
const contents = paths
|
|
4185
|
+
.filter((skillPath) => existsSync(skillPath))
|
|
4186
|
+
.map((skillPath) => readFileSync(skillPath, "utf8"));
|
|
4187
|
+
const commandNamesCurrent =
|
|
4188
|
+
contents.length === skills.length &&
|
|
4189
|
+
skills.every((skill, index) =>
|
|
4190
|
+
contents[index]?.includes(`name: ${hermesCommandName(skill.commandName)}`)
|
|
4191
|
+
);
|
|
4192
|
+
const slashCommandsCurrent =
|
|
4193
|
+
contents.length === skills.length &&
|
|
4194
|
+
skills.every((skill, index) =>
|
|
4195
|
+
contents[index]?.includes(`/${hermesCommandName(skill.commandName)}`)
|
|
4196
|
+
);
|
|
4197
|
+
const forbiddenSyntax = contents.some((content) =>
|
|
4198
|
+
/\$sellable:|\/sellable:|mcp__sellable__|request_user_input|AskUserQuestion/.test(
|
|
4199
|
+
content
|
|
4200
|
+
)
|
|
4201
|
+
);
|
|
4202
|
+
return {
|
|
4203
|
+
root,
|
|
4204
|
+
paths,
|
|
4205
|
+
allPresent,
|
|
4206
|
+
commandNamesCurrent,
|
|
4207
|
+
slashCommandsCurrent,
|
|
4208
|
+
forbiddenSyntax,
|
|
4209
|
+
};
|
|
4210
|
+
}
|
|
4211
|
+
|
|
3280
4212
|
function installClaude(opts) {
|
|
3281
4213
|
if (!commandExists("claude")) {
|
|
3282
4214
|
const message =
|
|
@@ -3356,7 +4288,7 @@ function canonicalClaudeMcpServer(opts) {
|
|
|
3356
4288
|
type: "stdio",
|
|
3357
4289
|
command,
|
|
3358
4290
|
args,
|
|
3359
|
-
env:
|
|
4291
|
+
env: mcpEnvForHost("claude", opts),
|
|
3360
4292
|
alwaysLoad: true,
|
|
3361
4293
|
};
|
|
3362
4294
|
}
|
|
@@ -3385,7 +4317,7 @@ function claudeMcpServerMatches(server, canonical) {
|
|
|
3385
4317
|
server.type === canonical.type &&
|
|
3386
4318
|
server.command === canonical.command &&
|
|
3387
4319
|
JSON.stringify(server.args || []) === JSON.stringify(canonical.args) &&
|
|
3388
|
-
server.env
|
|
4320
|
+
JSON.stringify(server.env || {}) === JSON.stringify(canonical.env)
|
|
3389
4321
|
);
|
|
3390
4322
|
}
|
|
3391
4323
|
|
|
@@ -3444,12 +4376,10 @@ function patchClaudeAlwaysLoad(opts) {
|
|
|
3444
4376
|
server.args = [...canonical.args];
|
|
3445
4377
|
touched = true;
|
|
3446
4378
|
}
|
|
3447
|
-
const
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
};
|
|
3452
|
-
if (existingDriver !== "claude") {
|
|
4379
|
+
const existingEnv =
|
|
4380
|
+
server.env && typeof server.env === "object" ? server.env : {};
|
|
4381
|
+
server.env = { ...canonical.env };
|
|
4382
|
+
if (JSON.stringify(existingEnv) !== JSON.stringify(server.env)) {
|
|
3453
4383
|
touched = true;
|
|
3454
4384
|
}
|
|
3455
4385
|
}
|
|
@@ -3558,9 +4488,16 @@ function warningCheck(ok, label) {
|
|
|
3558
4488
|
return { ok, label, required: false };
|
|
3559
4489
|
}
|
|
3560
4490
|
|
|
4491
|
+
function firstRunCommandForHost(host) {
|
|
4492
|
+
if (host === "hermes") return "/sellable-create-campaign";
|
|
4493
|
+
if (host === "codex") return "$sellable:create-campaign";
|
|
4494
|
+
if (host === "claude") return "/sellable:create-campaign";
|
|
4495
|
+
return "/sellable:create-campaign, $sellable:create-campaign, or /sellable-create-campaign";
|
|
4496
|
+
}
|
|
4497
|
+
|
|
3561
4498
|
async function verify(opts) {
|
|
3562
4499
|
const startedAt = new Date().toISOString();
|
|
3563
|
-
const stored = readStoredAuth();
|
|
4500
|
+
const stored = readStoredAuth(opts);
|
|
3564
4501
|
const checks = [];
|
|
3565
4502
|
const hasWatchModeDriverMarker = (content, driver) =>
|
|
3566
4503
|
new RegExp(`SELLABLE_WATCH_MODE_DRIVER[\\s\\S]{0,80}${driver}`).test(
|
|
@@ -3568,12 +4505,12 @@ async function verify(opts) {
|
|
|
3568
4505
|
) || new RegExp(`[?&]driver=${driver}(?:\\b|&)`).test(content);
|
|
3569
4506
|
|
|
3570
4507
|
if (stored?.token && stored?.workspaceId) {
|
|
3571
|
-
checks.push(warningCheck(true, `Auth config: ${authPath()}`));
|
|
4508
|
+
checks.push(warningCheck(true, `Auth config: ${authPath(opts)}`));
|
|
3572
4509
|
} else {
|
|
3573
4510
|
checks.push(
|
|
3574
4511
|
warningCheck(
|
|
3575
4512
|
true,
|
|
3576
|
-
`Auth: not yet signed in — sign in on first run of
|
|
4513
|
+
`Auth: not yet signed in — sign in on first run of ${firstRunCommandForHost(opts.host)}`
|
|
3577
4514
|
)
|
|
3578
4515
|
);
|
|
3579
4516
|
}
|
|
@@ -3860,6 +4797,88 @@ async function verify(opts) {
|
|
|
3860
4797
|
)
|
|
3861
4798
|
);
|
|
3862
4799
|
}
|
|
4800
|
+
if (opts.host === "hermes" || opts.host === "all") {
|
|
4801
|
+
let hermesConfig = null;
|
|
4802
|
+
let hermesConfigError = "";
|
|
4803
|
+
try {
|
|
4804
|
+
hermesConfig = readHermesConfig().config;
|
|
4805
|
+
} catch (err) {
|
|
4806
|
+
hermesConfigError = err instanceof Error ? err.message : String(err);
|
|
4807
|
+
}
|
|
4808
|
+
const npmPresent = opts.server !== "package" || commandExists(packageManagerCommand());
|
|
4809
|
+
const skillChecks = hermesSkillChecks();
|
|
4810
|
+
const configMatches = hermesConfig
|
|
4811
|
+
? hermesMcpServerMatches(hermesConfig, opts)
|
|
4812
|
+
: false;
|
|
4813
|
+
const hasHermesDriver =
|
|
4814
|
+
hermesConfig?.mcp_servers?.sellable?.env?.SELLABLE_WATCH_MODE_DRIVER ===
|
|
4815
|
+
"hermes";
|
|
4816
|
+
|
|
4817
|
+
const check =
|
|
4818
|
+
opts.host === "hermes" ? requiredCheck : warningCheck;
|
|
4819
|
+
checks.push(
|
|
4820
|
+
check(
|
|
4821
|
+
!hermesConfigError,
|
|
4822
|
+
hermesConfigError || `Hermes config readable: ${hermesConfigPath()}`
|
|
4823
|
+
)
|
|
4824
|
+
);
|
|
4825
|
+
checks.push(
|
|
4826
|
+
check(
|
|
4827
|
+
configMatches,
|
|
4828
|
+
configMatches
|
|
4829
|
+
? "Hermes Sellable MCP entry canonical"
|
|
4830
|
+
: "Hermes Sellable MCP entry missing or stale"
|
|
4831
|
+
)
|
|
4832
|
+
);
|
|
4833
|
+
checks.push(
|
|
4834
|
+
check(
|
|
4835
|
+
hasHermesDriver,
|
|
4836
|
+
hasHermesDriver
|
|
4837
|
+
? "Hermes watch mode driver pinned to hermes"
|
|
4838
|
+
: "Hermes watch mode driver missing"
|
|
4839
|
+
)
|
|
4840
|
+
);
|
|
4841
|
+
checks.push(
|
|
4842
|
+
check(
|
|
4843
|
+
skillChecks.allPresent,
|
|
4844
|
+
skillChecks.allPresent
|
|
4845
|
+
? "Hermes Sellable skills present"
|
|
4846
|
+
: "Hermes Sellable skills missing"
|
|
4847
|
+
)
|
|
4848
|
+
);
|
|
4849
|
+
checks.push(
|
|
4850
|
+
check(
|
|
4851
|
+
skillChecks.commandNamesCurrent,
|
|
4852
|
+
skillChecks.commandNamesCurrent
|
|
4853
|
+
? "Hermes skill names current"
|
|
4854
|
+
: "Hermes skill names stale"
|
|
4855
|
+
)
|
|
4856
|
+
);
|
|
4857
|
+
checks.push(
|
|
4858
|
+
check(
|
|
4859
|
+
skillChecks.slashCommandsCurrent,
|
|
4860
|
+
skillChecks.slashCommandsCurrent
|
|
4861
|
+
? "Hermes slash command hints current"
|
|
4862
|
+
: "Hermes slash command hints stale"
|
|
4863
|
+
)
|
|
4864
|
+
);
|
|
4865
|
+
checks.push(
|
|
4866
|
+
check(
|
|
4867
|
+
!skillChecks.forbiddenSyntax,
|
|
4868
|
+
!skillChecks.forbiddenSyntax
|
|
4869
|
+
? "Hermes skills use Hermes MCP/tool syntax"
|
|
4870
|
+
: "Hermes skills contain Claude/Codex-only syntax"
|
|
4871
|
+
)
|
|
4872
|
+
);
|
|
4873
|
+
checks.push(
|
|
4874
|
+
check(
|
|
4875
|
+
npmPresent,
|
|
4876
|
+
npmPresent
|
|
4877
|
+
? "Hermes package runtime command present"
|
|
4878
|
+
: `Hermes package runtime command missing: ${packageManagerCommand()}`
|
|
4879
|
+
)
|
|
4880
|
+
);
|
|
4881
|
+
}
|
|
3863
4882
|
|
|
3864
4883
|
let runtimeMcp;
|
|
3865
4884
|
try {
|
|
@@ -3870,6 +4889,7 @@ async function verify(opts) {
|
|
|
3870
4889
|
cwd: process.cwd(),
|
|
3871
4890
|
env: {
|
|
3872
4891
|
...process.env,
|
|
4892
|
+
...runtimeMcpEnvForVerify(opts),
|
|
3873
4893
|
SELLABLE_API_URL: opts.apiUrl,
|
|
3874
4894
|
SELLABLE_TOKEN: opts.token || process.env.SELLABLE_TOKEN || "",
|
|
3875
4895
|
SELLABLE_WORKSPACE_ID:
|
|
@@ -3925,6 +4945,11 @@ async function verify(opts) {
|
|
|
3925
4945
|
install: INSTALL_PACKAGE_SPEC,
|
|
3926
4946
|
mcp: opts.mcpPackage,
|
|
3927
4947
|
},
|
|
4948
|
+
sellableConfig: {
|
|
4949
|
+
path: authPath(opts),
|
|
4950
|
+
explicitPath: opts.sellableConfigPath || null,
|
|
4951
|
+
configsDir: opts.sellableConfigsDir || null,
|
|
4952
|
+
},
|
|
3928
4953
|
checks,
|
|
3929
4954
|
runtimeMcp,
|
|
3930
4955
|
threadExposureNotice:
|
|
@@ -4042,7 +5067,7 @@ function printNextSteps(installedHosts, authReused) {
|
|
|
4042
5067
|
` ${C.bold}Almost there — install an agent first.${C.reset}`
|
|
4043
5068
|
);
|
|
4044
5069
|
console.log(
|
|
4045
|
-
` ${C.yellow}!${C.reset} No agent CLI found (Claude Code or
|
|
5070
|
+
` ${C.yellow}!${C.reset} No agent CLI/profile found (Claude Code, Codex, or Hermes)`
|
|
4046
5071
|
);
|
|
4047
5072
|
console.log("");
|
|
4048
5073
|
console.log("");
|
|
@@ -4086,6 +5111,7 @@ function printNextSteps(installedHosts, authReused) {
|
|
|
4086
5111
|
|
|
4087
5112
|
const hasClaude = installedHosts.includes("Claude Code");
|
|
4088
5113
|
const hasCodex = installedHosts.includes("Codex");
|
|
5114
|
+
const hasHermes = installedHosts.includes("Hermes");
|
|
4089
5115
|
|
|
4090
5116
|
// Header
|
|
4091
5117
|
if (authReused) {
|
|
@@ -4101,6 +5127,11 @@ function printNextSteps(installedHosts, authReused) {
|
|
|
4101
5127
|
console.log(` ${C.green}✓${C.reset} Skills installed`);
|
|
4102
5128
|
console.log(` ${C.green}✓${C.reset} Codex custom agents installed`);
|
|
4103
5129
|
}
|
|
5130
|
+
if (hasHermes) {
|
|
5131
|
+
console.log(
|
|
5132
|
+
` ${C.green}✓${C.reset} Hermes skills and MCP config installed`
|
|
5133
|
+
);
|
|
5134
|
+
}
|
|
4104
5135
|
if (hasClaude) {
|
|
4105
5136
|
console.log(
|
|
4106
5137
|
` ${C.green}✓${C.reset} Claude slash commands and custom agents installed`
|
|
@@ -4144,6 +5175,19 @@ function printNextSteps(installedHosts, authReused) {
|
|
|
4144
5175
|
]);
|
|
4145
5176
|
console.log("");
|
|
4146
5177
|
}
|
|
5178
|
+
if (hasHermes) {
|
|
5179
|
+
printAgentBox("Using Hermes?", "hermes", [
|
|
5180
|
+
{ label: "Campaign", command: "/sellable-create-campaign" },
|
|
5181
|
+
{ label: "A/B Test", command: "/sellable-create-ab-test" },
|
|
5182
|
+
{ label: "Evergreen", command: "/sellable-create-evergreen-campaigns" },
|
|
5183
|
+
{ label: "Foundation", command: "/sellable-foundation" },
|
|
5184
|
+
{ label: "Content", command: "/sellable-content" },
|
|
5185
|
+
{ label: "Post", command: "/sellable-create-post" },
|
|
5186
|
+
{ label: "Refresh", command: "/sellable-refresh-sender-engagement" },
|
|
5187
|
+
{ label: "Refill", command: "/sellable-refill-sends" },
|
|
5188
|
+
]);
|
|
5189
|
+
console.log("");
|
|
5190
|
+
}
|
|
4147
5191
|
|
|
4148
5192
|
console.log("");
|
|
4149
5193
|
console.log(` ${"─".repeat(63)}`);
|
|
@@ -4173,7 +5217,7 @@ function removeTomlSection(content, header) {
|
|
|
4173
5217
|
return content.replace(pattern, "");
|
|
4174
5218
|
}
|
|
4175
5219
|
|
|
4176
|
-
function runUninstall() {
|
|
5220
|
+
function runUninstall(opts = { host: "all" }) {
|
|
4177
5221
|
console.log("");
|
|
4178
5222
|
console.log(` ${C.bold}Uninstalling Sellable…${C.reset}`);
|
|
4179
5223
|
console.log("");
|
|
@@ -4184,7 +5228,7 @@ function runUninstall() {
|
|
|
4184
5228
|
|
|
4185
5229
|
// 1) Surgical removal from Codex config.toml
|
|
4186
5230
|
const codexConfigPath = join(codexHome(), "config.toml");
|
|
4187
|
-
if (existsSync(codexConfigPath)) {
|
|
5231
|
+
if ((opts.host === "codex" || opts.host === "all") && existsSync(codexConfigPath)) {
|
|
4188
5232
|
try {
|
|
4189
5233
|
const before = readFileSync(codexConfigPath, "utf8");
|
|
4190
5234
|
const ts = new Date()
|
|
@@ -4221,10 +5265,11 @@ function runUninstall() {
|
|
|
4221
5265
|
` ${C.yellow}!${C.reset} Could not edit Codex config: ${err instanceof Error ? err.message : String(err)}`
|
|
4222
5266
|
);
|
|
4223
5267
|
}
|
|
4224
|
-
} else {
|
|
5268
|
+
} else if (opts.host === "codex" || opts.host === "all") {
|
|
4225
5269
|
skipped.push(`Codex config not found at ${codexConfigPath}`);
|
|
4226
5270
|
}
|
|
4227
5271
|
|
|
5272
|
+
if (opts.host === "codex" || opts.host === "all") {
|
|
4228
5273
|
for (const agent of [...codexCustomAgents(), ...legacyCodexCustomAgents()]) {
|
|
4229
5274
|
const agentPath = join(codexHome(), "agents", agent.filename);
|
|
4230
5275
|
if (!existsSync(agentPath)) continue;
|
|
@@ -4237,9 +5282,10 @@ function runUninstall() {
|
|
|
4237
5282
|
);
|
|
4238
5283
|
}
|
|
4239
5284
|
}
|
|
5285
|
+
}
|
|
4240
5286
|
|
|
4241
5287
|
// 2) Claude Code MCP removal
|
|
4242
|
-
if (commandExists("claude")) {
|
|
5288
|
+
if ((opts.host === "claude" || opts.host === "all") && commandExists("claude")) {
|
|
4243
5289
|
const r = spawnSync("claude", ["mcp", "remove", "sellable"], {
|
|
4244
5290
|
encoding: "utf8",
|
|
4245
5291
|
});
|
|
@@ -4248,10 +5294,11 @@ function runUninstall() {
|
|
|
4248
5294
|
} else {
|
|
4249
5295
|
skipped.push(`Claude Code MCP (none registered or already removed)`);
|
|
4250
5296
|
}
|
|
4251
|
-
} else {
|
|
5297
|
+
} else if (opts.host === "claude" || opts.host === "all") {
|
|
4252
5298
|
skipped.push(`Claude Code CLI not found`);
|
|
4253
5299
|
}
|
|
4254
5300
|
|
|
5301
|
+
if (opts.host === "claude" || opts.host === "all") {
|
|
4255
5302
|
for (const agent of [
|
|
4256
5303
|
...claudeCustomAgents(),
|
|
4257
5304
|
...legacyClaudeCustomAgents(),
|
|
@@ -4279,10 +5326,66 @@ function runUninstall() {
|
|
|
4279
5326
|
);
|
|
4280
5327
|
}
|
|
4281
5328
|
}
|
|
5329
|
+
}
|
|
5330
|
+
|
|
5331
|
+
// 3) Hermes config and skill removal
|
|
5332
|
+
if (opts.host === "hermes" || opts.host === "all") {
|
|
5333
|
+
const configPath = hermesConfigPath();
|
|
5334
|
+
if (existsSync(configPath)) {
|
|
5335
|
+
try {
|
|
5336
|
+
const before = readFileSync(configPath, "utf8");
|
|
5337
|
+
const ts = new Date()
|
|
5338
|
+
.toISOString()
|
|
5339
|
+
.replace(/[-:]/g, "")
|
|
5340
|
+
.replace(/\..+/, "");
|
|
5341
|
+
const backupPath = `${configPath}.bak-sellable-uninstall-${ts}`;
|
|
5342
|
+
writeFileSync(backupPath, before, { mode: 0o600 });
|
|
5343
|
+
const config = parseHermesConfig(before, configPath);
|
|
5344
|
+
if (
|
|
5345
|
+
config.mcp_servers &&
|
|
5346
|
+
typeof config.mcp_servers === "object" &&
|
|
5347
|
+
!Array.isArray(config.mcp_servers) &&
|
|
5348
|
+
"sellable" in config.mcp_servers
|
|
5349
|
+
) {
|
|
5350
|
+
const nextMcpServers = { ...config.mcp_servers };
|
|
5351
|
+
delete nextMcpServers.sellable;
|
|
5352
|
+
writeHermesConfig(
|
|
5353
|
+
configPath,
|
|
5354
|
+
{ ...config, mcp_servers: nextMcpServers },
|
|
5355
|
+
{ dryRun: false }
|
|
5356
|
+
);
|
|
5357
|
+
removed.push(`Hermes config (sellable removed in ${configPath})`);
|
|
5358
|
+
removed.push(` backup: ${backupPath}`);
|
|
5359
|
+
} else {
|
|
5360
|
+
skipped.push(`Hermes config had no sellable MCP server`);
|
|
5361
|
+
}
|
|
5362
|
+
} catch (err) {
|
|
5363
|
+
console.log(
|
|
5364
|
+
` ${C.yellow}!${C.reset} Could not edit Hermes config: ${err instanceof Error ? err.message : String(err)}`
|
|
5365
|
+
);
|
|
5366
|
+
}
|
|
5367
|
+
} else {
|
|
5368
|
+
skipped.push(`Hermes config not found at ${configPath}`);
|
|
5369
|
+
}
|
|
5370
|
+
|
|
5371
|
+
const skillsRoot = hermesSkillsRoot();
|
|
5372
|
+
if (existsSync(skillsRoot)) {
|
|
5373
|
+
try {
|
|
5374
|
+
rmSync(skillsRoot, { recursive: true, force: true });
|
|
5375
|
+
removed.push(`${skillsRoot}`);
|
|
5376
|
+
} catch (err) {
|
|
5377
|
+
console.log(
|
|
5378
|
+
` ${C.yellow}!${C.reset} Could not remove ${skillsRoot}: ${err instanceof Error ? err.message : String(err)}`
|
|
5379
|
+
);
|
|
5380
|
+
}
|
|
5381
|
+
} else {
|
|
5382
|
+
skipped.push(`Hermes Sellable skills not found at ${skillsRoot}`);
|
|
5383
|
+
}
|
|
5384
|
+
}
|
|
4282
5385
|
|
|
4283
|
-
//
|
|
5386
|
+
// 4) Surgical removal of Sellable-installed artifacts inside ~/.sellable/
|
|
4284
5387
|
const sellableDir = join(homedir(), ".sellable");
|
|
4285
|
-
if (existsSync(sellableDir)) {
|
|
5388
|
+
if (opts.host === "all" && existsSync(sellableDir)) {
|
|
4286
5389
|
const tracked = [
|
|
4287
5390
|
"config.json",
|
|
4288
5391
|
"update-check.json",
|
|
@@ -4321,13 +5424,13 @@ function runUninstall() {
|
|
|
4321
5424
|
// ignore
|
|
4322
5425
|
}
|
|
4323
5426
|
}
|
|
4324
|
-
} else {
|
|
5427
|
+
} else if (opts.host === "all") {
|
|
4325
5428
|
skipped.push(`~/.sellable not present`);
|
|
4326
5429
|
}
|
|
4327
5430
|
|
|
4328
|
-
//
|
|
5431
|
+
// 5) Self-shim
|
|
4329
5432
|
const shimPath = join(homedir(), ".local", "bin", "sellable");
|
|
4330
|
-
if (existsSync(shimPath)) {
|
|
5433
|
+
if (opts.host === "all" && existsSync(shimPath)) {
|
|
4331
5434
|
try {
|
|
4332
5435
|
rmSync(shimPath, { force: true });
|
|
4333
5436
|
removed.push(`${shimPath}`);
|
|
@@ -4375,6 +5478,7 @@ async function main() {
|
|
|
4375
5478
|
const authSetFlags = rawArgs.slice(3);
|
|
4376
5479
|
let dryRun = false;
|
|
4377
5480
|
let workspaceId = "";
|
|
5481
|
+
let sellableConfigPath = process.env.SELLABLE_CONFIG_PATH || "";
|
|
4378
5482
|
for (let i = 0; i < authSetFlags.length; i += 1) {
|
|
4379
5483
|
const flag = authSetFlags[i];
|
|
4380
5484
|
if (flag === "--dry-run") {
|
|
@@ -4391,6 +5495,16 @@ async function main() {
|
|
|
4391
5495
|
i += 1;
|
|
4392
5496
|
continue;
|
|
4393
5497
|
}
|
|
5498
|
+
if (flag === "--sellable-config-path") {
|
|
5499
|
+
const value = authSetFlags[i + 1];
|
|
5500
|
+
if (!value || value.startsWith("--")) {
|
|
5501
|
+
console.error("Missing value for --sellable-config-path");
|
|
5502
|
+
process.exit(2);
|
|
5503
|
+
}
|
|
5504
|
+
sellableConfigPath = value;
|
|
5505
|
+
i += 1;
|
|
5506
|
+
continue;
|
|
5507
|
+
}
|
|
4394
5508
|
console.error(`Unknown auth set option: ${flag}`);
|
|
4395
5509
|
process.exit(2);
|
|
4396
5510
|
}
|
|
@@ -4415,11 +5529,18 @@ async function main() {
|
|
|
4415
5529
|
// Skip installSelfShim() — by definition the user has the shim already
|
|
4416
5530
|
// (they invoked `sellable auth set` from it).
|
|
4417
5531
|
const apiUrl = process.env.SELLABLE_API_URL || DEFAULT_API_URL;
|
|
4418
|
-
writeAuthSetConfig({
|
|
5532
|
+
writeAuthSetConfig({
|
|
5533
|
+
token,
|
|
5534
|
+
workspaceId,
|
|
5535
|
+
apiUrl,
|
|
5536
|
+
dryRun,
|
|
5537
|
+
sellableConfigPath,
|
|
5538
|
+
});
|
|
5539
|
+
const configPath = authPath({ sellableConfigPath });
|
|
4419
5540
|
if (dryRun) {
|
|
4420
|
-
console.log(`Dry run: token would be saved to ${
|
|
5541
|
+
console.log(`Dry run: token would be saved to ${configPath}`);
|
|
4421
5542
|
} else {
|
|
4422
|
-
console.log(`✓ Token saved to ${
|
|
5543
|
+
console.log(`✓ Token saved to ${configPath}`);
|
|
4423
5544
|
}
|
|
4424
5545
|
console.log(` apiUrl: ${apiUrl}`);
|
|
4425
5546
|
if (workspaceId) {
|
|
@@ -4440,6 +5561,13 @@ async function main() {
|
|
|
4440
5561
|
console.log(` Codex: $sellable:create-post`);
|
|
4441
5562
|
console.log(` Codex: $sellable:refresh-sender-engagement`);
|
|
4442
5563
|
console.log(` Codex: $sellable:refill-sends`);
|
|
5564
|
+
console.log(` Hermes: /sellable-create-campaign`);
|
|
5565
|
+
console.log(` Hermes: /sellable-create-evergreen-campaigns`);
|
|
5566
|
+
console.log(` Hermes: /sellable-foundation`);
|
|
5567
|
+
console.log(` Hermes: /sellable-content`);
|
|
5568
|
+
console.log(` Hermes: /sellable-create-post`);
|
|
5569
|
+
console.log(` Hermes: /sellable-refresh-sender-engagement`);
|
|
5570
|
+
console.log(` Hermes: /sellable-refill-sends`);
|
|
4443
5571
|
process.exit(0);
|
|
4444
5572
|
}
|
|
4445
5573
|
if (rawArgs[0] === "prefs") {
|
|
@@ -4482,13 +5610,22 @@ async function main() {
|
|
|
4482
5610
|
process.exit(0);
|
|
4483
5611
|
}
|
|
4484
5612
|
if (rawArgs[0] === "uninstall") {
|
|
4485
|
-
|
|
5613
|
+
const opts = parseArgs(rawArgs.slice(1));
|
|
5614
|
+
runUninstall(opts);
|
|
4486
5615
|
process.exit(0);
|
|
4487
5616
|
}
|
|
4488
5617
|
if (rawArgs[0] === "create") {
|
|
4489
5618
|
printCreateCommandHint();
|
|
4490
5619
|
process.exit(0);
|
|
4491
5620
|
}
|
|
5621
|
+
if (
|
|
5622
|
+
rawArgs[0] === "hermes" &&
|
|
5623
|
+
rawArgs[1] === "profile" &&
|
|
5624
|
+
rawArgs[2] === "bootstrap"
|
|
5625
|
+
) {
|
|
5626
|
+
runHermesProfileBootstrap(rawArgs.slice(3));
|
|
5627
|
+
process.exit(0);
|
|
5628
|
+
}
|
|
4492
5629
|
const opts = parseArgs(process.argv.slice(2));
|
|
4493
5630
|
if (opts.help) {
|
|
4494
5631
|
console.log(usage());
|
|
@@ -4500,7 +5637,7 @@ async function main() {
|
|
|
4500
5637
|
if (!opts.verifyOnly) {
|
|
4501
5638
|
printBanner();
|
|
4502
5639
|
console.log(
|
|
4503
|
-
` ${C.bold}Connecting Sellable to Claude Code and
|
|
5640
|
+
` ${C.bold}Connecting Sellable to Claude Code, Codex, and Hermes…${C.reset}`
|
|
4504
5641
|
);
|
|
4505
5642
|
console.log("");
|
|
4506
5643
|
} else if (!opts.json) {
|
|
@@ -4536,6 +5673,15 @@ async function main() {
|
|
|
4536
5673
|
installedHosts.push("Codex");
|
|
4537
5674
|
}
|
|
4538
5675
|
}
|
|
5676
|
+
if (
|
|
5677
|
+
opts.host === "hermes" ||
|
|
5678
|
+
(opts.host === "all" && hermesLikelyInstalled())
|
|
5679
|
+
) {
|
|
5680
|
+
const result = installHermes(opts);
|
|
5681
|
+
if (result.installed) {
|
|
5682
|
+
installedHosts.push("Hermes");
|
|
5683
|
+
}
|
|
5684
|
+
}
|
|
4539
5685
|
}
|
|
4540
5686
|
|
|
4541
5687
|
if (opts.dryRun) {
|