@sellable/install 0.1.211 → 0.1.213

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.
@@ -72,8 +72,9 @@ const CODEX_PLUGIN_COMPAT_VERSIONS = [
72
72
  "0.1.40",
73
73
  "0.1.41",
74
74
  ];
75
- const INSTALL_PACKAGE_SPEC =
75
+ const RAW_INSTALL_PACKAGE_SPEC =
76
76
  process.env.SELLABLE_INSTALL_PACKAGE_SPEC || "@sellable/install@latest";
77
+ const INSTALL_PACKAGE_SPEC = normalizeWindowsPackageSpec(RAW_INSTALL_PACKAGE_SPEC);
77
78
 
78
79
  const useColor = Boolean(output.isTTY) && process.env.NO_COLOR === undefined;
79
80
  const C = {
@@ -89,6 +90,15 @@ const C = {
89
90
 
90
91
  let VERBOSE = false;
91
92
 
93
+ function normalizeWindowsPackageSpec(value) {
94
+ if (typeof value !== "string") return value;
95
+ if (/^[A-Za-z]:\//.test(value)) return value.replace(/\//g, "\\");
96
+ if (/^\/[A-Za-z]\//.test(value)) {
97
+ return `${value[1]}:${value.slice(2)}`.replace(/\//g, "\\");
98
+ }
99
+ return value;
100
+ }
101
+
92
102
  function logVerbose(line) {
93
103
  if (VERBOSE) console.log(line);
94
104
  }
@@ -263,7 +273,7 @@ function parseArgs(argv) {
263
273
  } else if (arg === "--api-url") {
264
274
  opts.apiUrl = next();
265
275
  } else if (arg === "--mcp-package") {
266
- opts.mcpPackage = next();
276
+ opts.mcpPackage = normalizeWindowsPackageSpec(next());
267
277
  } else if (arg === "--local-command") {
268
278
  opts.localCommand = next();
269
279
  } else if (arg === "--hosted-url") {
@@ -320,10 +330,6 @@ function isWindowsPlatform(platform = process.platform) {
320
330
  return platform === "win32";
321
331
  }
322
332
 
323
- function packageRunnerCommand(platform = process.platform) {
324
- return isWindowsPlatform(platform) ? "npx.cmd" : "npx";
325
- }
326
-
327
333
  function packageManagerCommand(platform = process.platform) {
328
334
  return isWindowsPlatform(platform) ? "npm.cmd" : "npm";
329
335
  }
@@ -541,6 +547,14 @@ function upsertTomlTable(content, tableName, block) {
541
547
  return `${content.trimEnd()}\n\n${normalizedBlock}\n`;
542
548
  }
543
549
 
550
+ function readTomlTable(content, tableName) {
551
+ const tablePattern = new RegExp(
552
+ `(^|\\n)\\[${escapeRegExp(tableName)}\\]\\n([\\s\\S]*?)(?=\\n\\[[^\\n]+\\]|$)`
553
+ );
554
+ const match = content.match(tablePattern);
555
+ return match ? match[2] : "";
556
+ }
557
+
544
558
  function upsertTomlBoolean(content, tableName, key, value) {
545
559
  const line = `${key} = ${value ? "true" : "false"}`;
546
560
  const tablePattern = new RegExp(
@@ -668,12 +682,13 @@ function codexPluginMcp(opts) {
668
682
  };
669
683
  }
670
684
 
685
+ const [command, args] = mcpCommand(opts);
671
686
  return {
672
687
  mcpServers: {
673
688
  sellable: {
674
689
  type: "stdio",
675
- command: packageRunnerCommand(),
676
- args: ["-y", opts.mcpPackage],
690
+ command,
691
+ args,
677
692
  env: {
678
693
  SELLABLE_WATCH_MODE_DRIVER: "codex",
679
694
  },
@@ -771,46 +786,54 @@ function codexSkillOpenAiYaml(displayName, description) {
771
786
  `;
772
787
  }
773
788
 
774
- function createCampaignHostContract(host) {
789
+ function installedHostContract(host, commandName) {
775
790
  if (host === "codex") {
776
791
  return `## Installed Host Contract
777
792
 
778
- This installed skill is running in Codex. Treat Codex as the current host, use
779
- \`request_user_input\` for structured questions when it is exposed, use
780
- \`$sellable:create-campaign\` in user-facing retry instructions, and call
781
- \`mcp__sellable__bootstrap_create_campaign\` with \`host: "Codex"\` plus the
782
- current Codex model and reasoning when available.
793
+ This installed skill is running in Codex. When the shared workflow body or
794
+ fallback text mentions both Claude Code and Codex for internal parity, choose
795
+ the Codex instruction for customer-facing language and host functions.
783
796
 
784
- Do not describe this run as Claude Code and do not recommend Claude Code-only
785
- commands or settings from this Codex skill.`;
786
- }
797
+ - Customer-facing command: \`$sellable:${commandName}\`
798
+ - Structured question function: \`request_user_input\`
799
+ - Bootstrap host label: \`host: "Codex"\`
800
+ - Install/reload blocker label: Codex install/reload problem
801
+ - Reload instruction: fully quit and reopen Codex Desktop, then start a new thread
787
802
 
803
+ Do not tell Codex users to run \`/sellable:${commandName}\`, use
804
+ \`AskUserQuestion\`, or restart Claude Code. Do not describe this run as Claude Code.`;
805
+ }
788
806
  if (host === "claude") {
789
807
  return `## Installed Host Contract
790
808
 
791
- This installed command is running in Claude Code. Treat Claude Code as the
792
- current host, use \`AskUserQuestion\` for structured questions, use
793
- \`/sellable:create-campaign\` in user-facing retry instructions, and call
794
- \`mcp__sellable__bootstrap_create_campaign\` with \`host: "Claude Code"\` plus
795
- the current Claude Code model and reasoning when available.
809
+ This installed command is running in Claude Code. When the shared workflow body
810
+ or fallback text mentions both Claude Code and Codex for internal parity, choose
811
+ the Claude Code instruction for customer-facing language and host functions.
796
812
 
797
- Do not describe this run as Codex and do not recommend Codex-only commands or
798
- settings from this Claude Code command.`;
799
- }
813
+ - Customer-facing command: \`/sellable:${commandName}\`
814
+ - Structured question function: \`AskUserQuestion\`
815
+ - Bootstrap host label: \`host: "Claude Code"\`
816
+ - Install/reload blocker label: Claude Code install/reload problem
817
+ - Reload instruction: fully quit and reopen Claude Code, then start a new session
800
818
 
819
+ Do not tell Claude Code users to run \`$sellable:${commandName}\`, use
820
+ \`request_user_input\`, or restart Codex Desktop. Do not describe this run as Codex.`;
821
+ }
801
822
  return "";
802
823
  }
803
824
 
804
- function addCreateCampaignHostContract(markdown, host) {
805
- const contract = createCampaignHostContract(host);
806
- if (!contract) return markdown;
825
+ function stampInstalledHost(markdown, host, commandName) {
826
+ const preamble = installedHostContract(host, commandName);
827
+ if (!preamble || markdown.includes("## Installed Host Contract")) {
828
+ return markdown;
829
+ }
807
830
  return String(markdown).replace(
808
- "\n# Sellable Create Campaign\n",
809
- `\n# Sellable Create Campaign\n\n${contract}\n`
831
+ /(^# .+\n\n)/m,
832
+ `$1${preamble}\n\n`
810
833
  );
811
834
  }
812
835
 
813
- function createCampaignSkillMd({ host = "shared" } = {}) {
836
+ function createCampaignSkillMd(host = "shared") {
814
837
  // Single source of truth: ../skill-templates/create-campaign.md, which is
815
838
  // copied from the canonical mcp/sellable/skills/create-campaign/SKILL.md at
816
839
  // publish time via scripts/sync-skill-templates.mjs (prepublishOnly hook).
@@ -825,15 +848,16 @@ function createCampaignSkillMd({ host = "shared" } = {}) {
825
848
  "create-campaign.md"
826
849
  );
827
850
  if (existsSync(templatePath)) {
828
- return addCreateCampaignHostContract(
851
+ return stampInstalledHost(
829
852
  readFileSync(templatePath, "utf8"),
830
- host
853
+ host,
854
+ "create-campaign"
831
855
  );
832
856
  }
833
857
  } catch {
834
858
  // fall through to hardcoded fallback below
835
859
  }
836
- return `---
860
+ return stampInstalledHost(`---
837
861
  name: create-campaign
838
862
  description: Create a Sellable campaign through the approval-gated workflow.
839
863
  allowed-tools:
@@ -856,17 +880,11 @@ person/company this campaign is for, then I’ll turn that into a campaign brief
856
880
  before anything is created.
857
881
  \`\`\`
858
882
 
859
- Exception: if \`bootstrap_create_campaign.modelQuality.status === "warn"\` and
860
- \`bootstrap_create_campaign.modelQuality.metadataStale !== true\`, the first
861
- visible campaign message must be the model-quality warning from
862
- \`modelQuality.message\`. Ask the user to switch to the configured minimum model
863
- or explicitly continue anyway before identity setup, research, lead filtering,
864
- message generation, or launch review. If \`metadataStale === true\`, continue
865
- normally and do not ask the user to switch.
866
-
867
- If \`bootstrap_create_campaign.modelQuality.metadataStale === true\`, continue
868
- normally. Do not ask the user to switch models; this is an accepted Codex host
869
- metadata mismatch.
883
+ Exception: if \`bootstrap_create_campaign.modelQuality.status === "warn"\`, the
884
+ first visible campaign message must be the model-quality warning from
885
+ \`modelQuality.message\`. Only trust that warning when bootstrap received active
886
+ turn/runtime metadata or an explicit user-confirmed model. Do not warn from
887
+ config defaults, stale host labels, or inferred model names.
870
888
 
871
889
  If a linked/local skill file is stale or missing, silently use the installed
872
890
  \`sellable@sellable\` plugin copy. Do not tell the user about the stale link,
@@ -884,6 +902,56 @@ clear business decisions, tradeoffs, and approval gates. Use product language:
884
902
  - "I can create a draft shell for you to watch with approval gates before
885
903
  sourcing", not mutation jargon
886
904
 
905
+ ## Active Model Metadata
906
+
907
+ Before calling \`bootstrap_create_campaign\`, collect the active host model data
908
+ only from a source that can describe this turn:
909
+
910
+ - Codex: if \`mcp__node_repl__js\` is available, inspect only
911
+ \`nodeRepl.requestMeta["x-codex-turn-metadata"].model\` and
912
+ \`nodeRepl.requestMeta["x-codex-turn-metadata"].reasoning_effort\`. Pass those
913
+ values as \`model\` and \`reasoningEffort\`, with
914
+ \`modelMetadataSource: "codex_turn_metadata"\`.
915
+ - Codex fallback/cross-check: if active turn metadata is unavailable, you may
916
+ inspect \`~/.codex/config.toml\` for \`model\` and
917
+ \`model_reasoning_effort\`, but pass
918
+ \`modelMetadataSource: "codex_config_fallback"\` and do not treat it as a
919
+ reason to ask the user to switch models. Active turn metadata wins if it
920
+ differs from config.
921
+ - Claude Code / Opus: use active Claude runtime metadata only if the current
922
+ host exposes model and effort for this same session, with
923
+ \`modelMetadataSource: "claude_runtime_metadata"\`. Current Claude Code MCP
924
+ tool calls may not include model or effort metadata; when they do not, omit
925
+ \`model\`, \`reasoningEffort\`, and \`modelMetadataSource\`.
926
+ - Claude Code session-context self-report: if your current Claude session
927
+ context explicitly states both the exact model ID and effort/thinking level,
928
+ report it internally in this shape and pass it to bootstrap with
929
+ \`modelMetadataSource: "claude_session_context"\`:
930
+
931
+ \`\`\`text
932
+ - Model (name): ...
933
+ - Model (ID): ...
934
+ - Reasoning effort: ...
935
+ - Source: active Claude Code session context
936
+ \`\`\`
937
+
938
+ Use this only when the values are explicitly present in the current session
939
+ context. Do not infer an ID from the friendly name, do not infer effort from
940
+ \`alwaysThinkingEnabled\`, and do not show this self-report to the user during
941
+ normal campaign setup.
942
+ - Do not run a nested \`claude -p\`, inspect \`~/.claude/settings.json\`, or read
943
+ Claude CLI defaults as proof of the user's active Claude Code session. Those
944
+ checks can validate a new child session or saved defaults, but not this
945
+ session's actual model and effort.
946
+ - If the user explicitly provides active Claude \`/status\` or \`/model\` output
947
+ that includes both model and effort, pass it with
948
+ \`modelMetadataSource: "user_confirmed"\`. If it is missing either model or
949
+ effort, treat the metadata as unknown and continue.
950
+
951
+ Never invent the model or reasoning effort. Never pass config defaults as active
952
+ metadata. If bootstrap returns \`modelQuality.status === "unknown"\`, continue
953
+ without asking the user to switch models.
954
+
887
955
  When explaining source decisions, show the concrete counts behind the
888
956
  logic: lanes searched, timeframe, raw result counts, finalist posts or preview
889
957
  rows, sampled people, sampled fits as n/N (%), estimated usable people, and the
@@ -1305,13 +1373,19 @@ updates.
1305
1373
  - Do not call \`mcp__sellable__get_campaigns\`.
1306
1374
  - Do not call \`mcp__sellable__get_campaign\` to hunt for IDs.
1307
1375
  - Do not call \`mcp__sellable__create_campaign({ campaignId: ... })\` unless the user supplied that id.
1308
- 5. Call \`mcp__sellable__bootstrap_create_campaign({ flowVersion: "v2", campaignId?, host?, model?, reasoningEffort? })\`.
1309
- Pass the current host, model, and reasoning when the host exposes them.
1376
+ 5. Call \`mcp__sellable__bootstrap_create_campaign({ flowVersion: "v2", campaignId?, host?, model?, reasoningEffort?, modelMetadataSource? })\`.
1377
+ Pass model metadata only when collected by the Active Model Metadata rules
1378
+ above. For Codex active turn metadata, pass
1379
+ \`modelMetadataSource: "codex_turn_metadata"\`. For explicit Claude session
1380
+ context, pass \`modelMetadataSource: "claude_session_context"\`. For explicit
1381
+ user-confirmed Claude \`/status\` or \`/model\` output, pass
1382
+ \`modelMetadataSource: "user_confirmed"\` only when it includes both model and
1383
+ effort.
1310
1384
  6. If \`safeToProceed !== true\`, stop and show \`blockingErrors\` + \`nextStep\`.
1311
- 7. If \`modelQuality.status === "warn"\` and \`modelQuality.metadataStale !== true\`,
1312
- show \`modelQuality.message\` before any setup/research and wait for the user
1313
- to switch or explicitly continue. If \`metadataStale === true\`, continue
1314
- normally and do not tell the user to switch.
1385
+ 7. If \`modelQuality.status === "warn"\`, show \`modelQuality.message\` before
1386
+ any setup/research and wait for the user to switch or explicitly continue. If
1387
+ \`modelQuality.status === "unknown"\`, continue; do not tell the user to
1388
+ switch models.
1315
1389
 
1316
1390
  ## Execute Workflow
1317
1391
 
@@ -1392,10 +1466,10 @@ updates.
1392
1466
  If subskill lookup fails, use
1393
1467
  \`mcp__sellable__search_subskill_prompts({ query: "create-campaign-v2" })\`,
1394
1468
  then retry \`get_subskill_prompt\`.
1395
- `;
1469
+ `, host, "create-campaign");
1396
1470
  }
1397
1471
 
1398
- function createAbTestSkillMd() {
1472
+ function createAbTestSkillMd(host = "shared") {
1399
1473
  try {
1400
1474
  const here = dirname(fileURLToPath(import.meta.url));
1401
1475
  const templatePath = join(
@@ -1405,12 +1479,16 @@ function createAbTestSkillMd() {
1405
1479
  "create-ab-test.md"
1406
1480
  );
1407
1481
  if (existsSync(templatePath)) {
1408
- return readFileSync(templatePath, "utf8");
1482
+ return stampInstalledHost(
1483
+ readFileSync(templatePath, "utf8"),
1484
+ host,
1485
+ "create-ab-test"
1486
+ );
1409
1487
  }
1410
1488
  } catch {
1411
1489
  // fall through to hardcoded fallback below
1412
1490
  }
1413
- return `---
1491
+ return stampInstalledHost(`---
1414
1492
  name: create-ab-test
1415
1493
  description: Create a Sellable campaign A/B test from a clean source lead list.
1416
1494
  visibility: public
@@ -1426,11 +1504,11 @@ allowed-tools:
1426
1504
  # Sellable Create A/B Test
1427
1505
 
1428
1506
  Use \`prepare_campaign_ab_test\` to create clean A/B split lead lists and review-copy campaigns. Do not call \`export_table_csv\` from generated campaign workflow tables as lead source data, and do not call \`start_campaign\`.
1429
- `;
1507
+ `, host, "create-ab-test");
1430
1508
  }
1431
1509
 
1432
- function genericSellableSkillMd({ name, title, description }) {
1433
- return `---
1510
+ function genericSellableSkillMd({ name, title, description, host = "shared" }) {
1511
+ return stampInstalledHost(`---
1434
1512
  name: ${name}
1435
1513
  description: ${yamlString(description)}
1436
1514
  allowed-tools:
@@ -1465,11 +1543,11 @@ Desktop, then start a new thread.
1465
1543
  ## MCP Prompt Fallback
1466
1544
 
1467
1545
  If subskill lookup fails, use \`mcp__sellable__search_subskill_prompts({ query: "${name}" })\`, then retry \`get_subskill_prompt\`.
1468
- `;
1546
+ `, host, name);
1469
1547
  }
1470
1548
 
1471
- function foundationSkillMd() {
1472
- return `---
1549
+ function foundationSkillMd(host = "shared") {
1550
+ return stampInstalledHost(`---
1473
1551
  name: foundation
1474
1552
  description: ${yamlString(FOUNDATION_SKILL_DESCRIPTION)}
1475
1553
  allowed-tools:
@@ -1528,11 +1606,11 @@ Desktop, then start a new thread.
1528
1606
  If exact subskill lookup fails, use
1529
1607
  \`mcp__sellable__search_subskill_prompts({ query: "foundation", includePublic: true, includeInternal: true })\`,
1530
1608
  then retry \`get_subskill_prompt\`.
1531
- `;
1609
+ `, host, "foundation");
1532
1610
  }
1533
1611
 
1534
- function contentSkillMd() {
1535
- return `---
1612
+ function contentSkillMd(host = "shared") {
1613
+ return stampInstalledHost(`---
1536
1614
  name: content
1537
1615
  description: Add transcripts and rough ideas, cluster recurring themes, ideate post seeds, and hand draft requests to create-post.
1538
1616
  allowed-tools:
@@ -1598,11 +1676,11 @@ Desktop, then start a new thread.
1598
1676
  If exact subskill lookup fails, use
1599
1677
  \`mcp__sellable__search_subskill_prompts({ query: "content", includePublic: true, includeInternal: true })\`,
1600
1678
  then retry \`get_subskill_prompt\`.
1601
- `;
1679
+ `, host, "content");
1602
1680
  }
1603
1681
 
1604
- function createPostSkillMd() {
1605
- return `---
1682
+ function createPostSkillMd(host = "shared") {
1683
+ return stampInstalledHost(`---
1606
1684
  name: create-post
1607
1685
  description: Capture rough LinkedIn post ideas, develop a valuable premise, research current hooks and audience tension, then save validated drafts in the user's voice.
1608
1686
  allowed-tools:
@@ -1672,7 +1750,7 @@ Desktop, then start a new thread.
1672
1750
  If exact subskill lookup fails, use
1673
1751
  \`mcp__sellable__search_subskill_prompts({ query: "create-post", includePublic: true, includeInternal: true })\`,
1674
1752
  then retry \`get_subskill_prompt\`.
1675
- `;
1753
+ `, host, "create-post");
1676
1754
  }
1677
1755
 
1678
1756
  function createCampaignSoulMd() {
@@ -1869,7 +1947,7 @@ function codexPluginSkills() {
1869
1947
  dir: "sellable-create-campaign",
1870
1948
  displayName: "Sellable Create Campaign",
1871
1949
  description: "Create a Sellable campaign with approval gates",
1872
- skillMd: createCampaignSkillMd({ host: "codex" }),
1950
+ skillMd: createCampaignSkillMd("codex"),
1873
1951
  soulMd: createCampaignSoulMd(),
1874
1952
  },
1875
1953
  {
@@ -2093,7 +2171,7 @@ function claudeCommands() {
2093
2171
  filename: join("sellable", "create-campaign.md"),
2094
2172
  description: "Create a Sellable campaign through the approval-gated workflow.",
2095
2173
  argumentHint: "[campaign goal, company, or offer]",
2096
- skillMd: createCampaignSkillMd({ host: "claude" }),
2174
+ skillMd: createCampaignSkillMd("claude"),
2097
2175
  allowedTools: ["AskUserQuestion", "Task", ...CREATE_CAMPAIGN_ALLOWED_TOOLS],
2098
2176
  },
2099
2177
  {
@@ -2313,6 +2391,7 @@ enabled = false`
2313
2391
  "default_mode_request_user_input",
2314
2392
  true
2315
2393
  );
2394
+ content = upsertCodexMcpServerConfig(content, opts);
2316
2395
  content = upsertTomlTable(
2317
2396
  content,
2318
2397
  "agents",
@@ -2471,6 +2550,68 @@ function codexMcpAddArgs(opts) {
2471
2550
  ];
2472
2551
  }
2473
2552
 
2553
+ function upsertCodexMcpServerConfig(content, opts) {
2554
+ if (opts.server === "hosted") {
2555
+ content = upsertTomlTable(
2556
+ content,
2557
+ "mcp_servers.sellable",
2558
+ `[mcp_servers.sellable]
2559
+ url = ${quoteToml(withHostedWatchModeDriver(opts.hostedUrl, "codex"))}`
2560
+ );
2561
+ content = removeTomlSection(content, "mcp_servers.sellable.env");
2562
+ return content;
2563
+ }
2564
+
2565
+ const [command, args] = mcpCommand(opts);
2566
+ content = upsertTomlTable(
2567
+ content,
2568
+ "mcp_servers.sellable",
2569
+ `[mcp_servers.sellable]
2570
+ command = ${quoteToml(command)}
2571
+ args = ${tomlArray(args)}`
2572
+ );
2573
+ content = upsertTomlTable(
2574
+ content,
2575
+ "mcp_servers.sellable.env",
2576
+ `[mcp_servers.sellable.env]
2577
+ SELLABLE_WATCH_MODE_DRIVER = "codex"`
2578
+ );
2579
+ return content;
2580
+ }
2581
+
2582
+ function codexMcpServerMatches(content, opts) {
2583
+ const server = readTomlTable(content, "mcp_servers.sellable");
2584
+ if (!server) return false;
2585
+ if (opts.server === "hosted") {
2586
+ return (
2587
+ server.includes(
2588
+ `url = ${quoteToml(withHostedWatchModeDriver(opts.hostedUrl, "codex"))}`
2589
+ ) &&
2590
+ !/\bcommand\s*=/.test(server) &&
2591
+ !/\bargs\s*=/.test(server)
2592
+ );
2593
+ }
2594
+
2595
+ const [command, args] = mcpCommand(opts);
2596
+ const env = readTomlTable(content, "mcp_servers.sellable.env");
2597
+ return (
2598
+ server.includes(`command = ${quoteToml(command)}`) &&
2599
+ server.includes(`args = ${tomlArray(args)}`) &&
2600
+ env.includes('SELLABLE_WATCH_MODE_DRIVER = "codex"')
2601
+ );
2602
+ }
2603
+
2604
+ function codexPluginMcpServerMatches(content, opts) {
2605
+ if (!content) return false;
2606
+ try {
2607
+ const actual = JSON.parse(content)?.mcpServers?.sellable;
2608
+ const expected = codexPluginMcp(opts).mcpServers.sellable;
2609
+ return JSON.stringify(actual) === JSON.stringify(expected);
2610
+ } catch {
2611
+ return false;
2612
+ }
2613
+ }
2614
+
2474
2615
  function installClaude(opts) {
2475
2616
  if (!commandExists("claude")) {
2476
2617
  const message =
@@ -2954,6 +3095,30 @@ async function verify(opts) {
2954
3095
  : "Codex Desktop plugin watch mode driver missing"
2955
3096
  )
2956
3097
  );
3098
+ const codexMcpEntriesCanonical = codexMcpServerMatches(
3099
+ configContent,
3100
+ opts
3101
+ );
3102
+ checks.push(
3103
+ warningCheck(
3104
+ codexMcpEntriesCanonical,
3105
+ codexMcpEntriesCanonical
3106
+ ? "Codex CLI Sellable MCP entry canonical"
3107
+ : "Codex CLI Sellable MCP entry stale"
3108
+ )
3109
+ );
3110
+ const codexPluginMcpEntryCanonical = codexPluginMcpServerMatches(
3111
+ pluginMcpContent,
3112
+ opts
3113
+ );
3114
+ checks.push(
3115
+ warningCheck(
3116
+ codexPluginMcpEntryCanonical,
3117
+ codexPluginMcpEntryCanonical
3118
+ ? "Codex Desktop plugin Sellable MCP entry canonical"
3119
+ : "Codex Desktop plugin Sellable MCP entry stale"
3120
+ )
3121
+ );
2957
3122
  const hasCodexAgentRegistrations = codexCustomAgents().every((agent) =>
2958
3123
  configContent.includes(`[agents.${agent.name}]`)
2959
3124
  );
@@ -3,6 +3,8 @@ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
3
3
 
4
4
  const MAX_STDERR_LINES = 80;
5
5
  const DEFAULT_VERIFY_TIMEOUT_MS = 10_000;
6
+ const DEFAULT_VERIFY_ATTEMPTS = 2;
7
+ const DEFAULT_VERIFY_RETRY_DELAY_MS = 750;
6
8
 
7
9
  export const REQUIRED_SELLABLE_MCP_TOOLS = [
8
10
  "get_auth_status",
@@ -125,6 +127,15 @@ export function missingRequiredTools(tools, requiredTools) {
125
127
  return requiredTools.filter((name) => !available.has(name));
126
128
  }
127
129
 
130
+ function sleep(ms) {
131
+ return new Promise((resolve) => setTimeout(resolve, ms));
132
+ }
133
+
134
+ function numberFromEnv(name, fallback) {
135
+ const parsed = Number(process.env[name] || "");
136
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
137
+ }
138
+
128
139
  function textFromToolResult(result) {
129
140
  return (result?.content || [])
130
141
  .filter((part) => part?.type === "text" && typeof part.text === "string")
@@ -291,7 +302,39 @@ async function verifyCreateCampaignSmoke({
291
302
  };
292
303
  }
293
304
 
294
- export async function verifySellableMcpRuntime({
305
+ function summarizeAttempt(result, attempt) {
306
+ return {
307
+ attempt,
308
+ ok: result.ok === true,
309
+ availableToolCount: result.availableTools?.length || 0,
310
+ missingToolCount: result.missingTools?.length || 0,
311
+ error: result.error || null,
312
+ createCampaignSmoke: result.createCampaignSmoke
313
+ ? {
314
+ ok: result.createCampaignSmoke.ok === true,
315
+ error: result.createCampaignSmoke.error || null,
316
+ missingTools: result.createCampaignSmoke.missingTools || [],
317
+ calls: (result.createCampaignSmoke.calls || []).map((call) => ({
318
+ name: call.name,
319
+ ok: call.ok === true,
320
+ })),
321
+ }
322
+ : null,
323
+ stderrTail: result.stderrTail || [],
324
+ startedAt: result.startedAt,
325
+ completedAt: result.completedAt,
326
+ };
327
+ }
328
+
329
+ function shouldRetryRuntimeVerification(result, requiredTools) {
330
+ if (result.ok || result.skipped) return false;
331
+ if (result.error) return true;
332
+ const availableToolCount = result.availableTools?.length || 0;
333
+ const missingToolCount = result.missingTools?.length || 0;
334
+ return availableToolCount === 0 || missingToolCount === requiredTools.length;
335
+ }
336
+
337
+ async function verifySellableMcpRuntimeOnce({
295
338
  command,
296
339
  args = [],
297
340
  cwd = process.cwd(),
@@ -389,3 +432,51 @@ export async function verifySellableMcpRuntime({
389
432
  completedAt: new Date().toISOString(),
390
433
  };
391
434
  }
435
+
436
+ export async function verifySellableMcpRuntime({
437
+ command,
438
+ args = [],
439
+ cwd = process.cwd(),
440
+ env = process.env,
441
+ requiredTools = REQUIRED_SELLABLE_MCP_TOOLS,
442
+ timeoutMs = Number(process.env.SELLABLE_VERIFY_TIMEOUT_MS || "") ||
443
+ DEFAULT_VERIFY_TIMEOUT_MS,
444
+ attempts = numberFromEnv("SELLABLE_VERIFY_ATTEMPTS", DEFAULT_VERIFY_ATTEMPTS),
445
+ retryDelayMs = numberFromEnv(
446
+ "SELLABLE_VERIFY_RETRY_DELAY_MS",
447
+ DEFAULT_VERIFY_RETRY_DELAY_MS
448
+ ),
449
+ }) {
450
+ const attemptSummaries = [];
451
+ let lastResult = null;
452
+
453
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
454
+ const result = await verifySellableMcpRuntimeOnce({
455
+ command,
456
+ args,
457
+ cwd,
458
+ env,
459
+ requiredTools,
460
+ timeoutMs,
461
+ });
462
+ lastResult = result;
463
+ attemptSummaries.push(summarizeAttempt(result, attempt));
464
+
465
+ if (
466
+ attempt >= attempts ||
467
+ !shouldRetryRuntimeVerification(result, requiredTools)
468
+ ) {
469
+ return {
470
+ ...result,
471
+ attempts: attemptSummaries,
472
+ };
473
+ }
474
+
475
+ await sleep(retryDelayMs);
476
+ }
477
+
478
+ return {
479
+ ...lastResult,
480
+ attempts: attemptSummaries,
481
+ };
482
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/install",
3
- "version": "0.1.211",
3
+ "version": "0.1.213",
4
4
  "type": "module",
5
5
  "description": "One-command installer for Sellable MCP in Claude Code and Codex",
6
6
  "bin": {
@@ -179,17 +179,11 @@ person/company this campaign is for, then I’ll turn that into a campaign brief
179
179
  before we move into lead sourcing.
180
180
  ```
181
181
 
182
- Exception: if `bootstrap_create_campaign.modelQuality.status === "warn"` and
183
- `bootstrap_create_campaign.modelQuality.metadataStale !== true`, the first
184
- visible campaign message must be the model-quality warning from
185
- `modelQuality.message`. Ask the user to switch to the configured minimum model
186
- or explicitly continue anyway before identity setup, research, lead filtering,
187
- message generation, or launch review. If `metadataStale === true`, continue
188
- normally and do not ask the user to switch.
189
-
190
- If `bootstrap_create_campaign.modelQuality.metadataStale === true`, continue
191
- normally. Do not ask the user to switch models; this is an accepted Codex host
192
- metadata mismatch.
182
+ Exception: if `bootstrap_create_campaign.modelQuality.status === "warn"`, the
183
+ first visible campaign message must be the model-quality warning from
184
+ `modelQuality.message`. Only trust that warning when bootstrap received active
185
+ turn/runtime metadata or an explicit user-confirmed model. Do not warn from
186
+ config defaults, stale host labels, or inferred model names.
193
187
 
194
188
  If a linked/local skill file is stale or missing, silently use the installed
195
189
  `sellable@sellable` plugin copy. Do not tell the user about the stale link,
@@ -207,6 +201,55 @@ clear business decisions, tradeoffs, and approval gates. Use product language:
207
201
  - "I can create a draft shell for you to watch with approval gates before
208
202
  sourcing", not mutation jargon
209
203
 
204
+ ## Active Model Metadata
205
+
206
+ Before calling `bootstrap_create_campaign`, collect the active host model data
207
+ only from a source that can describe this turn:
208
+
209
+ - Codex: if `mcp__node_repl__js` is available, inspect only
210
+ `nodeRepl.requestMeta["x-codex-turn-metadata"].model` and
211
+ `nodeRepl.requestMeta["x-codex-turn-metadata"].reasoning_effort`. Pass those
212
+ values as `model` and `reasoningEffort`, with
213
+ `modelMetadataSource: "codex_turn_metadata"`.
214
+ - Codex fallback/cross-check: if active turn metadata is unavailable, you may
215
+ inspect `~/.codex/config.toml` for `model` and `model_reasoning_effort`, but
216
+ pass `modelMetadataSource: "codex_config_fallback"` and do not treat it as a
217
+ reason to ask the user to switch models. Active turn metadata wins if it
218
+ differs from config.
219
+ - Claude Code / Opus: use active Claude runtime metadata only if the current
220
+ host exposes model and effort for this same session, with
221
+ `modelMetadataSource: "claude_runtime_metadata"`. Current Claude Code MCP
222
+ tool calls may not include model or effort metadata; when they do not, omit
223
+ `model`, `reasoningEffort`, and `modelMetadataSource`.
224
+ - Claude Code session-context self-report: if your current Claude session
225
+ context explicitly states both the exact model ID and effort/thinking level,
226
+ report it internally in this shape and pass it to bootstrap with
227
+ `modelMetadataSource: "claude_session_context"`:
228
+
229
+ ```text
230
+ - Model (name): ...
231
+ - Model (ID): ...
232
+ - Reasoning effort: ...
233
+ - Source: active Claude Code session context
234
+ ```
235
+
236
+ Use this only when the values are explicitly present in the current session
237
+ context. Do not infer an ID from the friendly name, do not infer effort from
238
+ `alwaysThinkingEnabled`, and do not show this self-report to the user during
239
+ normal campaign setup.
240
+ - Do not run a nested `claude -p`, inspect `~/.claude/settings.json`, or read
241
+ Claude CLI defaults as proof of the user's active Claude Code session. Those
242
+ checks can validate a new child session or saved defaults, but not this
243
+ session's actual model and effort.
244
+ - If the user explicitly provides active Claude `/status` or `/model` output
245
+ that includes both model and effort, pass it with
246
+ `modelMetadataSource: "user_confirmed"`. If it is missing either model or
247
+ effort, treat the metadata as unknown and continue.
248
+
249
+ Never invent the model or reasoning effort. Never pass config defaults as active
250
+ metadata. If bootstrap returns `modelQuality.status === "unknown"`, continue
251
+ without asking the user to switch models.
252
+
210
253
  Approval and safety copy should be tasteful. State what the current approval
211
254
  covers once, in one short sentence, then move on. Do not append repeated
212
255
  "nothing starts / no leads import / no sending" disclaimers to routine progress
@@ -775,10 +818,9 @@ messages, and wait for final launch approval.
775
818
  What's your LinkedIn profile URL or handle?
776
819
  ```
777
820
 
778
- Codex only: do not silently ask intake or approval questions as plain chat when
779
- `request_user_input` is unavailable in an interactive Codex session. Claude Code
780
- uses `AskUserQuestion`; do not apply this Codex setup blocker in Claude Code.
781
- In Codex, stop and tell the user:
821
+ Do not silently ask Codex intake or approval questions as plain chat when
822
+ `request_user_input` is unavailable in an interactive session. Stop and tell
823
+ the user:
782
824
 
783
825
  ```text
784
826
  I need Codex’s quick question panel to collect campaign inputs and approvals cleanly.
@@ -817,18 +859,17 @@ there.
817
859
  ## Bootstrap
818
860
 
819
861
  MCP tool access is required. First call `mcp__sellable__get_auth_status({})`
820
- directly. If that tool is unavailable, stop and say this is a Sellable
821
- install/reload problem for the current host, not a campaign problem. Tell the
822
- user to run `curl -fsSL "https://app.sellable.dev/api/v2/cli/install" | sh` so
823
- the packaged MCP server and the current host integration are installed. If they
824
- want an agent-readable checklist, tell them:
862
+ directly. If that tool is unavailable, stop and say this is a Codex
863
+ install/reload problem, not a campaign problem. Tell the user to
864
+ run `curl -fsSL "https://app.sellable.dev/api/v2/cli/install" | sh` so the
865
+ packaged MCP server, Codex Desktop plugin, and Sellable skill bundle are
866
+ installed. If they want an agent-readable checklist, tell them:
825
867
  `Install Sellable CLI and skills using https://app.sellable.dev/agent-install.txt`.
826
868
  For CLI verification, tell them to run
827
869
  `sellable --verify-only --host all --json --artifact "$HOME/.local/sellable/app-sellable-dev/installer/.last-verify.json"`.
828
- After that, they must fully quit and reopen the current host app before starting
829
- a new thread. In Codex, say Codex Desktop. In Claude Code, say Claude Code. Do
830
- not use `scripts/mcp/sellable-tool-call.mjs`, `npm run`, `node`, or any local
831
- harness as a fallback for this interactive skill.
870
+ After that, they must fully quit and reopen Codex Desktop before starting a new
871
+ thread. Do not use `scripts/mcp/sellable-tool-call.mjs`, `npm run`, `node`, or
872
+ any local harness as a fallback for this interactive skill.
832
873
  Do not mention prompt loading, local skill files, missing linked versions,
833
874
  plugin cache paths, MCP namespaces, or runbooks in customer-facing progress
834
875
  updates.
@@ -940,15 +981,19 @@ updates.
940
981
  - Do not call `mcp__sellable__get_campaigns`.
941
982
  - Do not call `mcp__sellable__get_campaign` to hunt for IDs.
942
983
  - Do not call `mcp__sellable__create_campaign({ campaignId: ... })` unless the user supplied that id.
943
- 6. Call `mcp__sellable__bootstrap_create_campaign({ flowVersion: "v2", campaignId?, host, model?, reasoningEffort? })`.
944
- Pass the explicit current host label: `host: "Codex"` from Codex and
945
- `host: "Claude Code"` from Claude Code. Also pass the current model and
946
- reasoning when the host exposes them.
984
+ 6. Call `mcp__sellable__bootstrap_create_campaign({ flowVersion: "v2", campaignId?, host?, model?, reasoningEffort?, modelMetadataSource? })`.
985
+ Pass model metadata only when collected by the Active Model Metadata rules
986
+ above. For Codex active turn metadata, pass
987
+ `modelMetadataSource: "codex_turn_metadata"`. For explicit Claude session
988
+ context, pass `modelMetadataSource: "claude_session_context"`. For explicit
989
+ user-confirmed Claude `/status` or `/model` output, pass
990
+ `modelMetadataSource: "user_confirmed"` only when it includes both model and
991
+ effort.
947
992
  7. If `safeToProceed !== true`, stop and show `blockingErrors` + `nextStep`.
948
- 8. If `modelQuality.status === "warn"` and `modelQuality.metadataStale !== true`,
949
- show `modelQuality.message` before any setup/research and wait for the user
950
- to switch or explicitly continue. If `metadataStale === true`, continue
951
- normally and do not tell the user to switch.
993
+ 8. If `modelQuality.status === "warn"`, show `modelQuality.message` before any
994
+ setup/research and wait for the user to switch or explicitly continue. If
995
+ `modelQuality.status === "unknown"`, continue; do not tell the user to
996
+ switch models.
952
997
 
953
998
  ## Execute Workflow
954
999