@sellable/install 0.1.2 → 0.1.4

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 CHANGED
@@ -30,4 +30,8 @@ Auth is stored once at:
30
30
 
31
31
  Claude Code and Codex are configured to launch the same packaged MCP server.
32
32
 
33
+ For Codex Desktop, the installer also writes a local Sellable plugin bundle into
34
+ `~/.sellable/codex-marketplace`, includes the Sellable skill entrypoints, and
35
+ enables it in `~/.codex/config.toml`.
36
+
33
37
  If only one host is installed, `--host all` installs the available host and tells you how to add the other one later.
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawnSync } from "node:child_process";
3
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
4
4
  import { dirname, join } from "node:path";
5
5
  import { homedir } from "node:os";
6
6
  import { stdin as input, stdout as output } from "node:process";
@@ -9,6 +9,8 @@ import { createInterface } from "node:readline/promises";
9
9
  const DEFAULT_API_URL = "https://app.sellable.dev";
10
10
  const DEFAULT_SERVER_PACKAGE =
11
11
  process.env.SELLABLE_MCP_PACKAGE || "@sellable/mcp";
12
+ const CODEX_PLUGIN_VERSION = "0.1.4";
13
+ const INSTALL_PACKAGE_SPEC = `@sellable/install@${CODEX_PLUGIN_VERSION}`;
12
14
 
13
15
  function usage() {
14
16
  return `Sellable agent installer
@@ -176,7 +178,7 @@ async function promptForMissingAuth(opts) {
176
178
  [
177
179
  "Missing Sellable token/workspace id.",
178
180
  `Create a token at ${opts.apiUrl}/settings, then rerun:`,
179
- " npx -y @sellable/install --host all --token <token> --workspace-id <workspace_id>",
181
+ ` npx -y ${INSTALL_PACKAGE_SPEC} --host all --token <token> --workspace-id <workspace_id>`,
180
182
  "",
181
183
  "You can also use SELLABLE_TOKEN and SELLABLE_WORKSPACE_ID.",
182
184
  ].join("\n")
@@ -225,6 +227,441 @@ function readExisting(path) {
225
227
  }
226
228
  }
227
229
 
230
+ function codexHome() {
231
+ return process.env.CODEX_HOME?.trim() || join(homedir(), ".codex");
232
+ }
233
+
234
+ function quoteToml(value) {
235
+ return `"${String(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
236
+ }
237
+
238
+ function escapeRegExp(value) {
239
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
240
+ }
241
+
242
+ function upsertTomlTable(content, tableName, block) {
243
+ const tablePattern = new RegExp(
244
+ `(^|\\n)\\[${escapeRegExp(tableName)}\\]\\n[\\s\\S]*?(?=\\n\\[[^\\n]+\\]|$)`
245
+ );
246
+ const normalizedBlock = block.trimEnd();
247
+
248
+ if (tablePattern.test(content)) {
249
+ return content.replace(tablePattern, (_, prefix) => `${prefix}${normalizedBlock}`);
250
+ }
251
+
252
+ return `${content.trimEnd()}\n\n${normalizedBlock}\n`;
253
+ }
254
+
255
+ function writeFile(path, content, opts, mode = 0o644) {
256
+ console.log(`Writing ${path}`);
257
+ if (opts.dryRun) return;
258
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
259
+ writeFileSync(path, content, { mode });
260
+ }
261
+
262
+ function codexPluginManifest(opts) {
263
+ return {
264
+ name: "sellable",
265
+ version: CODEX_PLUGIN_VERSION,
266
+ description:
267
+ "Sellable MCP tools for campaign creation, engagement, interviews, and workflow sequencing.",
268
+ author: {
269
+ name: "Sellable",
270
+ url: "https://sellable.dev/",
271
+ },
272
+ homepage: "https://sellable.dev/",
273
+ repository: "https://github.com/csreyes/sellable",
274
+ license: "UNLICENSED",
275
+ keywords: ["sellable", "linkedin", "outbound", "campaigns", "mcp"],
276
+ mcpServers: "./.mcp.json",
277
+ interface: {
278
+ displayName: "Sellable",
279
+ shortDescription: "Sellable MCP tools for outbound campaign workflows",
280
+ longDescription:
281
+ "Loads the Sellable MCP server into Codex Desktop so Sellable skills can create campaigns, find leads, draft messages, and manage approval-gated launch workflows.",
282
+ developerName: "Sellable",
283
+ category: "Productivity",
284
+ capabilities: ["Interactive", "Write"],
285
+ websiteURL: "https://sellable.dev/",
286
+ privacyPolicyURL: "https://sellable.dev/privacy",
287
+ termsOfServiceURL: "https://sellable.dev/terms",
288
+ defaultPrompt: [
289
+ "Create a Sellable campaign",
290
+ "Find LinkedIn leads",
291
+ "Build an outbound sequence",
292
+ ],
293
+ brandColor: "#8B5CF6",
294
+ screenshots: [],
295
+ },
296
+ };
297
+ }
298
+
299
+ function codexPluginMcp(opts) {
300
+ if (opts.server === "hosted") {
301
+ return {
302
+ mcpServers: {
303
+ sellable: {
304
+ type: "http",
305
+ url: opts.hostedUrl,
306
+ },
307
+ },
308
+ };
309
+ }
310
+
311
+ if (opts.server === "local") {
312
+ const [command, args] = mcpCommand(opts);
313
+ return {
314
+ mcpServers: {
315
+ sellable: {
316
+ type: "stdio",
317
+ command,
318
+ args,
319
+ },
320
+ },
321
+ };
322
+ }
323
+
324
+ return {
325
+ mcpServers: {
326
+ sellable: {
327
+ type: "stdio",
328
+ command: "npx",
329
+ args: ["-y", opts.mcpPackage],
330
+ },
331
+ },
332
+ };
333
+ }
334
+
335
+ const CREATE_CAMPAIGN_ALLOWED_TOOLS = [
336
+ "mcp__sellable__get_auth_status",
337
+ "mcp__sellable__bootstrap_create_campaign",
338
+ "mcp__sellable__get_subskill_prompt",
339
+ "mcp__sellable__search_subskill_prompts",
340
+ "mcp__sellable__get_provider_prompt",
341
+ "mcp__sellable__get_message_prompt",
342
+ "mcp__sellable__get_active_workspace",
343
+ "mcp__sellable__list_senders",
344
+ "mcp__sellable__get_sender",
345
+ "mcp__sellable__enrich_sender",
346
+ "mcp__sellable__complete_sender_research",
347
+ "mcp__sellable__fetch_linkedin_profile",
348
+ "mcp__sellable__fetch_linkedin_posts",
349
+ "mcp__sellable__get_linkedin_profile",
350
+ "mcp__sellable__fetch_company",
351
+ "mcp__sellable__fetch_company_posts",
352
+ "mcp__sellable__lookup_sales_nav_filter",
353
+ "mcp__sellable__search_sales_nav",
354
+ "mcp__sellable__search_prospeo",
355
+ "mcp__sellable__search_signals",
356
+ "mcp__sellable__fetch_post_engagers",
357
+ "mcp__sellable__enrich_with_prospeo",
358
+ "mcp__sellable__bulk_enrich_with_prospeo",
359
+ "mcp__sellable__save_domain_filters",
360
+ "mcp__sellable__add_rubric_item",
361
+ "mcp__sellable__upsert_rubric",
362
+ "mcp__sellable__set_headline_icp_criteria",
363
+ "mcp__sellable__check_rubric",
364
+ "mcp__sellable__create_campaign",
365
+ "mcp__sellable__save_rubrics",
366
+ "mcp__sellable__wait_for_rubric_results",
367
+ "mcp__sellable__update_campaign_brief",
368
+ "mcp__sellable__update_campaign",
369
+ "mcp__sellable__get_campaign",
370
+ "mcp__sellable__get_campaign_context",
371
+ "mcp__sellable__get_campaign_framework",
372
+ "mcp__sellable__get_campaign_navigation_state",
373
+ "mcp__sellable__confirm_lead_list",
374
+ "mcp__sellable__import_leads",
375
+ "mcp__sellable__wait_for_lead_list_ready",
376
+ "mcp__sellable__wait_for_campaign_table_ready",
377
+ "mcp__sellable__get_rows",
378
+ "mcp__sellable__get_rows_minimal",
379
+ "mcp__sellable__get_table_rows",
380
+ "mcp__sellable__load_csv_linkedin_leads",
381
+ "mcp__sellable__load_csv_domains",
382
+ "mcp__sellable__queue_cells",
383
+ "mcp__sellable__generate_messages",
384
+ "mcp__sellable__get_campaign_messages_preview",
385
+ "mcp__sellable__attach_sequence",
386
+ "mcp__sellable__attach_recommended_sequence",
387
+ "mcp__sellable__start_campaign",
388
+ ];
389
+
390
+ function allowedToolsYaml(tools) {
391
+ return tools.map((tool) => ` - ${tool}`).join("\n");
392
+ }
393
+
394
+ function codexSkillOpenAiYaml(displayName, description) {
395
+ return `interface:
396
+ display_name: "${displayName.replaceAll('"', '\\"')}"
397
+ short_description: "${description.replaceAll('"', '\\"')}"
398
+
399
+ dependencies:
400
+ tools:
401
+ - type: "mcp"
402
+ value: "sellable"
403
+ description: "Sellable MCP server"
404
+ `;
405
+ }
406
+
407
+ function createCampaignSkillMd() {
408
+ return `---
409
+ name: sellable:create-campaign
410
+ description: Create a Sellable campaign through the approval-gated workflow.
411
+ allowed-tools:
412
+ ${allowedToolsYaml(CREATE_CAMPAIGN_ALLOWED_TOOLS)}
413
+ ---
414
+
415
+ # Sellable Create Campaign
416
+
417
+ Use this as the customer-facing entrypoint for Sellable campaign creation.
418
+
419
+ ## Bootstrap
420
+
421
+ MCP tool access is required. First load the bootstrap tools in one tool-discovery
422
+ pass:
423
+
424
+ - \`ToolSearch("select:mcp__sellable__get_auth_status")\`
425
+ - \`ToolSearch("select:mcp__sellable__bootstrap_create_campaign")\`
426
+ - \`ToolSearch("select:mcp__sellable__get_subskill_prompt")\`
427
+ - \`ToolSearch("select:mcp__sellable__search_subskill_prompts")\`
428
+
429
+ Do not stop before attempting this ToolSearch preload. If the
430
+ \`mcp__sellable__*\` tools are still not exposed after ToolSearch, stop and say
431
+ this is a Codex install/reload problem, not a campaign problem. Tell the user to
432
+ run \`npx -y ${INSTALL_PACKAGE_SPEC} --host all\` so the packaged MCP server,
433
+ Codex Desktop plugin, and Sellable skill bundle are installed. If they want a
434
+ CLI verification, tell them to run \`sellable --verify-only --host all\`. After
435
+ that, they must fully quit and reopen Codex Desktop before starting a new
436
+ thread. Do not use \`scripts/mcp/sellable-tool-call.mjs\`, \`npm run\`,
437
+ \`node\`, or any local harness as a fallback for this interactive skill.
438
+
439
+ 1. Call \`mcp__sellable__get_auth_status({})\`.
440
+ 2. If auth is not OK, stop and show the returned guidance.
441
+ 3. Detect optional campaign id in the user request (\`cmp_...\`).
442
+ 4. If no campaign id is provided, stay in fresh-create mode and do not call campaign discovery/resume helpers to find one.
443
+ - Do not call \`mcp__sellable__get_campaigns\`.
444
+ - Do not call \`mcp__sellable__get_campaign\` to hunt for IDs.
445
+ - Do not call \`mcp__sellable__create_campaign({ campaignId: ... })\` unless the user supplied that id.
446
+ 5. Call \`mcp__sellable__bootstrap_create_campaign({ flowVersion: "v2", campaignId? })\`.
447
+ 6. If \`safeToProceed !== true\`, stop and show \`blockingErrors\` + \`nextStep\`.
448
+
449
+ ## Execute Workflow
450
+
451
+ 1. Load canonical prompt via
452
+ \`mcp__sellable__get_subskill_prompt({ subskillName: "create-campaign-v2" })\`.
453
+ 2. Follow that prompt exactly.
454
+ 3. For message generation, load the full \`generate-messages\` prompt in the
455
+ same run with chunked
456
+ \`mcp__sellable__get_subskill_prompt({ subskillName: "generate-messages", offset, limit })\`
457
+ calls until \`hasMore\` is false. Do not synthesize
458
+ \`message-validation.md\` from the brief, lead review, or general knowledge.
459
+ 4. Treat message quality as the gate before minting. Do not create a campaign,
460
+ show a commit gate, or mint anything until \`message-validation.md\` proves
461
+ the full generate-messages workflow ran and \`message-review.md\` recommends
462
+ \`approve-message\` against the gold-standard rules.
463
+ 5. Do not create or mutate the live campaign until the approval gate returns
464
+ \`approve\`.
465
+ 6. Do not ask the user to run another command.
466
+
467
+ ## Fallback
468
+
469
+ If subskill lookup fails, use
470
+ \`mcp__sellable__search_subskill_prompts({ query: "create-campaign-v2" })\`,
471
+ then retry \`get_subskill_prompt\`.
472
+ `;
473
+ }
474
+
475
+ function genericSellableSkillMd({ name, title, description }) {
476
+ return `---
477
+ name: sellable:${name}
478
+ description: ${description}
479
+ allowed-tools:
480
+ - mcp__sellable__get_auth_status
481
+ - mcp__sellable__get_subskill_prompt
482
+ - mcp__sellable__search_subskill_prompts
483
+ ---
484
+
485
+ # ${title}
486
+
487
+ Use this as the customer-facing entrypoint for the Sellable \`${name}\` workflow.
488
+
489
+ ## Bootstrap
490
+
491
+ MCP tool access is required. Do not inspect repo files, run shell commands, use
492
+ \`npm\`, \`node\`, local harness scripts, or read local prompt files to emulate
493
+ this workflow.
494
+
495
+ Before giving up, try to load the required tools with ToolSearch:
496
+
497
+ - \`ToolSearch("select:mcp__sellable__get_auth_status")\`
498
+ - \`ToolSearch("select:mcp__sellable__get_subskill_prompt")\`
499
+ - \`ToolSearch("select:mcp__sellable__search_subskill_prompts")\`
500
+
501
+ If the \`mcp__sellable__*\` tools are still not exposed after ToolSearch, stop
502
+ and say this is a Codex install/reload problem. Tell the user to run
503
+ \`npx -y ${INSTALL_PACKAGE_SPEC} --host all\`, fully quit and reopen Codex
504
+ Desktop, then start a new thread.
505
+
506
+ 1. Call \`mcp__sellable__get_auth_status({})\`.
507
+ 2. If auth is not OK, stop and show the returned guidance.
508
+
509
+ ## Execute Workflow
510
+
511
+ 1. Load canonical prompt via \`mcp__sellable__get_subskill_prompt({ subskillName: "${name}" })\`.
512
+ 2. Follow that prompt exactly.
513
+
514
+ ## MCP Prompt Fallback
515
+
516
+ If subskill lookup fails, use \`mcp__sellable__search_subskill_prompts({ query: "${name}" })\`, then retry \`get_subskill_prompt\`.
517
+ `;
518
+ }
519
+
520
+ function codexPluginSkills() {
521
+ return [
522
+ {
523
+ dir: "sellable-create-campaign",
524
+ displayName: "Sellable Create Campaign",
525
+ description: "Create a Sellable campaign with approval gates",
526
+ skillMd: createCampaignSkillMd(),
527
+ },
528
+ {
529
+ dir: "sellable-engage",
530
+ displayName: "Sellable Engage",
531
+ description: "Discover posts and draft comments for a comment campaign",
532
+ skillMd: genericSellableSkillMd({
533
+ name: "engage",
534
+ title: "Sellable Engage",
535
+ description:
536
+ "Find high-signal LinkedIn posts and draft thoughtful engagement comments, then load approved comments into the sender's comment campaign.",
537
+ }),
538
+ },
539
+ {
540
+ dir: "sellable-interview",
541
+ displayName: "Sellable Interview",
542
+ description: "Build a commenting style guide and ICP profile",
543
+ skillMd: genericSellableSkillMd({
544
+ name: "interview",
545
+ title: "Sellable Interview",
546
+ description:
547
+ "Voice-first interview to build your commenting style guide and ICP profile.",
548
+ }),
549
+ },
550
+ {
551
+ dir: "sellable-workflow-sequences",
552
+ displayName: "Sellable Workflow Sequences",
553
+ description: "Create workflow tables and attach outbound sequences",
554
+ skillMd: genericSellableSkillMd({
555
+ name: "workflow-sequences",
556
+ title: "Sellable Workflow Sequences",
557
+ description:
558
+ "Create workflow tables via Sellable MCP, attach outbound sequence templates, and avoid manual sequence-column runCondition mistakes.",
559
+ }),
560
+ },
561
+ ];
562
+ }
563
+
564
+ function writeCodexPluginSkills(pluginRoot, opts) {
565
+ for (const skill of codexPluginSkills()) {
566
+ const skillRoot = join(pluginRoot, "skills", skill.dir);
567
+ writeFile(join(skillRoot, "SKILL.md"), skill.skillMd, opts);
568
+ writeFile(
569
+ join(skillRoot, "agents", "openai.yaml"),
570
+ codexSkillOpenAiYaml(skill.displayName, skill.description),
571
+ opts
572
+ );
573
+ }
574
+ }
575
+
576
+ function installCodexDesktopPlugin(opts) {
577
+ const home = codexHome();
578
+ const configPath = join(home, "config.toml");
579
+ const marketplaceRoot = join(homedir(), ".sellable", "codex-marketplace");
580
+ const marketplacePath = join(marketplaceRoot, ".agents", "plugins", "marketplace.json");
581
+ const pluginRoot = join(marketplaceRoot, "plugins", "sellable");
582
+ const cacheRoot = join(home, "plugins", "cache", "sellable", "sellable");
583
+ const pluginCache = join(cacheRoot, CODEX_PLUGIN_VERSION);
584
+
585
+ const marketplace = {
586
+ name: "sellable",
587
+ interface: {
588
+ displayName: "Sellable",
589
+ },
590
+ plugins: [
591
+ {
592
+ name: "sellable",
593
+ source: {
594
+ source: "local",
595
+ path: "./plugins/sellable",
596
+ },
597
+ policy: {
598
+ installation: "AVAILABLE",
599
+ authentication: "ON_INSTALL",
600
+ },
601
+ category: "Productivity",
602
+ },
603
+ ],
604
+ };
605
+
606
+ const manifest = codexPluginManifest(opts);
607
+ const mcp = codexPluginMcp(opts);
608
+
609
+ writeFile(marketplacePath, `${JSON.stringify(marketplace, null, 2)}\n`, opts);
610
+ writeFile(
611
+ join(pluginRoot, ".codex-plugin", "plugin.json"),
612
+ `${JSON.stringify(manifest, null, 2)}\n`,
613
+ opts
614
+ );
615
+ writeFile(join(pluginRoot, ".mcp.json"), `${JSON.stringify(mcp, null, 2)}\n`, opts);
616
+ writeCodexPluginSkills(pluginRoot, opts);
617
+
618
+ if (!opts.dryRun) {
619
+ rmSync(cacheRoot, { recursive: true, force: true });
620
+ }
621
+ writeFile(
622
+ join(pluginCache, ".codex-plugin", "plugin.json"),
623
+ `${JSON.stringify(manifest, null, 2)}\n`,
624
+ opts
625
+ );
626
+ writeFile(join(pluginCache, ".mcp.json"), `${JSON.stringify(mcp, null, 2)}\n`, opts);
627
+ writeCodexPluginSkills(pluginCache, opts);
628
+
629
+ if (!opts.dryRun) {
630
+ mkdirSync(home, { recursive: true, mode: 0o700 });
631
+ let content = existsSync(configPath) ? readFileSync(configPath, "utf8") : "";
632
+ content = upsertTomlTable(
633
+ content,
634
+ "marketplaces.sellable",
635
+ `[marketplaces.sellable]
636
+ source_type = "local"
637
+ source = ${quoteToml(marketplaceRoot)}
638
+ last_updated = ${quoteToml(new Date().toISOString())}`
639
+ );
640
+ content = upsertTomlTable(
641
+ content,
642
+ 'plugins."sellable@sellable"',
643
+ `[plugins."sellable@sellable"]
644
+ enabled = true`
645
+ );
646
+ content = upsertTomlTable(
647
+ content,
648
+ 'plugins."sellable@sellable-local"',
649
+ `[plugins."sellable@sellable-local"]
650
+ enabled = false`
651
+ );
652
+ writeFileSync(configPath, `${content.trimEnd()}\n`, { mode: 0o600 });
653
+ } else {
654
+ console.log(`+ upsert [marketplaces.sellable] in ${configPath}`);
655
+ console.log(`+ enable [plugins."sellable@sellable"] in ${configPath}`);
656
+ console.log(`+ disable stale [plugins."sellable@sellable-local"] in ${configPath}`);
657
+ }
658
+
659
+ console.log("Codex Desktop plugin installed:");
660
+ console.log(`- marketplace: ${marketplaceRoot}`);
661
+ console.log(`- plugin: sellable@sellable`);
662
+ console.log(`- cache: ${pluginCache}`);
663
+ }
664
+
228
665
  function writeAuth(opts) {
229
666
  if (!opts.token || !opts.workspaceId) {
230
667
  throw new Error(
@@ -299,6 +736,7 @@ function installCodex(opts) {
299
736
  }
300
737
  if (opts.server === "hosted") {
301
738
  run("codex", ["mcp", "add", "sellable", "--url", opts.hostedUrl], opts);
739
+ installCodexDesktopPlugin(opts);
302
740
  return true;
303
741
  }
304
742
  const [command, args] = mcpCommand(opts);
@@ -308,6 +746,7 @@ function installCodex(opts) {
308
746
  allowFail: true,
309
747
  });
310
748
  run("codex", ["mcp", "add", "sellable", "--", command, ...args], opts);
749
+ installCodexDesktopPlugin(opts);
311
750
  return true;
312
751
  }
313
752
 
@@ -322,6 +761,29 @@ function verify(opts) {
322
761
  }
323
762
  if (opts.host === "codex" || opts.host === "all") {
324
763
  console.log(commandExists("codex") ? "Codex CLI present" : "Codex CLI missing");
764
+ const pluginPath = join(
765
+ codexHome(),
766
+ "plugins",
767
+ "cache",
768
+ "sellable",
769
+ "sellable",
770
+ CODEX_PLUGIN_VERSION,
771
+ ".codex-plugin",
772
+ "plugin.json"
773
+ );
774
+ const skillPath = join(
775
+ codexHome(),
776
+ "plugins",
777
+ "cache",
778
+ "sellable",
779
+ "sellable",
780
+ CODEX_PLUGIN_VERSION,
781
+ "skills",
782
+ "sellable-create-campaign",
783
+ "SKILL.md"
784
+ );
785
+ console.log(existsSync(pluginPath) ? "Codex Desktop plugin present" : "Codex Desktop plugin missing");
786
+ console.log(existsSync(skillPath) ? "Codex skill bundle present" : "Codex skill bundle missing");
325
787
  }
326
788
  }
327
789
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/install",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "type": "module",
5
5
  "description": "One-command installer for Sellable MCP in Claude Code and Codex",
6
6
  "bin": {