@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.
- package/bin/sellable-install.mjs +235 -70
- package/lib/runtime-verify.mjs +92 -1
- package/package.json +1 -1
- package/skill-templates/create-campaign.md +77 -32
package/bin/sellable-install.mjs
CHANGED
|
@@ -72,8 +72,9 @@ const CODEX_PLUGIN_COMPAT_VERSIONS = [
|
|
|
72
72
|
"0.1.40",
|
|
73
73
|
"0.1.41",
|
|
74
74
|
];
|
|
75
|
-
const
|
|
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
|
|
676
|
-
args
|
|
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
|
|
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.
|
|
779
|
-
|
|
780
|
-
|
|
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
|
-
|
|
785
|
-
|
|
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.
|
|
792
|
-
|
|
793
|
-
|
|
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
|
-
|
|
798
|
-
|
|
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
|
|
805
|
-
const
|
|
806
|
-
if (!
|
|
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
|
-
|
|
809
|
-
|
|
831
|
+
/(^# .+\n\n)/m,
|
|
832
|
+
`$1${preamble}\n\n`
|
|
810
833
|
);
|
|
811
834
|
}
|
|
812
835
|
|
|
813
|
-
function createCampaignSkillMd(
|
|
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
|
|
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"
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
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
|
|
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"
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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
|
);
|
package/lib/runtime-verify.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
@@ -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"
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
779
|
-
`request_user_input` is unavailable in an interactive
|
|
780
|
-
|
|
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
|
|
821
|
-
install/reload problem
|
|
822
|
-
|
|
823
|
-
|
|
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
|
|
829
|
-
|
|
830
|
-
|
|
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
|
|
944
|
-
Pass
|
|
945
|
-
|
|
946
|
-
|
|
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"
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
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
|
|