@sellable/mcp 0.1.27 → 0.1.28

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
@@ -33,7 +33,7 @@ command.
33
33
  For Claude Code and Codex CLI:
34
34
 
35
35
  ```bash
36
- npx -y @sellable/install --host all --token skt_live_your_token_here --workspace-id your_workspace_id
36
+ npx -y @sellable/install@latest --host all --token skt_live_your_token_here --workspace-id your_workspace_id
37
37
  ```
38
38
 
39
39
  Agent-readable install instructions are available at:
@@ -52,6 +52,24 @@ sh /tmp/sellable-install.sh
52
52
  Phase 112 v1 uses package stdio MCP through `@sellable/mcp`. Hosted HTTP
53
53
  MCP is a future/advanced mode until the hosted endpoint exists.
54
54
 
55
+ Package-mode installs launch `@sellable/mcp@latest`, so each fresh host MCP
56
+ start resolves the newest stable npm release. The MCP server also runs a cached
57
+ startup/auth update check and tells the agent to run
58
+ `npx -y @sellable/install@latest` when the installed runtime is behind npm.
59
+
60
+ ### Publishing `@sellable/mcp`
61
+
62
+ Publish from the repo root with:
63
+
64
+ ```bash
65
+ npm run mcp:publish
66
+ ```
67
+
68
+ The script loads `.env` and `prod.env`, builds `mcp/sellable`, verifies npm auth,
69
+ and publishes with a temporary npm config file. Prefer `NPM_TOKEN=<npm token>` in
70
+ `prod.env`; `NODE_AUTH_TOKEN`, `SELLABLE_NPM_TOKEN`, and `NPM_PUBLISH_TOKEN` are
71
+ also supported.
72
+
55
73
  ### 2. Generate API Token
56
74
 
57
75
  1. Go to https://app.sellable.dev/settings
@@ -63,7 +81,7 @@ MCP is a future/advanced mode until the hosted endpoint exists.
63
81
  Install MCP server:
64
82
 
65
83
  ```bash
66
- claude mcp add --transport stdio sellable -- npx -y @sellable/mcp
84
+ claude mcp add --transport stdio sellable -- npx -y @sellable/mcp@latest
67
85
  ```
68
86
 
69
87
  Create auth config at `~/.sellable/config.json`:
@@ -83,14 +101,14 @@ The token is provided when you generate it. Use `list_workspaces` +
83
101
  For customer/package installs, use the public installer:
84
102
 
85
103
  ```bash
86
- npx -y @sellable/install@0.1.26 --host codex --token skt_live_your_token_here --workspace-id your_workspace_id
104
+ npx -y @sellable/install@latest --host codex --token skt_live_your_token_here --workspace-id your_workspace_id
87
105
  ```
88
106
 
89
107
  If you already have `~/.sellable/config.json`, rerun/verify without rewriting
90
108
  auth:
91
109
 
92
110
  ```bash
93
- npx -y @sellable/install@0.1.26 --host codex
111
+ npx -y @sellable/install@latest --host codex
94
112
  sellable --verify-only --host codex
95
113
  ```
96
114
 
package/dist/server.js CHANGED
@@ -29,6 +29,7 @@ import { attachRecommendedSequence, attachSequence, createWorkflowTable, sequenc
29
29
  import { listTables, tableToolDefinitions } from "./tools/tables.js";
30
30
  import { handleVerifyTableRow, verifyRowToolDefinitions, } from "./tools/verify-row.js";
31
31
  import { addTeammate, createWorkspace, getActiveWorkspace, listWorkspaces, setActiveWorkspace, workspaceToolDefinitions, } from "./tools/workspaces.js";
32
+ import { checkForUpdates, logUpdateNotice } from "./update-check.js";
32
33
  const server = new Server({
33
34
  name: "sellable-mcp",
34
35
  version: "1.0.0",
@@ -515,6 +516,11 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
515
516
  async function main() {
516
517
  const startTime = Date.now();
517
518
  console.error(`[sellable-mcp] pid=${process.pid} starting...`);
519
+ void checkForUpdates()
520
+ .then(logUpdateNotice)
521
+ .catch(() => {
522
+ // Update checks are advisory and must never block MCP startup.
523
+ });
518
524
  const transport = new StdioServerTransport();
519
525
  // Exit cleanly when parent disconnects stdin (Claude Code closed/reconnected)
520
526
  process.stdin.on("end", () => {
@@ -1,3 +1,4 @@
1
+ import { type SellableUpdateStatus } from "../update-check.js";
1
2
  export type AuthStatus = {
2
3
  ok: boolean;
3
4
  configPath: string;
@@ -10,6 +11,7 @@ export type AuthStatus = {
10
11
  checkedAt: string;
11
12
  /** Short message the LLM MUST show the user after auth succeeds. */
12
13
  _userNotice: string | null;
14
+ update: SellableUpdateStatus | null;
13
15
  error: null | {
14
16
  type: "config" | "auth" | "workspace" | "api";
15
17
  status?: number;
@@ -1,14 +1,28 @@
1
1
  import { getApi, SellableApiError } from "../api.js";
2
2
  import { getConfig, getConfigPath } from "../auth.js";
3
+ import { checkForUpdates } from "../update-check.js";
3
4
  function maskToken(token) {
4
5
  if (token.length <= 10)
5
6
  return `${token.slice(0, 4)}...`;
6
7
  return `${token.slice(0, 8)}...${token.slice(-2)}`;
7
8
  }
9
+ async function getUpdateStatus() {
10
+ try {
11
+ return await checkForUpdates();
12
+ }
13
+ catch {
14
+ return null;
15
+ }
16
+ }
17
+ function appendUpdateNotice(notice, update) {
18
+ if (!update?._userNotice)
19
+ return notice;
20
+ return notice ? `${notice}\n${update._userNotice}` : update._userNotice;
21
+ }
8
22
  export const authToolDefinitions = [
9
23
  {
10
24
  name: "get_auth_status",
11
- description: "Verify Sellable authentication early. Call this before any campaign work. Returns ok=false with clear guidance when the token is missing, revoked, or the active workspace is not set. IMPORTANT: When auth succeeds, ALWAYS tell the user the _userNotice message so they know which workspace is active.",
25
+ description: "Verify Sellable authentication early. Call this before any campaign work. Also performs a cached Sellable package update check. Returns ok=false with clear guidance when the token is missing, revoked, or the active workspace is not set. IMPORTANT: When auth succeeds, ALWAYS tell the user the _userNotice message so they know which workspace is active and whether an update is available.",
12
26
  inputSchema: {
13
27
  type: "object",
14
28
  properties: {},
@@ -20,6 +34,7 @@ export const authToolDefinitions = [
20
34
  export async function getAuthStatus() {
21
35
  const configPath = getConfigPath();
22
36
  const checkedAt = new Date().toISOString();
37
+ const update = await getUpdateStatus();
23
38
  const base = {
24
39
  ok: false,
25
40
  configPath,
@@ -29,7 +44,8 @@ export async function getAuthStatus() {
29
44
  activeWorkspaceName: null,
30
45
  tokenPrefix: null,
31
46
  workspacesCount: null,
32
- _userNotice: null,
47
+ _userNotice: appendUpdateNotice(null, update),
48
+ update,
33
49
  checkedAt,
34
50
  };
35
51
  try {
@@ -87,7 +103,8 @@ export async function getAuthStatus() {
87
103
  activeWorkspaceName: workspaceName,
88
104
  tokenPrefix: maskToken(config.token),
89
105
  workspacesCount: workspaces.length,
90
- _userNotice: notice,
106
+ _userNotice: appendUpdateNotice(notice, update),
107
+ update,
91
108
  checkedAt,
92
109
  error: null,
93
110
  };
@@ -9,7 +9,7 @@ type BootstrapCreateCampaignInput = {
9
9
  refreshContext?: boolean;
10
10
  };
11
11
  type BootstrapCheck = {
12
- key: "auth" | "framework" | "resume_context" | "subskill_catalog" | "create_campaign_subskill";
12
+ key: "auth" | "update" | "framework" | "resume_context" | "subskill_catalog" | "create_campaign_subskill";
13
13
  ok: boolean;
14
14
  blocking: boolean;
15
15
  detail: string;
@@ -10,6 +10,9 @@ function toGuidance(check, message) {
10
10
  if (check === "auth") {
11
11
  return "Run get_auth_status and fix auth/workspace setup before continuing.";
12
12
  }
13
+ if (check === "update") {
14
+ return "Run npx -y @sellable/install@latest, then restart Claude Code/Codex so Sellable reloads.";
15
+ }
13
16
  if (check === "framework") {
14
17
  return "Fix framework files and rerun bootstrap_create_campaign.";
15
18
  }
@@ -101,6 +104,22 @@ export async function bootstrapCreateCampaign(input = {}) {
101
104
  if (!ok) {
102
105
  pushBlockingError(blockingErrors, "auth", authCheck.status.error?.message || "Authentication check failed.");
103
106
  }
107
+ const update = authCheck.status.update;
108
+ if (update && !update.disabled) {
109
+ requiredChecks.push({
110
+ key: "update",
111
+ ok: !update.blocking,
112
+ blocking: update.blocking,
113
+ detail: update.updateAvailable
114
+ ? update._userNotice || "A newer Sellable MCP package is available."
115
+ : update.mcp.latestVersion
116
+ ? `Sellable MCP is current (${update.mcp.currentVersion ?? "unknown"}).`
117
+ : "Sellable MCP update check completed without latest-version data.",
118
+ });
119
+ if (update.blocking) {
120
+ pushBlockingError(blockingErrors, "update", update._userNotice || "Sellable MCP update required.");
121
+ }
122
+ }
104
123
  }
105
124
  catch (error) {
106
125
  const message = toErrorMessage(error);
@@ -2205,10 +2205,18 @@ export async function confirmLeadList(input) {
2205
2205
  ...(shouldSetCurrentStep ? { currentStep: effectiveCurrentStep } : {}),
2206
2206
  });
2207
2207
  const campaignTableId = importResult.workflowTableId ?? importResult.campaignTableId;
2208
- if (campaignTableId) {
2208
+ // Persist currentStep if the caller asked for it. Do NOT touch
2209
+ // selectedLeadListId here: the campaign table id is already saved to
2210
+ // CampaignOffer.workflowTableId by /api/v3/campaign-builder/import-leads,
2211
+ // and selectedLeadListId must remain pointing at the source lead list
2212
+ // (config.type === "lead_list") so the UI's Continue handler — which feeds
2213
+ // selectedLeadListId back into /api/v3/campaign-builder/import-leads as
2214
+ // sourceLeadListId — can find a valid lead list. Overwriting it with the
2215
+ // campaign table id makes that endpoint 404 with "Lead list not found"
2216
+ // and silently breaks the Leads → Filter Continue button.
2217
+ if (shouldSetCurrentStep) {
2209
2218
  await api.put(`/api/v2/campaign-offers/${campaignOfferId}`, {
2210
- selectedLeadListId: campaignTableId,
2211
- ...(shouldSetCurrentStep ? { currentStep: effectiveCurrentStep } : {}),
2219
+ currentStep: effectiveCurrentStep,
2212
2220
  });
2213
2221
  }
2214
2222
  return {
@@ -0,0 +1,25 @@
1
+ export type PackageUpdateStatus = {
2
+ packageName: string;
3
+ currentVersion: string | null;
4
+ latestVersion: string | null;
5
+ outdated: boolean;
6
+ error?: string;
7
+ };
8
+ export type SellableUpdateStatus = {
9
+ checkedAt: string;
10
+ cachePath: string;
11
+ fromCache: boolean;
12
+ disabled: boolean;
13
+ mcp: PackageUpdateStatus;
14
+ installer: PackageUpdateStatus;
15
+ updateAvailable: boolean;
16
+ blocking: boolean;
17
+ guidance: string | null;
18
+ _userNotice: string | null;
19
+ };
20
+ export declare function checkForUpdates(options?: {
21
+ force?: boolean;
22
+ timeoutMs?: number;
23
+ ttlMs?: number;
24
+ }): Promise<SellableUpdateStatus>;
25
+ export declare function logUpdateNotice(status: SellableUpdateStatus): void;
@@ -0,0 +1,210 @@
1
+ import * as fs from "fs";
2
+ import * as os from "os";
3
+ import * as path from "path";
4
+ const MCP_PACKAGE = "@sellable/mcp";
5
+ const INSTALL_PACKAGE = "@sellable/install";
6
+ const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000;
7
+ const DEFAULT_TIMEOUT_MS = 1500;
8
+ function cachePath() {
9
+ return path.join(os.homedir(), ".sellable", "update-check.json");
10
+ }
11
+ function readCurrentVersion() {
12
+ const entrypointDir = process.argv[1]
13
+ ? path.dirname(path.resolve(process.argv[1]))
14
+ : process.cwd();
15
+ const candidates = [
16
+ path.resolve(entrypointDir, "..", "package.json"),
17
+ path.resolve(process.cwd(), "mcp/sellable/package.json"),
18
+ ];
19
+ for (const pkgPath of candidates) {
20
+ try {
21
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
22
+ if (pkg.name === MCP_PACKAGE && typeof pkg.version === "string") {
23
+ return pkg.version;
24
+ }
25
+ }
26
+ catch {
27
+ // Keep looking; version detection is advisory only.
28
+ }
29
+ }
30
+ return null;
31
+ }
32
+ function registryPackagePath(packageName) {
33
+ return encodeURIComponent(packageName);
34
+ }
35
+ async function fetchLatestVersion(packageName, timeoutMs) {
36
+ const controller = new AbortController();
37
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
38
+ try {
39
+ const response = await fetch(`https://registry.npmjs.org/${registryPackagePath(packageName)}/latest`, {
40
+ signal: controller.signal,
41
+ headers: {
42
+ Accept: "application/json",
43
+ "User-Agent": "sellable-mcp-update-check",
44
+ },
45
+ });
46
+ if (!response.ok) {
47
+ throw new Error(`npm registry returned ${response.status}`);
48
+ }
49
+ const body = (await response.json());
50
+ if (typeof body.version !== "string" || !body.version) {
51
+ throw new Error("npm registry response did not include a version");
52
+ }
53
+ return body.version;
54
+ }
55
+ finally {
56
+ clearTimeout(timer);
57
+ }
58
+ }
59
+ function parseVersion(version) {
60
+ if (!version)
61
+ return null;
62
+ const match = version.match(/^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/);
63
+ if (!match)
64
+ return null;
65
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
66
+ }
67
+ function isVersionOutdated(currentVersion, latestVersion) {
68
+ const current = parseVersion(currentVersion);
69
+ const latest = parseVersion(latestVersion);
70
+ if (!current || !latest)
71
+ return false;
72
+ for (let i = 0; i < 3; i += 1) {
73
+ if (current[i] < latest[i])
74
+ return true;
75
+ if (current[i] > latest[i])
76
+ return false;
77
+ }
78
+ return false;
79
+ }
80
+ function readCache(ttlMs) {
81
+ const file = cachePath();
82
+ try {
83
+ const raw = JSON.parse(fs.readFileSync(file, "utf8"));
84
+ if (!raw.checkedAtMs || Date.now() - raw.checkedAtMs > ttlMs)
85
+ return null;
86
+ return {
87
+ ...raw,
88
+ cachePath: file,
89
+ fromCache: true,
90
+ };
91
+ }
92
+ catch {
93
+ return null;
94
+ }
95
+ }
96
+ function writeCache(status) {
97
+ const file = cachePath();
98
+ try {
99
+ fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
100
+ const { fromCache: _fromCache, ...cacheableStatus } = status;
101
+ const cache = {
102
+ ...cacheableStatus,
103
+ cachePath: file,
104
+ checkedAtMs: Date.now(),
105
+ };
106
+ fs.writeFileSync(file, `${JSON.stringify(cache, null, 2)}\n`, {
107
+ mode: 0o600,
108
+ });
109
+ }
110
+ catch {
111
+ // Update checks must never break MCP startup or tool execution.
112
+ }
113
+ }
114
+ function disabledStatus(currentVersion) {
115
+ return {
116
+ checkedAt: new Date().toISOString(),
117
+ cachePath: cachePath(),
118
+ fromCache: false,
119
+ disabled: true,
120
+ mcp: {
121
+ packageName: MCP_PACKAGE,
122
+ currentVersion,
123
+ latestVersion: null,
124
+ outdated: false,
125
+ },
126
+ installer: {
127
+ packageName: INSTALL_PACKAGE,
128
+ currentVersion: null,
129
+ latestVersion: null,
130
+ outdated: false,
131
+ },
132
+ updateAvailable: false,
133
+ blocking: false,
134
+ guidance: null,
135
+ _userNotice: null,
136
+ };
137
+ }
138
+ function buildNotice(mcp, installer) {
139
+ if (!mcp.outdated)
140
+ return null;
141
+ const versions = mcp.currentVersion && mcp.latestVersion
142
+ ? `${mcp.currentVersion} -> ${mcp.latestVersion}`
143
+ : "a newer version is available";
144
+ const installerVersion = installer.latestVersion
145
+ ? ` The latest installer is ${installer.latestVersion}.`
146
+ : "";
147
+ return `Sellable update available: ${versions}. Run: npx -y @sellable/install@latest.${installerVersion}`;
148
+ }
149
+ export async function checkForUpdates(options) {
150
+ const currentVersion = readCurrentVersion();
151
+ const ttlMs = options?.ttlMs ??
152
+ (Number(process.env.SELLABLE_UPDATE_CHECK_TTL_MS || "") || DEFAULT_TTL_MS);
153
+ const timeoutMs = options?.timeoutMs ??
154
+ (Number(process.env.SELLABLE_UPDATE_CHECK_TIMEOUT_MS || "") ||
155
+ DEFAULT_TIMEOUT_MS);
156
+ if (process.env.SELLABLE_UPDATE_CHECK_DISABLED === "1") {
157
+ return disabledStatus(currentVersion);
158
+ }
159
+ if (!options?.force) {
160
+ const cached = readCache(ttlMs);
161
+ if (cached)
162
+ return cached;
163
+ }
164
+ const [mcpResult, installerResult] = await Promise.allSettled([
165
+ fetchLatestVersion(MCP_PACKAGE, timeoutMs),
166
+ fetchLatestVersion(INSTALL_PACKAGE, timeoutMs),
167
+ ]);
168
+ const mcpLatest = mcpResult.status === "fulfilled" ? mcpResult.value : null;
169
+ const installerLatest = installerResult.status === "fulfilled" ? installerResult.value : null;
170
+ const mcp = {
171
+ packageName: MCP_PACKAGE,
172
+ currentVersion,
173
+ latestVersion: mcpLatest,
174
+ outdated: isVersionOutdated(currentVersion, mcpLatest),
175
+ ...(mcpResult.status === "rejected"
176
+ ? { error: mcpResult.reason?.message || String(mcpResult.reason) }
177
+ : {}),
178
+ };
179
+ const installer = {
180
+ packageName: INSTALL_PACKAGE,
181
+ currentVersion: null,
182
+ latestVersion: installerLatest,
183
+ outdated: false,
184
+ ...(installerResult.status === "rejected"
185
+ ? {
186
+ error: installerResult.reason?.message || String(installerResult.reason),
187
+ }
188
+ : {}),
189
+ };
190
+ const notice = buildNotice(mcp, installer);
191
+ const status = {
192
+ checkedAt: new Date().toISOString(),
193
+ cachePath: cachePath(),
194
+ fromCache: false,
195
+ disabled: false,
196
+ mcp,
197
+ installer,
198
+ updateAvailable: mcp.outdated,
199
+ blocking: false,
200
+ guidance: notice ? "Run npx -y @sellable/install@latest" : null,
201
+ _userNotice: notice,
202
+ };
203
+ writeCache(status);
204
+ return status;
205
+ }
206
+ export function logUpdateNotice(status) {
207
+ if (status._userNotice) {
208
+ console.error(`[sellable-mcp] ${status._userNotice}`);
209
+ }
210
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/mcp",
3
- "version": "0.1.27",
3
+ "version": "0.1.28",
4
4
  "type": "module",
5
5
  "description": "Sellable MCP server for Claude Code and Codex campaign workflows",
6
6
  "main": "dist/index.js",
@@ -118,6 +118,8 @@ Validated draft directory:
118
118
  loading, runbooks, local skill files, skill versions, or "Sellable token".
119
119
  "Quick question panel" is only acceptable when explaining a Codex/Claude setup
120
120
  blocker.
121
+ - Before campaign mint, do not call `check_rubric`; Phase 84 only writes and
122
+ validates draft artifacts.
121
123
  - Never narrate local draft housekeeping to the user. If you create directories,
122
124
  save drafts, write artifacts, or persist intermediate state, translate it into
123
125
  the campaign benefit: consistent brief, approved lead source, reviewed message,
@@ -394,6 +396,9 @@ should test for this campaign. Those can run in parallel and usually take
394
396
  - Before the user chooses `approve`, the mutating live-campaign tools are
395
397
  forbidden: `create_campaign`, `save_rubrics`, `import_leads`,
396
398
  `confirm_lead_list`, `update_campaign`, `queue_cells`, and `start_campaign`.
399
+ - During pre-approval validation, do not call `check_rubric`; use the
400
+ campaignless lead-filter artifacts and only use campaign-backed scoring after
401
+ the approval flow allows it.
397
402
  - Resume state is based on the presence and completeness of the chained artifacts,
398
403
  not on inline validation blocks inside `brief.md`.
399
404
  - Preserve the Phase 83 thesis. Do not rewrite product, ICP, offer, or message