@sellable/install 0.1.207 → 0.1.209
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/bin/sellable-install.mjs +369 -30
- package/lib/runtime-verify.mjs +242 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -55,11 +55,13 @@ Agents should use the agent-readable handoff instead of guessing commands:
|
|
|
55
55
|
Install Sellable CLI and skills using https://app.sellable.dev/agent-install.txt
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
-
The direct npm installer
|
|
59
|
-
|
|
58
|
+
The direct npm installer is only a package-level troubleshooting fallback for
|
|
59
|
+
maintainers. Public installs should keep using the curl endpoint above because
|
|
60
|
+
it bootstraps Node, repairs legacy state, installs skills, and runs runtime
|
|
61
|
+
verification in one path:
|
|
60
62
|
|
|
61
63
|
```bash
|
|
62
|
-
|
|
64
|
+
curl -fsSL "https://app.sellable.dev/api/v2/cli/install" | sh
|
|
63
65
|
```
|
|
64
66
|
|
|
65
67
|
For CI/scripted installs, get a Sellable API token from:
|
package/bin/sellable-install.mjs
CHANGED
|
@@ -136,13 +136,22 @@ function usage() {
|
|
|
136
136
|
return `Sellable agent installer
|
|
137
137
|
|
|
138
138
|
Usage:
|
|
139
|
+
curl -fsSL "https://app.sellable.dev/api/v2/cli/install" | sh
|
|
139
140
|
sellable create
|
|
141
|
+
sellable prefs set prompt-sharing true|false
|
|
142
|
+
sellable setup claude-permissions --allow-common-setup
|
|
140
143
|
sellable-install [options]
|
|
144
|
+
|
|
145
|
+
Advanced fallback:
|
|
141
146
|
npx -y @sellable/install@latest -- [options]
|
|
142
147
|
|
|
143
148
|
Commands:
|
|
144
149
|
create Show how to launch public Sellable workflows.
|
|
145
150
|
auth set <token> Save a Sellable API token for first-run auth.
|
|
151
|
+
prefs set prompt-sharing true|false
|
|
152
|
+
Save the ask-first prompt-sharing preference.
|
|
153
|
+
setup claude-permissions --allow-common-setup
|
|
154
|
+
Add common Sellable Bash permissions to Claude settings.
|
|
146
155
|
uninstall Remove Sellable host config and installed artifacts.
|
|
147
156
|
|
|
148
157
|
Options:
|
|
@@ -427,6 +436,83 @@ function readExisting(path) {
|
|
|
427
436
|
}
|
|
428
437
|
}
|
|
429
438
|
|
|
439
|
+
function sellableHostEnvPath() {
|
|
440
|
+
return join(homedir(), ".local", "sellable", "app-sellable-dev", ".env");
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function writePromptSharingPreference(value, opts = { dryRun: false }) {
|
|
444
|
+
const normalized = String(value || "").trim().toLowerCase();
|
|
445
|
+
if (normalized !== "true" && normalized !== "false") {
|
|
446
|
+
throw new Error("prompt-sharing must be true or false");
|
|
447
|
+
}
|
|
448
|
+
const envPath = sellableHostEnvPath();
|
|
449
|
+
const key = "SELLABLE_SHARE_SESSION_USER_PROMPTS";
|
|
450
|
+
const line = `${key}=${normalized}`;
|
|
451
|
+
const existing = existsSync(envPath) ? readFileSync(envPath, "utf8") : "";
|
|
452
|
+
const lines = existing
|
|
453
|
+
.split(/\r?\n/)
|
|
454
|
+
.filter((existingLine) => existingLine.trim() !== "");
|
|
455
|
+
let replaced = false;
|
|
456
|
+
const nextLines = lines.map((existingLine) => {
|
|
457
|
+
if (existingLine.startsWith(`${key}=`)) {
|
|
458
|
+
replaced = true;
|
|
459
|
+
return line;
|
|
460
|
+
}
|
|
461
|
+
return existingLine;
|
|
462
|
+
});
|
|
463
|
+
if (!replaced) nextLines.push(line);
|
|
464
|
+
|
|
465
|
+
if (!opts.dryRun) {
|
|
466
|
+
mkdirSync(dirname(envPath), { recursive: true, mode: 0o700 });
|
|
467
|
+
writeFileSync(envPath, `${nextLines.join("\n")}\n`, { mode: 0o600 });
|
|
468
|
+
}
|
|
469
|
+
return envPath;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const CLAUDE_COMMON_SETUP_ALLOW = [
|
|
473
|
+
"Bash(sellable *)",
|
|
474
|
+
"Bash(export PATH=$HOME/.local/bin:$PATH && sellable *)",
|
|
475
|
+
];
|
|
476
|
+
|
|
477
|
+
function claudeSettingsPath() {
|
|
478
|
+
return join(homedir(), ".claude", "settings.json");
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function allowClaudeCommonSetup(opts = { dryRun: false }) {
|
|
482
|
+
const settingsPath = claudeSettingsPath();
|
|
483
|
+
let settings = {};
|
|
484
|
+
if (existsSync(settingsPath)) {
|
|
485
|
+
try {
|
|
486
|
+
settings = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
487
|
+
} catch (err) {
|
|
488
|
+
throw new Error(
|
|
489
|
+
`Could not parse ${settingsPath}: ${
|
|
490
|
+
err instanceof Error ? err.message : String(err)
|
|
491
|
+
}`
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
if (!settings || typeof settings !== "object" || Array.isArray(settings)) {
|
|
496
|
+
settings = {};
|
|
497
|
+
}
|
|
498
|
+
const permissions =
|
|
499
|
+
settings.permissions &&
|
|
500
|
+
typeof settings.permissions === "object" &&
|
|
501
|
+
!Array.isArray(settings.permissions)
|
|
502
|
+
? settings.permissions
|
|
503
|
+
: {};
|
|
504
|
+
const allow = Array.isArray(permissions.allow) ? permissions.allow : [];
|
|
505
|
+
const nextAllow = [...allow];
|
|
506
|
+
for (const entry of CLAUDE_COMMON_SETUP_ALLOW) {
|
|
507
|
+
if (!nextAllow.includes(entry)) nextAllow.push(entry);
|
|
508
|
+
}
|
|
509
|
+
settings.permissions = { ...permissions, allow: nextAllow };
|
|
510
|
+
if (!opts.dryRun) {
|
|
511
|
+
writeJson(settingsPath, settings, opts);
|
|
512
|
+
}
|
|
513
|
+
return settingsPath;
|
|
514
|
+
}
|
|
515
|
+
|
|
430
516
|
function codexHome() {
|
|
431
517
|
return process.env.CODEX_HOME?.trim() || join(homedir(), ".codex");
|
|
432
518
|
}
|
|
@@ -1315,7 +1401,7 @@ local harness scripts, or read local prompt files to emulate this workflow.
|
|
|
1315
1401
|
|
|
1316
1402
|
If the Sellable MCP tool is unavailable, stop and say this is a Codex
|
|
1317
1403
|
install/reload problem. Tell the user to run
|
|
1318
|
-
\`
|
|
1404
|
+
\`curl -fsSL "https://app.sellable.dev/api/v2/cli/install" | sh\`, fully quit and reopen Codex
|
|
1319
1405
|
Desktop, then start a new thread.
|
|
1320
1406
|
|
|
1321
1407
|
1. Call \`mcp__sellable__get_auth_status({})\`.
|
|
@@ -1363,7 +1449,7 @@ emulate this workflow.
|
|
|
1363
1449
|
|
|
1364
1450
|
If the Sellable MCP prompt tools are unavailable, stop and say this is a Codex
|
|
1365
1451
|
install/reload problem. Tell the user to run
|
|
1366
|
-
\`
|
|
1452
|
+
\`curl -fsSL "https://app.sellable.dev/api/v2/cli/install" | sh\`, fully quit and reopen Codex
|
|
1367
1453
|
Desktop, then start a new thread.
|
|
1368
1454
|
|
|
1369
1455
|
## Execute Workflow
|
|
@@ -1440,7 +1526,7 @@ emulate this workflow.
|
|
|
1440
1526
|
|
|
1441
1527
|
If the Sellable MCP prompt tools are unavailable, stop and say this is a Codex
|
|
1442
1528
|
install/reload problem. Tell the user to run
|
|
1443
|
-
\`
|
|
1529
|
+
\`curl -fsSL "https://app.sellable.dev/api/v2/cli/install" | sh\`, fully quit and reopen Codex
|
|
1444
1530
|
Desktop, then start a new thread.
|
|
1445
1531
|
|
|
1446
1532
|
## Execute Workflow
|
|
@@ -1512,7 +1598,7 @@ emulate this workflow.
|
|
|
1512
1598
|
|
|
1513
1599
|
If the Sellable MCP prompt tools are unavailable, stop and say this is a Codex
|
|
1514
1600
|
install/reload problem. Tell the user to run
|
|
1515
|
-
\`
|
|
1601
|
+
\`curl -fsSL "https://app.sellable.dev/api/v2/cli/install" | sh\`, fully quit and reopen Codex
|
|
1516
1602
|
Desktop, then start a new thread.
|
|
1517
1603
|
|
|
1518
1604
|
## Execute Workflow
|
|
@@ -1868,12 +1954,7 @@ function legacyClaudeCustomAgents() {
|
|
|
1868
1954
|
}
|
|
1869
1955
|
|
|
1870
1956
|
function legacyClaudeCommands() {
|
|
1871
|
-
return [
|
|
1872
|
-
{
|
|
1873
|
-
name: "sellable-create-campaign",
|
|
1874
|
-
filename: join("sellable", "create-campaign.md"),
|
|
1875
|
-
},
|
|
1876
|
-
];
|
|
1957
|
+
return [];
|
|
1877
1958
|
}
|
|
1878
1959
|
|
|
1879
1960
|
function tomlArray(values) {
|
|
@@ -1930,6 +2011,72 @@ function claudeCustomAgents() {
|
|
|
1930
2011
|
}));
|
|
1931
2012
|
}
|
|
1932
2013
|
|
|
2014
|
+
function stripFrontmatter(markdown) {
|
|
2015
|
+
return String(markdown).replace(/^---\n[\s\S]*?\n---\n?/, "").trimStart();
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2018
|
+
function claudeCommandMd(command) {
|
|
2019
|
+
const allowedTools =
|
|
2020
|
+
command.allowedTools && command.allowedTools.length > 0
|
|
2021
|
+
? `allowed-tools:\n${allowedToolsYaml(command.allowedTools)}\n`
|
|
2022
|
+
: "";
|
|
2023
|
+
return `---
|
|
2024
|
+
name: sellable:${command.name}
|
|
2025
|
+
description: ${yamlString(command.description)}
|
|
2026
|
+
argument-hint: ${yamlString(command.argumentHint || "[instructions]")}
|
|
2027
|
+
${allowedTools}---
|
|
2028
|
+
|
|
2029
|
+
# Sellable ${command.title}
|
|
2030
|
+
|
|
2031
|
+
This Claude Code command is the customer-facing entrypoint for
|
|
2032
|
+
\`/sellable:${command.name}\`.
|
|
2033
|
+
|
|
2034
|
+
${stripFrontmatter(command.skillMd)}
|
|
2035
|
+
`;
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
function claudeCommands() {
|
|
2039
|
+
return [
|
|
2040
|
+
{
|
|
2041
|
+
name: "create-campaign",
|
|
2042
|
+
title: "Create Campaign",
|
|
2043
|
+
filename: join("sellable", "create-campaign.md"),
|
|
2044
|
+
description: "Create a Sellable campaign through the approval-gated workflow.",
|
|
2045
|
+
argumentHint: "[campaign goal, company, or offer]",
|
|
2046
|
+
skillMd: createCampaignSkillMd(),
|
|
2047
|
+
allowedTools: ["AskUserQuestion", "Task", ...CREATE_CAMPAIGN_ALLOWED_TOOLS],
|
|
2048
|
+
},
|
|
2049
|
+
{
|
|
2050
|
+
name: "foundation",
|
|
2051
|
+
title: "Foundation",
|
|
2052
|
+
filename: join("sellable", "foundation.md"),
|
|
2053
|
+
description: FOUNDATION_SKILL_DESCRIPTION,
|
|
2054
|
+
argumentHint: "[founder/company context]",
|
|
2055
|
+
skillMd: foundationSkillMd(),
|
|
2056
|
+
},
|
|
2057
|
+
{
|
|
2058
|
+
name: "content",
|
|
2059
|
+
title: "Content",
|
|
2060
|
+
filename: join("sellable", "content.md"),
|
|
2061
|
+
description:
|
|
2062
|
+
"Add transcripts and rough ideas, cluster themes, and hand post requests to create-post.",
|
|
2063
|
+
argumentHint: "[idea, transcript, or content goal]",
|
|
2064
|
+
skillMd: contentSkillMd(),
|
|
2065
|
+
},
|
|
2066
|
+
{
|
|
2067
|
+
name: "create-post",
|
|
2068
|
+
title: "Create Post",
|
|
2069
|
+
filename: join("sellable", "create-post.md"),
|
|
2070
|
+
description: "Capture ideas and draft LinkedIn posts in the user's voice.",
|
|
2071
|
+
argumentHint: "[post idea or source material]",
|
|
2072
|
+
skillMd: createPostSkillMd(),
|
|
2073
|
+
},
|
|
2074
|
+
].map((command) => ({
|
|
2075
|
+
...command,
|
|
2076
|
+
content: claudeCommandMd(command),
|
|
2077
|
+
}));
|
|
2078
|
+
}
|
|
2079
|
+
|
|
1933
2080
|
function writeCodexPluginSkills(pluginRoot, opts) {
|
|
1934
2081
|
const skills = codexPluginSkills();
|
|
1935
2082
|
const currentSkillDirs = new Set(skills.map((skill) => skill.dir));
|
|
@@ -1982,6 +2129,7 @@ function writeClaudeCustomAgents(opts) {
|
|
|
1982
2129
|
for (const agent of claudeCustomAgents()) {
|
|
1983
2130
|
writeFile(join(home, "agents", agent.filename), agent.content, opts);
|
|
1984
2131
|
}
|
|
2132
|
+
writeClaudeCommands(opts);
|
|
1985
2133
|
for (const agent of legacyClaudeCustomAgents()) {
|
|
1986
2134
|
const legacyPath = join(home, "agents", agent.filename);
|
|
1987
2135
|
logVerbose(
|
|
@@ -1992,6 +2140,13 @@ function writeClaudeCustomAgents(opts) {
|
|
|
1992
2140
|
removeLegacyClaudeCommands(home, opts);
|
|
1993
2141
|
}
|
|
1994
2142
|
|
|
2143
|
+
function writeClaudeCommands(opts) {
|
|
2144
|
+
const home = claudeHome();
|
|
2145
|
+
for (const command of claudeCommands()) {
|
|
2146
|
+
writeFile(join(home, "commands", command.filename), command.content, opts);
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
|
|
1995
2150
|
function removeLegacyClaudeCommands(home, opts) {
|
|
1996
2151
|
for (const command of legacyClaudeCommands()) {
|
|
1997
2152
|
const commandPath = join(home, "commands", command.filename);
|
|
@@ -2277,12 +2432,15 @@ function installClaude(opts) {
|
|
|
2277
2432
|
throw new Error(message);
|
|
2278
2433
|
}
|
|
2279
2434
|
writeClaudeCustomAgents(opts);
|
|
2435
|
+
removeClaudeMcp(opts);
|
|
2280
2436
|
if (opts.server === "hosted") {
|
|
2281
2437
|
run(
|
|
2282
2438
|
"claude",
|
|
2283
2439
|
[
|
|
2284
2440
|
"mcp",
|
|
2285
2441
|
"add",
|
|
2442
|
+
"--scope",
|
|
2443
|
+
"user",
|
|
2286
2444
|
"--transport",
|
|
2287
2445
|
"http",
|
|
2288
2446
|
"sellable",
|
|
@@ -2294,14 +2452,20 @@ function installClaude(opts) {
|
|
|
2294
2452
|
return true;
|
|
2295
2453
|
}
|
|
2296
2454
|
const [command, args] = mcpCommand(opts);
|
|
2297
|
-
run("claude", ["mcp", "remove", "sellable"], {
|
|
2298
|
-
...opts,
|
|
2299
|
-
dryRun: opts.dryRun,
|
|
2300
|
-
allowFail: true,
|
|
2301
|
-
});
|
|
2302
2455
|
run(
|
|
2303
2456
|
"claude",
|
|
2304
|
-
[
|
|
2457
|
+
[
|
|
2458
|
+
"mcp",
|
|
2459
|
+
"add",
|
|
2460
|
+
"--scope",
|
|
2461
|
+
"user",
|
|
2462
|
+
"--transport",
|
|
2463
|
+
"stdio",
|
|
2464
|
+
"sellable",
|
|
2465
|
+
"--",
|
|
2466
|
+
command,
|
|
2467
|
+
...args,
|
|
2468
|
+
],
|
|
2305
2469
|
opts
|
|
2306
2470
|
);
|
|
2307
2471
|
// Patch ~/.claude.json to add `alwaysLoad: true` so Claude Code v2.1.121+
|
|
@@ -2312,6 +2476,63 @@ function installClaude(opts) {
|
|
|
2312
2476
|
return true;
|
|
2313
2477
|
}
|
|
2314
2478
|
|
|
2479
|
+
function removeClaudeMcp(opts) {
|
|
2480
|
+
for (const scope of ["local", "project", "user"]) {
|
|
2481
|
+
run("claude", ["mcp", "remove", "--scope", scope, "sellable"], {
|
|
2482
|
+
...opts,
|
|
2483
|
+
dryRun: opts.dryRun,
|
|
2484
|
+
allowFail: true,
|
|
2485
|
+
});
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
|
|
2489
|
+
function canonicalClaudeMcpServer(opts) {
|
|
2490
|
+
if (opts.server === "hosted") {
|
|
2491
|
+
return {
|
|
2492
|
+
type: "http",
|
|
2493
|
+
url: withHostedWatchModeDriver(opts.hostedUrl, "claude"),
|
|
2494
|
+
alwaysLoad: true,
|
|
2495
|
+
};
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2498
|
+
const [command, args] = mcpCommand(opts);
|
|
2499
|
+
return {
|
|
2500
|
+
type: "stdio",
|
|
2501
|
+
command,
|
|
2502
|
+
args,
|
|
2503
|
+
env: { SELLABLE_WATCH_MODE_DRIVER: "claude" },
|
|
2504
|
+
alwaysLoad: true,
|
|
2505
|
+
};
|
|
2506
|
+
}
|
|
2507
|
+
|
|
2508
|
+
function collectClaudeSellableServers(config) {
|
|
2509
|
+
const servers = [];
|
|
2510
|
+
if (config?.mcpServers?.sellable) {
|
|
2511
|
+
servers.push(config.mcpServers.sellable);
|
|
2512
|
+
}
|
|
2513
|
+
for (const projectPath of Object.keys(config?.projects || {})) {
|
|
2514
|
+
const server = config.projects[projectPath]?.mcpServers?.sellable;
|
|
2515
|
+
if (server) {
|
|
2516
|
+
servers.push(server);
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
return servers;
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2522
|
+
function claudeMcpServerMatches(server, canonical) {
|
|
2523
|
+
if (!server || typeof server !== "object") return false;
|
|
2524
|
+
if (server.alwaysLoad !== true) return false;
|
|
2525
|
+
if (canonical.type === "http") {
|
|
2526
|
+
return server.type === canonical.type && server.url === canonical.url;
|
|
2527
|
+
}
|
|
2528
|
+
return (
|
|
2529
|
+
server.type === canonical.type &&
|
|
2530
|
+
server.command === canonical.command &&
|
|
2531
|
+
JSON.stringify(server.args || []) === JSON.stringify(canonical.args) &&
|
|
2532
|
+
server.env?.SELLABLE_WATCH_MODE_DRIVER === "claude"
|
|
2533
|
+
);
|
|
2534
|
+
}
|
|
2535
|
+
|
|
2315
2536
|
function patchClaudeAlwaysLoad(opts) {
|
|
2316
2537
|
if (opts.dryRun) {
|
|
2317
2538
|
logVerbose(
|
|
@@ -2329,18 +2550,44 @@ function patchClaudeAlwaysLoad(opts) {
|
|
|
2329
2550
|
try {
|
|
2330
2551
|
const raw = readFileSync(claudeJsonPath, "utf8");
|
|
2331
2552
|
const config = JSON.parse(raw);
|
|
2553
|
+
const canonical = canonicalClaudeMcpServer(opts);
|
|
2332
2554
|
let touched = false;
|
|
2333
2555
|
const patchSellableServer = (server) => {
|
|
2334
2556
|
if (!server || typeof server !== "object") return;
|
|
2335
|
-
if (server
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
server.
|
|
2343
|
-
|
|
2557
|
+
if (opts.server === "hosted") {
|
|
2558
|
+
for (const staleKey of ["command", "args", "env"]) {
|
|
2559
|
+
if (staleKey in server) {
|
|
2560
|
+
delete server[staleKey];
|
|
2561
|
+
touched = true;
|
|
2562
|
+
}
|
|
2563
|
+
}
|
|
2564
|
+
if (server.type !== canonical.type) {
|
|
2565
|
+
server.type = canonical.type;
|
|
2566
|
+
touched = true;
|
|
2567
|
+
}
|
|
2568
|
+
if (server.url !== canonical.url) {
|
|
2569
|
+
server.url = canonical.url;
|
|
2570
|
+
touched = true;
|
|
2571
|
+
}
|
|
2572
|
+
} else {
|
|
2573
|
+
for (const staleKey of ["url"]) {
|
|
2574
|
+
if (staleKey in server) {
|
|
2575
|
+
delete server[staleKey];
|
|
2576
|
+
touched = true;
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
if (server.type !== canonical.type) {
|
|
2580
|
+
server.type = canonical.type;
|
|
2581
|
+
touched = true;
|
|
2582
|
+
}
|
|
2583
|
+
if (server.command !== canonical.command) {
|
|
2584
|
+
server.command = canonical.command;
|
|
2585
|
+
touched = true;
|
|
2586
|
+
}
|
|
2587
|
+
if (JSON.stringify(server.args || []) !== JSON.stringify(canonical.args)) {
|
|
2588
|
+
server.args = [...canonical.args];
|
|
2589
|
+
touched = true;
|
|
2590
|
+
}
|
|
2344
2591
|
const existingDriver = server.env?.SELLABLE_WATCH_MODE_DRIVER;
|
|
2345
2592
|
server.env = {
|
|
2346
2593
|
...(server.env && typeof server.env === "object" ? server.env : {}),
|
|
@@ -2350,12 +2597,16 @@ function patchClaudeAlwaysLoad(opts) {
|
|
|
2350
2597
|
touched = true;
|
|
2351
2598
|
}
|
|
2352
2599
|
}
|
|
2600
|
+
if (server.alwaysLoad !== true) {
|
|
2601
|
+
server.alwaysLoad = true;
|
|
2602
|
+
touched = true;
|
|
2603
|
+
}
|
|
2353
2604
|
};
|
|
2354
|
-
// Top-level mcpServers.sellable (
|
|
2605
|
+
// Top-level mcpServers.sellable (user scope)
|
|
2355
2606
|
if (config.mcpServers?.sellable) {
|
|
2356
2607
|
patchSellableServer(config.mcpServers.sellable);
|
|
2357
2608
|
}
|
|
2358
|
-
// Per-project mcpServers.sellable (
|
|
2609
|
+
// Per-project mcpServers.sellable (old local/project installs)
|
|
2359
2610
|
for (const projectPath of Object.keys(config.projects || {})) {
|
|
2360
2611
|
const projServers = config.projects[projectPath]?.mcpServers;
|
|
2361
2612
|
if (projServers?.sellable) {
|
|
@@ -2469,6 +2720,34 @@ async function verify(opts) {
|
|
|
2469
2720
|
: "Claude custom agent MCP tool allowlists missing"
|
|
2470
2721
|
)
|
|
2471
2722
|
);
|
|
2723
|
+
const claudeCommandPaths = claudeCommands().map((command) =>
|
|
2724
|
+
join(claudeHome(), "commands", command.filename)
|
|
2725
|
+
);
|
|
2726
|
+
const hasClaudeCommands = claudeCommandPaths.every((commandPath) =>
|
|
2727
|
+
existsSync(commandPath)
|
|
2728
|
+
);
|
|
2729
|
+
checks.push(
|
|
2730
|
+
warningCheck(
|
|
2731
|
+
hasClaudeCommands,
|
|
2732
|
+
hasClaudeCommands
|
|
2733
|
+
? "Claude slash commands present"
|
|
2734
|
+
: "Claude slash commands missing"
|
|
2735
|
+
)
|
|
2736
|
+
);
|
|
2737
|
+
const claudeCommandsHaveCurrentNames = claudeCommands().every((command) => {
|
|
2738
|
+
const commandPath = join(claudeHome(), "commands", command.filename);
|
|
2739
|
+
if (!existsSync(commandPath)) return false;
|
|
2740
|
+
const content = readFileSync(commandPath, "utf8");
|
|
2741
|
+
return content.includes(`name: sellable:${command.name}`);
|
|
2742
|
+
});
|
|
2743
|
+
checks.push(
|
|
2744
|
+
warningCheck(
|
|
2745
|
+
claudeCommandsHaveCurrentNames,
|
|
2746
|
+
claudeCommandsHaveCurrentNames
|
|
2747
|
+
? "Claude slash command names current"
|
|
2748
|
+
: "Claude slash command names stale"
|
|
2749
|
+
)
|
|
2750
|
+
);
|
|
2472
2751
|
const legacyClaudePaths = [
|
|
2473
2752
|
...legacyClaudeCustomAgents().map((agent) =>
|
|
2474
2753
|
join(claudeHome(), "agents", agent.filename)
|
|
@@ -2504,6 +2783,25 @@ async function verify(opts) {
|
|
|
2504
2783
|
: "Claude watch mode driver missing"
|
|
2505
2784
|
)
|
|
2506
2785
|
);
|
|
2786
|
+
let claudeMcpEntriesCanonical = false;
|
|
2787
|
+
try {
|
|
2788
|
+
const config = claudeConfigContent ? JSON.parse(claudeConfigContent) : {};
|
|
2789
|
+
const servers = collectClaudeSellableServers(config);
|
|
2790
|
+
const canonical = canonicalClaudeMcpServer(opts);
|
|
2791
|
+
claudeMcpEntriesCanonical =
|
|
2792
|
+
servers.length > 0 &&
|
|
2793
|
+
servers.every((server) => claudeMcpServerMatches(server, canonical));
|
|
2794
|
+
} catch {
|
|
2795
|
+
claudeMcpEntriesCanonical = false;
|
|
2796
|
+
}
|
|
2797
|
+
checks.push(
|
|
2798
|
+
warningCheck(
|
|
2799
|
+
claudeMcpEntriesCanonical,
|
|
2800
|
+
claudeMcpEntriesCanonical
|
|
2801
|
+
? "Claude Sellable MCP entries canonical"
|
|
2802
|
+
: "Claude Sellable MCP entries stale"
|
|
2803
|
+
)
|
|
2804
|
+
);
|
|
2507
2805
|
}
|
|
2508
2806
|
if (opts.host === "codex" || opts.host === "all") {
|
|
2509
2807
|
const hasCodexCli = commandExists("codex");
|
|
@@ -2690,8 +2988,8 @@ async function verify(opts) {
|
|
|
2690
2988
|
(runtimeRequired ? requiredCheck : warningCheck)(
|
|
2691
2989
|
runtimeMcp.ok,
|
|
2692
2990
|
runtimeMcp.ok
|
|
2693
|
-
? `MCP runtime exposes required tools (${runtimeMcp.availableTools.length} total)`
|
|
2694
|
-
: `MCP runtime
|
|
2991
|
+
? `MCP runtime exposes required campaign tools (${runtimeMcp.availableTools.length} total) and create-campaign smoke passed`
|
|
2992
|
+
: `MCP runtime verification failed: ${runtimeMcp.missingTools.join(", ") || runtimeMcp.createCampaignSmoke?.error || runtimeMcp.error || "unknown failure"}`
|
|
2695
2993
|
)
|
|
2696
2994
|
);
|
|
2697
2995
|
}
|
|
@@ -2886,7 +3184,9 @@ function printNextSteps(installedHosts, authReused) {
|
|
|
2886
3184
|
console.log(` ${C.green}✓${C.reset} Codex custom agents installed`);
|
|
2887
3185
|
}
|
|
2888
3186
|
if (hasClaude) {
|
|
2889
|
-
console.log(
|
|
3187
|
+
console.log(
|
|
3188
|
+
` ${C.green}✓${C.reset} Claude slash commands and custom agents installed`
|
|
3189
|
+
);
|
|
2890
3190
|
}
|
|
2891
3191
|
if (authReused) {
|
|
2892
3192
|
console.log(
|
|
@@ -3200,6 +3500,45 @@ async function main() {
|
|
|
3200
3500
|
console.log(` Codex: $sellable:create-post`);
|
|
3201
3501
|
process.exit(0);
|
|
3202
3502
|
}
|
|
3503
|
+
if (rawArgs[0] === "prefs") {
|
|
3504
|
+
const dryRun = rawArgs.includes("--dry-run");
|
|
3505
|
+
const args = rawArgs.filter((arg) => arg !== "--dry-run");
|
|
3506
|
+
if (
|
|
3507
|
+
args[1] !== "set" ||
|
|
3508
|
+
args[2] !== "prompt-sharing" ||
|
|
3509
|
+
(args[3] !== "true" && args[3] !== "false") ||
|
|
3510
|
+
args.length !== 4
|
|
3511
|
+
) {
|
|
3512
|
+
console.error(
|
|
3513
|
+
"Usage: sellable prefs set prompt-sharing true|false [--dry-run]"
|
|
3514
|
+
);
|
|
3515
|
+
process.exit(2);
|
|
3516
|
+
}
|
|
3517
|
+
const envPath = writePromptSharingPreference(args[3], { dryRun });
|
|
3518
|
+
console.log(
|
|
3519
|
+
`✓ Sellable prompt sharing preference set to ${args[3]} at ${envPath}`
|
|
3520
|
+
);
|
|
3521
|
+
process.exit(0);
|
|
3522
|
+
}
|
|
3523
|
+
if (rawArgs[0] === "setup") {
|
|
3524
|
+
const dryRun = rawArgs.includes("--dry-run");
|
|
3525
|
+
const args = rawArgs.filter((arg) => arg !== "--dry-run");
|
|
3526
|
+
if (args[1] !== "claude-permissions") {
|
|
3527
|
+
console.error(
|
|
3528
|
+
`Unknown setup subcommand: ${args[1] ?? "(none)"}\nKnown: claude-permissions`
|
|
3529
|
+
);
|
|
3530
|
+
process.exit(2);
|
|
3531
|
+
}
|
|
3532
|
+
if (!args.includes("--allow-common-setup")) {
|
|
3533
|
+
console.error(
|
|
3534
|
+
"Usage: sellable setup claude-permissions --allow-common-setup [--dry-run]"
|
|
3535
|
+
);
|
|
3536
|
+
process.exit(2);
|
|
3537
|
+
}
|
|
3538
|
+
const settingsPath = allowClaudeCommonSetup({ dryRun });
|
|
3539
|
+
console.log(`✓ Claude Sellable permissions updated at ${settingsPath}`);
|
|
3540
|
+
process.exit(0);
|
|
3541
|
+
}
|
|
3203
3542
|
if (rawArgs[0] === "uninstall") {
|
|
3204
3543
|
runUninstall();
|
|
3205
3544
|
process.exit(0);
|
package/lib/runtime-verify.mjs
CHANGED
|
@@ -6,12 +6,64 @@ const DEFAULT_VERIFY_TIMEOUT_MS = 10_000;
|
|
|
6
6
|
|
|
7
7
|
export const REQUIRED_SELLABLE_MCP_TOOLS = [
|
|
8
8
|
"get_auth_status",
|
|
9
|
+
"start_cli_login",
|
|
10
|
+
"wait_for_cli_login",
|
|
9
11
|
"bootstrap_create_campaign",
|
|
12
|
+
"get_subskill_prompt",
|
|
13
|
+
"get_subskill_asset",
|
|
14
|
+
"search_subskill_prompts",
|
|
10
15
|
"create_campaign",
|
|
11
16
|
"import_leads",
|
|
17
|
+
"confirm_lead_list",
|
|
18
|
+
"wait_for_lead_list_ready",
|
|
19
|
+
"wait_for_campaign_table_ready",
|
|
12
20
|
"update_campaign",
|
|
13
|
-
"
|
|
14
|
-
"
|
|
21
|
+
"update_campaign_brief",
|
|
22
|
+
"get_campaign",
|
|
23
|
+
"get_campaign_context",
|
|
24
|
+
"get_campaign_framework",
|
|
25
|
+
"get_campaign_navigation_state",
|
|
26
|
+
"get_campaign_messages_preview",
|
|
27
|
+
"attach_sequence",
|
|
28
|
+
"attach_recommended_sequence",
|
|
29
|
+
"start_campaign_message_preparation",
|
|
30
|
+
"get_campaign_message_preparation_status",
|
|
31
|
+
"cancel_campaign_message_preparation",
|
|
32
|
+
"start_campaign",
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
export const CREATE_CAMPAIGN_SMOKE_CALLS = [
|
|
36
|
+
{
|
|
37
|
+
name: "get_auth_status",
|
|
38
|
+
arguments: {},
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: "bootstrap_create_campaign",
|
|
42
|
+
arguments: {
|
|
43
|
+
flowVersion: "v2",
|
|
44
|
+
includeTableCheck: false,
|
|
45
|
+
host: "sellable-install-verify",
|
|
46
|
+
model: "installer-smoke",
|
|
47
|
+
reasoningEffort: "none",
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: "get_subskill_prompt",
|
|
52
|
+
arguments: {
|
|
53
|
+
subskillName: "create-campaign-v2",
|
|
54
|
+
offset: 0,
|
|
55
|
+
limit: 1200,
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: "get_subskill_asset",
|
|
60
|
+
arguments: {
|
|
61
|
+
subskillName: "create-campaign-v2",
|
|
62
|
+
assetPath: "core/flow.v2.json",
|
|
63
|
+
offset: 0,
|
|
64
|
+
limit: 1200,
|
|
65
|
+
},
|
|
66
|
+
},
|
|
15
67
|
];
|
|
16
68
|
|
|
17
69
|
function collectStderrLines(stream) {
|
|
@@ -73,6 +125,172 @@ export function missingRequiredTools(tools, requiredTools) {
|
|
|
73
125
|
return requiredTools.filter((name) => !available.has(name));
|
|
74
126
|
}
|
|
75
127
|
|
|
128
|
+
function textFromToolResult(result) {
|
|
129
|
+
return (result?.content || [])
|
|
130
|
+
.filter((part) => part?.type === "text" && typeof part.text === "string")
|
|
131
|
+
.map((part) => part.text)
|
|
132
|
+
.join("\n");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function parseJsonText(text) {
|
|
136
|
+
if (!text) return null;
|
|
137
|
+
try {
|
|
138
|
+
return JSON.parse(text);
|
|
139
|
+
} catch {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function summarizeToolResult(name, result) {
|
|
145
|
+
const text = redact(textFromToolResult(result));
|
|
146
|
+
const parsed = parseJsonText(text);
|
|
147
|
+
const summary = {
|
|
148
|
+
isError: result?.isError === true,
|
|
149
|
+
textChars: text.length,
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
if (name === "get_auth_status" && parsed) {
|
|
153
|
+
return {
|
|
154
|
+
...summary,
|
|
155
|
+
authOk: parsed.ok === true,
|
|
156
|
+
errorType: parsed.error?.type || null,
|
|
157
|
+
activeWorkspaceId: parsed.activeWorkspaceId || null,
|
|
158
|
+
activeWorkspaceName: parsed.activeWorkspaceName || null,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (name === "bootstrap_create_campaign" && parsed) {
|
|
163
|
+
return {
|
|
164
|
+
...summary,
|
|
165
|
+
safeToProceed: parsed.safeToProceed === true,
|
|
166
|
+
flowVersion: parsed.flowVersion || null,
|
|
167
|
+
blockingChecks: Array.isArray(parsed.blockingErrors)
|
|
168
|
+
? parsed.blockingErrors.map((entry) => entry?.check).filter(Boolean)
|
|
169
|
+
: [],
|
|
170
|
+
requiredChecks: Array.isArray(parsed.requiredChecks)
|
|
171
|
+
? parsed.requiredChecks.map((entry) => ({
|
|
172
|
+
key: entry?.key || null,
|
|
173
|
+
ok: entry?.ok === true,
|
|
174
|
+
blocking: entry?.blocking === true,
|
|
175
|
+
}))
|
|
176
|
+
: [],
|
|
177
|
+
nextStepChars:
|
|
178
|
+
typeof parsed.nextStep === "string" ? parsed.nextStep.length : 0,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (name === "get_subskill_prompt" && parsed) {
|
|
183
|
+
return {
|
|
184
|
+
...summary,
|
|
185
|
+
subskillName: parsed.name || null,
|
|
186
|
+
promptChars: typeof parsed.prompt === "string" ? parsed.prompt.length : 0,
|
|
187
|
+
hasMore: parsed.hasMore === true,
|
|
188
|
+
nextOffset:
|
|
189
|
+
typeof parsed.nextOffset === "number" ? parsed.nextOffset : null,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (name === "get_subskill_asset" && parsed) {
|
|
194
|
+
return {
|
|
195
|
+
...summary,
|
|
196
|
+
subskillName: parsed.subskillName || null,
|
|
197
|
+
assetPath: parsed.assetPath || null,
|
|
198
|
+
assetChars: typeof parsed.content === "string" ? parsed.content.length : 0,
|
|
199
|
+
hasMore: parsed.hasMore === true,
|
|
200
|
+
nextOffset:
|
|
201
|
+
typeof parsed.nextOffset === "number" ? parsed.nextOffset : null,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return summary;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function verifyCreateCampaignSmoke({
|
|
209
|
+
client,
|
|
210
|
+
availableTools,
|
|
211
|
+
timeoutMs,
|
|
212
|
+
smokeCalls = CREATE_CAMPAIGN_SMOKE_CALLS,
|
|
213
|
+
}) {
|
|
214
|
+
const startedAt = new Date().toISOString();
|
|
215
|
+
const available = new Set(availableTools || []);
|
|
216
|
+
const missingTools = smokeCalls
|
|
217
|
+
.map((call) => call.name)
|
|
218
|
+
.filter((name) => !available.has(name));
|
|
219
|
+
const calls = [];
|
|
220
|
+
|
|
221
|
+
if (missingTools.length > 0) {
|
|
222
|
+
return {
|
|
223
|
+
ok: false,
|
|
224
|
+
missingTools,
|
|
225
|
+
calls,
|
|
226
|
+
error: `Missing smoke tool(s): ${missingTools.join(", ")}`,
|
|
227
|
+
startedAt,
|
|
228
|
+
completedAt: new Date().toISOString(),
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
for (const call of smokeCalls) {
|
|
233
|
+
const callStartedAt = new Date().toISOString();
|
|
234
|
+
try {
|
|
235
|
+
const result = await client.callTool(
|
|
236
|
+
{ name: call.name, arguments: call.arguments },
|
|
237
|
+
undefined,
|
|
238
|
+
{
|
|
239
|
+
timeout: timeoutMs,
|
|
240
|
+
maxTotalTimeout: timeoutMs,
|
|
241
|
+
}
|
|
242
|
+
);
|
|
243
|
+
const summary = summarizeToolResult(call.name, result);
|
|
244
|
+
calls.push({
|
|
245
|
+
name: call.name,
|
|
246
|
+
arguments: call.arguments,
|
|
247
|
+
ok: summary.isError !== true,
|
|
248
|
+
summary,
|
|
249
|
+
startedAt: callStartedAt,
|
|
250
|
+
completedAt: new Date().toISOString(),
|
|
251
|
+
});
|
|
252
|
+
if (summary.isError === true) {
|
|
253
|
+
return {
|
|
254
|
+
ok: false,
|
|
255
|
+
missingTools: [],
|
|
256
|
+
calls,
|
|
257
|
+
error: `${call.name} returned an MCP tool error`,
|
|
258
|
+
startedAt,
|
|
259
|
+
completedAt: new Date().toISOString(),
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
} catch (err) {
|
|
263
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
264
|
+
calls.push({
|
|
265
|
+
name: call.name,
|
|
266
|
+
arguments: call.arguments,
|
|
267
|
+
ok: false,
|
|
268
|
+
summary: null,
|
|
269
|
+
error: redact(error),
|
|
270
|
+
startedAt: callStartedAt,
|
|
271
|
+
completedAt: new Date().toISOString(),
|
|
272
|
+
});
|
|
273
|
+
return {
|
|
274
|
+
ok: false,
|
|
275
|
+
missingTools: [],
|
|
276
|
+
calls,
|
|
277
|
+
error: redact(error),
|
|
278
|
+
startedAt,
|
|
279
|
+
completedAt: new Date().toISOString(),
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
ok: true,
|
|
286
|
+
missingTools: [],
|
|
287
|
+
calls,
|
|
288
|
+
error: null,
|
|
289
|
+
startedAt,
|
|
290
|
+
completedAt: new Date().toISOString(),
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
76
294
|
export async function verifySellableMcpRuntime({
|
|
77
295
|
command,
|
|
78
296
|
args = [],
|
|
@@ -116,6 +334,7 @@ export async function verifySellableMcpRuntime({
|
|
|
116
334
|
|
|
117
335
|
let availableTools = [];
|
|
118
336
|
let missingTools = [...requiredTools];
|
|
337
|
+
let createCampaignSmoke = null;
|
|
119
338
|
let error = null;
|
|
120
339
|
|
|
121
340
|
try {
|
|
@@ -126,6 +345,25 @@ export async function verifySellableMcpRuntime({
|
|
|
126
345
|
});
|
|
127
346
|
availableTools = toolList.tools.map((tool) => tool.name).sort();
|
|
128
347
|
missingTools = missingRequiredTools(toolList.tools, requiredTools);
|
|
348
|
+
if (missingTools.length === 0) {
|
|
349
|
+
createCampaignSmoke = await verifyCreateCampaignSmoke({
|
|
350
|
+
client,
|
|
351
|
+
availableTools,
|
|
352
|
+
timeoutMs,
|
|
353
|
+
});
|
|
354
|
+
} else {
|
|
355
|
+
createCampaignSmoke = {
|
|
356
|
+
ok: false,
|
|
357
|
+
missingTools: CREATE_CAMPAIGN_SMOKE_CALLS.map((call) => call.name).filter(
|
|
358
|
+
(name) => !availableTools.includes(name)
|
|
359
|
+
),
|
|
360
|
+
calls: [],
|
|
361
|
+
error:
|
|
362
|
+
"Skipping create-campaign smoke because required MCP tools are missing.",
|
|
363
|
+
startedAt: new Date().toISOString(),
|
|
364
|
+
completedAt: new Date().toISOString(),
|
|
365
|
+
};
|
|
366
|
+
}
|
|
129
367
|
} catch (err) {
|
|
130
368
|
error = err instanceof Error ? err.message : String(err);
|
|
131
369
|
} finally {
|
|
@@ -137,13 +375,14 @@ export async function verifySellableMcpRuntime({
|
|
|
137
375
|
}
|
|
138
376
|
|
|
139
377
|
return {
|
|
140
|
-
ok: !error && missingTools.length === 0,
|
|
378
|
+
ok: !error && missingTools.length === 0 && createCampaignSmoke?.ok === true,
|
|
141
379
|
skipped: false,
|
|
142
380
|
command,
|
|
143
381
|
args,
|
|
144
382
|
requiredTools,
|
|
145
383
|
availableTools,
|
|
146
384
|
missingTools,
|
|
385
|
+
createCampaignSmoke,
|
|
147
386
|
stderrTail: stderrLines,
|
|
148
387
|
error: error ? redact(error) : null,
|
|
149
388
|
startedAt,
|