@sellable/install 0.1.205 → 0.1.207

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,19 +3,27 @@
3
3
  Installs Sellable MCP for Claude Code and Codex.
4
4
 
5
5
  ```bash
6
- npx -y @sellable/install@latest --host all
6
+ curl -fsSL "https://app.sellable.dev/api/v2/cli/install" | sh
7
7
  ```
8
8
 
9
- Paste that command in your terminal, not inside the Codex chat. Mac/Linux
10
- users can use Terminal. Windows users can use PowerShell or Windows Terminal;
11
- if PowerShell blocks `npx`, run:
9
+ Paste that command in your terminal, not inside the Codex chat. It downloads a
10
+ reviewable bootstrapper that installs or reuses Node >=20, runs
11
+ `@sellable/install`, and verifies the Sellable MCP runtime.
12
+
13
+ Windows users can use PowerShell or Windows Terminal:
12
14
 
13
15
  ```powershell
14
- npx.cmd -y @sellable/install@latest --host all
16
+ iwr "https://app.sellable.dev/api/v2/cli/install.ps1" | iex
15
17
  ```
16
18
 
17
19
  After install, restart Codex Desktop so the Sellable skill appears.
18
20
 
21
+ Verify the runtime tools with:
22
+
23
+ ```bash
24
+ sellable --verify-only --host all --json --artifact "$HOME/.local/sellable/app-sellable-dev/installer/.last-verify.json"
25
+ ```
26
+
19
27
  After install, `sellable create` is a terminal helper that prints the correct
20
28
  agent command for launching a campaign:
21
29
 
@@ -33,7 +41,7 @@ Sellable sign-in on the first campaign run with a magic-link handoff.
33
41
  The installer uses package stdio MCP by default:
34
42
 
35
43
  ```bash
36
- npx -y @sellable/mcp@latest
44
+ npm exec --yes --package @sellable/mcp@latest -- sellable-mcp
37
45
  ```
38
46
 
39
47
  That keeps new Claude Code/Codex MCP starts on the latest stable package. The
@@ -41,6 +49,19 @@ MCP server also checks npm at startup and during `get_auth_status`, caching the
41
49
  result at `~/.sellable/update-check.json` so users are prompted to rerun the
42
50
  latest installer only when an update is actually available.
43
51
 
52
+ Agents should use the agent-readable handoff instead of guessing commands:
53
+
54
+ ```text
55
+ Install Sellable CLI and skills using https://app.sellable.dev/agent-install.txt
56
+ ```
57
+
58
+ The direct npm installer remains available as a troubleshooting fallback when
59
+ Node/npm/npx are already known-good:
60
+
61
+ ```bash
62
+ npx -y @sellable/install@latest --host all
63
+ ```
64
+
44
65
  For CI/scripted installs, get a Sellable API token from:
45
66
 
46
67
  ```text
@@ -14,6 +14,10 @@ import { homedir } from "node:os";
14
14
  import { dirname, join, relative } from "node:path";
15
15
  import { stdout as output } from "node:process";
16
16
  import { fileURLToPath } from "node:url";
17
+ import {
18
+ REQUIRED_SELLABLE_MCP_TOOLS,
19
+ verifySellableMcpRuntime,
20
+ } from "../lib/runtime-verify.mjs";
17
21
 
18
22
  const DEFAULT_API_URL = "https://app.sellable.dev";
19
23
  const DEFAULT_SERVER_PACKAGE =
@@ -153,6 +157,8 @@ Options:
153
157
  --verbose Print every file write and shell command.
154
158
  --dry-run Print actions without writing or running host commands.
155
159
  --verify-only Verify installed host config where possible.
160
+ --json Print machine-readable verification JSON.
161
+ --artifact <path> Write verification JSON to a file.
156
162
  --help Show help.
157
163
 
158
164
  Auth:
@@ -201,7 +207,9 @@ function printCreateCommandHint() {
201
207
  console.log(` ${C.grey}If those commands are missing, run:${C.reset}`);
202
208
  console.log("");
203
209
  console.log(` ${C.cyan}sellable --host all${C.reset}`);
204
- console.log(` ${C.cyan}sellable --verify-only --host all${C.reset}`);
210
+ console.log(
211
+ ` ${C.cyan}sellable --verify-only --host all --json${C.reset}`
212
+ );
205
213
  console.log("");
206
214
  }
207
215
 
@@ -217,6 +225,8 @@ function parseArgs(argv) {
217
225
  hostedUrl: process.env.SELLABLE_MCP_HOSTED_URL || "",
218
226
  dryRun: false,
219
227
  verifyOnly: false,
228
+ json: false,
229
+ artifactPath: process.env.SELLABLE_VERIFY_ARTIFACT || "",
220
230
  verbose: false,
221
231
  };
222
232
 
@@ -253,6 +263,10 @@ function parseArgs(argv) {
253
263
  opts.dryRun = true;
254
264
  } else if (arg === "--verify-only") {
255
265
  opts.verifyOnly = true;
266
+ } else if (arg === "--json") {
267
+ opts.json = true;
268
+ } else if (arg === "--artifact") {
269
+ opts.artifactPath = next();
256
270
  } else if (arg === "--verbose") {
257
271
  opts.verbose = true;
258
272
  } else {
@@ -310,7 +324,7 @@ function commandExists(command, platform = process.platform) {
310
324
  ? spawnSync("where.exe", [command], {
311
325
  encoding: "utf8",
312
326
  })
313
- : spawnSync("sh", ["-lc", `command -v ${command}`], {
327
+ : spawnSync("sh", ["-c", `command -v ${command}`], {
314
328
  encoding: "utf8",
315
329
  });
316
330
  return result.status === 0;
@@ -321,7 +335,7 @@ function commandExistsDetails(command, platform = process.platform) {
321
335
  ? spawnSync("where.exe", [command], {
322
336
  encoding: "utf8",
323
337
  })
324
- : spawnSync("sh", ["-lc", `command -v ${command}`], {
338
+ : spawnSync("sh", ["-c", `command -v ${command}`], {
325
339
  encoding: "utf8",
326
340
  });
327
341
  return {
@@ -331,6 +345,10 @@ function commandExistsDetails(command, platform = process.platform) {
331
345
  };
332
346
  }
333
347
 
348
+ function shellQuote(value) {
349
+ return `'${String(value).replace(/'/g, `'\\''`)}'`;
350
+ }
351
+
334
352
  function getMcpVersion() {
335
353
  try {
336
354
  const r = spawnSync(
@@ -710,6 +728,12 @@ person/company this campaign is for, then I’ll turn that into a campaign brief
710
728
  before anything is created.
711
729
  \`\`\`
712
730
 
731
+ Exception: if \`bootstrap_create_campaign.modelQuality.status === "warn"\`,
732
+ the first visible campaign message must be the model-quality warning from
733
+ \`modelQuality.message\`. Ask the user to switch to the configured minimum model
734
+ or explicitly continue anyway before identity setup, research, lead filtering,
735
+ message generation, or launch review.
736
+
713
737
  If a linked/local skill file is stale or missing, silently use the installed
714
738
  \`sellable@sellable\` plugin copy. Do not tell the user about the stale link,
715
739
  the old version, or the replacement path.
@@ -1127,12 +1151,15 @@ there.
1127
1151
  MCP tool access is required. First call \`mcp__sellable__get_auth_status({})\`
1128
1152
  directly. If that tool is unavailable, stop and say this is a Codex
1129
1153
  install/reload problem, not a campaign problem. Tell the user to
1130
- run \`npx -y ${INSTALL_PACKAGE_SPEC} --host all\` so the packaged MCP server,
1131
- Codex Desktop plugin, and Sellable skill bundle are installed. If they want a
1132
- CLI verification, tell them to run \`sellable --verify-only --host all\`. After
1133
- that, they must fully quit and reopen Codex Desktop before starting a new
1134
- thread. Do not use \`scripts/mcp/sellable-tool-call.mjs\`, \`npm run\`,
1135
- \`node\`, or any local harness as a fallback for this interactive skill.
1154
+ run \`curl -fsSL "https://app.sellable.dev/api/v2/cli/install" | sh\` so the
1155
+ packaged MCP server, Codex Desktop plugin, and Sellable skill bundle are
1156
+ installed. If they want an agent-readable checklist, tell them:
1157
+ \`Install Sellable CLI and skills using https://app.sellable.dev/agent-install.txt\`.
1158
+ For CLI verification, tell them to run
1159
+ \`sellable --verify-only --host all --json --artifact "$HOME/.local/sellable/app-sellable-dev/installer/.last-verify.json"\`.
1160
+ After that, they must fully quit and reopen Codex Desktop before starting a new
1161
+ thread. Do not use \`scripts/mcp/sellable-tool-call.mjs\`, \`npm run\`, \`node\`,
1162
+ or any local harness as a fallback for this interactive skill.
1136
1163
  Do not mention prompt loading, local skill files, missing linked versions,
1137
1164
  plugin cache paths, MCP namespaces, or runbooks in customer-facing progress
1138
1165
  updates.
@@ -1144,8 +1171,11 @@ updates.
1144
1171
  - Do not call \`mcp__sellable__get_campaigns\`.
1145
1172
  - Do not call \`mcp__sellable__get_campaign\` to hunt for IDs.
1146
1173
  - Do not call \`mcp__sellable__create_campaign({ campaignId: ... })\` unless the user supplied that id.
1147
- 5. Call \`mcp__sellable__bootstrap_create_campaign({ flowVersion: "v2", campaignId? })\`.
1174
+ 5. Call \`mcp__sellable__bootstrap_create_campaign({ flowVersion: "v2", campaignId?, host?, model?, reasoningEffort? })\`.
1175
+ Pass the current host, model, and reasoning when the host exposes them.
1148
1176
  6. If \`safeToProceed !== true\`, stop and show \`blockingErrors\` + \`nextStep\`.
1177
+ 7. If \`modelQuality.status === "warn"\`, show \`modelQuality.message\` before any
1178
+ setup/research and wait for the user to switch or explicitly continue.
1149
1179
 
1150
1180
  ## Execute Workflow
1151
1181
 
@@ -2141,11 +2171,33 @@ function writeAuth(opts) {
2141
2171
  }
2142
2172
 
2143
2173
  function installSelfShim(opts) {
2144
- const binPath = join(homedir(), ".local", "bin", "sellable");
2174
+ const installDir =
2175
+ process.env.SELLABLE_INSTALL_DIR || join(homedir(), ".local", "bin");
2176
+ const binPath = join(installDir, "sellable");
2177
+ const npmCommand =
2178
+ process.env.SELLABLE_INSTALL_NPM_COMMAND || packageManagerCommand();
2179
+ const nodeBin = process.env.SELLABLE_INSTALL_NODE_BIN || "";
2180
+ const pathPrefix = nodeBin
2181
+ ? `PATH=${shellQuote(nodeBin)}:$PATH\nexport PATH\n`
2182
+ : "";
2145
2183
  const shim = `#!/usr/bin/env sh
2146
- exec npx -y ${INSTALL_PACKAGE_SPEC} "$@"
2184
+ ${pathPrefix}
2185
+ exec ${shellQuote(npmCommand)} exec --yes --package ${shellQuote(INSTALL_PACKAGE_SPEC)} -- sellable "$@"
2147
2186
  `;
2148
2187
  writeFile(binPath, shim, opts, 0o755);
2188
+
2189
+ const shouldWriteCmdShim =
2190
+ isWindowsPlatform() || process.env.SELLABLE_INSTALL_WRITE_CMD_SHIM === "1";
2191
+ if (shouldWriteCmdShim) {
2192
+ const cmdPath = `${binPath}.cmd`;
2193
+ const npmCmdCommand =
2194
+ process.env.SELLABLE_INSTALL_NPM_CMD_COMMAND ||
2195
+ process.env.SELLABLE_INSTALL_NPM_COMMAND ||
2196
+ packageManagerCommand("win32");
2197
+ const cmdPathPrefix = nodeBin ? `set "PATH=${nodeBin};%PATH%"\r\n` : "";
2198
+ const cmdShim = `@echo off\r\n${cmdPathPrefix}"${npmCmdCommand}" exec --yes --package ${INSTALL_PACKAGE_SPEC} -- sellable %*\r\n`;
2199
+ writeFile(cmdPath, cmdShim, opts, 0o755);
2200
+ }
2149
2201
  }
2150
2202
 
2151
2203
  const WATCH_MODE_DRIVER_ENV = {
@@ -2171,7 +2223,10 @@ function withHostedWatchModeDriver(rawUrl, driver) {
2171
2223
 
2172
2224
  function mcpCommand(opts) {
2173
2225
  if (opts.server === "package") {
2174
- return [packageRunnerCommand(), ["-y", opts.mcpPackage]];
2226
+ return [
2227
+ packageManagerCommand(),
2228
+ ["exec", "--yes", "--package", opts.mcpPackage, "--", "sellable-mcp"],
2229
+ ];
2175
2230
  }
2176
2231
  if (opts.server === "local") {
2177
2232
  if (!opts.localCommand) {
@@ -2350,7 +2405,16 @@ function installCodex(opts) {
2350
2405
  return { installed: true, ...info };
2351
2406
  }
2352
2407
 
2353
- function verify(opts) {
2408
+ function requiredCheck(ok, label) {
2409
+ return { ok, label, required: true };
2410
+ }
2411
+
2412
+ function warningCheck(ok, label) {
2413
+ return { ok, label, required: false };
2414
+ }
2415
+
2416
+ async function verify(opts) {
2417
+ const startedAt = new Date().toISOString();
2354
2418
  const stored = readStoredAuth();
2355
2419
  const checks = [];
2356
2420
  const hasWatchModeDriverMarker = (content, driver) =>
@@ -2359,45 +2423,52 @@ function verify(opts) {
2359
2423
  ) || new RegExp(`[?&]driver=${driver}(?:\\b|&)`).test(content);
2360
2424
 
2361
2425
  if (stored?.token && stored?.workspaceId) {
2362
- checks.push({ ok: true, label: `Auth config: ${authPath()}` });
2426
+ checks.push(warningCheck(true, `Auth config: ${authPath()}`));
2363
2427
  } else {
2364
- checks.push({
2365
- ok: true,
2366
- label: `Auth: not yet signed in — sign in on first run of /sellable:create-campaign`,
2367
- });
2428
+ checks.push(
2429
+ warningCheck(
2430
+ true,
2431
+ `Auth: not yet signed in — sign in on first run of /sellable:create-campaign`
2432
+ )
2433
+ );
2368
2434
  }
2369
2435
 
2370
2436
  if (opts.host === "claude" || opts.host === "all") {
2371
- checks.push({
2372
- ok: commandExists("claude"),
2373
- label: commandExists("claude")
2374
- ? "Claude CLI present"
2375
- : "Claude CLI missing",
2376
- });
2437
+ const hasClaudeCli = commandExists("claude");
2438
+ checks.push(
2439
+ warningCheck(
2440
+ hasClaudeCli,
2441
+ hasClaudeCli ? "Claude CLI present" : "Claude CLI missing"
2442
+ )
2443
+ );
2377
2444
  const claudeAgentPaths = claudeCustomAgents().map((agent) =>
2378
2445
  join(claudeHome(), "agents", agent.filename)
2379
2446
  );
2380
2447
  const hasClaudeScouts = claudeAgentPaths.every((agentPath) =>
2381
2448
  existsSync(agentPath)
2382
2449
  );
2383
- checks.push({
2384
- ok: hasClaudeScouts,
2385
- label: hasClaudeScouts
2386
- ? "Claude custom agents present"
2387
- : "Claude custom agents missing",
2388
- });
2450
+ checks.push(
2451
+ warningCheck(
2452
+ hasClaudeScouts,
2453
+ hasClaudeScouts
2454
+ ? "Claude custom agents present"
2455
+ : "Claude custom agents missing"
2456
+ )
2457
+ );
2389
2458
  const claudeAgentsHaveTools = claudeCustomAgents().every((agent) => {
2390
2459
  const agentPath = join(claudeHome(), "agents", agent.filename);
2391
2460
  if (!existsSync(agentPath)) return false;
2392
2461
  const content = readFileSync(agentPath, "utf8");
2393
2462
  return agent.tools.every((tool) => content.includes(tool));
2394
2463
  });
2395
- checks.push({
2396
- ok: claudeAgentsHaveTools,
2397
- label: claudeAgentsHaveTools
2398
- ? "Claude custom agent MCP tool allowlists present"
2399
- : "Claude custom agent MCP tool allowlists missing",
2400
- });
2464
+ checks.push(
2465
+ warningCheck(
2466
+ claudeAgentsHaveTools,
2467
+ claudeAgentsHaveTools
2468
+ ? "Claude custom agent MCP tool allowlists present"
2469
+ : "Claude custom agent MCP tool allowlists missing"
2470
+ )
2471
+ );
2401
2472
  const legacyClaudePaths = [
2402
2473
  ...legacyClaudeCustomAgents().map((agent) =>
2403
2474
  join(claudeHome(), "agents", agent.filename)
@@ -2409,12 +2480,14 @@ function verify(opts) {
2409
2480
  const hasNoLegacyClaudeArtifacts = legacyClaudePaths.every(
2410
2481
  (artifactPath) => !existsSync(artifactPath)
2411
2482
  );
2412
- checks.push({
2413
- ok: hasNoLegacyClaudeArtifacts,
2414
- label: hasNoLegacyClaudeArtifacts
2415
- ? "Legacy Claude Sellable host artifacts cleaned"
2416
- : "Legacy Claude Sellable host artifacts still present",
2417
- });
2483
+ checks.push(
2484
+ warningCheck(
2485
+ hasNoLegacyClaudeArtifacts,
2486
+ hasNoLegacyClaudeArtifacts
2487
+ ? "Legacy Claude Sellable host artifacts cleaned"
2488
+ : "Legacy Claude Sellable host artifacts still present"
2489
+ )
2490
+ );
2418
2491
  const claudeJsonPath = join(homedir(), ".claude.json");
2419
2492
  const claudeConfigContent = existsSync(claudeJsonPath)
2420
2493
  ? readFileSync(claudeJsonPath, "utf8")
@@ -2423,18 +2496,23 @@ function verify(opts) {
2423
2496
  claudeConfigContent,
2424
2497
  "claude"
2425
2498
  );
2426
- checks.push({
2427
- ok: hasClaudeWatchModeDriver,
2428
- label: hasClaudeWatchModeDriver
2429
- ? "Claude watch mode driver pinned to claude"
2430
- : "Claude watch mode driver missing",
2431
- });
2499
+ checks.push(
2500
+ warningCheck(
2501
+ hasClaudeWatchModeDriver,
2502
+ hasClaudeWatchModeDriver
2503
+ ? "Claude watch mode driver pinned to claude"
2504
+ : "Claude watch mode driver missing"
2505
+ )
2506
+ );
2432
2507
  }
2433
2508
  if (opts.host === "codex" || opts.host === "all") {
2434
- checks.push({
2435
- ok: commandExists("codex"),
2436
- label: commandExists("codex") ? "Codex CLI present" : "Codex CLI missing",
2437
- });
2509
+ const hasCodexCli = commandExists("codex");
2510
+ checks.push(
2511
+ warningCheck(
2512
+ hasCodexCli,
2513
+ hasCodexCli ? "Codex CLI present" : "Codex CLI missing"
2514
+ )
2515
+ );
2438
2516
  const pluginPath = join(
2439
2517
  codexHome(),
2440
2518
  "plugins",
@@ -2461,30 +2539,36 @@ function verify(opts) {
2461
2539
  const hasSkillBundles = skillPaths.every((skillPath) =>
2462
2540
  existsSync(skillPath)
2463
2541
  );
2464
- checks.push({
2465
- ok: existsSync(pluginPath),
2466
- label: existsSync(pluginPath)
2467
- ? "Codex Desktop plugin present"
2468
- : "Codex Desktop plugin missing",
2469
- });
2470
- checks.push({
2471
- ok: hasSkillBundles,
2472
- label: hasSkillBundles
2473
- ? "Codex skill bundles present"
2474
- : "Codex skill bundles missing",
2475
- });
2542
+ checks.push(
2543
+ warningCheck(
2544
+ existsSync(pluginPath),
2545
+ existsSync(pluginPath)
2546
+ ? "Codex Desktop plugin present"
2547
+ : "Codex Desktop plugin missing"
2548
+ )
2549
+ );
2550
+ checks.push(
2551
+ warningCheck(
2552
+ hasSkillBundles,
2553
+ hasSkillBundles
2554
+ ? "Codex skill bundles present"
2555
+ : "Codex skill bundles missing"
2556
+ )
2557
+ );
2476
2558
  const codexAgentPaths = codexCustomAgents().map((agent) =>
2477
2559
  join(codexHome(), "agents", agent.filename)
2478
2560
  );
2479
2561
  const hasCodexScouts = codexAgentPaths.every((agentPath) =>
2480
2562
  existsSync(agentPath)
2481
2563
  );
2482
- checks.push({
2483
- ok: hasCodexScouts,
2484
- label: hasCodexScouts
2485
- ? "Codex custom agents present"
2486
- : "Codex custom agents missing",
2487
- });
2564
+ checks.push(
2565
+ warningCheck(
2566
+ hasCodexScouts,
2567
+ hasCodexScouts
2568
+ ? "Codex custom agents present"
2569
+ : "Codex custom agents missing"
2570
+ )
2571
+ );
2488
2572
  const configPath = join(codexHome(), "config.toml");
2489
2573
  const configContent = existsSync(configPath)
2490
2574
  ? readFileSync(configPath, "utf8")
@@ -2506,27 +2590,33 @@ function verify(opts) {
2506
2590
  pluginMcpContent,
2507
2591
  "codex"
2508
2592
  );
2509
- checks.push({
2510
- ok: hasCodexMcpDriver,
2511
- label: hasCodexMcpDriver
2512
- ? "Codex CLI watch mode driver pinned to codex"
2513
- : "Codex CLI watch mode driver missing",
2514
- });
2515
- checks.push({
2516
- ok: hasCodexPluginDriver,
2517
- label: hasCodexPluginDriver
2518
- ? "Codex Desktop plugin watch mode driver pinned to codex"
2519
- : "Codex Desktop plugin watch mode driver missing",
2520
- });
2593
+ checks.push(
2594
+ warningCheck(
2595
+ hasCodexMcpDriver,
2596
+ hasCodexMcpDriver
2597
+ ? "Codex CLI watch mode driver pinned to codex"
2598
+ : "Codex CLI watch mode driver missing"
2599
+ )
2600
+ );
2601
+ checks.push(
2602
+ warningCheck(
2603
+ hasCodexPluginDriver,
2604
+ hasCodexPluginDriver
2605
+ ? "Codex Desktop plugin watch mode driver pinned to codex"
2606
+ : "Codex Desktop plugin watch mode driver missing"
2607
+ )
2608
+ );
2521
2609
  const hasCodexAgentRegistrations = codexCustomAgents().every((agent) =>
2522
2610
  configContent.includes(`[agents.${agent.name}]`)
2523
2611
  );
2524
- checks.push({
2525
- ok: hasCodexAgentRegistrations,
2526
- label: hasCodexAgentRegistrations
2527
- ? "Codex custom agents registered"
2528
- : "Codex custom agents unregistered",
2529
- });
2612
+ checks.push(
2613
+ warningCheck(
2614
+ hasCodexAgentRegistrations,
2615
+ hasCodexAgentRegistrations
2616
+ ? "Codex custom agents registered"
2617
+ : "Codex custom agents unregistered"
2618
+ )
2619
+ );
2530
2620
  const hasNoLegacyCodexAgents = legacyCodexCustomAgents().every((agent) => {
2531
2621
  const agentPath = join(codexHome(), "agents", agent.filename);
2532
2622
  return (
@@ -2534,27 +2624,126 @@ function verify(opts) {
2534
2624
  !configContent.includes(`[agents.${agent.name}]`)
2535
2625
  );
2536
2626
  });
2537
- checks.push({
2538
- ok: hasNoLegacyCodexAgents,
2539
- label: hasNoLegacyCodexAgents
2540
- ? "Legacy Codex custom agents cleaned"
2541
- : "Legacy Codex custom agents still present",
2542
- });
2627
+ checks.push(
2628
+ warningCheck(
2629
+ hasNoLegacyCodexAgents,
2630
+ hasNoLegacyCodexAgents
2631
+ ? "Legacy Codex custom agents cleaned"
2632
+ : "Legacy Codex custom agents still present"
2633
+ )
2634
+ );
2543
2635
  const hasFlag = configContent.includes(
2544
2636
  "default_mode_request_user_input = true"
2545
2637
  );
2546
- checks.push({
2547
- ok: hasFlag,
2548
- label: hasFlag
2549
- ? "Codex Default-mode request_user_input enabled"
2550
- : "Codex Default-mode request_user_input missing",
2638
+ checks.push(
2639
+ warningCheck(
2640
+ hasFlag,
2641
+ hasFlag
2642
+ ? "Codex Default-mode request_user_input enabled"
2643
+ : "Codex Default-mode request_user_input missing"
2644
+ )
2645
+ );
2646
+ }
2647
+
2648
+ let runtimeMcp;
2649
+ try {
2650
+ const [command, args] = mcpCommand(opts);
2651
+ runtimeMcp = await verifySellableMcpRuntime({
2652
+ command,
2653
+ args,
2654
+ cwd: process.cwd(),
2655
+ env: {
2656
+ ...process.env,
2657
+ SELLABLE_API_URL: opts.apiUrl,
2658
+ SELLABLE_TOKEN: opts.token || process.env.SELLABLE_TOKEN || "",
2659
+ SELLABLE_WORKSPACE_ID:
2660
+ opts.workspaceId || process.env.SELLABLE_WORKSPACE_ID || "",
2661
+ },
2662
+ requiredTools: REQUIRED_SELLABLE_MCP_TOOLS,
2551
2663
  });
2664
+ } catch (err) {
2665
+ runtimeMcp = {
2666
+ ok: false,
2667
+ skipped: false,
2668
+ command: null,
2669
+ args: [],
2670
+ requiredTools: REQUIRED_SELLABLE_MCP_TOOLS,
2671
+ availableTools: [],
2672
+ missingTools: REQUIRED_SELLABLE_MCP_TOOLS,
2673
+ stderrTail: [],
2674
+ error: err instanceof Error ? err.message : String(err),
2675
+ startedAt,
2676
+ completedAt: new Date().toISOString(),
2677
+ };
2678
+ }
2679
+
2680
+ const runtimeRequired = opts.server !== "hosted";
2681
+ if (runtimeMcp.skipped) {
2682
+ checks.push(
2683
+ warningCheck(
2684
+ false,
2685
+ `MCP runtime verification skipped: ${runtimeMcp.reason || "not available"}`
2686
+ )
2687
+ );
2688
+ } else {
2689
+ checks.push(
2690
+ (runtimeRequired ? requiredCheck : warningCheck)(
2691
+ runtimeMcp.ok,
2692
+ runtimeMcp.ok
2693
+ ? `MCP runtime exposes required tools (${runtimeMcp.availableTools.length} total)`
2694
+ : `MCP runtime missing tools: ${runtimeMcp.missingTools.join(", ") || runtimeMcp.error || "unknown failure"}`
2695
+ )
2696
+ );
2552
2697
  }
2553
2698
 
2554
- for (const c of checks) {
2555
- if (c.ok) logMilestone(c.label);
2556
- else logWarn(c.label);
2699
+ const requiredFailures = checks.filter(
2700
+ (check) => check.required && !check.ok
2701
+ );
2702
+ const summary = {
2703
+ ok: requiredFailures.length === 0,
2704
+ version: getInstallVersion(),
2705
+ host: opts.host,
2706
+ server: opts.server,
2707
+ apiUrl: opts.apiUrl,
2708
+ packages: {
2709
+ install: INSTALL_PACKAGE_SPEC,
2710
+ mcp: opts.mcpPackage,
2711
+ },
2712
+ checks,
2713
+ runtimeMcp,
2714
+ threadExposureNotice:
2715
+ "If this verification passes but an already-open Codex or Claude session cannot see Sellable tools, restart that agent session so it reloads MCP/plugin surfaces.",
2716
+ startedAt,
2717
+ completedAt: new Date().toISOString(),
2718
+ };
2719
+
2720
+ if (opts.artifactPath) {
2721
+ writeFile(
2722
+ opts.artifactPath,
2723
+ `${JSON.stringify(summary, null, 2)}\n`,
2724
+ { ...opts, dryRun: false },
2725
+ 0o600
2726
+ );
2557
2727
  }
2728
+
2729
+ if (opts.json) {
2730
+ console.log(JSON.stringify(summary, null, 2));
2731
+ } else {
2732
+ for (const c of checks) {
2733
+ if (c.ok) logMilestone(c.label);
2734
+ else logWarn(c.label);
2735
+ }
2736
+ }
2737
+
2738
+ if (requiredFailures.length > 0) {
2739
+ throw new Error(
2740
+ `Sellable verification failed: ${requiredFailures
2741
+ .map((check) => check.label)
2742
+ .join("; ")}`
2743
+ );
2744
+ }
2745
+
2746
+ return summary;
2558
2747
  }
2559
2748
 
2560
2749
  function visibleLen(s) {
@@ -2619,7 +2808,9 @@ function printInstallAgentBox(title, installCmd, docsUrl) {
2619
2808
  console.log(line(` ${C.bold}STEP 2${C.reset} Then re-run Sellable:`));
2620
2809
  console.log(blank());
2621
2810
  console.log(
2622
- line(` ${C.cyan}npx -y @sellable/install@latest${C.reset}`)
2811
+ line(
2812
+ ` ${C.cyan}curl -fsSL "https://app.sellable.dev/api/v2/cli/install" | sh${C.reset}`
2813
+ )
2623
2814
  );
2624
2815
  console.log(blank());
2625
2816
  console.log(line(` ${C.grey}Docs: ${docsUrl}${C.reset}`));
@@ -2938,7 +3129,9 @@ function runUninstall() {
2938
3129
  console.log(` ${C.bold}Uninstalled.${C.reset}`);
2939
3130
  console.log("");
2940
3131
  console.log(` Reinstall anytime:`);
2941
- console.log(` ${C.cyan}npx -y @sellable/install@latest${C.reset}`);
3132
+ console.log(
3133
+ ` ${C.cyan}curl -fsSL "https://app.sellable.dev/api/v2/cli/install" | sh${C.reset}`
3134
+ );
2942
3135
  console.log("");
2943
3136
  }
2944
3137
 
@@ -3029,7 +3222,7 @@ async function main() {
3029
3222
  ` ${C.bold}Connecting Sellable to Claude Code and Codex…${C.reset}`
3030
3223
  );
3031
3224
  console.log("");
3032
- } else {
3225
+ } else if (!opts.json) {
3033
3226
  printBanner();
3034
3227
  console.log(` ${C.bold}Verifying Sellable install…${C.reset}`);
3035
3228
  console.log("");
@@ -3068,7 +3261,7 @@ async function main() {
3068
3261
  console.log("");
3069
3262
  logStep("Dry run complete; no files were written.");
3070
3263
  } else if (opts.verifyOnly) {
3071
- verify(opts);
3264
+ await verify(opts);
3072
3265
  }
3073
3266
 
3074
3267
  if (!opts.verifyOnly) {
@@ -0,0 +1,152 @@
1
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
3
+
4
+ const MAX_STDERR_LINES = 80;
5
+ const DEFAULT_VERIFY_TIMEOUT_MS = 10_000;
6
+
7
+ export const REQUIRED_SELLABLE_MCP_TOOLS = [
8
+ "get_auth_status",
9
+ "bootstrap_create_campaign",
10
+ "create_campaign",
11
+ "import_leads",
12
+ "update_campaign",
13
+ "get_subskill_prompt",
14
+ "get_subskill_asset",
15
+ ];
16
+
17
+ function collectStderrLines(stream) {
18
+ const lines = [];
19
+ if (!stream) return lines;
20
+
21
+ let buffer = "";
22
+ stream.on("data", (chunk) => {
23
+ buffer += chunk.toString("utf8");
24
+ const parts = buffer.split(/\r?\n/);
25
+ buffer = parts.pop() ?? "";
26
+ for (const line of parts) {
27
+ if (!line) continue;
28
+ lines.push(redact(line));
29
+ if (lines.length > MAX_STDERR_LINES) lines.shift();
30
+ }
31
+ });
32
+ stream.on("end", () => {
33
+ if (!buffer) return;
34
+ lines.push(redact(buffer));
35
+ if (lines.length > MAX_STDERR_LINES) lines.shift();
36
+ });
37
+
38
+ return lines;
39
+ }
40
+
41
+ function redact(value) {
42
+ let out = String(value ?? "");
43
+ const secretValues = [
44
+ process.env.SELLABLE_TOKEN,
45
+ process.env.SUPABASE_SERVICE_ROLE_KEY,
46
+ process.env.OPENAI_API_KEY,
47
+ process.env.ANTHROPIC_API_KEY,
48
+ ].filter(Boolean);
49
+
50
+ for (const secret of secretValues) {
51
+ out = out.split(secret).join("[redacted]");
52
+ }
53
+
54
+ out = out.replace(
55
+ /(https?:\/\/[^:\s/]+:)([^@\s/]+)(@)/g,
56
+ "$1[redacted]$3"
57
+ );
58
+ out = out.replace(/skt_(?:live|test|dev)_[A-Za-z0-9_-]+/g, "[redacted-token]");
59
+ out = out.replace(/Bearer\s+[A-Za-z0-9._-]+/g, "Bearer [redacted]");
60
+ return out;
61
+ }
62
+
63
+ function makeEnv(env) {
64
+ const out = {};
65
+ for (const [key, value] of Object.entries(env || process.env)) {
66
+ if (typeof value === "string") out[key] = value;
67
+ }
68
+ return out;
69
+ }
70
+
71
+ export function missingRequiredTools(tools, requiredTools) {
72
+ const available = new Set((tools || []).map((tool) => tool.name));
73
+ return requiredTools.filter((name) => !available.has(name));
74
+ }
75
+
76
+ export async function verifySellableMcpRuntime({
77
+ command,
78
+ args = [],
79
+ cwd = process.cwd(),
80
+ env = process.env,
81
+ requiredTools = REQUIRED_SELLABLE_MCP_TOOLS,
82
+ timeoutMs = Number(process.env.SELLABLE_VERIFY_TIMEOUT_MS || "") ||
83
+ DEFAULT_VERIFY_TIMEOUT_MS,
84
+ }) {
85
+ const startedAt = new Date().toISOString();
86
+
87
+ if (command === "hosted") {
88
+ return {
89
+ ok: false,
90
+ skipped: true,
91
+ reason:
92
+ "Hosted MCP verification is not available through stdio tools/list.",
93
+ command,
94
+ args,
95
+ requiredTools,
96
+ availableTools: [],
97
+ missingTools: requiredTools,
98
+ stderrTail: [],
99
+ startedAt,
100
+ completedAt: new Date().toISOString(),
101
+ };
102
+ }
103
+
104
+ const transport = new StdioClientTransport({
105
+ command,
106
+ args,
107
+ cwd,
108
+ env: makeEnv(env),
109
+ stderr: "pipe",
110
+ });
111
+ const stderrLines = collectStderrLines(transport.stderr);
112
+ const client = new Client(
113
+ { name: "sellable-install-verify", version: "1.0.0" },
114
+ { capabilities: {} }
115
+ );
116
+
117
+ let availableTools = [];
118
+ let missingTools = [...requiredTools];
119
+ let error = null;
120
+
121
+ try {
122
+ await client.connect(transport);
123
+ const toolList = await client.listTools(undefined, {
124
+ timeout: timeoutMs,
125
+ maxTotalTimeout: timeoutMs,
126
+ });
127
+ availableTools = toolList.tools.map((tool) => tool.name).sort();
128
+ missingTools = missingRequiredTools(toolList.tools, requiredTools);
129
+ } catch (err) {
130
+ error = err instanceof Error ? err.message : String(err);
131
+ } finally {
132
+ try {
133
+ await client.close();
134
+ } catch {
135
+ // Preserve the verification result even if the child exits noisily.
136
+ }
137
+ }
138
+
139
+ return {
140
+ ok: !error && missingTools.length === 0,
141
+ skipped: false,
142
+ command,
143
+ args,
144
+ requiredTools,
145
+ availableTools,
146
+ missingTools,
147
+ stderrTail: stderrLines,
148
+ error: error ? redact(error) : null,
149
+ startedAt,
150
+ completedAt: new Date().toISOString(),
151
+ };
152
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/install",
3
- "version": "0.1.205",
3
+ "version": "0.1.207",
4
4
  "type": "module",
5
5
  "description": "One-command installer for Sellable MCP in Claude Code and Codex",
6
6
  "bin": {
@@ -12,10 +12,14 @@
12
12
  },
13
13
  "files": [
14
14
  "bin",
15
+ "lib",
15
16
  "agents",
16
17
  "skill-templates",
17
18
  "README.md"
18
19
  ],
20
+ "dependencies": {
21
+ "@modelcontextprotocol/sdk": "^1.25.2"
22
+ },
19
23
  "keywords": [
20
24
  "sellable",
21
25
  "mcp",
@@ -100,20 +100,31 @@ The default path stays the existing first campaign-table execution slice:
100
100
  review the normal `reviewBatchLimit:15`, approve reviewed draft rows, then move
101
101
  to Settings/sequence/final greenlight. Only call
102
102
  `start_campaign_message_preparation` when the user explicitly asks for more
103
- prepared messages or a send count. If the user says "prepare/generate X
103
+ prepared messages, a send count, or language like "fill up/load sends for these
104
+ senders." Treat those requests as capacity-fill preparation: calculate the
105
+ bounded target from sender capacity when needed, then let the preparation job
106
+ queue pending `Enrich Prospect` cells, wait for ICP/rubric and Generate Message
107
+ cells to cascade, and mark ready or approve only the target cohort. Do not
108
+ count `checkedRows` as enriched rows; it is only the table cursor. Use
109
+ `progress.enrichedRows`, `progress.needsEnrichRows`, `activeCellCount`,
110
+ `preparedMessages`, `approvedRows`, target, estimated row budget remaining, and
111
+ `stopReason` to explain progress. If the user says "prepare/generate X
104
112
  messages", set `targetPreparedMessages:X`, omit `maxRowsToCheck`, and keep
105
- `approvalMode:"mark_ready"`. The backend calibrates on at least 100 rows,
106
- estimates the row budget from observed rubric/pass yield, caps
113
+ `approvalMode:"mark_ready"`. The backend calibrates on at least 100 actually
114
+ enriched rows, estimates the row budget from observed rubric/pass yield, caps
107
115
  `maxRowsToCheck` at 2500, then adapts later batches up to 250 rows while
108
- recalculating yield. Poll
109
- `get_campaign_message_preparation_status` for preparation-job status: checked
110
- rows, passed/prepared/approved count, target, estimated row budget remaining,
111
- and stop reason. If the user says "approve X messages", use
116
+ recalculating yield. If the user says "approve X messages", use
112
117
  `approvalMode:"approve"` but still do not launch. If the user says "schedule X
113
- sends", use `approvalMode:"approve"` to approve exactly the bounded X-message
114
- cohort during preparation, then continue through sender, sequence, and final
115
- launch greenlight; the launch path must verify that bounded cohort and must not
116
- broad approve-all.
118
+ sends" or asks to fill sender sends, use `approvalMode:"approve"` to approve
119
+ exactly the bounded X-message cohort during preparation, then continue through
120
+ sender, sequence, and final launch greenlight; the launch path must verify that
121
+ bounded cohort and must not broad approve-all.
122
+ When approving reviewed draft rows in the campaign table, resolve the actual
123
+ visible `Approved` cells with `select_campaign_cells({ columnRole: "approved",
124
+ rowSelector: { type: "rowIds", rowIds } })` and `update_cell` those returned
125
+ cell IDs. Do not rely on `approveCellId` from `get_rows` unless it matches the
126
+ semantic selector result; it may be a row-level helper and leave the UI checkbox
127
+ unchecked.
117
128
  Treat `campaignId` as `CampaignOffer.id`. Low level selector/queue tools are
118
129
  diagnostics and recovery only for this lane. If the user asks to stop
119
130
  preparation, the target is wrong, or status shows the wrong campaign/table, call
@@ -168,6 +179,12 @@ person/company this campaign is for, then I’ll turn that into a campaign brief
168
179
  before we move into lead sourcing.
169
180
  ```
170
181
 
182
+ Exception: if `bootstrap_create_campaign.modelQuality.status === "warn"`,
183
+ the first visible campaign message must be the model-quality warning from
184
+ `modelQuality.message`. Ask the user to switch to the configured minimum model
185
+ or explicitly continue anyway before identity setup, research, lead filtering,
186
+ message generation, or launch review.
187
+
171
188
  If a linked/local skill file is stale or missing, silently use the installed
172
189
  `sellable@sellable` plugin copy. Do not tell the user about the stale link,
173
190
  the old version, or the replacement path.
@@ -795,12 +812,15 @@ there.
795
812
  MCP tool access is required. First call `mcp__sellable__get_auth_status({})`
796
813
  directly. If that tool is unavailable, stop and say this is a Codex
797
814
  install/reload problem, not a campaign problem. Tell the user to
798
- run `npx -y @sellable/install@latest --host all` so the packaged MCP server,
799
- Codex Desktop plugin, and Sellable skill bundle are installed. If they want a
800
- CLI verification, tell them to run `sellable --verify-only --host all`. After
801
- that, they must fully quit and reopen Codex Desktop before starting a new
802
- thread. Do not use `scripts/mcp/sellable-tool-call.mjs`, `npm run`,
803
- `node`, or any local harness as a fallback for this interactive skill.
815
+ run `curl -fsSL "https://app.sellable.dev/api/v2/cli/install" | sh` so the
816
+ packaged MCP server, Codex Desktop plugin, and Sellable skill bundle are
817
+ installed. If they want an agent-readable checklist, tell them:
818
+ `Install Sellable CLI and skills using https://app.sellable.dev/agent-install.txt`.
819
+ For CLI verification, tell them to run
820
+ `sellable --verify-only --host all --json --artifact "$HOME/.local/sellable/app-sellable-dev/installer/.last-verify.json"`.
821
+ After that, they must fully quit and reopen Codex Desktop before starting a new
822
+ thread. Do not use `scripts/mcp/sellable-tool-call.mjs`, `npm run`, `node`, or
823
+ any local harness as a fallback for this interactive skill.
804
824
  Do not mention prompt loading, local skill files, missing linked versions,
805
825
  plugin cache paths, MCP namespaces, or runbooks in customer-facing progress
806
826
  updates.
@@ -912,8 +932,11 @@ updates.
912
932
  - Do not call `mcp__sellable__get_campaigns`.
913
933
  - Do not call `mcp__sellable__get_campaign` to hunt for IDs.
914
934
  - Do not call `mcp__sellable__create_campaign({ campaignId: ... })` unless the user supplied that id.
915
- 6. Call `mcp__sellable__bootstrap_create_campaign({ flowVersion: "v2", campaignId? })`.
935
+ 6. Call `mcp__sellable__bootstrap_create_campaign({ flowVersion: "v2", campaignId?, host?, model?, reasoningEffort? })`.
936
+ Pass the current host, model, and reasoning when the host exposes them.
916
937
  7. If `safeToProceed !== true`, stop and show `blockingErrors` + `nextStep`.
938
+ 8. If `modelQuality.status === "warn"`, show `modelQuality.message` before any
939
+ setup/research and wait for the user to switch or explicitly continue.
917
940
 
918
941
  ## Execute Workflow
919
942