@sellable/install 0.1.212 → 0.1.214

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
@@ -31,9 +31,9 @@ agent command for launching a campaign:
31
31
  sellable create
32
32
  ```
33
33
 
34
- Campaign creation, foundation memory, content capture/ideation, and post
35
- drafting run inside Claude Code or Codex, where the Sellable MCP tools and
36
- approval flows are available.
34
+ Campaign creation, foundation memory, content capture/ideation, post drafting,
35
+ and inbox reply management run inside Claude Code or Codex, where the Sellable
36
+ MCP tools and approval flows are available.
37
37
 
38
38
  Install is auth-free by default. If you do not pass a token, the agent handles
39
39
  Sellable sign-in on the first campaign run with a magic-link handoff.
@@ -97,17 +97,20 @@ Use the same public entrypoints in both hosts:
97
97
  - Claude Code: `/sellable:foundation`
98
98
  - Claude Code: `/sellable:content`
99
99
  - Claude Code: `/sellable:create-post`
100
+ - Claude Code: `/sellable:inbox`
100
101
  - Codex: `$sellable:create-campaign`
101
102
  - Codex: `$sellable:create-ab-test`
102
103
  - Codex: `$sellable:foundation`
103
104
  - Codex: `$sellable:content`
104
105
  - Codex: `$sellable:create-post`
106
+ - Codex: `$sellable:inbox`
105
107
  - Codex Desktop plugin: `sellable@sellable`
106
108
  - Codex visible skill: `Sellable Create Campaign`
107
109
  - Codex visible skill: `Sellable Create A/B Test`
108
110
  - Codex visible skill: `Sellable Foundation`
109
111
  - Codex visible skill: `Sellable Content`
110
112
  - Codex visible skill: `Sellable Create Post`
113
+ - Codex visible skill: `Sellable Inbox`
111
114
  - Internal MCP workflow prompt: `create-campaign-v2`
112
115
  - Internal/backward-compatible memory prompt: `interview`
113
116
 
@@ -182,7 +182,7 @@ Options:
182
182
 
183
183
  Auth:
184
184
  Install is auth-free by default. Sign in happens on the first run of
185
- /sellable:create-campaign, /sellable:foundation, /sellable:content, or /sellable:create-post in Claude Code or Codex,
185
+ /sellable:create-campaign, /sellable:foundation, /sellable:content, /sellable:create-post, or /sellable:inbox in Claude Code or Codex,
186
186
  where the agent walks you through signup or sign-in and stores credentials
187
187
  in ~/.sellable/config.json.
188
188
 
@@ -211,6 +211,7 @@ function printCreateCommandHint() {
211
211
  { label: "Foundation", command: "/sellable:foundation" },
212
212
  { label: "Content", command: "/sellable:content" },
213
213
  { label: "Post", command: "/sellable:create-post" },
214
+ { label: "Inbox", command: "/sellable:inbox" },
214
215
  ]);
215
216
  console.log("");
216
217
  console.log("");
@@ -220,6 +221,7 @@ function printCreateCommandHint() {
220
221
  { label: "Foundation", command: "$sellable:foundation" },
221
222
  { label: "Content", command: "$sellable:content" },
222
223
  { label: "Post", command: "$sellable:create-post" },
224
+ { label: "Inbox", command: "$sellable:inbox" },
223
225
  ]);
224
226
  console.log("");
225
227
  console.log(` ${"─".repeat(63)}`);
@@ -330,10 +332,6 @@ function isWindowsPlatform(platform = process.platform) {
330
332
  return platform === "win32";
331
333
  }
332
334
 
333
- function packageRunnerCommand(platform = process.platform) {
334
- return isWindowsPlatform(platform) ? "npx.cmd" : "npx";
335
- }
336
-
337
335
  function packageManagerCommand(platform = process.platform) {
338
336
  return isWindowsPlatform(platform) ? "npm.cmd" : "npm";
339
337
  }
@@ -551,6 +549,14 @@ function upsertTomlTable(content, tableName, block) {
551
549
  return `${content.trimEnd()}\n\n${normalizedBlock}\n`;
552
550
  }
553
551
 
552
+ function readTomlTable(content, tableName) {
553
+ const tablePattern = new RegExp(
554
+ `(^|\\n)\\[${escapeRegExp(tableName)}\\]\\n([\\s\\S]*?)(?=\\n\\[[^\\n]+\\]|$)`
555
+ );
556
+ const match = content.match(tablePattern);
557
+ return match ? match[2] : "";
558
+ }
559
+
554
560
  function upsertTomlBoolean(content, tableName, key, value) {
555
561
  const line = `${key} = ${value ? "true" : "false"}`;
556
562
  const tablePattern = new RegExp(
@@ -678,12 +684,13 @@ function codexPluginMcp(opts) {
678
684
  };
679
685
  }
680
686
 
687
+ const [command, args] = mcpCommand(opts);
681
688
  return {
682
689
  mcpServers: {
683
690
  sellable: {
684
691
  type: "stdio",
685
- command: packageRunnerCommand(),
686
- args: ["-y", opts.mcpPackage],
692
+ command,
693
+ args,
687
694
  env: {
688
695
  SELLABLE_WATCH_MODE_DRIVER: "codex",
689
696
  },
@@ -1748,6 +1755,67 @@ then retry \`get_subskill_prompt\`.
1748
1755
  `, host, "create-post");
1749
1756
  }
1750
1757
 
1758
+ function inboxSkillMd(host = "shared") {
1759
+ return stampInstalledHost(`---
1760
+ name: inbox
1761
+ description: Search Sellable inbox threads, review reply eligibility, and manage approval-gated LinkedIn replies.
1762
+ allowed-tools:
1763
+ - mcp__sellable__get_auth_status
1764
+ - mcp__sellable__search_inbox_threads
1765
+ - mcp__sellable__get_inbox_thread
1766
+ - mcp__sellable__check_inbox_reply_eligibility
1767
+ - mcp__sellable__update_inbox_draft
1768
+ - mcp__sellable__send_inbox_draft
1769
+ - mcp__sellable__send_inbox_manual_reply
1770
+ ---
1771
+
1772
+ # Sellable Inbox
1773
+
1774
+ Use this as the customer-facing entrypoint for Sellable inbox reply management.
1775
+ It can search LinkedIn inbox threads, load thread history, check product reply
1776
+ eligibility, update an existing draft, send an approved draft, or send one exact
1777
+ manual reply.
1778
+
1779
+ ## Bootstrap
1780
+
1781
+ MCP tool access is required. First call \`mcp__sellable__get_auth_status({})\`.
1782
+ Do not inspect repo files, run shell commands, use \`npm\`, \`node\`, local
1783
+ harness scripts, browser automation, or product API routes to emulate this
1784
+ workflow.
1785
+
1786
+ If the Sellable MCP tool is unavailable, stop and say this is a Codex
1787
+ install/reload problem. Tell the user to run
1788
+ \`curl -fsSL "https://app.sellable.dev/api/v2/cli/install" | sh\`, fully quit and reopen Codex
1789
+ Desktop, then start a new thread.
1790
+
1791
+ ## Read Workflow
1792
+
1793
+ 1. Call \`mcp__sellable__get_auth_status({})\`.
1794
+ 2. Use \`mcp__sellable__search_inbox_threads({ limit, search?, filter?, status?, senderId?, classification?, includeReplyEligibility? })\` to find candidate threads.
1795
+ 3. Use \`mcp__sellable__get_inbox_thread({ threadId })\` before summarizing or drafting.
1796
+ 4. Use \`mcp__sellable__check_inbox_reply_eligibility({ threadId })\` before proposing any reply send path.
1797
+
1798
+ ## Approval Gates
1799
+
1800
+ Never call \`mcp__sellable__update_inbox_draft\`,
1801
+ \`mcp__sellable__send_inbox_draft\`, or
1802
+ \`mcp__sellable__send_inbox_manual_reply\` until the user has explicitly
1803
+ approved all of:
1804
+
1805
+ - workspace/account context
1806
+ - sender
1807
+ - thread or recipient
1808
+ - exact body
1809
+ - exact tool to call
1810
+ - expected side effect
1811
+
1812
+ Draft edits only edit an existing draft. They do not create a new reply draft.
1813
+ Send tools support one thread at a time; do not batch sends. If any approval,
1814
+ snapshot, hash, version, eligibility, or idempotency value is missing or stale,
1815
+ stop and re-read the thread instead of guessing.
1816
+ `, host, "inbox");
1817
+ }
1818
+
1751
1819
  function createCampaignSoulMd() {
1752
1820
  return `# Sellable Campaign GTM Engineer Soul
1753
1821
 
@@ -1969,6 +2037,12 @@ function codexPluginSkills() {
1969
2037
  description: "Capture ideas and draft LinkedIn posts in your voice",
1970
2038
  skillMd: createPostSkillMd(),
1971
2039
  },
2040
+ {
2041
+ dir: "sellable-inbox",
2042
+ displayName: "Sellable Inbox",
2043
+ description: "Search and manage approval-gated LinkedIn inbox replies",
2044
+ skillMd: inboxSkillMd(),
2045
+ },
1972
2046
  ];
1973
2047
  }
1974
2048
 
@@ -2194,6 +2268,23 @@ function claudeCommands() {
2194
2268
  argumentHint: "[post idea or source material]",
2195
2269
  skillMd: createPostSkillMd(),
2196
2270
  },
2271
+ {
2272
+ name: "inbox",
2273
+ title: "Inbox",
2274
+ filename: join("sellable", "inbox.md"),
2275
+ description: "Search and manage approval-gated Sellable inbox replies.",
2276
+ argumentHint: "[thread, sender, recipient, or reply goal]",
2277
+ skillMd: inboxSkillMd(),
2278
+ allowedTools: [
2279
+ "mcp__sellable__get_auth_status",
2280
+ "mcp__sellable__search_inbox_threads",
2281
+ "mcp__sellable__get_inbox_thread",
2282
+ "mcp__sellable__check_inbox_reply_eligibility",
2283
+ "mcp__sellable__update_inbox_draft",
2284
+ "mcp__sellable__send_inbox_draft",
2285
+ "mcp__sellable__send_inbox_manual_reply",
2286
+ ],
2287
+ },
2197
2288
  ].map((command) => ({
2198
2289
  ...command,
2199
2290
  content: claudeCommandMd(command),
@@ -2386,6 +2477,7 @@ enabled = false`
2386
2477
  "default_mode_request_user_input",
2387
2478
  true
2388
2479
  );
2480
+ content = upsertCodexMcpServerConfig(content, opts);
2389
2481
  content = upsertTomlTable(
2390
2482
  content,
2391
2483
  "agents",
@@ -2544,6 +2636,68 @@ function codexMcpAddArgs(opts) {
2544
2636
  ];
2545
2637
  }
2546
2638
 
2639
+ function upsertCodexMcpServerConfig(content, opts) {
2640
+ if (opts.server === "hosted") {
2641
+ content = upsertTomlTable(
2642
+ content,
2643
+ "mcp_servers.sellable",
2644
+ `[mcp_servers.sellable]
2645
+ url = ${quoteToml(withHostedWatchModeDriver(opts.hostedUrl, "codex"))}`
2646
+ );
2647
+ content = removeTomlSection(content, "mcp_servers.sellable.env");
2648
+ return content;
2649
+ }
2650
+
2651
+ const [command, args] = mcpCommand(opts);
2652
+ content = upsertTomlTable(
2653
+ content,
2654
+ "mcp_servers.sellable",
2655
+ `[mcp_servers.sellable]
2656
+ command = ${quoteToml(command)}
2657
+ args = ${tomlArray(args)}`
2658
+ );
2659
+ content = upsertTomlTable(
2660
+ content,
2661
+ "mcp_servers.sellable.env",
2662
+ `[mcp_servers.sellable.env]
2663
+ SELLABLE_WATCH_MODE_DRIVER = "codex"`
2664
+ );
2665
+ return content;
2666
+ }
2667
+
2668
+ function codexMcpServerMatches(content, opts) {
2669
+ const server = readTomlTable(content, "mcp_servers.sellable");
2670
+ if (!server) return false;
2671
+ if (opts.server === "hosted") {
2672
+ return (
2673
+ server.includes(
2674
+ `url = ${quoteToml(withHostedWatchModeDriver(opts.hostedUrl, "codex"))}`
2675
+ ) &&
2676
+ !/\bcommand\s*=/.test(server) &&
2677
+ !/\bargs\s*=/.test(server)
2678
+ );
2679
+ }
2680
+
2681
+ const [command, args] = mcpCommand(opts);
2682
+ const env = readTomlTable(content, "mcp_servers.sellable.env");
2683
+ return (
2684
+ server.includes(`command = ${quoteToml(command)}`) &&
2685
+ server.includes(`args = ${tomlArray(args)}`) &&
2686
+ env.includes('SELLABLE_WATCH_MODE_DRIVER = "codex"')
2687
+ );
2688
+ }
2689
+
2690
+ function codexPluginMcpServerMatches(content, opts) {
2691
+ if (!content) return false;
2692
+ try {
2693
+ const actual = JSON.parse(content)?.mcpServers?.sellable;
2694
+ const expected = codexPluginMcp(opts).mcpServers.sellable;
2695
+ return JSON.stringify(actual) === JSON.stringify(expected);
2696
+ } catch {
2697
+ return false;
2698
+ }
2699
+ }
2700
+
2547
2701
  function installClaude(opts) {
2548
2702
  if (!commandExists("claude")) {
2549
2703
  const message =
@@ -3027,6 +3181,30 @@ async function verify(opts) {
3027
3181
  : "Codex Desktop plugin watch mode driver missing"
3028
3182
  )
3029
3183
  );
3184
+ const codexMcpEntriesCanonical = codexMcpServerMatches(
3185
+ configContent,
3186
+ opts
3187
+ );
3188
+ checks.push(
3189
+ warningCheck(
3190
+ codexMcpEntriesCanonical,
3191
+ codexMcpEntriesCanonical
3192
+ ? "Codex CLI Sellable MCP entry canonical"
3193
+ : "Codex CLI Sellable MCP entry stale"
3194
+ )
3195
+ );
3196
+ const codexPluginMcpEntryCanonical = codexPluginMcpServerMatches(
3197
+ pluginMcpContent,
3198
+ opts
3199
+ );
3200
+ checks.push(
3201
+ warningCheck(
3202
+ codexPluginMcpEntryCanonical,
3203
+ codexPluginMcpEntryCanonical
3204
+ ? "Codex Desktop plugin Sellable MCP entry canonical"
3205
+ : "Codex Desktop plugin Sellable MCP entry stale"
3206
+ )
3207
+ );
3030
3208
  const hasCodexAgentRegistrations = codexCustomAgents().every((agent) =>
3031
3209
  configContent.includes(`[agents.${agent.name}]`)
3032
3210
  );
@@ -3330,6 +3508,7 @@ function printNextSteps(installedHosts, authReused) {
3330
3508
  { label: "Foundation", command: "/sellable:foundation" },
3331
3509
  { label: "Content", command: "/sellable:content" },
3332
3510
  { label: "Post", command: "/sellable:create-post" },
3511
+ { label: "Inbox", command: "/sellable:inbox" },
3333
3512
  ]);
3334
3513
  console.log("");
3335
3514
  console.log("");
@@ -3340,6 +3519,7 @@ function printNextSteps(installedHosts, authReused) {
3340
3519
  { label: "Foundation", command: "$sellable:foundation" },
3341
3520
  { label: "Content", command: "$sellable:content" },
3342
3521
  { label: "Post", command: "$sellable:create-post" },
3522
+ { label: "Inbox", command: "$sellable:inbox" },
3343
3523
  ]);
3344
3524
  console.log("");
3345
3525
  }
@@ -3614,14 +3794,16 @@ async function main() {
3614
3794
  console.log(` apiUrl: ${apiUrl}`);
3615
3795
  console.log(` Continue in your agent:`);
3616
3796
  console.log(` Claude Code: /sellable:create-campaign`);
3617
- console.log(` Claude Code: /sellable:foundation`);
3618
- console.log(` Claude Code: /sellable:content`);
3619
- console.log(` Claude Code: /sellable:create-post`);
3620
- console.log(` Codex: $sellable:create-campaign`);
3621
- console.log(` Codex: $sellable:foundation`);
3622
- console.log(` Codex: $sellable:content`);
3623
- console.log(` Codex: $sellable:create-post`);
3624
- process.exit(0);
3797
+ console.log(` Claude Code: /sellable:foundation`);
3798
+ console.log(` Claude Code: /sellable:content`);
3799
+ console.log(` Claude Code: /sellable:create-post`);
3800
+ console.log(` Claude Code: /sellable:inbox`);
3801
+ console.log(` Codex: $sellable:create-campaign`);
3802
+ console.log(` Codex: $sellable:foundation`);
3803
+ console.log(` Codex: $sellable:content`);
3804
+ console.log(` Codex: $sellable:create-post`);
3805
+ console.log(` Codex: $sellable:inbox`);
3806
+ process.exit(0);
3625
3807
  }
3626
3808
  if (rawArgs[0] === "prefs") {
3627
3809
  const dryRun = rawArgs.includes("--dry-run");
@@ -3,6 +3,8 @@ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
3
3
 
4
4
  const MAX_STDERR_LINES = 80;
5
5
  const DEFAULT_VERIFY_TIMEOUT_MS = 10_000;
6
+ const DEFAULT_VERIFY_ATTEMPTS = 2;
7
+ const DEFAULT_VERIFY_RETRY_DELAY_MS = 750;
6
8
 
7
9
  export const REQUIRED_SELLABLE_MCP_TOOLS = [
8
10
  "get_auth_status",
@@ -125,6 +127,15 @@ export function missingRequiredTools(tools, requiredTools) {
125
127
  return requiredTools.filter((name) => !available.has(name));
126
128
  }
127
129
 
130
+ function sleep(ms) {
131
+ return new Promise((resolve) => setTimeout(resolve, ms));
132
+ }
133
+
134
+ function numberFromEnv(name, fallback) {
135
+ const parsed = Number(process.env[name] || "");
136
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
137
+ }
138
+
128
139
  function textFromToolResult(result) {
129
140
  return (result?.content || [])
130
141
  .filter((part) => part?.type === "text" && typeof part.text === "string")
@@ -291,7 +302,39 @@ async function verifyCreateCampaignSmoke({
291
302
  };
292
303
  }
293
304
 
294
- export async function verifySellableMcpRuntime({
305
+ function summarizeAttempt(result, attempt) {
306
+ return {
307
+ attempt,
308
+ ok: result.ok === true,
309
+ availableToolCount: result.availableTools?.length || 0,
310
+ missingToolCount: result.missingTools?.length || 0,
311
+ error: result.error || null,
312
+ createCampaignSmoke: result.createCampaignSmoke
313
+ ? {
314
+ ok: result.createCampaignSmoke.ok === true,
315
+ error: result.createCampaignSmoke.error || null,
316
+ missingTools: result.createCampaignSmoke.missingTools || [],
317
+ calls: (result.createCampaignSmoke.calls || []).map((call) => ({
318
+ name: call.name,
319
+ ok: call.ok === true,
320
+ })),
321
+ }
322
+ : null,
323
+ stderrTail: result.stderrTail || [],
324
+ startedAt: result.startedAt,
325
+ completedAt: result.completedAt,
326
+ };
327
+ }
328
+
329
+ function shouldRetryRuntimeVerification(result, requiredTools) {
330
+ if (result.ok || result.skipped) return false;
331
+ if (result.error) return true;
332
+ const availableToolCount = result.availableTools?.length || 0;
333
+ const missingToolCount = result.missingTools?.length || 0;
334
+ return availableToolCount === 0 || missingToolCount === requiredTools.length;
335
+ }
336
+
337
+ async function verifySellableMcpRuntimeOnce({
295
338
  command,
296
339
  args = [],
297
340
  cwd = process.cwd(),
@@ -389,3 +432,51 @@ export async function verifySellableMcpRuntime({
389
432
  completedAt: new Date().toISOString(),
390
433
  };
391
434
  }
435
+
436
+ export async function verifySellableMcpRuntime({
437
+ command,
438
+ args = [],
439
+ cwd = process.cwd(),
440
+ env = process.env,
441
+ requiredTools = REQUIRED_SELLABLE_MCP_TOOLS,
442
+ timeoutMs = Number(process.env.SELLABLE_VERIFY_TIMEOUT_MS || "") ||
443
+ DEFAULT_VERIFY_TIMEOUT_MS,
444
+ attempts = numberFromEnv("SELLABLE_VERIFY_ATTEMPTS", DEFAULT_VERIFY_ATTEMPTS),
445
+ retryDelayMs = numberFromEnv(
446
+ "SELLABLE_VERIFY_RETRY_DELAY_MS",
447
+ DEFAULT_VERIFY_RETRY_DELAY_MS
448
+ ),
449
+ }) {
450
+ const attemptSummaries = [];
451
+ let lastResult = null;
452
+
453
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
454
+ const result = await verifySellableMcpRuntimeOnce({
455
+ command,
456
+ args,
457
+ cwd,
458
+ env,
459
+ requiredTools,
460
+ timeoutMs,
461
+ });
462
+ lastResult = result;
463
+ attemptSummaries.push(summarizeAttempt(result, attempt));
464
+
465
+ if (
466
+ attempt >= attempts ||
467
+ !shouldRetryRuntimeVerification(result, requiredTools)
468
+ ) {
469
+ return {
470
+ ...result,
471
+ attempts: attemptSummaries,
472
+ };
473
+ }
474
+
475
+ await sleep(retryDelayMs);
476
+ }
477
+
478
+ return {
479
+ ...lastResult,
480
+ attempts: attemptSummaries,
481
+ };
482
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/install",
3
- "version": "0.1.212",
3
+ "version": "0.1.214",
4
4
  "type": "module",
5
5
  "description": "One-command installer for Sellable MCP in Claude Code and Codex",
6
6
  "bin": {
@@ -80,6 +80,14 @@ It bootstraps auth and host capabilities, then loads the internal
80
80
  the internal campaign workflow prompt and `core/flow.v2.json` through MCP. Keep
81
81
  this wrapper thin; the packaged workflow is the operational source of truth.
82
82
 
83
+ ## Inbox Reply Tool Boundary
84
+
85
+ Inbox reply tools are outside campaign creation. If a user asks to search,
86
+ review, edit, or send inbox replies, treat that as a separate inbox workflow
87
+ using the existing product /api/v3/inbox routes. Any draft edit or send requires
88
+ exact approval for the workspace, sender, thread, body, tool, and expected side
89
+ effect before the write tool is called.
90
+
83
91
  CampaignOffer state and the watch link are the customer-facing source of truth.
84
92
  Disk artifacts are optional debug/UAT diagnostics; normal customer runs should
85
93
  not create, link, or surface local draft files unless the user explicitly asks