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