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