@sellable/mcp 0.1.307 → 0.1.309

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.
@@ -1,9 +1,57 @@
1
+ import { existsSync, readFileSync } from "fs";
2
+ import { join } from "path";
3
+ import { resolveSkillsDir } from "../skills.js";
4
+ const DEFAULT_MODEL_QUALITY_CONFIG = {
5
+ version: 1,
6
+ hosts: {
7
+ claude: {
8
+ label: "Claude Code",
9
+ minimumModel: "Claude Opus 4.8",
10
+ familyKeywords: ["opus"],
11
+ minimumVersion: "4.8",
12
+ minimumReasoningEffort: "high",
13
+ acceptedReasoningEfforts: [
14
+ "high",
15
+ "extra high",
16
+ "extra-high",
17
+ "xhigh",
18
+ "extra_high",
19
+ ],
20
+ recommendedReasoningEffort: "high or better",
21
+ },
22
+ codex: {
23
+ label: "Codex",
24
+ minimumModel: "GPT 5.5",
25
+ familyKeywords: ["gpt"],
26
+ minimumVersion: "5.5",
27
+ minimumReasoningEffort: "high",
28
+ acceptedReasoningEfforts: [
29
+ "high",
30
+ "extra high",
31
+ "extra-high",
32
+ "xhigh",
33
+ "extra_high",
34
+ ],
35
+ recommendedReasoningEffort: "high or better",
36
+ },
37
+ },
38
+ warningCopy: {
39
+ ok: "Campaign model settings meet the configured minimum: {minimumSummary}.",
40
+ unknownSettings: "Model settings were not provided by the host. Best campaigns need at least {minimumSummary}. Confirm the user is on one of those settings, or ask them to switch before continuing.",
41
+ belowMinimum: "Best campaigns need at least {minimumSummary}. Current settings look below that: {currentSettings}. Please switch before continuing, or explicitly say to continue anyway.",
42
+ },
43
+ };
44
+ let cachedConfig = null;
1
45
  const normalize = (value) => String(value ?? "")
2
46
  .trim()
3
47
  .toLowerCase();
4
48
  const normalizeHost = (host) => {
5
49
  const normalized = normalize(host);
6
- if (normalized.includes("claude") || normalized.includes("clod")) {
50
+ if (normalized.includes("claude") ||
51
+ normalized.includes("clod") ||
52
+ normalized.includes("opus") ||
53
+ normalized.includes("sonnet") ||
54
+ normalized.includes("haiku")) {
7
55
  return "claude";
8
56
  }
9
57
  if (normalized.includes("codex") ||
@@ -13,20 +61,101 @@ const normalizeHost = (host) => {
13
61
  }
14
62
  return "unknown";
15
63
  };
16
- const isHighClaudeReasoning = (reasoning) => {
17
- const normalized = normalize(reasoning).replace(/[_\s-]+/g, "");
18
- return ["high", "xhigh", "extrahigh"].includes(normalized);
19
- };
20
- const isExtraHighCodexReasoning = (reasoning) => {
21
- const normalized = normalize(reasoning).replace(/[_\s-]+/g, "");
22
- return ["xhigh", "extrahigh"].includes(normalized);
64
+ const normalizeReasoning = (reasoning) => normalize(reasoning).replace(/[_\s-]+/g, "");
65
+ const compareVersion = (candidate, minimum) => {
66
+ const candidateParts = candidate.split(".").map((part) => Number(part));
67
+ const minimumParts = minimum.split(".").map((part) => Number(part));
68
+ const length = Math.max(candidateParts.length, minimumParts.length);
69
+ for (let index = 0; index < length; index += 1) {
70
+ const candidatePart = candidateParts[index] ?? 0;
71
+ const minimumPart = minimumParts[index] ?? 0;
72
+ if (candidatePart > minimumPart)
73
+ return 1;
74
+ if (candidatePart < minimumPart)
75
+ return -1;
76
+ }
77
+ return 0;
23
78
  };
79
+ const extractModelVersions = (model) => Array.from(normalize(model).matchAll(/\b(\d+(?:\.\d+){0,2})\b/g)).map((match) => match[1]);
80
+ function mergeConfig(rawConfig) {
81
+ return {
82
+ ...DEFAULT_MODEL_QUALITY_CONFIG,
83
+ ...rawConfig,
84
+ hosts: {
85
+ claude: {
86
+ ...DEFAULT_MODEL_QUALITY_CONFIG.hosts.claude,
87
+ ...(rawConfig.hosts?.claude || {}),
88
+ },
89
+ codex: {
90
+ ...DEFAULT_MODEL_QUALITY_CONFIG.hosts.codex,
91
+ ...(rawConfig.hosts?.codex || {}),
92
+ },
93
+ },
94
+ warningCopy: {
95
+ ...DEFAULT_MODEL_QUALITY_CONFIG.warningCopy,
96
+ ...(rawConfig.warningCopy || {}),
97
+ },
98
+ };
99
+ }
100
+ export function getCampaignModelQualityConfig() {
101
+ if (cachedConfig)
102
+ return cachedConfig;
103
+ const configPath = join(resolveSkillsDir(), "create-campaign", "core", "model-quality.json");
104
+ if (!existsSync(configPath)) {
105
+ cachedConfig = DEFAULT_MODEL_QUALITY_CONFIG;
106
+ return cachedConfig;
107
+ }
108
+ try {
109
+ cachedConfig = mergeConfig(JSON.parse(readFileSync(configPath, "utf-8")));
110
+ }
111
+ catch {
112
+ cachedConfig = DEFAULT_MODEL_QUALITY_CONFIG;
113
+ }
114
+ return cachedConfig;
115
+ }
116
+ export function getCampaignModelMinimumSummary(config = getCampaignModelQualityConfig()) {
117
+ return `${config.hosts.claude.minimumModel} with ${config.hosts.claude.minimumReasoningEffort} reasoning or ${config.hosts.codex.minimumModel} with ${config.hosts.codex.minimumReasoningEffort} reasoning`;
118
+ }
119
+ function formatCopy(template, values) {
120
+ return Object.entries(values).reduce((text, [key, value]) => text.replaceAll(`{${key}}`, value), template);
121
+ }
122
+ function acceptsReasoning(reasoning, hostConfig) {
123
+ const normalized = normalizeReasoning(reasoning);
124
+ return hostConfig.acceptedReasoningEfforts
125
+ .map((value) => normalizeReasoning(value))
126
+ .includes(normalized);
127
+ }
128
+ function modelMeetsMinimum(model, hostConfig) {
129
+ const normalizedModel = normalize(model).replace(/[_-]+/g, " ");
130
+ if (!normalizedModel)
131
+ return false;
132
+ const familyMatches = hostConfig.familyKeywords.every((keyword) => normalizedModel.includes(normalize(keyword)));
133
+ if (!familyMatches)
134
+ return false;
135
+ const versions = extractModelVersions(normalizedModel);
136
+ if (!hostConfig.minimumVersion)
137
+ return true;
138
+ return versions.some((version) => compareVersion(version, hostConfig.minimumVersion) >= 0);
139
+ }
140
+ function findAcceptedHostConfig(host, model, config) {
141
+ const candidates = host === "unknown"
142
+ ? [
143
+ ["claude", config.hosts.claude],
144
+ ["codex", config.hosts.codex],
145
+ ]
146
+ : [[host, config.hosts[host]]];
147
+ return candidates.find(([, hostConfig]) => modelMeetsMinimum(model, hostConfig));
148
+ }
24
149
  export function evaluateCampaignModelQuality(input = {}) {
25
- const host = normalizeHost(input.host);
150
+ const config = getCampaignModelQualityConfig();
151
+ const host = normalizeHost([input.host, input.model].filter(Boolean).join(" "));
26
152
  const model = input.model?.trim() || null;
27
153
  const reasoningEffort = input.reasoningEffort?.trim() || null;
28
- const recommendedModel = host === "claude" ? "Opus 4.8" : "GPT 5.5";
29
- const recommendedReasoningEffort = host === "claude" ? "high or extra high" : "extra high";
154
+ const recommendationHost = host === "claude" ? "claude" : "codex";
155
+ const recommendedHostConfig = config.hosts[recommendationHost];
156
+ const minimumSummary = getCampaignModelMinimumSummary(config);
157
+ const recommendedModel = recommendedHostConfig.minimumModel;
158
+ const recommendedReasoningEffort = recommendedHostConfig.recommendedReasoningEffort;
30
159
  if (!model && !reasoningEffort) {
31
160
  return {
32
161
  status: "unknown",
@@ -35,30 +164,37 @@ export function evaluateCampaignModelQuality(input = {}) {
35
164
  reasoningEffort,
36
165
  recommendedModel,
37
166
  recommendedReasoningEffort,
167
+ minimumSummary,
38
168
  confirmationRequired: true,
39
- message: "Model settings were not provided by the host. Before campaign-critical generation, confirm the user is using Claude Opus 4.8 at high/extra high or Codex GPT 5.5 at extra high.",
169
+ message: formatCopy(config.warningCopy.unknownSettings, {
170
+ minimumSummary,
171
+ currentSettings: "unknown",
172
+ }),
40
173
  };
41
174
  }
42
- const normalizedModel = normalize(model);
43
- const ok = host === "claude"
44
- ? normalizedModel.includes("opus") &&
45
- normalizedModel.includes("4.8") &&
46
- isHighClaudeReasoning(reasoningEffort)
47
- : normalizedModel.includes("gpt") &&
48
- normalizedModel.includes("5.5") &&
49
- isExtraHighCodexReasoning(reasoningEffort);
175
+ const acceptedHostConfig = findAcceptedHostConfig(host, model, config);
176
+ const ok = Boolean(acceptedHostConfig && acceptsReasoning(reasoningEffort, acceptedHostConfig[1]));
50
177
  if (ok) {
51
178
  return {
52
179
  status: "ok",
53
- host,
180
+ host: acceptedHostConfig?.[0] ?? host,
54
181
  model,
55
182
  reasoningEffort,
56
- recommendedModel,
57
- recommendedReasoningEffort,
183
+ recommendedModel: acceptedHostConfig?.[1].minimumModel ?? recommendedModel,
184
+ recommendedReasoningEffort: acceptedHostConfig?.[1].recommendedReasoningEffort ??
185
+ recommendedReasoningEffort,
186
+ minimumSummary,
58
187
  confirmationRequired: false,
59
- message: "Recommended campaign-quality model settings are active.",
188
+ message: formatCopy(config.warningCopy.ok, {
189
+ minimumSummary,
190
+ currentSettings: "current settings",
191
+ }),
60
192
  };
61
193
  }
194
+ const currentSettings = [
195
+ model ? `model "${model}"` : "unknown model",
196
+ reasoningEffort ? `reasoning "${reasoningEffort}"` : "unknown reasoning",
197
+ ].join(", ");
62
198
  return {
63
199
  status: "warn",
64
200
  host,
@@ -66,7 +202,11 @@ export function evaluateCampaignModelQuality(input = {}) {
66
202
  reasoningEffort,
67
203
  recommendedModel,
68
204
  recommendedReasoningEffort,
205
+ minimumSummary,
69
206
  confirmationRequired: true,
70
- message: `For best campaign quality, switch to ${recommendedModel} with ${recommendedReasoningEffort} reasoning before lead filtering, message generation, or launch review.`,
207
+ message: formatCopy(config.warningCopy.belowMinimum, {
208
+ minimumSummary,
209
+ currentSettings,
210
+ }),
71
211
  };
72
212
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/mcp",
3
- "version": "0.1.307",
3
+ "version": "0.1.309",
4
4
  "type": "module",
5
5
  "description": "Sellable MCP server for Claude Code and Codex campaign workflows",
6
6
  "main": "dist/index.js",
@@ -114,6 +114,12 @@ sends", use `approvalMode:"approve"` to approve exactly the bounded X-message
114
114
  cohort during preparation, then continue through sender, sequence, and final
115
115
  launch greenlight; the launch path must verify that bounded cohort and must not
116
116
  broad approve-all.
117
+ When approving reviewed draft rows in the campaign table, resolve the actual
118
+ visible `Approved` cells with `select_campaign_cells({ columnRole: "approved",
119
+ rowSelector: { type: "rowIds", rowIds } })` and `update_cell` those returned
120
+ cell IDs. Do not rely on `approveCellId` from `get_rows` unless it matches the
121
+ semantic selector result; it may be a row-level helper and leave the UI checkbox
122
+ unchecked.
117
123
  Treat `campaignId` as `CampaignOffer.id`. Low level selector/queue tools are
118
124
  diagnostics and recovery only for this lane. If the user asks to stop
119
125
  preparation, the target is wrong, or status shows the wrong campaign/table, call
@@ -168,6 +174,12 @@ person/company this campaign is for, then I’ll turn that into a campaign brief
168
174
  before we move into lead sourcing.
169
175
  ```
170
176
 
177
+ Exception: if `bootstrap_create_campaign.modelQuality.status === "warn"`,
178
+ the first visible campaign message must be the model-quality warning from
179
+ `modelQuality.message`. Ask the user to switch to the configured minimum model
180
+ or explicitly continue anyway before identity setup, research, lead filtering,
181
+ message generation, or launch review.
182
+
171
183
  If a linked/local skill file is stale or missing, silently use the installed
172
184
  `sellable@sellable` plugin copy. Do not tell the user about the stale link,
173
185
  the old version, or the replacement path.
@@ -912,8 +924,11 @@ updates.
912
924
  - Do not call `mcp__sellable__get_campaigns`.
913
925
  - Do not call `mcp__sellable__get_campaign` to hunt for IDs.
914
926
  - 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? })`.
927
+ 6. Call `mcp__sellable__bootstrap_create_campaign({ flowVersion: "v2", campaignId?, host?, model?, reasoningEffort? })`.
928
+ Pass the current host, model, and reasoning when the host exposes them.
916
929
  7. If `safeToProceed !== true`, stop and show `blockingErrors` + `nextStep`.
930
+ 8. If `modelQuality.status === "warn"`, show `modelQuality.message` before any
931
+ setup/research and wait for the user to switch or explicitly continue.
917
932
 
918
933
  ## Execute Workflow
919
934
 
@@ -0,0 +1,28 @@
1
+ {
2
+ "version": 1,
3
+ "hosts": {
4
+ "claude": {
5
+ "label": "Claude Code",
6
+ "minimumModel": "Claude Opus 4.8",
7
+ "familyKeywords": ["opus"],
8
+ "minimumVersion": "4.8",
9
+ "minimumReasoningEffort": "high",
10
+ "acceptedReasoningEfforts": ["high", "extra high", "extra-high", "xhigh", "extra_high"],
11
+ "recommendedReasoningEffort": "high or better"
12
+ },
13
+ "codex": {
14
+ "label": "Codex",
15
+ "minimumModel": "GPT 5.5",
16
+ "familyKeywords": ["gpt"],
17
+ "minimumVersion": "5.5",
18
+ "minimumReasoningEffort": "high",
19
+ "acceptedReasoningEfforts": ["high", "extra high", "extra-high", "xhigh", "extra_high"],
20
+ "recommendedReasoningEffort": "high or better"
21
+ }
22
+ },
23
+ "warningCopy": {
24
+ "ok": "Campaign model settings meet the configured minimum: {minimumSummary}.",
25
+ "unknownSettings": "Model settings were not provided by the host. Best campaigns need at least {minimumSummary}. Confirm the user is on one of those settings, or ask them to switch before continuing.",
26
+ "belowMinimum": "Best campaigns need at least {minimumSummary}. Current settings look below that: {currentSettings}. Please switch before continuing, or explicitly say to continue anyway."
27
+ }
28
+ }