@sellable/install 0.1.219 → 0.1.220
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 +9 -7
- package/bin/sellable-install.mjs +368 -46
- package/lib/runtime-verify.mjs +2 -0
- package/package.json +1 -1
- package/skill-templates/create-campaign.md +44 -18
package/README.md
CHANGED
|
@@ -35,8 +35,9 @@ Campaign creation, foundation memory, content capture/ideation, and post
|
|
|
35
35
|
drafting run inside Claude Code or Codex, where the Sellable MCP tools and
|
|
36
36
|
approval flows are available.
|
|
37
37
|
|
|
38
|
-
Install is auth-free by default.
|
|
39
|
-
Sellable
|
|
38
|
+
Install is auth-free by default. The normal path is first-run login: launch a
|
|
39
|
+
Sellable workflow in Claude Code or Codex and the agent handles Sellable
|
|
40
|
+
sign-in with a browser magic-link handoff.
|
|
40
41
|
|
|
41
42
|
The installer uses package stdio MCP by default:
|
|
42
43
|
|
|
@@ -64,14 +65,15 @@ verification in one path:
|
|
|
64
65
|
curl -fsSL "https://app.sellable.dev/api/v2/cli/install" | sh
|
|
65
66
|
```
|
|
66
67
|
|
|
67
|
-
For
|
|
68
|
+
For scripted fallback after browser login, paste the command shown by Sellable:
|
|
68
69
|
|
|
69
|
-
```
|
|
70
|
-
|
|
70
|
+
```bash
|
|
71
|
+
sellable auth set <token> --workspace-id <workspace_id>
|
|
71
72
|
```
|
|
72
73
|
|
|
73
|
-
|
|
74
|
-
`SELLABLE_WORKSPACE_ID`.
|
|
74
|
+
For CI/env-only installs, operators can still pass `--token` / `SELLABLE_TOKEN`
|
|
75
|
+
plus `--workspace-id` / `SELLABLE_WORKSPACE_ID`. Do not use env vars as the
|
|
76
|
+
primary human setup path.
|
|
75
77
|
|
|
76
78
|
Auth is stored once at:
|
|
77
79
|
|
package/bin/sellable-install.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import { homedir } from "node:os";
|
|
14
14
|
import { dirname, join, relative } from "node:path";
|
|
15
15
|
import { stdout as output } from "node:process";
|
|
16
|
+
import { createInterface } from "node:readline/promises";
|
|
16
17
|
import { fileURLToPath } from "node:url";
|
|
17
18
|
import {
|
|
18
19
|
REQUIRED_SELLABLE_MCP_TOOLS,
|
|
@@ -22,6 +23,7 @@ import {
|
|
|
22
23
|
const DEFAULT_API_URL = "https://app.sellable.dev";
|
|
23
24
|
const DEFAULT_SERVER_PACKAGE =
|
|
24
25
|
process.env.SELLABLE_MCP_PACKAGE || "@sellable/mcp@latest";
|
|
26
|
+
const CODEX_INSTALL_URL = "https://chatgpt.com/codex/install";
|
|
25
27
|
|
|
26
28
|
function getInstallVersion() {
|
|
27
29
|
try {
|
|
@@ -175,6 +177,8 @@ Options:
|
|
|
175
177
|
--hosted-url <url> Hosted MCP URL for --server hosted.
|
|
176
178
|
--verbose Print every file write and shell command.
|
|
177
179
|
--dry-run Print actions without writing or running host commands.
|
|
180
|
+
--install-codex-cli Install Codex CLI without prompting if it is missing or blocked.
|
|
181
|
+
--no-install-codex-cli Do not prompt to install Codex CLI; install Desktop config only when possible.
|
|
178
182
|
--verify-only Verify installed host config where possible.
|
|
179
183
|
--json Print machine-readable verification JSON.
|
|
180
184
|
--artifact <path> Write verification JSON to a file.
|
|
@@ -247,6 +251,7 @@ function parseArgs(argv) {
|
|
|
247
251
|
json: false,
|
|
248
252
|
artifactPath: process.env.SELLABLE_VERIFY_ARTIFACT || "",
|
|
249
253
|
verbose: false,
|
|
254
|
+
codexCliInstall: codexCliInstallPreference(),
|
|
250
255
|
};
|
|
251
256
|
|
|
252
257
|
for (let i = 0; i < argv.length; i += 1) {
|
|
@@ -280,6 +285,10 @@ function parseArgs(argv) {
|
|
|
280
285
|
opts.hostedUrl = next();
|
|
281
286
|
} else if (arg === "--dry-run") {
|
|
282
287
|
opts.dryRun = true;
|
|
288
|
+
} else if (arg === "--install-codex-cli") {
|
|
289
|
+
opts.codexCliInstall = "install";
|
|
290
|
+
} else if (arg === "--no-install-codex-cli") {
|
|
291
|
+
opts.codexCliInstall = "skip";
|
|
283
292
|
} else if (arg === "--verify-only") {
|
|
284
293
|
opts.verifyOnly = true;
|
|
285
294
|
} else if (arg === "--json") {
|
|
@@ -303,6 +312,14 @@ function parseArgs(argv) {
|
|
|
303
312
|
return opts;
|
|
304
313
|
}
|
|
305
314
|
|
|
315
|
+
function codexCliInstallPreference() {
|
|
316
|
+
const raw = process.env.SELLABLE_INSTALL_CODEX_CLI || "";
|
|
317
|
+
const value = raw.trim().toLowerCase();
|
|
318
|
+
if (["1", "true", "yes", "y", "install"].includes(value)) return "install";
|
|
319
|
+
if (["0", "false", "no", "n", "skip"].includes(value)) return "skip";
|
|
320
|
+
return "ask";
|
|
321
|
+
}
|
|
322
|
+
|
|
306
323
|
function redact(value) {
|
|
307
324
|
if (!value) return "";
|
|
308
325
|
if (value.length <= 10) return "[redacted]";
|
|
@@ -360,6 +377,175 @@ function commandExistsDetails(command, platform = process.platform) {
|
|
|
360
377
|
};
|
|
361
378
|
}
|
|
362
379
|
|
|
380
|
+
function summarizeSpawnFailure(result) {
|
|
381
|
+
const errorMessage = result.error?.message || "";
|
|
382
|
+
const stderr = (result.stderr || "").trim();
|
|
383
|
+
const stdout = (result.stdout || "").trim();
|
|
384
|
+
return (
|
|
385
|
+
errorMessage ||
|
|
386
|
+
stderr ||
|
|
387
|
+
stdout ||
|
|
388
|
+
(Number.isInteger(result.status) ? `exit ${result.status}` : "unknown failure")
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function codexCliStatus(opts = {}) {
|
|
393
|
+
const details = commandExistsDetails("codex");
|
|
394
|
+
if (!details.exists) {
|
|
395
|
+
return {
|
|
396
|
+
exists: false,
|
|
397
|
+
usable: false,
|
|
398
|
+
reason: "Codex CLI not found on PATH",
|
|
399
|
+
details,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
if (opts.dryRun) {
|
|
403
|
+
return {
|
|
404
|
+
exists: true,
|
|
405
|
+
usable: true,
|
|
406
|
+
reason: "dry-run",
|
|
407
|
+
details,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const result = spawnSync("codex", ["mcp", "--help"], {
|
|
412
|
+
encoding: "utf8",
|
|
413
|
+
stdio: "pipe",
|
|
414
|
+
timeout: 10000,
|
|
415
|
+
});
|
|
416
|
+
if (result.status === 0) {
|
|
417
|
+
return {
|
|
418
|
+
exists: true,
|
|
419
|
+
usable: true,
|
|
420
|
+
reason: "Codex CLI responded to `codex mcp --help`",
|
|
421
|
+
details,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return {
|
|
426
|
+
exists: true,
|
|
427
|
+
usable: false,
|
|
428
|
+
reason: `Codex CLI found but not usable: ${summarizeSpawnFailure(result)}`,
|
|
429
|
+
details,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function codexDesktopLikelyInstalled(status = null) {
|
|
434
|
+
if (process.env.SELLABLE_INSTALL_ASSUME_CODEX_DESKTOP === "1") return true;
|
|
435
|
+
const home = codexHome();
|
|
436
|
+
const probes = [
|
|
437
|
+
status?.details?.stdout || "",
|
|
438
|
+
process.env.PATH || "",
|
|
439
|
+
].join("\n");
|
|
440
|
+
|
|
441
|
+
if (/WindowsApps[\\/]+codex\.exe/i.test(probes)) return true;
|
|
442
|
+
if (existsSync(join(home, "config.toml"))) return true;
|
|
443
|
+
if (existsSync(join(home, "plugins", ".plugin-appserver"))) return true;
|
|
444
|
+
if (existsSync(join(home, "plugins", "cache"))) return true;
|
|
445
|
+
if (process.platform === "darwin" && existsSync("/Applications/Codex.app")) {
|
|
446
|
+
return true;
|
|
447
|
+
}
|
|
448
|
+
return false;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function isNonInteractiveInstall() {
|
|
452
|
+
return (
|
|
453
|
+
process.env.CI === "true" ||
|
|
454
|
+
process.env.SELLABLE_NON_INTERACTIVE === "1" ||
|
|
455
|
+
process.env.SELLABLE_NON_INTERACTIVE === "true" ||
|
|
456
|
+
process.env.SELLABLE_NON_INTERACTIVE === "yes"
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
async function askYesNo(question, defaultAnswer = false) {
|
|
461
|
+
if (!process.stdin.isTTY || !output.isTTY || isNonInteractiveInstall()) {
|
|
462
|
+
return null;
|
|
463
|
+
}
|
|
464
|
+
const suffix = defaultAnswer ? " [Y/n] " : " [y/N] ";
|
|
465
|
+
const rl = createInterface({
|
|
466
|
+
input: process.stdin,
|
|
467
|
+
output: process.stdout,
|
|
468
|
+
});
|
|
469
|
+
try {
|
|
470
|
+
const answer = (await rl.question(`${question}${suffix}`)).trim().toLowerCase();
|
|
471
|
+
if (!answer) return defaultAnswer;
|
|
472
|
+
return ["y", "yes"].includes(answer);
|
|
473
|
+
} finally {
|
|
474
|
+
rl.close();
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function codexInstallCommand(platform = process.platform) {
|
|
479
|
+
const override = process.env.SELLABLE_CODEX_INSTALL_COMMAND;
|
|
480
|
+
if (override) {
|
|
481
|
+
return isWindowsPlatform(platform)
|
|
482
|
+
? ["cmd.exe", ["/d", "/s", "/c", override]]
|
|
483
|
+
: ["sh", ["-c", override]];
|
|
484
|
+
}
|
|
485
|
+
if (isWindowsPlatform(platform)) {
|
|
486
|
+
return [
|
|
487
|
+
"powershell.exe",
|
|
488
|
+
[
|
|
489
|
+
"-NoProfile",
|
|
490
|
+
"-ExecutionPolicy",
|
|
491
|
+
"Bypass",
|
|
492
|
+
"-Command",
|
|
493
|
+
`$env:CODEX_NON_INTERACTIVE='1'; irm ${CODEX_INSTALL_URL}.ps1 | iex`,
|
|
494
|
+
],
|
|
495
|
+
];
|
|
496
|
+
}
|
|
497
|
+
return [
|
|
498
|
+
"sh",
|
|
499
|
+
["-c", `curl -fsSL ${CODEX_INSTALL_URL}.sh | CODEX_NON_INTERACTIVE=1 sh`],
|
|
500
|
+
];
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function codexManualInstallCommand(platform = process.platform) {
|
|
504
|
+
return isWindowsPlatform(platform)
|
|
505
|
+
? `$env:CODEX_NON_INTERACTIVE=1; irm ${CODEX_INSTALL_URL}.ps1 | iex`
|
|
506
|
+
: `curl -fsSL ${CODEX_INSTALL_URL}.sh | sh`;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
async function maybeInstallCodexCli(status, opts) {
|
|
510
|
+
if (opts.dryRun) return false;
|
|
511
|
+
if (opts.codexCliInstall === "skip") {
|
|
512
|
+
logWarn(`${status.reason}. Skipping Codex CLI install by request.`);
|
|
513
|
+
return false;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
let shouldInstall = opts.codexCliInstall === "install";
|
|
517
|
+
if (!shouldInstall) {
|
|
518
|
+
const answer = await askYesNo(
|
|
519
|
+
`${status.reason}. Install the Codex CLI now?`,
|
|
520
|
+
false
|
|
521
|
+
);
|
|
522
|
+
if (answer === null) {
|
|
523
|
+
logWarn(
|
|
524
|
+
`${status.reason}. Non-interactive shell; skipping Codex CLI install. Re-run with --install-codex-cli to install it.`
|
|
525
|
+
);
|
|
526
|
+
return false;
|
|
527
|
+
}
|
|
528
|
+
shouldInstall = answer;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
if (!shouldInstall) {
|
|
532
|
+
logWarn(`${status.reason}. Continuing with Codex Desktop plugin setup only.`);
|
|
533
|
+
return false;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const [command, args] = codexInstallCommand();
|
|
537
|
+
logStep("Installing Codex CLI using the official Codex installer...");
|
|
538
|
+
try {
|
|
539
|
+
run(command, args, opts);
|
|
540
|
+
return true;
|
|
541
|
+
} catch (err) {
|
|
542
|
+
logWarn(
|
|
543
|
+
`Codex CLI install failed: ${err instanceof Error ? err.message : String(err)}`
|
|
544
|
+
);
|
|
545
|
+
return false;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
363
549
|
function shellQuote(value) {
|
|
364
550
|
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
365
551
|
}
|
|
@@ -419,13 +605,26 @@ async function loadAuthIfPresent(opts) {
|
|
|
419
605
|
return opts;
|
|
420
606
|
}
|
|
421
607
|
|
|
608
|
+
function redactTokensForLog(value) {
|
|
609
|
+
if (Array.isArray(value)) {
|
|
610
|
+
return value.map((item) => redactTokensForLog(item));
|
|
611
|
+
}
|
|
612
|
+
if (value && typeof value === "object") {
|
|
613
|
+
return Object.fromEntries(
|
|
614
|
+
Object.entries(value).map(([key, item]) => [
|
|
615
|
+
key,
|
|
616
|
+
key.toLowerCase().includes("token") && typeof item === "string"
|
|
617
|
+
? redact(item)
|
|
618
|
+
: redactTokensForLog(item),
|
|
619
|
+
])
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
return value;
|
|
623
|
+
}
|
|
624
|
+
|
|
422
625
|
function writeJson(path, data, opts) {
|
|
423
626
|
if (VERBOSE) {
|
|
424
|
-
const redacted = JSON.stringify(
|
|
425
|
-
{ ...data, token: redact(data.token) },
|
|
426
|
-
null,
|
|
427
|
-
2
|
|
428
|
-
);
|
|
627
|
+
const redacted = JSON.stringify(redactTokensForLog(data), null, 2);
|
|
429
628
|
logVerbose(`${C.grey}Writing ${path}: ${redacted}${C.reset}`);
|
|
430
629
|
}
|
|
431
630
|
if (opts.dryRun) return;
|
|
@@ -442,6 +641,56 @@ function readExisting(path) {
|
|
|
442
641
|
}
|
|
443
642
|
}
|
|
444
643
|
|
|
644
|
+
function mergeAuthConfig(raw, auth) {
|
|
645
|
+
const existing =
|
|
646
|
+
raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
|
|
647
|
+
const authFields = {
|
|
648
|
+
token: auth.token,
|
|
649
|
+
activeWorkspaceId: auth.activeWorkspaceId,
|
|
650
|
+
apiUrl: auth.apiUrl,
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
if (existing.activeEnv && existing.environments) {
|
|
654
|
+
const activeEnv = existing.activeEnv;
|
|
655
|
+
const envConfig = existing.environments[activeEnv];
|
|
656
|
+
if (
|
|
657
|
+
!envConfig ||
|
|
658
|
+
typeof envConfig !== "object" ||
|
|
659
|
+
Array.isArray(envConfig)
|
|
660
|
+
) {
|
|
661
|
+
throw new Error(
|
|
662
|
+
`Unknown active environment '${activeEnv}' in ${authPath()}`
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
return {
|
|
666
|
+
...existing,
|
|
667
|
+
environments: {
|
|
668
|
+
...existing.environments,
|
|
669
|
+
[activeEnv]: {
|
|
670
|
+
...envConfig,
|
|
671
|
+
...authFields,
|
|
672
|
+
},
|
|
673
|
+
},
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
return {
|
|
678
|
+
...existing,
|
|
679
|
+
...authFields,
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function writeAuthSetConfig({ token, workspaceId, apiUrl, dryRun }) {
|
|
684
|
+
const configPath = authPath();
|
|
685
|
+
const raw = readExisting(configPath) || {};
|
|
686
|
+
const config = mergeAuthConfig(raw, {
|
|
687
|
+
token,
|
|
688
|
+
activeWorkspaceId: workspaceId || null,
|
|
689
|
+
apiUrl,
|
|
690
|
+
});
|
|
691
|
+
writeJson(configPath, config, { dryRun });
|
|
692
|
+
}
|
|
693
|
+
|
|
445
694
|
function sellableHostEnvPath() {
|
|
446
695
|
return join(homedir(), ".local", "sellable", "app-sellable-dev", ".env");
|
|
447
696
|
}
|
|
@@ -742,6 +991,9 @@ const CREATE_CAMPAIGN_ALLOWED_TOOLS = [
|
|
|
742
991
|
"mcp__sellable__record_campaign_review_batch",
|
|
743
992
|
"mcp__sellable__queue_campaign_cells",
|
|
744
993
|
"mcp__sellable__wait_for_campaign_processing",
|
|
994
|
+
"mcp__sellable__refill_campaign_sends",
|
|
995
|
+
"mcp__sellable__resolve_campaign_fill_route",
|
|
996
|
+
"mcp__sellable__fill_campaign_horizon",
|
|
745
997
|
"mcp__sellable__start_campaign_message_preparation",
|
|
746
998
|
"mcp__sellable__get_campaign_message_preparation_status",
|
|
747
999
|
"mcp__sellable__cancel_campaign_message_preparation",
|
|
@@ -2455,6 +2707,27 @@ function writeAuth(opts) {
|
|
|
2455
2707
|
return { written: true, reused: false };
|
|
2456
2708
|
}
|
|
2457
2709
|
|
|
2710
|
+
function resolveWindowsNpmCommand() {
|
|
2711
|
+
const explicit =
|
|
2712
|
+
process.env.SELLABLE_INSTALL_NPM_CMD_COMMAND ||
|
|
2713
|
+
process.env.SELLABLE_INSTALL_NPM_COMMAND;
|
|
2714
|
+
if (explicit) return explicit;
|
|
2715
|
+
|
|
2716
|
+
const nodeBin = process.env.SELLABLE_INSTALL_NODE_BIN || "";
|
|
2717
|
+
for (const name of ["npm.cmd", "npm"]) {
|
|
2718
|
+
const candidate = nodeBin ? join(nodeBin, name) : "";
|
|
2719
|
+
if (candidate && existsSync(candidate)) return candidate;
|
|
2720
|
+
}
|
|
2721
|
+
|
|
2722
|
+
if (isWindowsPlatform()) {
|
|
2723
|
+
const details = commandExistsDetails("npm.cmd");
|
|
2724
|
+
const first = details.stdout.split(/\r?\n/).find((line) => line.trim());
|
|
2725
|
+
if (details.exists && first) return first.trim();
|
|
2726
|
+
}
|
|
2727
|
+
|
|
2728
|
+
return packageManagerCommand("win32");
|
|
2729
|
+
}
|
|
2730
|
+
|
|
2458
2731
|
function installSelfShim(opts) {
|
|
2459
2732
|
const installDir =
|
|
2460
2733
|
process.env.SELLABLE_INSTALL_DIR || join(homedir(), ".local", "bin");
|
|
@@ -2475,12 +2748,11 @@ exec ${shellQuote(npmCommand)} exec --yes --package ${shellQuote(INSTALL_PACKAGE
|
|
|
2475
2748
|
isWindowsPlatform() || process.env.SELLABLE_INSTALL_WRITE_CMD_SHIM === "1";
|
|
2476
2749
|
if (shouldWriteCmdShim) {
|
|
2477
2750
|
const cmdPath = `${binPath}.cmd`;
|
|
2478
|
-
const npmCmdCommand =
|
|
2479
|
-
process.env.SELLABLE_INSTALL_NPM_CMD_COMMAND ||
|
|
2480
|
-
process.env.SELLABLE_INSTALL_NPM_COMMAND ||
|
|
2481
|
-
packageManagerCommand("win32");
|
|
2751
|
+
const npmCmdCommand = resolveWindowsNpmCommand();
|
|
2482
2752
|
const cmdPathPrefix = nodeBin ? `set "PATH=${nodeBin};%PATH%"\r\n` : "";
|
|
2483
|
-
const cmdShim =
|
|
2753
|
+
const cmdShim =
|
|
2754
|
+
`@echo off\r\n${cmdPathPrefix}` +
|
|
2755
|
+
`call "${npmCmdCommand}" exec --yes --package ${INSTALL_PACKAGE_SPEC} -- sellable %*\r\n`;
|
|
2484
2756
|
writeFile(cmdPath, cmdShim, opts, 0o755);
|
|
2485
2757
|
}
|
|
2486
2758
|
}
|
|
@@ -2820,32 +3092,70 @@ function patchClaudeAlwaysLoad(opts) {
|
|
|
2820
3092
|
}
|
|
2821
3093
|
}
|
|
2822
3094
|
|
|
2823
|
-
function
|
|
2824
|
-
|
|
3095
|
+
function registerCodexCliMcp(opts) {
|
|
3096
|
+
try {
|
|
3097
|
+
if (opts.server !== "hosted") {
|
|
3098
|
+
run("codex", ["mcp", "remove", "sellable"], {
|
|
3099
|
+
...opts,
|
|
3100
|
+
dryRun: opts.dryRun,
|
|
3101
|
+
allowFail: true,
|
|
3102
|
+
});
|
|
3103
|
+
}
|
|
3104
|
+
run("codex", codexMcpAddArgs(opts), opts);
|
|
3105
|
+
return true;
|
|
3106
|
+
} catch (err) {
|
|
3107
|
+
logWarn(
|
|
3108
|
+
`Codex CLI MCP registration skipped: ${err instanceof Error ? err.message : String(err)}`
|
|
3109
|
+
);
|
|
3110
|
+
return false;
|
|
3111
|
+
}
|
|
3112
|
+
}
|
|
3113
|
+
|
|
3114
|
+
async function installCodex(opts) {
|
|
3115
|
+
let cliStatus = codexCliStatus(opts);
|
|
3116
|
+
let desktopLikely = codexDesktopLikelyInstalled(cliStatus);
|
|
3117
|
+
let attemptedCliInstall = false;
|
|
3118
|
+
|
|
3119
|
+
if (!opts.dryRun && !cliStatus.usable) {
|
|
3120
|
+
attemptedCliInstall = await maybeInstallCodexCli(cliStatus, opts);
|
|
3121
|
+
if (attemptedCliInstall) {
|
|
3122
|
+
cliStatus = codexCliStatus(opts);
|
|
3123
|
+
desktopLikely = codexDesktopLikelyInstalled(cliStatus) || desktopLikely;
|
|
3124
|
+
if (!cliStatus.usable) {
|
|
3125
|
+
logWarn(
|
|
3126
|
+
`${cliStatus.reason}. Continuing with Codex Desktop plugin setup where possible.`
|
|
3127
|
+
);
|
|
3128
|
+
}
|
|
3129
|
+
}
|
|
3130
|
+
}
|
|
3131
|
+
|
|
3132
|
+
const shouldInstallDesktop =
|
|
3133
|
+
opts.host === "codex" ||
|
|
3134
|
+
cliStatus.usable ||
|
|
3135
|
+
desktopLikely ||
|
|
3136
|
+
attemptedCliInstall;
|
|
3137
|
+
if (!shouldInstallDesktop) {
|
|
2825
3138
|
const message =
|
|
2826
|
-
"Codex CLI not found. Install/login to Codex, then rerun: sellable --host codex";
|
|
3139
|
+
"Codex CLI not found and Codex Desktop was not detected. Install/login to Codex, then rerun: sellable --host codex";
|
|
2827
3140
|
if (opts.host === "all") {
|
|
2828
3141
|
logWarn(`Skipping Codex: ${message}`);
|
|
2829
3142
|
return { installed: false };
|
|
2830
3143
|
}
|
|
2831
3144
|
throw new Error(message);
|
|
2832
3145
|
}
|
|
3146
|
+
|
|
2833
3147
|
if (!opts.dryRun) {
|
|
2834
3148
|
mkdirSync(codexHome(), { recursive: true, mode: 0o700 });
|
|
2835
3149
|
}
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
3150
|
+
|
|
3151
|
+
const cliRegistered = cliStatus.usable ? registerCodexCliMcp(opts) : false;
|
|
3152
|
+
if (!cliRegistered && !opts.dryRun) {
|
|
3153
|
+
logWarn(
|
|
3154
|
+
"Codex CLI MCP registration did not run; writing Codex Desktop plugin and config directly."
|
|
3155
|
+
);
|
|
2840
3156
|
}
|
|
2841
|
-
run("codex", ["mcp", "remove", "sellable"], {
|
|
2842
|
-
...opts,
|
|
2843
|
-
dryRun: opts.dryRun,
|
|
2844
|
-
allowFail: true,
|
|
2845
|
-
});
|
|
2846
|
-
run("codex", codexMcpAddArgs(opts), opts);
|
|
2847
3157
|
const info = installCodexDesktopPlugin(opts);
|
|
2848
|
-
return { installed: true, ...info };
|
|
3158
|
+
return { installed: true, cliRegistered, ...info };
|
|
2849
3159
|
}
|
|
2850
3160
|
|
|
2851
3161
|
function requiredCheck(ok, label) {
|
|
@@ -2996,11 +3306,11 @@ async function verify(opts) {
|
|
|
2996
3306
|
);
|
|
2997
3307
|
}
|
|
2998
3308
|
if (opts.host === "codex" || opts.host === "all") {
|
|
2999
|
-
const
|
|
3309
|
+
const codexStatus = codexCliStatus(opts);
|
|
3000
3310
|
checks.push(
|
|
3001
3311
|
warningCheck(
|
|
3002
|
-
|
|
3003
|
-
|
|
3312
|
+
codexStatus.usable,
|
|
3313
|
+
codexStatus.usable ? "Codex CLI usable" : codexStatus.reason
|
|
3004
3314
|
)
|
|
3005
3315
|
);
|
|
3006
3316
|
const pluginPath = join(
|
|
@@ -3358,8 +3668,8 @@ function printNextSteps(installedHosts, authReused) {
|
|
|
3358
3668
|
console.log("");
|
|
3359
3669
|
printInstallAgentBox(
|
|
3360
3670
|
"Install Codex",
|
|
3361
|
-
|
|
3362
|
-
"https://
|
|
3671
|
+
codexManualInstallCommand(),
|
|
3672
|
+
"https://developers.openai.com/codex/quickstart"
|
|
3363
3673
|
);
|
|
3364
3674
|
console.log("");
|
|
3365
3675
|
console.log("");
|
|
@@ -3665,18 +3975,31 @@ async function main() {
|
|
|
3665
3975
|
}
|
|
3666
3976
|
const token = rawArgs[2];
|
|
3667
3977
|
const authSetFlags = rawArgs.slice(3);
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3978
|
+
let dryRun = false;
|
|
3979
|
+
let workspaceId = "";
|
|
3980
|
+
for (let i = 0; i < authSetFlags.length; i += 1) {
|
|
3981
|
+
const flag = authSetFlags[i];
|
|
3982
|
+
if (flag === "--dry-run") {
|
|
3983
|
+
dryRun = true;
|
|
3984
|
+
continue;
|
|
3985
|
+
}
|
|
3986
|
+
if (flag === "--workspace-id") {
|
|
3987
|
+
const value = authSetFlags[i + 1];
|
|
3988
|
+
if (!value || value.startsWith("--")) {
|
|
3989
|
+
console.error("Missing value for --workspace-id");
|
|
3990
|
+
process.exit(2);
|
|
3991
|
+
}
|
|
3992
|
+
workspaceId = value;
|
|
3993
|
+
i += 1;
|
|
3994
|
+
continue;
|
|
3995
|
+
}
|
|
3996
|
+
console.error(`Unknown auth set option: ${flag}`);
|
|
3674
3997
|
process.exit(2);
|
|
3675
3998
|
}
|
|
3676
3999
|
if (!token) {
|
|
3677
4000
|
console.error(
|
|
3678
|
-
"Usage: sellable auth set <token
|
|
3679
|
-
"
|
|
4001
|
+
"Usage: sellable auth set <token> [--workspace-id <id>]\n" +
|
|
4002
|
+
"Use this only when the Sellable browser login page shows a manual fallback command."
|
|
3680
4003
|
);
|
|
3681
4004
|
process.exit(2);
|
|
3682
4005
|
}
|
|
@@ -3688,23 +4011,22 @@ async function main() {
|
|
|
3688
4011
|
);
|
|
3689
4012
|
process.exit(2);
|
|
3690
4013
|
}
|
|
3691
|
-
// Bypass writeAuth()
|
|
3692
|
-
//
|
|
3693
|
-
//
|
|
4014
|
+
// Bypass writeAuth() because auth-set is a first-run browser fallback:
|
|
4015
|
+
// the workspace id is optional for legacy fallback pages, and existing
|
|
4016
|
+
// config metadata must be preserved.
|
|
3694
4017
|
// Skip installSelfShim() — by definition the user has the shim already
|
|
3695
4018
|
// (they invoked `sellable auth set` from it).
|
|
3696
4019
|
const apiUrl = process.env.SELLABLE_API_URL || DEFAULT_API_URL;
|
|
3697
|
-
|
|
3698
|
-
authPath(),
|
|
3699
|
-
{ token, activeWorkspaceId: null, apiUrl },
|
|
3700
|
-
{ dryRun }
|
|
3701
|
-
);
|
|
4020
|
+
writeAuthSetConfig({ token, workspaceId, apiUrl, dryRun });
|
|
3702
4021
|
if (dryRun) {
|
|
3703
4022
|
console.log(`Dry run: token would be saved to ${authPath()}`);
|
|
3704
4023
|
} else {
|
|
3705
4024
|
console.log(`✓ Token saved to ${authPath()}`);
|
|
3706
4025
|
}
|
|
3707
4026
|
console.log(` apiUrl: ${apiUrl}`);
|
|
4027
|
+
if (workspaceId) {
|
|
4028
|
+
console.log(` activeWorkspaceId: ${workspaceId}`);
|
|
4029
|
+
}
|
|
3708
4030
|
console.log(` Continue in your agent:`);
|
|
3709
4031
|
console.log(` Claude Code: /sellable:create-campaign`);
|
|
3710
4032
|
console.log(` Claude Code: /sellable:foundation`);
|
|
@@ -3805,7 +4127,7 @@ async function main() {
|
|
|
3805
4127
|
}
|
|
3806
4128
|
}
|
|
3807
4129
|
if (opts.host === "codex" || opts.host === "all") {
|
|
3808
|
-
const result = installCodex(opts);
|
|
4130
|
+
const result = await installCodex(opts);
|
|
3809
4131
|
if (result.installed) {
|
|
3810
4132
|
installedHosts.push("Codex");
|
|
3811
4133
|
}
|
package/lib/runtime-verify.mjs
CHANGED
|
@@ -28,6 +28,8 @@ export const REQUIRED_SELLABLE_MCP_TOOLS = [
|
|
|
28
28
|
"get_campaign_messages_preview",
|
|
29
29
|
"attach_sequence",
|
|
30
30
|
"attach_recommended_sequence",
|
|
31
|
+
"refill_campaign_sends",
|
|
32
|
+
"resolve_campaign_fill_route",
|
|
31
33
|
"start_campaign_message_preparation",
|
|
32
34
|
"get_campaign_message_preparation_status",
|
|
33
35
|
"cancel_campaign_message_preparation",
|
package/package.json
CHANGED
|
@@ -47,6 +47,9 @@ allowed-tools:
|
|
|
47
47
|
- mcp__sellable__record_campaign_review_batch
|
|
48
48
|
- mcp__sellable__queue_campaign_cells
|
|
49
49
|
- mcp__sellable__wait_for_campaign_processing
|
|
50
|
+
- mcp__sellable__refill_campaign_sends
|
|
51
|
+
- mcp__sellable__resolve_campaign_fill_route
|
|
52
|
+
- mcp__sellable__fill_campaign_horizon
|
|
50
53
|
- mcp__sellable__start_campaign_message_preparation
|
|
51
54
|
- mcp__sellable__get_campaign_message_preparation_status
|
|
52
55
|
- mcp__sellable__cancel_campaign_message_preparation
|
|
@@ -113,27 +116,46 @@ source only when safe relaxation and same-source sampling would still fail or
|
|
|
113
116
|
would pass bad-fit rows.
|
|
114
117
|
The default path stays the existing first campaign-table execution slice:
|
|
115
118
|
review the normal `reviewBatchLimit:15`, approve reviewed draft rows, then move
|
|
116
|
-
to Settings/sequence/final greenlight.
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
119
|
+
to Settings/sequence/final greenlight. For any plain post-mint fill request
|
|
120
|
+
such as "fill campaigns", "fill up", "refill sends", "max out sends", or
|
|
121
|
+
"load sends", first call `refill_campaign_sends({ mode:"plan", intent:"plain" })`.
|
|
122
|
+
If the user did not include `--yolo`, report the plan and stop. If the user did
|
|
123
|
+
include `--yolo`, call plan first, inspect blockers, then call
|
|
124
|
+
`refill_campaign_sends({ mode:"apply", yolo:true, planRevision, actionIds })`
|
|
125
|
+
using only the selected immutable action IDs from the fresh plan. Plain fill is
|
|
126
|
+
not an alias for `fill_campaign_horizon` or campaign creation. Keep
|
|
127
|
+
`resolve_campaign_fill_route`, `fill_campaign_horizon`, and
|
|
128
|
+
`start_campaign_message_preparation` as fallback/lower-level diagnostics only
|
|
129
|
+
when `refill_campaign_sends` is unavailable or when the refill command itself
|
|
130
|
+
returns an evergreen subplan. `fill_campaign_horizon` is evergreen-only and
|
|
131
|
+
must not be used for regular campaign refill. When more leads are needed, the
|
|
132
|
+
plan must recommend same-campaign source-ladder replenishment; do not create
|
|
133
|
+
warm-post-engager side campaigns, on-demand campaigns, or unrelated campaigns.
|
|
134
|
+
If fallback `resolve_campaign_fill_route` returns `route:"ask_create"`, ask
|
|
135
|
+
whether to create a normal campaign or evergreen campaigns; campaign creation is
|
|
136
|
+
never the default response to plain fill.
|
|
137
|
+
Treat active fills as capacity-fill preparation: calculate the bounded target
|
|
138
|
+
from sender capacity when needed, then let the preparation job queue pending
|
|
139
|
+
`Enrich Prospect` cells, wait for ICP/rubric and Generate Message cells to
|
|
140
|
+
cascade, and mark ready or approve only the target cohort. Do not count
|
|
141
|
+
`checkedRows` as enriched rows; it is only the table cursor. Use
|
|
124
142
|
`progress.enrichedRows`, `progress.needsEnrichRows`, `activeCellCount`,
|
|
125
|
-
`preparedMessages`, `approvedRows`,
|
|
126
|
-
|
|
127
|
-
|
|
143
|
+
`preparedMessages`, `approvedRows`, active preparation jobs, sender-health
|
|
144
|
+
blockers, target, estimated row budget remaining, and `stopReason` to explain
|
|
145
|
+
progress. If the user says "prepare/generate X messages", set
|
|
146
|
+
`targetPreparedMessages:X`, omit `maxRowsToCheck`, and keep
|
|
128
147
|
`approvalMode:"mark_ready"`. The backend calibrates on at least 100 actually
|
|
129
148
|
enriched rows, estimates the row budget from observed rubric/pass yield, caps
|
|
130
|
-
`maxRowsToCheck` at
|
|
131
|
-
|
|
149
|
+
`maxRowsToCheck` at 300, then continues in batches capped at 100 newly checked
|
|
150
|
+
rows. It will not pull another row batch while the current checked batch still
|
|
151
|
+
has queueable or active cells. If the user says "approve X messages", use
|
|
132
152
|
`approvalMode:"approve"` but still do not launch. If the user says "schedule X
|
|
133
|
-
sends" or asks to fill sender sends, use `approvalMode:"approve"`
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
153
|
+
sends" or asks to fill sender sends, use `approvalMode:"approve"` only when
|
|
154
|
+
the user explicitly asked for approval, approve exactly the bounded X-message
|
|
155
|
+
cohort during preparation, then re-read scheduled counts; if scheduler-owned
|
|
156
|
+
cells are not present, report prepared/approved/ready awaiting scheduler
|
|
157
|
+
instead of success. Final launch remains a separate explicit user greenlight
|
|
158
|
+
and must verify that bounded cohort and must not broad approve-all.
|
|
137
159
|
When approving reviewed draft rows in the campaign table, resolve the actual
|
|
138
160
|
visible `Approved` cells with `select_campaign_cells({ columnRole: "approved",
|
|
139
161
|
rowSelector: { type: "rowIds", rowIds } })` and `update_cell` those returned
|
|
@@ -891,7 +913,11 @@ updates.
|
|
|
891
913
|
|
|
892
914
|
1. Call `mcp__sellable__get_auth_status({})`.
|
|
893
915
|
2. If auth is not OK with `error.type === "config"` or `error.type === "auth"`,
|
|
894
|
-
the user has not signed in yet. Run the FTUX
|
|
916
|
+
the user has not signed in yet. Run first-run login through the FTUX
|
|
917
|
+
magic-link handoff. If a browser page or tool guidance gives the user a
|
|
918
|
+
manual fallback, it must be
|
|
919
|
+
`sellable auth set <token> --workspace-id <workspace_id>`. Do not instruct
|
|
920
|
+
the user to hand-edit JSON auth config.
|
|
895
921
|
|
|
896
922
|
a. Say to the user verbatim:
|
|
897
923
|
|