@sellable/install 0.1.302 → 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 +75 -1
- package/bin/sellable-install.mjs +555 -48
- package/lib/runtime-verify.mjs +4 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ Windows users can use PowerShell or Windows Terminal:
|
|
|
16
16
|
iwr "https://app.sellable.dev/api/v2/cli/install.ps1" | iex
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
After install, restart
|
|
19
|
+
After install, restart Codex Desktop, Claude Code, or Hermes so Sellable MCP
|
|
20
20
|
tools and skills reload. In an active Hermes session, `/reload-mcp` and
|
|
21
21
|
`/reload-skills` can reload the new config.
|
|
22
22
|
|
|
@@ -83,6 +83,80 @@ Auth is stored once at:
|
|
|
83
83
|
~/.sellable/config.json
|
|
84
84
|
```
|
|
85
85
|
|
|
86
|
+
## Hermes Profile-Scoped Installs
|
|
87
|
+
|
|
88
|
+
For a single local Hermes profile, point Sellable auth and memory at the profile
|
|
89
|
+
instead of the global `~/.sellable/config.json`:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
profile="$HOME/.hermes/profiles/acme"
|
|
93
|
+
sellable --host hermes \
|
|
94
|
+
--sellable-config-path "$profile/sellable/config.json" \
|
|
95
|
+
--sellable-configs-dir "$profile/sellable/configs"
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
That writes `SELLABLE_CONFIG_PATH` and `SELLABLE_CONFIGS_DIR` into
|
|
99
|
+
`mcp_servers.sellable.env`, so first-run login, `get_auth_status`, workspace
|
|
100
|
+
switches, and Sellable memory all use the same profile-local files. Hosted MCP
|
|
101
|
+
mode does not receive local filesystem paths.
|
|
102
|
+
|
|
103
|
+
For zero-shot local or Hostinger/Linux VPS bootstrap, use the profile command:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
sellable hermes profile bootstrap \
|
|
107
|
+
--profile acme \
|
|
108
|
+
--profiles-root /srv/hermes/profiles \
|
|
109
|
+
--workspace-id ws_acme \
|
|
110
|
+
--workspace-name Acme \
|
|
111
|
+
--token-file /run/secrets/sellable-acme-token \
|
|
112
|
+
--json
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
It creates:
|
|
116
|
+
|
|
117
|
+
```text
|
|
118
|
+
/srv/hermes/profiles/acme/config.yaml
|
|
119
|
+
/srv/hermes/profiles/acme/sellable/config.json
|
|
120
|
+
/srv/hermes/profiles/acme/sellable/configs/
|
|
121
|
+
/srv/hermes/profiles/acme/skills/sellable/
|
|
122
|
+
/srv/hermes/profiles/acme/.env # only when Slack token inputs are supplied
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
If no token is supplied, bootstrap still creates a pending
|
|
126
|
+
`sellable/config.json` so the profile path is concrete from the first MCP
|
|
127
|
+
launch. Finish auth with:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
sellable auth set <token> --workspace-id <workspace_id> \
|
|
131
|
+
--sellable-config-path /srv/hermes/profiles/acme/sellable/config.json
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Slack Socket Mode tokens can be written profile-locally:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
sellable hermes profile bootstrap \
|
|
138
|
+
--profile acme \
|
|
139
|
+
--profiles-root /srv/hermes/profiles \
|
|
140
|
+
--workspace-id ws_acme \
|
|
141
|
+
--slack-bot-token "$SLACK_BOT_TOKEN" \
|
|
142
|
+
--slack-app-token "$SLACK_APP_TOKEN"
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Customer profiles should use customer-scoped Sellable credentials. A shared
|
|
146
|
+
Sellable admin token can work for internal admin automation, but it is weaker
|
|
147
|
+
isolation and should not be the default for customer profiles. After changing
|
|
148
|
+
Hermes profile config, restart the gateway/session or run `/reload-mcp` and
|
|
149
|
+
`/reload-skills`.
|
|
150
|
+
|
|
151
|
+
Validate the active profile with the same path inputs:
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
SELLABLE_CONFIG_PATH=/srv/hermes/profiles/acme/sellable/config.json \
|
|
155
|
+
SELLABLE_CONFIGS_DIR=/srv/hermes/profiles/acme/sellable/configs \
|
|
156
|
+
sellable --verify-only --host hermes --json \
|
|
157
|
+
--artifact /srv/hermes/profiles/acme/sellable/verify-runtime.json
|
|
158
|
+
```
|
|
159
|
+
|
|
86
160
|
Claude Code, Codex, and Hermes are configured to launch the same packaged MCP server. The
|
|
87
161
|
installer also writes Sellable agent definitions from the packaged `agents/`
|
|
88
162
|
registry, but normal create-campaign runs use only the Message Drafting
|
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,7 +13,7 @@ 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";
|
|
@@ -168,6 +170,8 @@ Commands:
|
|
|
168
170
|
Save the ask-first prompt-sharing preference.
|
|
169
171
|
setup claude-permissions --allow-common-setup
|
|
170
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.
|
|
171
175
|
uninstall Remove Sellable host config and installed artifacts.
|
|
172
176
|
|
|
173
177
|
Options:
|
|
@@ -176,6 +180,10 @@ Options:
|
|
|
176
180
|
--token <token> Sellable API token. Also reads SELLABLE_TOKEN.
|
|
177
181
|
--workspace-id <id> Sellable workspace id. Also reads SELLABLE_WORKSPACE_ID.
|
|
178
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.
|
|
179
187
|
--mcp-package <pkg> MCP server package. Default: ${DEFAULT_SERVER_PACKAGE}
|
|
180
188
|
--local-command <cmd> Local MCP command for --server local.
|
|
181
189
|
--hosted-url <url> Hosted MCP URL for --server hosted.
|
|
@@ -199,7 +207,19 @@ Auth:
|
|
|
199
207
|
|
|
200
208
|
For CI/scripted installs, pass --token + --workspace-id or set
|
|
201
209
|
SELLABLE_TOKEN + SELLABLE_WORKSPACE_ID and the installer will write the
|
|
202
|
-
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.
|
|
203
223
|
`;
|
|
204
224
|
}
|
|
205
225
|
|
|
@@ -271,6 +291,8 @@ function parseArgs(argv) {
|
|
|
271
291
|
token: process.env.SELLABLE_TOKEN || "",
|
|
272
292
|
workspaceId: process.env.SELLABLE_WORKSPACE_ID || "",
|
|
273
293
|
apiUrl: process.env.SELLABLE_API_URL || DEFAULT_API_URL,
|
|
294
|
+
sellableConfigPath: process.env.SELLABLE_CONFIG_PATH || "",
|
|
295
|
+
sellableConfigsDir: process.env.SELLABLE_CONFIGS_DIR || "",
|
|
274
296
|
mcpPackage: DEFAULT_SERVER_PACKAGE,
|
|
275
297
|
localCommand: process.env.SELLABLE_MCP_LOCAL_COMMAND || "",
|
|
276
298
|
hostedUrl: process.env.SELLABLE_MCP_HOSTED_URL || "",
|
|
@@ -305,6 +327,10 @@ function parseArgs(argv) {
|
|
|
305
327
|
opts.workspaceId = next();
|
|
306
328
|
} else if (arg === "--api-url") {
|
|
307
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();
|
|
308
334
|
} else if (arg === "--mcp-package") {
|
|
309
335
|
opts.mcpPackage = normalizeWindowsPackageSpec(next());
|
|
310
336
|
} else if (arg === "--local-command") {
|
|
@@ -336,6 +362,11 @@ function parseArgs(argv) {
|
|
|
336
362
|
if (!["package", "local", "hosted"].includes(opts.server)) {
|
|
337
363
|
throw new Error("--server must be package, local, or hosted");
|
|
338
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
|
+
}
|
|
339
370
|
|
|
340
371
|
return opts;
|
|
341
372
|
}
|
|
@@ -593,8 +624,17 @@ function getMcpVersion() {
|
|
|
593
624
|
return "latest";
|
|
594
625
|
}
|
|
595
626
|
|
|
596
|
-
function
|
|
597
|
-
|
|
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
|
+
);
|
|
598
638
|
}
|
|
599
639
|
|
|
600
640
|
function normalizeStoredAuth(raw) {
|
|
@@ -615,15 +655,15 @@ function normalizeStoredAuth(raw) {
|
|
|
615
655
|
};
|
|
616
656
|
}
|
|
617
657
|
|
|
618
|
-
function readStoredAuth() {
|
|
619
|
-
const raw = readExisting(authPath());
|
|
658
|
+
function readStoredAuth(opts = {}) {
|
|
659
|
+
const raw = readExisting(authPath(opts));
|
|
620
660
|
return raw ? normalizeStoredAuth(raw) : null;
|
|
621
661
|
}
|
|
622
662
|
|
|
623
663
|
async function loadAuthIfPresent(opts) {
|
|
624
664
|
if (opts.token && opts.workspaceId) return opts;
|
|
625
665
|
|
|
626
|
-
const stored = readStoredAuth();
|
|
666
|
+
const stored = readStoredAuth(opts);
|
|
627
667
|
if (stored?.token && stored?.workspaceId) {
|
|
628
668
|
opts.token ||= stored.token;
|
|
629
669
|
opts.workspaceId ||= stored.workspaceId;
|
|
@@ -687,7 +727,7 @@ function mergeAuthConfig(raw, auth) {
|
|
|
687
727
|
Array.isArray(envConfig)
|
|
688
728
|
) {
|
|
689
729
|
throw new Error(
|
|
690
|
-
`Unknown active environment '${activeEnv}' in
|
|
730
|
+
`Unknown active environment '${activeEnv}' in configured Sellable auth path`
|
|
691
731
|
);
|
|
692
732
|
}
|
|
693
733
|
return {
|
|
@@ -708,8 +748,14 @@ function mergeAuthConfig(raw, auth) {
|
|
|
708
748
|
};
|
|
709
749
|
}
|
|
710
750
|
|
|
711
|
-
function writeAuthSetConfig({
|
|
712
|
-
|
|
751
|
+
function writeAuthSetConfig({
|
|
752
|
+
token,
|
|
753
|
+
workspaceId,
|
|
754
|
+
apiUrl,
|
|
755
|
+
dryRun,
|
|
756
|
+
sellableConfigPath,
|
|
757
|
+
}) {
|
|
758
|
+
const configPath = authPath({ sellableConfigPath });
|
|
713
759
|
const raw = readExisting(configPath) || {};
|
|
714
760
|
const config = mergeAuthConfig(raw, {
|
|
715
761
|
token,
|
|
@@ -860,6 +906,7 @@ function writeFile(path, content, opts, mode = 0o644) {
|
|
|
860
906
|
if (opts.dryRun) return;
|
|
861
907
|
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
862
908
|
writeFileSync(path, content, { mode });
|
|
909
|
+
chmodSync(path, mode);
|
|
863
910
|
}
|
|
864
911
|
|
|
865
912
|
function ensureSymlink(target, linkPath, opts) {
|
|
@@ -952,9 +999,7 @@ function codexPluginMcp(opts) {
|
|
|
952
999
|
type: "stdio",
|
|
953
1000
|
command,
|
|
954
1001
|
args,
|
|
955
|
-
env:
|
|
956
|
-
SELLABLE_WATCH_MODE_DRIVER: "codex",
|
|
957
|
-
},
|
|
1002
|
+
env: mcpEnvForHost("codex", opts),
|
|
958
1003
|
},
|
|
959
1004
|
},
|
|
960
1005
|
};
|
|
@@ -967,9 +1012,7 @@ function codexPluginMcp(opts) {
|
|
|
967
1012
|
type: "stdio",
|
|
968
1013
|
command,
|
|
969
1014
|
args,
|
|
970
|
-
env:
|
|
971
|
-
SELLABLE_WATCH_MODE_DRIVER: "codex",
|
|
972
|
-
},
|
|
1015
|
+
env: mcpEnvForHost("codex", opts),
|
|
973
1016
|
},
|
|
974
1017
|
},
|
|
975
1018
|
};
|
|
@@ -3385,7 +3428,7 @@ function writeAuth(opts) {
|
|
|
3385
3428
|
activeWorkspaceId: opts.workspaceId,
|
|
3386
3429
|
apiUrl: opts.apiUrl,
|
|
3387
3430
|
};
|
|
3388
|
-
writeJson(authPath(), config, opts);
|
|
3431
|
+
writeJson(authPath(opts), config, opts);
|
|
3389
3432
|
return { written: true, reused: false };
|
|
3390
3433
|
}
|
|
3391
3434
|
|
|
@@ -3445,6 +3488,40 @@ const WATCH_MODE_DRIVER_ENV = {
|
|
|
3445
3488
|
hermes: "SELLABLE_WATCH_MODE_DRIVER=hermes",
|
|
3446
3489
|
};
|
|
3447
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
|
+
|
|
3448
3525
|
function withHostedWatchModeDriver(rawUrl, driver) {
|
|
3449
3526
|
if (!WATCH_MODE_DRIVER_ENV[driver]) {
|
|
3450
3527
|
throw new Error(`Unknown watch mode driver: ${driver}`);
|
|
@@ -3498,8 +3575,7 @@ function codexMcpAddArgs(opts) {
|
|
|
3498
3575
|
"mcp",
|
|
3499
3576
|
"add",
|
|
3500
3577
|
"sellable",
|
|
3501
|
-
"--env",
|
|
3502
|
-
WATCH_MODE_DRIVER_ENV.codex,
|
|
3578
|
+
...mcpEnvVarArgs("codex", opts).flatMap((entry) => ["--env", entry]),
|
|
3503
3579
|
"--",
|
|
3504
3580
|
command,
|
|
3505
3581
|
...args,
|
|
@@ -3530,7 +3606,7 @@ args = ${tomlArray(args)}`
|
|
|
3530
3606
|
content,
|
|
3531
3607
|
"mcp_servers.sellable.env",
|
|
3532
3608
|
`[mcp_servers.sellable.env]
|
|
3533
|
-
|
|
3609
|
+
${tomlEnvTable("codex", opts)}`
|
|
3534
3610
|
);
|
|
3535
3611
|
return content;
|
|
3536
3612
|
}
|
|
@@ -3553,7 +3629,9 @@ function codexMcpServerMatches(content, opts) {
|
|
|
3553
3629
|
return (
|
|
3554
3630
|
server.includes(`command = ${quoteToml(command)}`) &&
|
|
3555
3631
|
server.includes(`args = ${tomlArray(args)}`) &&
|
|
3556
|
-
|
|
3632
|
+
Object.entries(mcpEnvForHost("codex", opts)).every(([key, value]) =>
|
|
3633
|
+
env.includes(`${key} = ${quoteToml(value)}`)
|
|
3634
|
+
)
|
|
3557
3635
|
);
|
|
3558
3636
|
}
|
|
3559
3637
|
|
|
@@ -3568,16 +3646,20 @@ function codexPluginMcpServerMatches(content, opts) {
|
|
|
3568
3646
|
}
|
|
3569
3647
|
}
|
|
3570
3648
|
|
|
3571
|
-
function hermesHome() {
|
|
3572
|
-
return
|
|
3649
|
+
function hermesHome(opts = {}) {
|
|
3650
|
+
return (
|
|
3651
|
+
opts.hermesHome ||
|
|
3652
|
+
process.env.HERMES_HOME?.trim() ||
|
|
3653
|
+
join(homedir(), ".hermes")
|
|
3654
|
+
);
|
|
3573
3655
|
}
|
|
3574
3656
|
|
|
3575
|
-
function hermesConfigPath() {
|
|
3576
|
-
return join(hermesHome(), "config.yaml");
|
|
3657
|
+
function hermesConfigPath(opts = {}) {
|
|
3658
|
+
return join(hermesHome(opts), "config.yaml");
|
|
3577
3659
|
}
|
|
3578
3660
|
|
|
3579
|
-
function hermesSkillsRoot() {
|
|
3580
|
-
return join(hermesHome(), "skills", "sellable");
|
|
3661
|
+
function hermesSkillsRoot(opts = {}) {
|
|
3662
|
+
return join(hermesHome(opts), "skills", "sellable");
|
|
3581
3663
|
}
|
|
3582
3664
|
|
|
3583
3665
|
function hermesLikelyInstalled() {
|
|
@@ -3606,8 +3688,8 @@ function parseHermesConfig(raw, configPath = hermesConfigPath()) {
|
|
|
3606
3688
|
return value;
|
|
3607
3689
|
}
|
|
3608
3690
|
|
|
3609
|
-
function readHermesConfig() {
|
|
3610
|
-
const configPath = hermesConfigPath();
|
|
3691
|
+
function readHermesConfig(opts = {}) {
|
|
3692
|
+
const configPath = hermesConfigPath(opts);
|
|
3611
3693
|
const raw = existsSync(configPath) ? readFileSync(configPath, "utf8") : "";
|
|
3612
3694
|
return { raw, config: parseHermesConfig(raw, configPath), configPath };
|
|
3613
3695
|
}
|
|
@@ -3628,9 +3710,7 @@ function hermesMcpServer(opts) {
|
|
|
3628
3710
|
command,
|
|
3629
3711
|
args,
|
|
3630
3712
|
enabled: true,
|
|
3631
|
-
env:
|
|
3632
|
-
SELLABLE_WATCH_MODE_DRIVER: "hermes",
|
|
3633
|
-
},
|
|
3713
|
+
env: mcpEnvForHost("hermes", opts),
|
|
3634
3714
|
};
|
|
3635
3715
|
}
|
|
3636
3716
|
|
|
@@ -3643,7 +3723,7 @@ function writeHermesConfig(configPath, config, opts) {
|
|
|
3643
3723
|
}
|
|
3644
3724
|
|
|
3645
3725
|
function writeHermesMcpServer(opts) {
|
|
3646
|
-
const { config, configPath } = readHermesConfig();
|
|
3726
|
+
const { config, configPath } = readHermesConfig(opts);
|
|
3647
3727
|
const existingMcpServers =
|
|
3648
3728
|
config.mcp_servers &&
|
|
3649
3729
|
typeof config.mcp_servers === "object" &&
|
|
@@ -3662,7 +3742,7 @@ function writeHermesMcpServer(opts) {
|
|
|
3662
3742
|
}
|
|
3663
3743
|
|
|
3664
3744
|
function installHermesSkills(opts) {
|
|
3665
|
-
const root = hermesSkillsRoot();
|
|
3745
|
+
const root = hermesSkillsRoot(opts);
|
|
3666
3746
|
for (const skill of hermesSkillDefinitions()) {
|
|
3667
3747
|
const skillRoot = join(root, skill.dir);
|
|
3668
3748
|
writeFile(join(skillRoot, "SKILL.md"), skill.skillMd, opts);
|
|
@@ -3676,7 +3756,7 @@ function installHermesSkills(opts) {
|
|
|
3676
3756
|
function installHermes(opts) {
|
|
3677
3757
|
// Parse config before writing skills so invalid user YAML cannot leave a
|
|
3678
3758
|
// half-installed Hermes skill tree behind.
|
|
3679
|
-
readHermesConfig();
|
|
3759
|
+
readHermesConfig(opts);
|
|
3680
3760
|
let skillsRoot = null;
|
|
3681
3761
|
try {
|
|
3682
3762
|
skillsRoot = installHermesSkills(opts);
|
|
@@ -3691,6 +3771,403 @@ function installHermes(opts) {
|
|
|
3691
3771
|
}
|
|
3692
3772
|
}
|
|
3693
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
|
+
|
|
3694
4171
|
function hermesMcpServerMatches(config, opts) {
|
|
3695
4172
|
const server = config?.mcp_servers?.sellable;
|
|
3696
4173
|
if (!server || typeof server !== "object") return false;
|
|
@@ -3810,7 +4287,7 @@ function canonicalClaudeMcpServer(opts) {
|
|
|
3810
4287
|
type: "stdio",
|
|
3811
4288
|
command,
|
|
3812
4289
|
args,
|
|
3813
|
-
env:
|
|
4290
|
+
env: mcpEnvForHost("claude", opts),
|
|
3814
4291
|
alwaysLoad: true,
|
|
3815
4292
|
};
|
|
3816
4293
|
}
|
|
@@ -3839,7 +4316,7 @@ function claudeMcpServerMatches(server, canonical) {
|
|
|
3839
4316
|
server.type === canonical.type &&
|
|
3840
4317
|
server.command === canonical.command &&
|
|
3841
4318
|
JSON.stringify(server.args || []) === JSON.stringify(canonical.args) &&
|
|
3842
|
-
server.env
|
|
4319
|
+
JSON.stringify(server.env || {}) === JSON.stringify(canonical.env)
|
|
3843
4320
|
);
|
|
3844
4321
|
}
|
|
3845
4322
|
|
|
@@ -3898,12 +4375,10 @@ function patchClaudeAlwaysLoad(opts) {
|
|
|
3898
4375
|
server.args = [...canonical.args];
|
|
3899
4376
|
touched = true;
|
|
3900
4377
|
}
|
|
3901
|
-
const
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
};
|
|
3906
|
-
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)) {
|
|
3907
4382
|
touched = true;
|
|
3908
4383
|
}
|
|
3909
4384
|
}
|
|
@@ -4021,7 +4496,7 @@ function firstRunCommandForHost(host) {
|
|
|
4021
4496
|
|
|
4022
4497
|
async function verify(opts) {
|
|
4023
4498
|
const startedAt = new Date().toISOString();
|
|
4024
|
-
const stored = readStoredAuth();
|
|
4499
|
+
const stored = readStoredAuth(opts);
|
|
4025
4500
|
const checks = [];
|
|
4026
4501
|
const hasWatchModeDriverMarker = (content, driver) =>
|
|
4027
4502
|
new RegExp(`SELLABLE_WATCH_MODE_DRIVER[\\s\\S]{0,80}${driver}`).test(
|
|
@@ -4029,7 +4504,7 @@ async function verify(opts) {
|
|
|
4029
4504
|
) || new RegExp(`[?&]driver=${driver}(?:\\b|&)`).test(content);
|
|
4030
4505
|
|
|
4031
4506
|
if (stored?.token && stored?.workspaceId) {
|
|
4032
|
-
checks.push(warningCheck(true, `Auth config: ${authPath()}`));
|
|
4507
|
+
checks.push(warningCheck(true, `Auth config: ${authPath(opts)}`));
|
|
4033
4508
|
} else {
|
|
4034
4509
|
checks.push(
|
|
4035
4510
|
warningCheck(
|
|
@@ -4413,6 +4888,7 @@ async function verify(opts) {
|
|
|
4413
4888
|
cwd: process.cwd(),
|
|
4414
4889
|
env: {
|
|
4415
4890
|
...process.env,
|
|
4891
|
+
...runtimeMcpEnvForVerify(opts),
|
|
4416
4892
|
SELLABLE_API_URL: opts.apiUrl,
|
|
4417
4893
|
SELLABLE_TOKEN: opts.token || process.env.SELLABLE_TOKEN || "",
|
|
4418
4894
|
SELLABLE_WORKSPACE_ID:
|
|
@@ -4468,6 +4944,11 @@ async function verify(opts) {
|
|
|
4468
4944
|
install: INSTALL_PACKAGE_SPEC,
|
|
4469
4945
|
mcp: opts.mcpPackage,
|
|
4470
4946
|
},
|
|
4947
|
+
sellableConfig: {
|
|
4948
|
+
path: authPath(opts),
|
|
4949
|
+
explicitPath: opts.sellableConfigPath || null,
|
|
4950
|
+
configsDir: opts.sellableConfigsDir || null,
|
|
4951
|
+
},
|
|
4471
4952
|
checks,
|
|
4472
4953
|
runtimeMcp,
|
|
4473
4954
|
threadExposureNotice:
|
|
@@ -4996,6 +5477,7 @@ async function main() {
|
|
|
4996
5477
|
const authSetFlags = rawArgs.slice(3);
|
|
4997
5478
|
let dryRun = false;
|
|
4998
5479
|
let workspaceId = "";
|
|
5480
|
+
let sellableConfigPath = process.env.SELLABLE_CONFIG_PATH || "";
|
|
4999
5481
|
for (let i = 0; i < authSetFlags.length; i += 1) {
|
|
5000
5482
|
const flag = authSetFlags[i];
|
|
5001
5483
|
if (flag === "--dry-run") {
|
|
@@ -5012,6 +5494,16 @@ async function main() {
|
|
|
5012
5494
|
i += 1;
|
|
5013
5495
|
continue;
|
|
5014
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
|
+
}
|
|
5015
5507
|
console.error(`Unknown auth set option: ${flag}`);
|
|
5016
5508
|
process.exit(2);
|
|
5017
5509
|
}
|
|
@@ -5036,11 +5528,18 @@ async function main() {
|
|
|
5036
5528
|
// Skip installSelfShim() — by definition the user has the shim already
|
|
5037
5529
|
// (they invoked `sellable auth set` from it).
|
|
5038
5530
|
const apiUrl = process.env.SELLABLE_API_URL || DEFAULT_API_URL;
|
|
5039
|
-
writeAuthSetConfig({
|
|
5531
|
+
writeAuthSetConfig({
|
|
5532
|
+
token,
|
|
5533
|
+
workspaceId,
|
|
5534
|
+
apiUrl,
|
|
5535
|
+
dryRun,
|
|
5536
|
+
sellableConfigPath,
|
|
5537
|
+
});
|
|
5538
|
+
const configPath = authPath({ sellableConfigPath });
|
|
5040
5539
|
if (dryRun) {
|
|
5041
|
-
console.log(`Dry run: token would be saved to ${
|
|
5540
|
+
console.log(`Dry run: token would be saved to ${configPath}`);
|
|
5042
5541
|
} else {
|
|
5043
|
-
console.log(`✓ Token saved to ${
|
|
5542
|
+
console.log(`✓ Token saved to ${configPath}`);
|
|
5044
5543
|
}
|
|
5045
5544
|
console.log(` apiUrl: ${apiUrl}`);
|
|
5046
5545
|
if (workspaceId) {
|
|
@@ -5118,6 +5617,14 @@ async function main() {
|
|
|
5118
5617
|
printCreateCommandHint();
|
|
5119
5618
|
process.exit(0);
|
|
5120
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
|
+
}
|
|
5121
5628
|
const opts = parseArgs(process.argv.slice(2));
|
|
5122
5629
|
if (opts.help) {
|
|
5123
5630
|
console.log(usage());
|
package/lib/runtime-verify.mjs
CHANGED
|
@@ -188,6 +188,10 @@ function summarizeToolResult(name, result) {
|
|
|
188
188
|
return {
|
|
189
189
|
...summary,
|
|
190
190
|
authOk: parsed.ok === true,
|
|
191
|
+
configPath: parsed.configPath || null,
|
|
192
|
+
configExists: parsed.configExists === true,
|
|
193
|
+
configsDir: parsed.configsDir || null,
|
|
194
|
+
tokenPresent: parsed.tokenPresent === true,
|
|
191
195
|
errorType: parsed.error?.type || null,
|
|
192
196
|
activeWorkspaceId: parsed.activeWorkspaceId || null,
|
|
193
197
|
activeWorkspaceName: parsed.activeWorkspaceName || null,
|