@sellable/install 0.1.218 → 0.1.219

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.
@@ -13,7 +13,6 @@ import {
13
13
  import { homedir } from "node:os";
14
14
  import { dirname, join, relative } from "node:path";
15
15
  import { stdout as output } from "node:process";
16
- import { createInterface } from "node:readline/promises";
17
16
  import { fileURLToPath } from "node:url";
18
17
  import {
19
18
  REQUIRED_SELLABLE_MCP_TOOLS,
@@ -23,7 +22,6 @@ import {
23
22
  const DEFAULT_API_URL = "https://app.sellable.dev";
24
23
  const DEFAULT_SERVER_PACKAGE =
25
24
  process.env.SELLABLE_MCP_PACKAGE || "@sellable/mcp@latest";
26
- const CODEX_INSTALL_URL = "https://chatgpt.com/codex/install";
27
25
 
28
26
  function getInstallVersion() {
29
27
  try {
@@ -177,8 +175,6 @@ Options:
177
175
  --hosted-url <url> Hosted MCP URL for --server hosted.
178
176
  --verbose Print every file write and shell command.
179
177
  --dry-run Print actions without writing or running host commands.
180
- --install-codex-cli Install Codex CLI without prompting if it is missing or blocked.
181
- --no-install-codex-cli Do not prompt to install Codex CLI; install Desktop config only when possible.
182
178
  --verify-only Verify installed host config where possible.
183
179
  --json Print machine-readable verification JSON.
184
180
  --artifact <path> Write verification JSON to a file.
@@ -251,7 +247,6 @@ function parseArgs(argv) {
251
247
  json: false,
252
248
  artifactPath: process.env.SELLABLE_VERIFY_ARTIFACT || "",
253
249
  verbose: false,
254
- codexCliInstall: codexCliInstallPreference(),
255
250
  };
256
251
 
257
252
  for (let i = 0; i < argv.length; i += 1) {
@@ -285,10 +280,6 @@ function parseArgs(argv) {
285
280
  opts.hostedUrl = next();
286
281
  } else if (arg === "--dry-run") {
287
282
  opts.dryRun = true;
288
- } else if (arg === "--install-codex-cli") {
289
- opts.codexCliInstall = "install";
290
- } else if (arg === "--no-install-codex-cli") {
291
- opts.codexCliInstall = "skip";
292
283
  } else if (arg === "--verify-only") {
293
284
  opts.verifyOnly = true;
294
285
  } else if (arg === "--json") {
@@ -312,14 +303,6 @@ function parseArgs(argv) {
312
303
  return opts;
313
304
  }
314
305
 
315
- function codexCliInstallPreference() {
316
- const raw = process.env.SELLABLE_INSTALL_CODEX_CLI || "";
317
- const value = raw.trim().toLowerCase();
318
- if (["1", "true", "yes", "y", "install"].includes(value)) return "install";
319
- if (["0", "false", "no", "n", "skip"].includes(value)) return "skip";
320
- return "ask";
321
- }
322
-
323
306
  function redact(value) {
324
307
  if (!value) return "";
325
308
  if (value.length <= 10) return "[redacted]";
@@ -377,175 +360,6 @@ function commandExistsDetails(command, platform = process.platform) {
377
360
  };
378
361
  }
379
362
 
380
- function summarizeSpawnFailure(result) {
381
- const errorMessage = result.error?.message || "";
382
- const stderr = (result.stderr || "").trim();
383
- const stdout = (result.stdout || "").trim();
384
- return (
385
- errorMessage ||
386
- stderr ||
387
- stdout ||
388
- (Number.isInteger(result.status) ? `exit ${result.status}` : "unknown failure")
389
- );
390
- }
391
-
392
- function codexCliStatus(opts = {}) {
393
- const details = commandExistsDetails("codex");
394
- if (!details.exists) {
395
- return {
396
- exists: false,
397
- usable: false,
398
- reason: "Codex CLI not found on PATH",
399
- details,
400
- };
401
- }
402
- if (opts.dryRun) {
403
- return {
404
- exists: true,
405
- usable: true,
406
- reason: "dry-run",
407
- details,
408
- };
409
- }
410
-
411
- const result = spawnSync("codex", ["mcp", "--help"], {
412
- encoding: "utf8",
413
- stdio: "pipe",
414
- timeout: 10000,
415
- });
416
- if (result.status === 0) {
417
- return {
418
- exists: true,
419
- usable: true,
420
- reason: "Codex CLI responded to `codex mcp --help`",
421
- details,
422
- };
423
- }
424
-
425
- return {
426
- exists: true,
427
- usable: false,
428
- reason: `Codex CLI found but not usable: ${summarizeSpawnFailure(result)}`,
429
- details,
430
- };
431
- }
432
-
433
- function codexDesktopLikelyInstalled(status = null) {
434
- if (process.env.SELLABLE_INSTALL_ASSUME_CODEX_DESKTOP === "1") return true;
435
- const home = codexHome();
436
- const probes = [
437
- status?.details?.stdout || "",
438
- process.env.PATH || "",
439
- ].join("\n");
440
-
441
- if (/WindowsApps[\\/]+codex\.exe/i.test(probes)) return true;
442
- if (existsSync(join(home, "config.toml"))) return true;
443
- if (existsSync(join(home, "plugins", ".plugin-appserver"))) return true;
444
- if (existsSync(join(home, "plugins", "cache"))) return true;
445
- if (process.platform === "darwin" && existsSync("/Applications/Codex.app")) {
446
- return true;
447
- }
448
- return false;
449
- }
450
-
451
- function isNonInteractiveInstall() {
452
- return (
453
- process.env.CI === "true" ||
454
- process.env.SELLABLE_NON_INTERACTIVE === "1" ||
455
- process.env.SELLABLE_NON_INTERACTIVE === "true" ||
456
- process.env.SELLABLE_NON_INTERACTIVE === "yes"
457
- );
458
- }
459
-
460
- async function askYesNo(question, defaultAnswer = false) {
461
- if (!process.stdin.isTTY || !output.isTTY || isNonInteractiveInstall()) {
462
- return null;
463
- }
464
- const suffix = defaultAnswer ? " [Y/n] " : " [y/N] ";
465
- const rl = createInterface({
466
- input: process.stdin,
467
- output: process.stdout,
468
- });
469
- try {
470
- const answer = (await rl.question(`${question}${suffix}`)).trim().toLowerCase();
471
- if (!answer) return defaultAnswer;
472
- return ["y", "yes"].includes(answer);
473
- } finally {
474
- rl.close();
475
- }
476
- }
477
-
478
- function codexInstallCommand(platform = process.platform) {
479
- const override = process.env.SELLABLE_CODEX_INSTALL_COMMAND;
480
- if (override) {
481
- return isWindowsPlatform(platform)
482
- ? ["cmd.exe", ["/d", "/s", "/c", override]]
483
- : ["sh", ["-c", override]];
484
- }
485
- if (isWindowsPlatform(platform)) {
486
- return [
487
- "powershell.exe",
488
- [
489
- "-NoProfile",
490
- "-ExecutionPolicy",
491
- "Bypass",
492
- "-Command",
493
- `$env:CODEX_NON_INTERACTIVE='1'; irm ${CODEX_INSTALL_URL}.ps1 | iex`,
494
- ],
495
- ];
496
- }
497
- return [
498
- "sh",
499
- ["-c", `curl -fsSL ${CODEX_INSTALL_URL}.sh | CODEX_NON_INTERACTIVE=1 sh`],
500
- ];
501
- }
502
-
503
- function codexManualInstallCommand(platform = process.platform) {
504
- return isWindowsPlatform(platform)
505
- ? `$env:CODEX_NON_INTERACTIVE=1; irm ${CODEX_INSTALL_URL}.ps1 | iex`
506
- : `curl -fsSL ${CODEX_INSTALL_URL}.sh | sh`;
507
- }
508
-
509
- async function maybeInstallCodexCli(status, opts) {
510
- if (opts.dryRun) return false;
511
- if (opts.codexCliInstall === "skip") {
512
- logWarn(`${status.reason}. Skipping Codex CLI install by request.`);
513
- return false;
514
- }
515
-
516
- let shouldInstall = opts.codexCliInstall === "install";
517
- if (!shouldInstall) {
518
- const answer = await askYesNo(
519
- `${status.reason}. Install the Codex CLI now?`,
520
- false
521
- );
522
- if (answer === null) {
523
- logWarn(
524
- `${status.reason}. Non-interactive shell; skipping Codex CLI install. Re-run with --install-codex-cli to install it.`
525
- );
526
- return false;
527
- }
528
- shouldInstall = answer;
529
- }
530
-
531
- if (!shouldInstall) {
532
- logWarn(`${status.reason}. Continuing with Codex Desktop plugin setup only.`);
533
- return false;
534
- }
535
-
536
- const [command, args] = codexInstallCommand();
537
- logStep("Installing Codex CLI using the official Codex installer...");
538
- try {
539
- run(command, args, opts);
540
- return true;
541
- } catch (err) {
542
- logWarn(
543
- `Codex CLI install failed: ${err instanceof Error ? err.message : String(err)}`
544
- );
545
- return false;
546
- }
547
- }
548
-
549
363
  function shellQuote(value) {
550
364
  return `'${String(value).replace(/'/g, `'\\''`)}'`;
551
365
  }
@@ -925,9 +739,9 @@ const CREATE_CAMPAIGN_ALLOWED_TOOLS = [
925
739
  "mcp__sellable__save_rubrics",
926
740
  "mcp__sellable__get_campaign_table_schema",
927
741
  "mcp__sellable__select_campaign_cells",
742
+ "mcp__sellable__record_campaign_review_batch",
928
743
  "mcp__sellable__queue_campaign_cells",
929
744
  "mcp__sellable__wait_for_campaign_processing",
930
- "mcp__sellable__fill_campaign_horizon",
931
745
  "mcp__sellable__start_campaign_message_preparation",
932
746
  "mcp__sellable__get_campaign_message_preparation_status",
933
747
  "mcp__sellable__cancel_campaign_message_preparation",
@@ -2641,27 +2455,6 @@ function writeAuth(opts) {
2641
2455
  return { written: true, reused: false };
2642
2456
  }
2643
2457
 
2644
- function resolveWindowsNpmCommand() {
2645
- const explicit =
2646
- process.env.SELLABLE_INSTALL_NPM_CMD_COMMAND ||
2647
- process.env.SELLABLE_INSTALL_NPM_COMMAND;
2648
- if (explicit) return explicit;
2649
-
2650
- const nodeBin = process.env.SELLABLE_INSTALL_NODE_BIN || "";
2651
- for (const name of ["npm.cmd", "npm"]) {
2652
- const candidate = nodeBin ? join(nodeBin, name) : "";
2653
- if (candidate && existsSync(candidate)) return candidate;
2654
- }
2655
-
2656
- if (isWindowsPlatform()) {
2657
- const details = commandExistsDetails("npm.cmd");
2658
- const first = details.stdout.split(/\r?\n/).find((line) => line.trim());
2659
- if (details.exists && first) return first.trim();
2660
- }
2661
-
2662
- return packageManagerCommand("win32");
2663
- }
2664
-
2665
2458
  function installSelfShim(opts) {
2666
2459
  const installDir =
2667
2460
  process.env.SELLABLE_INSTALL_DIR || join(homedir(), ".local", "bin");
@@ -2682,11 +2475,12 @@ exec ${shellQuote(npmCommand)} exec --yes --package ${shellQuote(INSTALL_PACKAGE
2682
2475
  isWindowsPlatform() || process.env.SELLABLE_INSTALL_WRITE_CMD_SHIM === "1";
2683
2476
  if (shouldWriteCmdShim) {
2684
2477
  const cmdPath = `${binPath}.cmd`;
2685
- const npmCmdCommand = resolveWindowsNpmCommand();
2478
+ const npmCmdCommand =
2479
+ process.env.SELLABLE_INSTALL_NPM_CMD_COMMAND ||
2480
+ process.env.SELLABLE_INSTALL_NPM_COMMAND ||
2481
+ packageManagerCommand("win32");
2686
2482
  const cmdPathPrefix = nodeBin ? `set "PATH=${nodeBin};%PATH%"\r\n` : "";
2687
- const cmdShim =
2688
- `@echo off\r\n${cmdPathPrefix}` +
2689
- `call "${npmCmdCommand}" exec --yes --package ${INSTALL_PACKAGE_SPEC} -- sellable %*\r\n`;
2483
+ const cmdShim = `@echo off\r\n${cmdPathPrefix}"${npmCmdCommand}" exec --yes --package ${INSTALL_PACKAGE_SPEC} -- sellable %*\r\n`;
2690
2484
  writeFile(cmdPath, cmdShim, opts, 0o755);
2691
2485
  }
2692
2486
  }
@@ -3026,70 +2820,32 @@ function patchClaudeAlwaysLoad(opts) {
3026
2820
  }
3027
2821
  }
3028
2822
 
3029
- function registerCodexCliMcp(opts) {
3030
- try {
3031
- if (opts.server !== "hosted") {
3032
- run("codex", ["mcp", "remove", "sellable"], {
3033
- ...opts,
3034
- dryRun: opts.dryRun,
3035
- allowFail: true,
3036
- });
3037
- }
3038
- run("codex", codexMcpAddArgs(opts), opts);
3039
- return true;
3040
- } catch (err) {
3041
- logWarn(
3042
- `Codex CLI MCP registration skipped: ${err instanceof Error ? err.message : String(err)}`
3043
- );
3044
- return false;
3045
- }
3046
- }
3047
-
3048
- async function installCodex(opts) {
3049
- let cliStatus = codexCliStatus(opts);
3050
- let desktopLikely = codexDesktopLikelyInstalled(cliStatus);
3051
- let attemptedCliInstall = false;
3052
-
3053
- if (!opts.dryRun && !cliStatus.usable) {
3054
- attemptedCliInstall = await maybeInstallCodexCli(cliStatus, opts);
3055
- if (attemptedCliInstall) {
3056
- cliStatus = codexCliStatus(opts);
3057
- desktopLikely = codexDesktopLikelyInstalled(cliStatus) || desktopLikely;
3058
- if (!cliStatus.usable) {
3059
- logWarn(
3060
- `${cliStatus.reason}. Continuing with Codex Desktop plugin setup where possible.`
3061
- );
3062
- }
3063
- }
3064
- }
3065
-
3066
- const shouldInstallDesktop =
3067
- opts.host === "codex" ||
3068
- cliStatus.usable ||
3069
- desktopLikely ||
3070
- attemptedCliInstall;
3071
- if (!shouldInstallDesktop) {
2823
+ function installCodex(opts) {
2824
+ if (!opts.dryRun && !commandExists("codex")) {
3072
2825
  const message =
3073
- "Codex CLI not found and Codex Desktop was not detected. Install/login to Codex, then rerun: sellable --host codex";
2826
+ "Codex CLI not found. Install/login to Codex, then rerun: sellable --host codex";
3074
2827
  if (opts.host === "all") {
3075
2828
  logWarn(`Skipping Codex: ${message}`);
3076
2829
  return { installed: false };
3077
2830
  }
3078
2831
  throw new Error(message);
3079
2832
  }
3080
-
3081
2833
  if (!opts.dryRun) {
3082
2834
  mkdirSync(codexHome(), { recursive: true, mode: 0o700 });
3083
2835
  }
3084
-
3085
- const cliRegistered = cliStatus.usable ? registerCodexCliMcp(opts) : false;
3086
- if (!cliRegistered && !opts.dryRun) {
3087
- logWarn(
3088
- "Codex CLI MCP registration did not run; writing Codex Desktop plugin and config directly."
3089
- );
2836
+ if (opts.server === "hosted") {
2837
+ run("codex", codexMcpAddArgs(opts), opts);
2838
+ const info = installCodexDesktopPlugin(opts);
2839
+ return { installed: true, ...info };
3090
2840
  }
2841
+ run("codex", ["mcp", "remove", "sellable"], {
2842
+ ...opts,
2843
+ dryRun: opts.dryRun,
2844
+ allowFail: true,
2845
+ });
2846
+ run("codex", codexMcpAddArgs(opts), opts);
3091
2847
  const info = installCodexDesktopPlugin(opts);
3092
- return { installed: true, cliRegistered, ...info };
2848
+ return { installed: true, ...info };
3093
2849
  }
3094
2850
 
3095
2851
  function requiredCheck(ok, label) {
@@ -3240,11 +2996,11 @@ async function verify(opts) {
3240
2996
  );
3241
2997
  }
3242
2998
  if (opts.host === "codex" || opts.host === "all") {
3243
- const codexStatus = codexCliStatus(opts);
2999
+ const hasCodexCli = commandExists("codex");
3244
3000
  checks.push(
3245
3001
  warningCheck(
3246
- codexStatus.usable,
3247
- codexStatus.usable ? "Codex CLI usable" : codexStatus.reason
3002
+ hasCodexCli,
3003
+ hasCodexCli ? "Codex CLI present" : "Codex CLI missing"
3248
3004
  )
3249
3005
  );
3250
3006
  const pluginPath = join(
@@ -3602,8 +3358,8 @@ function printNextSteps(installedHosts, authReused) {
3602
3358
  console.log("");
3603
3359
  printInstallAgentBox(
3604
3360
  "Install Codex",
3605
- codexManualInstallCommand(),
3606
- "https://developers.openai.com/codex/quickstart"
3361
+ "npm install -g @openai/codex",
3362
+ "https://github.com/openai/codex"
3607
3363
  );
3608
3364
  console.log("");
3609
3365
  console.log("");
@@ -4049,7 +3805,7 @@ async function main() {
4049
3805
  }
4050
3806
  }
4051
3807
  if (opts.host === "codex" || opts.host === "all") {
4052
- const result = await installCodex(opts);
3808
+ const result = installCodex(opts);
4053
3809
  if (result.installed) {
4054
3810
  installedHosts.push("Codex");
4055
3811
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/install",
3
- "version": "0.1.218",
3
+ "version": "0.1.219",
4
4
  "type": "module",
5
5
  "description": "One-command installer for Sellable MCP in Claude Code and Codex",
6
6
  "bin": {
@@ -44,9 +44,9 @@ allowed-tools:
44
44
  - mcp__sellable__save_rubrics
45
45
  - mcp__sellable__get_campaign_table_schema
46
46
  - mcp__sellable__select_campaign_cells
47
+ - mcp__sellable__record_campaign_review_batch
47
48
  - mcp__sellable__queue_campaign_cells
48
49
  - mcp__sellable__wait_for_campaign_processing
49
- - mcp__sellable__fill_campaign_horizon
50
50
  - mcp__sellable__start_campaign_message_preparation
51
51
  - mcp__sellable__get_campaign_message_preparation_status
52
52
  - mcp__sellable__cancel_campaign_message_preparation
@@ -105,16 +105,15 @@ most one direct `enrich_with_prospeo` sample when row evidence is too thin.
105
105
  After filter approval, the browser should move to Filter Leads with
106
106
  `currentStep: "apply-icp-rubric"` and show template waiting/approval copy until
107
107
  the template is approved.
108
+ If the bounded filter run later returns `0/N` passes, do not immediately find
109
+ new leads. Load `references/sample-validation-loop.md`, run the zero-pass
110
+ rule-relaxability audit, then choose exactly one recovery: revise saved
111
+ rubrics, record a fresh same-source review batch, or change lead source. Change
112
+ source only when safe relaxation and same-source sampling would still fail or
113
+ would pass bad-fit rows.
108
114
  The default path stays the existing first campaign-table execution slice:
109
115
  review the normal `reviewBatchLimit:15`, approve reviewed draft rows, then move
110
116
  to Settings/sequence/final greenlight. Only call
111
- `fill_campaign_horizon` for explicit source-cleanup horizon-fill requests, such
112
- as "fill sends from Signal Discovery but not John Cutler posts." Run
113
- `fill_campaign_horizon({ action:"audit", ... })` first, then apply with the
114
- returned `stateRevision`. It imports/prepares at most 300 eligible non-excluded
115
- source rows in the first pass, caps prep batches at 100, skips existing rows
116
- from excluded post/author sources, and does not launch the campaign. If the
117
- user only asks for generic extra sends with no source cleanup, use
118
117
  `start_campaign_message_preparation` when the user explicitly asks for more
119
118
  prepared messages, a send count, or language like "fill up/load sends for these
120
119
  senders." Treat those requests as capacity-fill preparation: calculate the
@@ -128,9 +127,8 @@ count `checkedRows` as enriched rows; it is only the table cursor. Use
128
127
  messages", set `targetPreparedMessages:X`, omit `maxRowsToCheck`, and keep
129
128
  `approvalMode:"mark_ready"`. The backend calibrates on at least 100 actually
130
129
  enriched rows, estimates the row budget from observed rubric/pass yield, caps
131
- `maxRowsToCheck` at 300, then continues in batches capped at 100 newly checked
132
- rows. It will not pull another row batch while the current checked batch still
133
- has queueable or active cells. If the user says "approve X messages", use
130
+ `maxRowsToCheck` at 2500, then adapts later batches up to 250 rows while
131
+ recalculating yield. If the user says "approve X messages", use
134
132
  `approvalMode:"approve"` but still do not launch. If the user says "schedule X
135
133
  sends" or asks to fill sender sends, use `approvalMode:"approve"` to approve
136
134
  exactly the bounded X-message cohort during preparation, then continue through
@@ -1081,7 +1079,11 @@ updates.
1081
1079
  rubrics and Message Drafting runs.
1082
1080
  After rubrics save, keep Filter Rules visible for approval; after approval,
1083
1081
  move to Filter Leads with `currentStep: "apply-icp-rubric"` and wait there
1084
- while Message Drafting finishes or the template is approved.
1082
+ while Message Drafting finishes or the template is approved. After template
1083
+ approval and bounded scoring, a `0/N` pass result must run the
1084
+ sample-validation zero-pass rule-relaxability audit before any lead-source
1085
+ revision; the three allowed recoveries are rubric revision, fresh
1086
+ same-source sample, or source revision.
1085
1087
  If filters are skipped, launch Message Drafting before moving to
1086
1088
  Messages/message review; updating `currentStep` to `messages` is not proof
1087
1089
  that the background worker started. Queue the bounded campaign-table