@sellable/mcp 0.1.26 → 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.
Files changed (29) hide show
  1. package/README.md +22 -4
  2. package/dist/index-dev.js +0 -0
  3. package/dist/index.js +0 -0
  4. package/dist/server.js +51 -3
  5. package/dist/tools/auth.d.ts +2 -0
  6. package/dist/tools/auth.js +20 -3
  7. package/dist/tools/bootstrap.d.ts +1 -1
  8. package/dist/tools/bootstrap.js +22 -3
  9. package/dist/tools/leads.js +11 -3
  10. package/dist/tools/prompts.d.ts +12 -0
  11. package/dist/tools/prompts.js +36 -10
  12. package/dist/update-check.d.ts +25 -0
  13. package/dist/update-check.js +210 -0
  14. package/package.json +1 -1
  15. package/skills/create-campaign-brief/references/examples/briefs/clover.md +1 -1
  16. package/skills/create-campaign-brief/references/examples/briefs/galley.md +1 -1
  17. package/skills/create-campaign-brief/references/examples/briefs/gelee.md +1 -1
  18. package/skills/create-campaign-brief/references/examples/briefs/hey-digital.md +1 -1
  19. package/skills/create-campaign-brief/references/examples/briefs/persona.md +1 -1
  20. package/skills/create-campaign-brief/references/examples/briefs/revvix.md +1 -1
  21. package/skills/create-campaign-brief/references/examples/briefs/sellable-dev.md +1 -1
  22. package/skills/create-campaign-brief/references/examples/briefs/superposition.md +1 -1
  23. package/skills/create-campaign-brief/references/examples/briefs/superpower.md +1 -1
  24. package/skills/create-campaign-brief/references/examples/briefs/westpark-villas.md +1 -1
  25. package/skills/create-campaign-brief/references/phase75-canonical-brief-template.md +1 -1
  26. package/skills/create-campaign-brief/references/phase75-good-brief-and-messaging-examples.md +4 -4
  27. package/skills/create-campaign-v2/SKILL.md +28 -14
  28. package/skills/create-campaign-v2/core/flow.v2.json +2 -2
  29. package/skills/create-campaign-v2/references/step-13-import-leads.md +6 -2
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/index-dev.js CHANGED
File without changes
package/dist/index.js CHANGED
File without changes
package/dist/server.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
3
  import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
4
- import { getSkillByName, listSkills, stripFrontmatter } from "./skills.js";
4
+ import { getSkillByName, listSkills } from "./skills.js";
5
5
  import { authToolDefinitions, getAuthStatus } from "./tools/auth.js";
6
6
  import { blueprintCommitToolDefinitions, handleAddColumn, handleCommitBlueprint, } from "./tools/blueprint-commit.js";
7
7
  import { bootstrapCreateCampaign, bootstrapToolDefinitions, } from "./tools/bootstrap.js";
@@ -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",
@@ -67,6 +68,35 @@ const allTools = [
67
68
  ...blueprintCommitToolDefinitions,
68
69
  ...verifyRowToolDefinitions,
69
70
  ];
71
+ function parseOptionalNumber(value) {
72
+ if (typeof value === "number" && Number.isFinite(value))
73
+ return value;
74
+ if (typeof value === "string" && value.trim() !== "") {
75
+ const parsed = Number(value);
76
+ if (Number.isFinite(parsed))
77
+ return parsed;
78
+ }
79
+ return undefined;
80
+ }
81
+ function formatSubskillPromptText(result) {
82
+ const header = result.chunkCount && result.chunkCount > 1
83
+ ? [
84
+ `# ${result.name} prompt chunk ${result.chunkIndex} of ${result.chunkCount}`,
85
+ "",
86
+ `Prompt length: ${result.promptLength} characters.`,
87
+ result.chunkingInstructions ?? "",
88
+ "",
89
+ ].join("\n")
90
+ : "";
91
+ const footer = result.hasMore && result.nextOffset !== null
92
+ ? [
93
+ "",
94
+ "---",
95
+ `Continue with offset ${result.nextOffset} and limit ${result.limit}.`,
96
+ ].join("\n")
97
+ : "";
98
+ return `${header}${result.prompt}${footer}`;
99
+ }
70
100
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
71
101
  tools: allTools,
72
102
  }));
@@ -447,16 +477,29 @@ server.setRequestHandler(ListPromptsRequestSchema, async () => ({
447
477
  prompts: listSkills("public").map((skill) => ({
448
478
  name: skill.name,
449
479
  description: skill.description,
480
+ arguments: [
481
+ {
482
+ name: "offset",
483
+ description: "Optional character offset for chunked prompt reads.",
484
+ required: false,
485
+ },
486
+ {
487
+ name: "limit",
488
+ description: "Optional max characters to return for chunked prompt reads.",
489
+ required: false,
490
+ },
491
+ ],
450
492
  })),
451
493
  }));
452
494
  // Get prompt content
453
495
  server.setRequestHandler(GetPromptRequestSchema, async (request) => {
454
496
  const { name } = request.params;
497
+ const args = request.params.arguments ?? {};
455
498
  const skill = getSkillByName(name);
456
499
  if (!skill) {
457
500
  throw new Error(`Unknown prompt: ${name}`);
458
501
  }
459
- const content = stripFrontmatter(skill.content);
502
+ const content = getSubskillPrompt(name, parseOptionalNumber(args.offset), parseOptionalNumber(args.limit));
460
503
  return {
461
504
  description: skill.description,
462
505
  messages: [
@@ -464,7 +507,7 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
464
507
  role: "user",
465
508
  content: {
466
509
  type: "text",
467
- text: content,
510
+ text: formatSubskillPromptText(content),
468
511
  },
469
512
  },
470
513
  ],
@@ -473,6 +516,11 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
473
516
  async function main() {
474
517
  const startTime = Date.now();
475
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
+ });
476
524
  const transport = new StdioServerTransport();
477
525
  // Exit cleanly when parent disconnects stdin (Claude Code closed/reconnected)
478
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);
@@ -215,10 +234,10 @@ export async function bootstrapCreateCampaign(input = {}) {
215
234
  : "";
216
235
  const nextStep = safeToProceed
217
236
  ? resumeDetected
218
- ? `Bootstrap complete.${workspaceNotice} Resume using campaign context, then load ${createCampaignSubskill?.name ?? "create-campaign"} instructions with get_subskill_prompt({ subskillName: "${createCampaignSubskill?.name ?? "create-campaign"}" }).`
237
+ ? `Bootstrap complete.${workspaceNotice} Resume using campaign context, then load ${createCampaignSubskill?.name ?? "create-campaign"} instructions with get_subskill_prompt({ subskillName: "${createCampaignSubskill?.name ?? "create-campaign"}" }); if the response has hasMore=true, continue with nextOffset until hasMore=false.`
219
238
  : flowVersion === "v2"
220
- ? `Bootstrap complete.${workspaceNotice} Load create-campaign-v2 instructions with get_subskill_prompt({ subskillName: "create-campaign-v2" }), then start with the create-campaign-brief interview stage. Do not call create_campaign until the v2 approval gate returns approve.`
221
- : `Bootstrap complete.${workspaceNotice} Load ${createCampaignSubskill?.name ?? "create-campaign"} instructions with get_subskill_prompt({ subskillName: "${createCampaignSubskill?.name ?? "create-campaign"}" }) and follow that flow before calling create_campaign.`
239
+ ? `Bootstrap complete.${workspaceNotice} Load create-campaign-v2 instructions with get_subskill_prompt({ subskillName: "create-campaign-v2" }); if the response has hasMore=true, continue with nextOffset until hasMore=false. Then start with the create-campaign-brief interview stage. Do not call create_campaign until the v2 approval gate returns approve.`
240
+ : `Bootstrap complete.${workspaceNotice} Load ${createCampaignSubskill?.name ?? "create-campaign"} instructions with get_subskill_prompt({ subskillName: "${createCampaignSubskill?.name ?? "create-campaign"}" }); if the response has hasMore=true, continue with nextOffset until hasMore=false. Follow that flow before calling create_campaign.`
222
241
  : "Bootstrap incomplete. Resolve blockingErrors and rerun bootstrap_create_campaign before provider/search/import tools.";
223
242
  // Strip prompt body from createCampaignSubskill — it's loaded via the host
224
243
  // skill prompt. Only return metadata to save context tokens.
@@ -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 {
@@ -20,6 +20,9 @@ export interface SubskillPromptResponse {
20
20
  limit?: number;
21
21
  hasMore?: boolean;
22
22
  nextOffset?: number | null;
23
+ chunkIndex?: number;
24
+ chunkCount?: number;
25
+ chunkingInstructions?: string;
23
26
  }
24
27
  export interface ListSubskillPromptsResponse {
25
28
  total: number;
@@ -44,6 +47,8 @@ export interface CompleteSenderResearchInput {
44
47
  credibilitySignalsFound?: number;
45
48
  notes?: string;
46
49
  }
50
+ export declare const DEFAULT_SUBSKILL_PROMPT_CHUNK_CHARS = 24000;
51
+ export declare const MAX_SUBSKILL_PROMPT_CHUNK_CHARS = 24000;
47
52
  export declare const promptToolDefinitions: ({
48
53
  name: string;
49
54
  description: string;
@@ -65,6 +70,7 @@ export declare const promptToolDefinitions: ({
65
70
  required: never[];
66
71
  additionalProperties?: undefined;
67
72
  };
73
+ _meta?: undefined;
68
74
  } | {
69
75
  name: string;
70
76
  description: string;
@@ -95,9 +101,13 @@ export declare const promptToolDefinitions: ({
95
101
  required: never[];
96
102
  additionalProperties: boolean;
97
103
  };
104
+ _meta?: undefined;
98
105
  } | {
99
106
  name: string;
100
107
  description: string;
108
+ _meta: {
109
+ "anthropic/maxResultSizeChars": number;
110
+ };
101
111
  inputSchema: {
102
112
  type: string;
103
113
  properties: {
@@ -158,6 +168,7 @@ export declare const promptToolDefinitions: ({
158
168
  required: string[];
159
169
  additionalProperties: boolean;
160
170
  };
171
+ _meta?: undefined;
161
172
  } | {
162
173
  name: string;
163
174
  description: string;
@@ -195,6 +206,7 @@ export declare const promptToolDefinitions: ({
195
206
  additionalProperties: boolean;
196
207
  required?: undefined;
197
208
  };
209
+ _meta?: undefined;
198
210
  })[];
199
211
  export declare function getMessagePrompt(): Promise<PromptResponse>;
200
212
  export declare function listSubskillPrompts(limit?: number, includePublic?: boolean, includeInternal?: boolean): ListSubskillPromptsResponse;
@@ -1,6 +1,8 @@
1
1
  import { getApi } from "../api.js";
2
2
  import { getSkillByName, listSkills, stripFrontmatter } from "../skills.js";
3
3
  import { markCreateCampaignPromptLoaded, markResearchPromptLoaded, markSenderResearchCompleted, } from "./flow-preflight.js";
4
+ export const DEFAULT_SUBSKILL_PROMPT_CHUNK_CHARS = 24_000;
5
+ export const MAX_SUBSKILL_PROMPT_CHUNK_CHARS = 24_000;
4
6
  export const promptToolDefinitions = [
5
7
  {
6
8
  name: "get_message_prompt",
@@ -36,7 +38,10 @@ export const promptToolDefinitions = [
36
38
  },
37
39
  {
38
40
  name: "get_subskill_prompt",
39
- description: "Load a Sellable subskill prompt by name. Internal subskills are not listed in prompts/list; use search_subskill_prompts when you are unsure of the exact name.",
41
+ description: "Load a Sellable subskill prompt by name. Large prompts are returned in portable chunks by default; continue with nextOffset until hasMore is false. Internal subskills are not listed in prompts/list; use search_subskill_prompts when you are unsure of the exact name.",
42
+ _meta: {
43
+ "anthropic/maxResultSizeChars": 200_000,
44
+ },
40
45
  inputSchema: {
41
46
  type: "object",
42
47
  properties: {
@@ -50,7 +55,7 @@ export const promptToolDefinitions = [
50
55
  },
51
56
  limit: {
52
57
  type: "number",
53
- description: "Optional max characters to return for chunked prompt reads. Use a bounded value for very large prompts.",
58
+ description: `Optional max characters to return for chunked prompt reads. Defaults to ${DEFAULT_SUBSKILL_PROMPT_CHUNK_CHARS} for large prompts and is capped at ${MAX_SUBSKILL_PROMPT_CHUNK_CHARS}.`,
54
59
  },
55
60
  },
56
61
  required: ["subskillName"],
@@ -141,11 +146,7 @@ export function listSubskillPrompts(limit, includePublic, includeInternal) {
141
146
  })),
142
147
  };
143
148
  }
144
- export function getSubskillPrompt(subskillName, offset, limit) {
145
- const skill = getSkillByName(subskillName);
146
- if (!skill) {
147
- throw new Error(`Unknown subskill prompt: ${subskillName}`);
148
- }
149
+ function markSubskillPromptLoaded(subskillName) {
149
150
  if (subskillName === "create-campaign") {
150
151
  markCreateCampaignPromptLoaded();
151
152
  }
@@ -158,17 +159,37 @@ export function getSubskillPrompt(subskillName, offset, limit) {
158
159
  if (subskillName === "research-prospect") {
159
160
  markResearchPromptLoaded("prospect", "research-prospect");
160
161
  }
162
+ }
163
+ export function getSubskillPrompt(subskillName, offset, limit) {
164
+ const skill = getSkillByName(subskillName);
165
+ if (!skill) {
166
+ throw new Error(`Unknown subskill prompt: ${subskillName}`);
167
+ }
168
+ markSubskillPromptLoaded(subskillName);
161
169
  const fullPrompt = stripFrontmatter(skill.content);
162
170
  const safeOffset = Math.min(Math.max(Number.isFinite(offset) ? Math.floor(offset ?? 0) : 0, 0), fullPrompt.length);
163
- const safeLimit = Number.isFinite(limit) && limit !== undefined
171
+ const requestedLimit = Number.isFinite(limit) && limit !== undefined
164
172
  ? Math.max(Math.floor(limit), 1)
165
173
  : null;
174
+ const defaultChunkLimit = fullPrompt.length > DEFAULT_SUBSKILL_PROMPT_CHUNK_CHARS
175
+ ? DEFAULT_SUBSKILL_PROMPT_CHUNK_CHARS
176
+ : null;
177
+ const safeLimit = requestedLimit === null
178
+ ? defaultChunkLimit
179
+ : Math.min(requestedLimit, MAX_SUBSKILL_PROMPT_CHUNK_CHARS);
166
180
  const prompt = safeLimit === null
167
181
  ? fullPrompt
168
182
  : fullPrompt.slice(safeOffset, safeOffset + safeLimit);
169
183
  const nextOffset = safeLimit === null
170
184
  ? null
171
185
  : Math.min(safeOffset + safeLimit, fullPrompt.length);
186
+ const hasMore = nextOffset !== null && nextOffset < fullPrompt.length;
187
+ const chunkCount = safeLimit === null
188
+ ? 1
189
+ : Math.max(Math.ceil(fullPrompt.length / safeLimit), 1);
190
+ const chunkIndex = safeLimit === null
191
+ ? 1
192
+ : Math.min(Math.floor(safeOffset / safeLimit) + 1, chunkCount);
172
193
  return {
173
194
  name: skill.name,
174
195
  description: skill.description,
@@ -177,8 +198,13 @@ export function getSubskillPrompt(subskillName, offset, limit) {
177
198
  promptLength: fullPrompt.length,
178
199
  offset: safeOffset,
179
200
  limit: safeLimit ?? undefined,
180
- hasMore: nextOffset !== null && nextOffset < fullPrompt.length,
181
- nextOffset: nextOffset !== null && nextOffset < fullPrompt.length ? nextOffset : null,
201
+ hasMore,
202
+ nextOffset: hasMore ? nextOffset : null,
203
+ chunkIndex,
204
+ chunkCount,
205
+ chunkingInstructions: hasMore
206
+ ? `Continue this same prompt load with get_subskill_prompt({ subskillName: "${skill.name}", offset: ${nextOffset}, limit: ${safeLimit} }) until hasMore is false. Treat all chunks as one prompt load; do not use saved local tool-output files or machine-specific paths.`
207
+ : undefined,
182
208
  };
183
209
  }
184
210
  export function completeSenderResearch(input) {
@@ -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.26",
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",
@@ -1,6 +1,6 @@
1
1
  # Clover Campaign Brief + Rubric Archive
2
2
 
3
- Source: [75-ALL-CLIENT-CAMPAIGN-CORPUS.json](/Users/christianreyes/dev/dittto-fresh/web/.planning/phases/75-canonical-campaign-brief-and-copy-calibration/75-ALL-CLIENT-CAMPAIGN-CORPUS.json)
3
+ Source: Phase 75 all-client campaign corpus, copied into this prompt reference archive.
4
4
 
5
5
  Campaigns captured: 4
6
6
 
@@ -1,6 +1,6 @@
1
1
  # Galley Campaign Brief + Rubric Archive
2
2
 
3
- Source: [75-ALL-CLIENT-CAMPAIGN-CORPUS.json](/Users/christianreyes/dev/dittto-fresh/web/.planning/phases/75-canonical-campaign-brief-and-copy-calibration/75-ALL-CLIENT-CAMPAIGN-CORPUS.json)
3
+ Source: Phase 75 all-client campaign corpus, copied into this prompt reference archive.
4
4
 
5
5
  Campaigns captured: 5
6
6
 
@@ -1,6 +1,6 @@
1
1
  # Gelee Campaign Brief + Rubric Archive
2
2
 
3
- Source: [75-ALL-CLIENT-CAMPAIGN-CORPUS.json](/Users/christianreyes/dev/dittto-fresh/web/.planning/phases/75-canonical-campaign-brief-and-copy-calibration/75-ALL-CLIENT-CAMPAIGN-CORPUS.json)
3
+ Source: Phase 75 all-client campaign corpus, copied into this prompt reference archive.
4
4
 
5
5
  Campaigns captured: 1
6
6
 
@@ -1,6 +1,6 @@
1
1
  # Hey Digital Campaign Brief + Rubric Archive
2
2
 
3
- Source: [75-ALL-CLIENT-CAMPAIGN-CORPUS.json](/Users/christianreyes/dev/dittto-fresh/web/.planning/phases/75-canonical-campaign-brief-and-copy-calibration/75-ALL-CLIENT-CAMPAIGN-CORPUS.json)
3
+ Source: Phase 75 all-client campaign corpus, copied into this prompt reference archive.
4
4
 
5
5
  Campaigns captured: 4
6
6
 
@@ -1,6 +1,6 @@
1
1
  # Persona Campaign Brief + Rubric Archive
2
2
 
3
- Source: [75-ALL-CLIENT-CAMPAIGN-CORPUS.json](/Users/christianreyes/dev/dittto-fresh/web/.planning/phases/75-canonical-campaign-brief-and-copy-calibration/75-ALL-CLIENT-CAMPAIGN-CORPUS.json)
3
+ Source: Phase 75 all-client campaign corpus, copied into this prompt reference archive.
4
4
 
5
5
  Campaigns captured: 3
6
6
 
@@ -1,6 +1,6 @@
1
1
  # Revvix Campaign Brief + Rubric Archive
2
2
 
3
- Source: [75-ALL-CLIENT-CAMPAIGN-CORPUS.json](/Users/christianreyes/dev/dittto-fresh/web/.planning/phases/75-canonical-campaign-brief-and-copy-calibration/75-ALL-CLIENT-CAMPAIGN-CORPUS.json)
3
+ Source: Phase 75 all-client campaign corpus, copied into this prompt reference archive.
4
4
 
5
5
  Campaigns captured: 8
6
6
 
@@ -1,6 +1,6 @@
1
1
  # sellable.dev Campaign Brief + Rubric Archive
2
2
 
3
- Source: [75-ALL-CLIENT-CAMPAIGN-CORPUS.json](/Users/christianreyes/dev/dittto-fresh/web/.planning/phases/75-canonical-campaign-brief-and-copy-calibration/75-ALL-CLIENT-CAMPAIGN-CORPUS.json)
3
+ Source: Phase 75 all-client campaign corpus, copied into this prompt reference archive.
4
4
 
5
5
  Campaigns captured: 304
6
6
 
@@ -1,6 +1,6 @@
1
1
  # superposition Campaign Brief + Rubric Archive
2
2
 
3
- Source: [75-ALL-CLIENT-CAMPAIGN-CORPUS.json](/Users/christianreyes/dev/dittto-fresh/web/.planning/phases/75-canonical-campaign-brief-and-copy-calibration/75-ALL-CLIENT-CAMPAIGN-CORPUS.json)
3
+ Source: Phase 75 all-client campaign corpus, copied into this prompt reference archive.
4
4
 
5
5
  Campaigns captured: 5
6
6
 
@@ -1,6 +1,6 @@
1
1
  # Superpower Campaign Brief + Rubric Archive
2
2
 
3
- Source: [75-ALL-CLIENT-CAMPAIGN-CORPUS.json](/Users/christianreyes/dev/dittto-fresh/web/.planning/phases/75-canonical-campaign-brief-and-copy-calibration/75-ALL-CLIENT-CAMPAIGN-CORPUS.json)
3
+ Source: Phase 75 all-client campaign corpus, copied into this prompt reference archive.
4
4
 
5
5
  Campaigns captured: 3
6
6
 
@@ -1,6 +1,6 @@
1
1
  # westpark villas Campaign Brief + Rubric Archive
2
2
 
3
- Source: [75-ALL-CLIENT-CAMPAIGN-CORPUS.json](/Users/christianreyes/dev/dittto-fresh/web/.planning/phases/75-canonical-campaign-brief-and-copy-calibration/75-ALL-CLIENT-CAMPAIGN-CORPUS.json)
3
+ Source: Phase 75 all-client campaign corpus, copied into this prompt reference archive.
4
4
 
5
5
  Campaigns captured: 26
6
6
 
@@ -167,7 +167,7 @@ Add only when the campaign truly needs them:
167
167
 
168
168
  Reference:
169
169
 
170
- - [75-MESSAGE-VARIANT-GENERATION-FLOW.md](/Users/christianreyes/dev/dittto-fresh/web/.planning/phases/75-canonical-campaign-brief-and-copy-calibration/75-MESSAGE-VARIANT-GENERATION-FLOW.md)
170
+ - `75-MESSAGE-VARIANT-GENERATION-FLOW.md` in the Phase 75 planning archive.
171
171
 
172
172
  ## Section Rules
173
173
 
@@ -1,10 +1,10 @@
1
1
  # Phase 75 Live Brief + Messaging Example Bank
2
2
 
3
- Source of truth: [75-LIVE-CAMPAIGN-CORPUS.json](/Users/christianreyes/dev/dittto-fresh/web/.planning/phases/75-canonical-campaign-brief-and-copy-calibration/75-LIVE-CAMPAIGN-CORPUS.json)
3
+ Source of truth: Phase 75 live campaign corpus used to generate this prompt reference.
4
4
 
5
- Full archive of client campaign briefs: [75-ALL-CLIENT-CAMPAIGN-CORPUS.json](/Users/christianreyes/dev/dittto-fresh/web/.planning/phases/75-canonical-campaign-brief-and-copy-calibration/75-ALL-CLIENT-CAMPAIGN-CORPUS.json)
5
+ Full archive of client campaign briefs: Phase 75 all-client campaign corpus, copied into this prompt reference archive.
6
6
 
7
- Direct copied archive of full campaign brief content + rubrics: [brief-rubric-archive/INDEX.md](/Users/christianreyes/dev/dittto-fresh/web/.planning/phases/75-canonical-campaign-brief-and-copy-calibration/brief-rubric-archive/INDEX.md)
7
+ Direct copied archive of full campaign brief content + rubrics: `brief-rubric-archive/INDEX.md` in the Phase 75 planning archive.
8
8
 
9
9
  This file distills the strongest live campaign briefs currently stored in production. The runtime gold set should stay narrow: active clients only, plus the one Hey Digital campaign that clearly performed and still teaches useful structure. The goal is not to preserve every brief shape forever. The goal is to preserve the patterns that consistently show up in the briefs that are actually usable, persuasive, and close to what Sellable has been shipping manually for clients.
10
10
 
@@ -20,7 +20,7 @@ Important note:
20
20
 
21
21
  Runtime gold pack:
22
22
 
23
- - [75-ACTIVE-RUNTIME-MESSAGE-PACK.md](/Users/christianreyes/dev/dittto-fresh/web/.planning/phases/75-canonical-campaign-brief-and-copy-calibration/75-ACTIVE-RUNTIME-MESSAGE-PACK.md)
23
+ - `75-ACTIVE-RUNTIME-MESSAGE-PACK.md` in the Phase 75 planning archive.
24
24
 
25
25
  ## Winning Campaigns In Scope
26
26
 
@@ -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
@@ -482,7 +487,9 @@ Sellable lead lists in the main three-option first batch; support them through
482
487
  custom/freeform input. If the user pastes up to 100 LinkedIn profile URLs or
483
488
  company domains, normalize the paste into a temporary local CSV and continue
484
489
  through the matching CSV preview path. Mixed, ambiguous, malformed, or oversized
485
- pastes should ask for a real CSV file instead of guessing.
490
+ pastes should ask for a real CSV file instead of guessing. Uploaded CSV support
491
+ is larger than paste support: LinkedIn profile CSVs can contain up to 7,500
492
+ rows; domain CSVs can contain up to 7,500 rows but only 1,000 unique domains.
486
493
 
487
494
  Avoid internal wording like `Which proof points should the message be allowed
488
495
  to lean on?` because it describes the artifact, not the founder decision.
@@ -555,6 +562,10 @@ usable leads`.
555
562
  - supplied LinkedIn profile CSVs call `load_csv_linkedin_leads` in preview mode
556
563
  only before approval; do not pass `confirmed: true`, `campaignOfferId`,
557
564
  `currentStep`, `leadListId`, `sourceLeadListId`, or provider-import params
565
+ during the preview. After approval, this branch is a two-stage batch path:
566
+ materialize the full supplied CSV into a Sellable lead-list table first, then
567
+ use the returned `leadListId` as `sourceLeadListId` for the bounded campaign
568
+ review import
558
569
  - supplied company/domain CSVs call `load_csv_domains`; pre-approval
559
570
  confirmation is allowed only without `campaignOfferId` or `currentStep` and
560
571
  only to produce a standalone `domainFilterId` for campaignless
@@ -766,12 +777,13 @@ Run the `generate-messages` skill in caller-declared `DRY MODE`. Its SKILL.md
766
777
  holds the full drafting contract (retrieval, proof inventory, candidates,
767
778
  finalizer pass, voice rules, safety). This step only covers orchestration.
768
779
  This is not optional: before any write to `message-validation.md`,
769
- `message-review.md`, `approval-packet.md`, or a commit-gate question, the current run must load the
770
- full `generate-messages` prompt. In hosted/from-scratch runs, load it with
771
- chunked `get_subskill_prompt({ subskillName: "generate-messages", offset,
772
- limit })` calls so every tool result stays small enough for the streamed
773
- harness. Start with `offset: 0, limit: 12000`, then keep calling with
774
- `offset: nextOffset` until `hasMore` is false.
780
+ `message-review.md`, `approval-packet.md`, or a commit-gate question, the
781
+ current run must load the full `generate-messages` prompt. In hosted/from-
782
+ scratch runs, load it with chunked `get_subskill_prompt({ subskillName:
783
+ "generate-messages", offset, limit })` calls so every tool result stays small
784
+ enough for the streamed harness. Start with `offset: 0`; the MCP will apply a
785
+ portable default limit when omitted. Keep calling with `offset: nextOffset`
786
+ until `hasMore` is false.
775
787
  In Codex-hosted runs, this is a quality gate, not just provenance: if the
776
788
  model cannot retrieve the complete prompt or cannot follow the required
777
789
  gold-standard deliberation flow, stop at `message-review` and ask for
@@ -1648,9 +1660,9 @@ Message` column's http_request writes those cells via the cascade.
1648
1660
  Load `core/auto-execute.yaml` exactly once at the start of Step 13. All
1649
1661
  subsequent steps read the already-parsed config. Do not re-load mid-run.
1650
1662
  Load each subskill prompt (`create-campaign-v2`, `research-sender`,
1651
- `generate-messages`) at most once per run if a tool result already
1652
- told you to load a prompt, load it and remember; do not re-request the
1653
- same prompt later.
1663
+ `generate-messages`) at most once per run. A multi-call chunk sequence counts
1664
+ as one load. If a tool result already told you to load a prompt, load all
1665
+ chunks once and remember; do not restart the same prompt from offset 0 later.
1654
1666
 
1655
1667
  After every `update_campaign({ currentStep: ... })` in the tail, narrate
1656
1668
  what changed and orient the user to what the watch link will show next —
@@ -1707,10 +1719,12 @@ atomic mint).
1707
1719
  `import_leads({ campaignOfferId, targetLeadCount: importLimit })`.
1708
1720
  If a manifest exists, branch by `sourceType`:
1709
1721
  - `supplied-linkedin-profiles`: revalidate file metadata and confirmation
1710
- token, confirm `load_csv_linkedin_leads` only after approval, persist the
1711
- returned `leadListId`, then call
1712
- `confirm_lead_list({ sourceLeadListId: leadListId, targetLeadCount:
1713
- importLimit })`.
1722
+ token, confirm `load_csv_linkedin_leads` only after approval to batch the
1723
+ supplied CSV into a Sellable lead list, persist the returned `leadListId`,
1724
+ then call `confirm_lead_list({ sourceLeadListId: leadListId,
1725
+ targetLeadCount: importLimit })`. Do not call `import_leads` for this
1726
+ branch; the lead list is the source, and `confirm_lead_list` imports the
1727
+ bounded review batch into the campaign table.
1714
1728
  - `existing-lead-list`: revalidate that the lead list still exists in the
1715
1729
  same workspace, reuse it as `sourceLeadListId`, then call
1716
1730
  `confirm_lead_list({ sourceLeadListId, targetLeadCount: importLimit })`.
@@ -345,7 +345,7 @@
345
345
  },
346
346
  "optionalRequiredArtifacts": ["lead-source-intake.json"],
347
347
  "toolRules": {
348
- "suppliedLinkedinProfiles": "Preview only before approval: call load_csv_linkedin_leads without confirmed, campaignOfferId, currentStep, leadListId, sourceLeadListId, or provider-import parameters; skip provider discovery.",
348
+ "suppliedLinkedinProfiles": "Preview only before approval: call load_csv_linkedin_leads without confirmed, campaignOfferId, currentStep, leadListId, sourceLeadListId, or provider-import parameters; skip provider discovery. After approval, batch/materialize the supplied CSV into a Sellable lead list first, then use the returned leadListId as sourceLeadListId for confirm_lead_list.",
349
349
  "suppliedDomains": "May confirm load_csv_domains before approval only without campaignOfferId/currentStep to produce a standalone domainFilterId, then run a campaignless Prospeo sample constrained by domainFilterId.",
350
350
  "existingLeadList": "Skip provider discovery and sample existing rows before approval; do not clone/import until after approval.",
351
351
  "forbiddenPreApproval": [
@@ -1022,7 +1022,7 @@
1022
1022
  "defaultWhenMissing": "discovered-provider-import",
1023
1023
  "branches": {
1024
1024
  "normal-discovery": "import_leads({ campaignOfferId, targetLeadCount: import.importLimit })",
1025
- "supplied-linkedin-profiles": "confirm load_csv_linkedin_leads after approval, then confirm_lead_list({ sourceLeadListId, targetLeadCount: import.importLimit })",
1025
+ "supplied-linkedin-profiles": "confirm load_csv_linkedin_leads after approval to batch/materialize the CSV into a lead list, persist returned leadListId, then confirm_lead_list({ sourceLeadListId: leadListId, targetLeadCount: import.importLimit }); do not call import_leads for this branch",
1026
1026
  "existing-lead-list": "confirm_lead_list({ sourceLeadListId: existingLeadListId, targetLeadCount: import.importLimit })",
1027
1027
  "supplied-domains": "reuse or confirm load_csv_domains, run campaign-associated search_prospeo({ campaignOfferId, domainFilterId }), then import_leads({ campaignOfferId, targetLeadCount: import.importLimit })"
1028
1028
  },
@@ -23,8 +23,12 @@ Supported branches:
23
23
 
24
24
  - **No manifest / normal discovery** — use the existing provider import path.
25
25
  - **Supplied LinkedIn profile CSV** — confirm `load_csv_linkedin_leads` only
26
- after approval, then import into the campaign table with
27
- `confirm_lead_list({ sourceLeadListId, targetLeadCount: importLimit })`.
26
+ after approval to batch/materialize the full uploaded CSV into a Sellable
27
+ lead-list table. Persist the returned `leadListId`, then import the bounded
28
+ review batch into the campaign table with
29
+ `confirm_lead_list({ sourceLeadListId: leadListId, targetLeadCount: importLimit })`.
30
+ Do not call `import_leads` for this branch; the materialized lead list is the
31
+ source.
28
32
  Do **not** call `wait_for_lead_list_ready` for this branch; there is no
29
33
  provider import job to wait on, and probing readiness can leave the campaign
30
34
  in an import-failed state before `confirm_lead_list`.