@sellable/mcp 0.1.328 → 0.1.330

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/dist/server.js CHANGED
@@ -6,6 +6,7 @@ import { getAuthStatus } from "./tools/auth.js";
6
6
  import { handleAddColumn, handleCommitBlueprint, } from "./tools/blueprint-commit.js";
7
7
  import { bootstrapCreateCampaign } from "./tools/bootstrap.js";
8
8
  import { prepareCampaignAbTest } from "./tools/campaign-ab-test.js";
9
+ import { fillCampaignHorizon } from "./tools/campaign-horizon-fill.js";
9
10
  import { cancelPrepareCampaignMessages, getPrepareCampaignMessagesStatus, startPrepareCampaignMessages, } from "./tools/campaign-message-preparation.js";
10
11
  import { getCampaignTableSchema, queueCampaignCells, reviseMessageTemplateAndRerun, selectCampaignCells, waitForCampaignProcessing, } from "./tools/campaign-processing.js";
11
12
  import { createCampaign, duplicateCampaign, getCampaign, getCampaignMessagesPreview, getCampaigns, pauseCampaign, startCampaign, updateCampaign, updateCampaignBrief, } from "./tools/campaigns.js";
@@ -178,6 +179,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
178
179
  case "get_campaign_messages_preview":
179
180
  result = await getCampaignMessagesPreview(args);
180
181
  break;
182
+ case "fill_campaign_horizon":
183
+ result = await fillCampaignHorizon(args);
184
+ if (args?.campaignId) {
185
+ markCampaignContextDirty(args.campaignId, "fill_campaign_horizon");
186
+ }
187
+ break;
181
188
  case "start_campaign_message_preparation":
182
189
  case "start_prepare_campaign_messages":
183
190
  result = await startPrepareCampaignMessages(args);
@@ -0,0 +1,93 @@
1
+ type FillCampaignHorizonInput = {
2
+ action: "audit" | "apply";
3
+ campaignId: string;
4
+ tableId?: string;
5
+ stateRevision?: string;
6
+ excludedPostIds?: string[];
7
+ excludedPostUrls?: string[];
8
+ excludedAuthorProfileUrls?: string[];
9
+ excludedAuthorNames?: string[];
10
+ targetPreparedMessages?: number;
11
+ maxRowsToCheck?: number;
12
+ batchSize?: number;
13
+ };
14
+ export declare const campaignHorizonFillToolDefinitions: {
15
+ name: string;
16
+ description: string;
17
+ inputSchema: {
18
+ type: string;
19
+ properties: {
20
+ action: {
21
+ type: string;
22
+ enum: string[];
23
+ description: string;
24
+ };
25
+ campaignId: {
26
+ type: string;
27
+ description: string;
28
+ };
29
+ tableId: {
30
+ type: string;
31
+ description: string;
32
+ };
33
+ stateRevision: {
34
+ type: string;
35
+ description: string;
36
+ };
37
+ excludedPostIds: {
38
+ type: string;
39
+ items: {
40
+ type: string;
41
+ };
42
+ maxItems: number;
43
+ description: string;
44
+ };
45
+ excludedPostUrls: {
46
+ type: string;
47
+ items: {
48
+ type: string;
49
+ };
50
+ maxItems: number;
51
+ description: string;
52
+ };
53
+ excludedAuthorProfileUrls: {
54
+ type: string;
55
+ items: {
56
+ type: string;
57
+ };
58
+ maxItems: number;
59
+ description: string;
60
+ };
61
+ excludedAuthorNames: {
62
+ type: string;
63
+ items: {
64
+ type: string;
65
+ };
66
+ maxItems: number;
67
+ description: string;
68
+ };
69
+ targetPreparedMessages: {
70
+ type: string;
71
+ minimum: number;
72
+ maximum: number;
73
+ description: string;
74
+ };
75
+ maxRowsToCheck: {
76
+ type: string;
77
+ minimum: number;
78
+ maximum: number;
79
+ description: string;
80
+ };
81
+ batchSize: {
82
+ type: string;
83
+ minimum: number;
84
+ maximum: number;
85
+ description: string;
86
+ };
87
+ };
88
+ required: string[];
89
+ additionalProperties: boolean;
90
+ };
91
+ }[];
92
+ export declare function fillCampaignHorizon(input: FillCampaignHorizonInput): Promise<unknown>;
93
+ export {};
@@ -0,0 +1,92 @@
1
+ import { getApi } from "../api.js";
2
+ async function postHorizonFill(body) {
3
+ const api = getApi();
4
+ return api.post("/api/v3/mcp/campaign-horizon-fill", body);
5
+ }
6
+ export const campaignHorizonFillToolDefinitions = [
7
+ {
8
+ name: "fill_campaign_horizon",
9
+ description: "Audit or apply a bounded CampaignOffer horizon fill from Signal Discovery/source lead-list rows. Use audit first to get stateRevision, then apply with that stateRevision. Apply imports at most 300 eligible non-excluded source rows, starts bounded message preparation in approval mode, skips rows from excluded posts/authors, and does not start or launch the campaign.",
10
+ inputSchema: {
11
+ type: "object",
12
+ properties: {
13
+ action: {
14
+ type: "string",
15
+ enum: ["audit", "apply"],
16
+ description: 'Use "audit" to inspect counts/receipt without writes. Use "apply" with the audit stateRevision to import/prep.',
17
+ },
18
+ campaignId: {
19
+ type: "string",
20
+ description: "CampaignOffer.id for the campaign to fill.",
21
+ },
22
+ tableId: {
23
+ type: "string",
24
+ description: "Optional workflow table id. Must match the campaign workflowTableId when provided.",
25
+ },
26
+ stateRevision: {
27
+ type: "string",
28
+ description: "Required for apply. Copy from the immediately preceding audit receipt.",
29
+ },
30
+ excludedPostIds: {
31
+ type: "array",
32
+ items: { type: "string" },
33
+ maxItems: 100,
34
+ description: "Exact SignalSearchPost ids to exclude from import/preparation.",
35
+ },
36
+ excludedPostUrls: {
37
+ type: "array",
38
+ items: { type: "string" },
39
+ maxItems: 100,
40
+ description: "Exact LinkedIn post URLs to exclude from import/preparation.",
41
+ },
42
+ excludedAuthorProfileUrls: {
43
+ type: "array",
44
+ items: { type: "string" },
45
+ maxItems: 100,
46
+ description: "Exact LinkedIn author profile URLs whose source rows should be excluded.",
47
+ },
48
+ excludedAuthorNames: {
49
+ type: "array",
50
+ items: { type: "string" },
51
+ maxItems: 25,
52
+ description: "Exact author names to exclude only when they resolve unambiguously inside campaign Signal Discovery posts.",
53
+ },
54
+ targetPreparedMessages: {
55
+ type: "number",
56
+ minimum: 1,
57
+ maximum: 300,
58
+ description: "Prepared/approved message target for the first pass. Backend caps this at 300.",
59
+ },
60
+ maxRowsToCheck: {
61
+ type: "number",
62
+ minimum: 1,
63
+ maximum: 300,
64
+ description: "Hard first-pass row cap. Backend caps import and prep at 300 rows.",
65
+ },
66
+ batchSize: {
67
+ type: "number",
68
+ minimum: 1,
69
+ maximum: 100,
70
+ description: "Preparation batch size. Backend caps newly checked rows at 100 per batch.",
71
+ },
72
+ },
73
+ required: ["action", "campaignId"],
74
+ additionalProperties: false,
75
+ },
76
+ },
77
+ ];
78
+ export function fillCampaignHorizon(input) {
79
+ return postHorizonFill({
80
+ action: input.action,
81
+ campaignId: input.campaignId,
82
+ tableId: input.tableId,
83
+ stateRevision: input.stateRevision,
84
+ excludedPostIds: input.excludedPostIds,
85
+ excludedPostUrls: input.excludedPostUrls,
86
+ excludedAuthorProfileUrls: input.excludedAuthorProfileUrls,
87
+ excludedAuthorNames: input.excludedAuthorNames,
88
+ targetPreparedMessages: input.targetPreparedMessages,
89
+ maxRowsToCheck: input.maxRowsToCheck,
90
+ batchSize: input.batchSize,
91
+ });
92
+ }
@@ -474,6 +474,83 @@ export declare const allTools: ({
474
474
  required: string[];
475
475
  additionalProperties: boolean;
476
476
  };
477
+ } | {
478
+ name: string;
479
+ description: string;
480
+ inputSchema: {
481
+ type: string;
482
+ properties: {
483
+ action: {
484
+ type: string;
485
+ enum: string[];
486
+ description: string;
487
+ };
488
+ campaignId: {
489
+ type: string;
490
+ description: string;
491
+ };
492
+ tableId: {
493
+ type: string;
494
+ description: string;
495
+ };
496
+ stateRevision: {
497
+ type: string;
498
+ description: string;
499
+ };
500
+ excludedPostIds: {
501
+ type: string;
502
+ items: {
503
+ type: string;
504
+ };
505
+ maxItems: number;
506
+ description: string;
507
+ };
508
+ excludedPostUrls: {
509
+ type: string;
510
+ items: {
511
+ type: string;
512
+ };
513
+ maxItems: number;
514
+ description: string;
515
+ };
516
+ excludedAuthorProfileUrls: {
517
+ type: string;
518
+ items: {
519
+ type: string;
520
+ };
521
+ maxItems: number;
522
+ description: string;
523
+ };
524
+ excludedAuthorNames: {
525
+ type: string;
526
+ items: {
527
+ type: string;
528
+ };
529
+ maxItems: number;
530
+ description: string;
531
+ };
532
+ targetPreparedMessages: {
533
+ type: string;
534
+ minimum: number;
535
+ maximum: number;
536
+ description: string;
537
+ };
538
+ maxRowsToCheck: {
539
+ type: string;
540
+ minimum: number;
541
+ maximum: number;
542
+ description: string;
543
+ };
544
+ batchSize: {
545
+ type: string;
546
+ minimum: number;
547
+ maximum: number;
548
+ description: string;
549
+ };
550
+ };
551
+ required: string[];
552
+ additionalProperties: boolean;
553
+ };
477
554
  } | {
478
555
  name: string;
479
556
  description: string;
@@ -2,6 +2,7 @@ import { authToolDefinitions } from "./auth.js";
2
2
  import { blueprintCommitToolDefinitions } from "./blueprint-commit.js";
3
3
  import { bootstrapToolDefinitions } from "./bootstrap.js";
4
4
  import { campaignAbTestToolDefinitions } from "./campaign-ab-test.js";
5
+ import { campaignHorizonFillToolDefinitions } from "./campaign-horizon-fill.js";
5
6
  import { campaignMessagePreparationToolDefinitions } from "./campaign-message-preparation.js";
6
7
  import { campaignProcessingToolDefinitions } from "./campaign-processing.js";
7
8
  import { campaignToolDefinitions } from "./campaigns.js";
@@ -42,6 +43,7 @@ import { workspaceToolDefinitions } from "./workspaces.js";
42
43
  export const allTools = [
43
44
  ...campaignToolDefinitions,
44
45
  ...campaignAbTestToolDefinitions,
46
+ ...campaignHorizonFillToolDefinitions,
45
47
  ...campaignMessagePreparationToolDefinitions,
46
48
  ...campaignProcessingToolDefinitions,
47
49
  ...authToolDefinitions,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/mcp",
3
- "version": "0.1.328",
3
+ "version": "0.1.330",
4
4
  "type": "module",
5
5
  "description": "Sellable MCP server for Claude Code and Codex campaign workflows",
6
6
  "main": "dist/index.js",
@@ -46,6 +46,7 @@ allowed-tools:
46
46
  - mcp__sellable__select_campaign_cells
47
47
  - mcp__sellable__queue_campaign_cells
48
48
  - mcp__sellable__wait_for_campaign_processing
49
+ - mcp__sellable__fill_campaign_horizon
49
50
  - mcp__sellable__start_campaign_message_preparation
50
51
  - mcp__sellable__get_campaign_message_preparation_status
51
52
  - mcp__sellable__cancel_campaign_message_preparation
@@ -107,6 +108,13 @@ the template is approved.
107
108
  The default path stays the existing first campaign-table execution slice:
108
109
  review the normal `reviewBatchLimit:15`, approve reviewed draft rows, then move
109
110
  to Settings/sequence/final greenlight. Only call
111
+ `fill_campaign_horizon` for explicit source-cleanup horizon-fill requests, such
112
+ as "fill sends from Signal Discovery but not John Cutler posts." Run
113
+ `fill_campaign_horizon({ action:"audit", ... })` first, then apply with the
114
+ returned `stateRevision`. It imports/prepares at most 300 eligible non-excluded
115
+ source rows in the first pass, caps prep batches at 100, skips existing rows
116
+ from excluded post/author sources, and does not launch the campaign. If the
117
+ user only asks for generic extra sends with no source cleanup, use
110
118
  `start_campaign_message_preparation` when the user explicitly asks for more
111
119
  prepared messages, a send count, or language like "fill up/load sends for these
112
120
  senders." Treat those requests as capacity-fill preparation: calculate the
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: create-evergreen-campaigns
3
- description: Reconcile a sender team's evergreen campaign structure — create-or-reuse the standing campaigns (per-sender post engagers + shared fallback lanes) idempotently, with sequences attached and prospect-safe message briefs. Never duplicates, never launches. Schedule-automation friendly ("make sure evergreen campaigns exist for these users").
3
+ description: Reconcile a sender team's evergreen campaign structure — create-or-reuse the standing campaigns (per-sender post engagers + shared fallback/source lanes) idempotently, with send-lane sequences and prospect-safe message briefs. Never duplicates, never launches. Schedule-automation friendly ("make sure evergreen campaigns exist for these users").
4
4
  visibility: internal
5
5
  ---
6
6
 
@@ -16,15 +16,28 @@ producing duplicates. Reconcile/create-reuse only; this skill never launches cam
16
16
  <inputs>
17
17
  The invoking prompt names the senders ("create evergreen campaigns for csreyes92 and thomas"). Resolve each via `list_senders`.
18
18
 
19
+ If the invoking prompt says scheduled, automation, cron, heartbeat, fresh-thread,
20
+ run without user input, or similar, run in **automation mode**:
21
+
22
+ - Do not ask the user to confirm templates, delivery format, or sample output.
23
+ - Reconcile structure and record any template/config polish as `created`,
24
+ `reused`, `repaired`, `flagged`, or `blocked`.
25
+ - Apply only safe metadata repairs that do not create rows, generate messages,
26
+ approve rows, launch campaigns, send messages, or spend paid InMail.
27
+
28
+ If the invoking prompt explicitly asks for interactive message polish or sample
29
+ proof, run in **interactive polish mode** and use the confirmation/sample steps
30
+ below.
31
+
19
32
  Default evergreen plan per workspace (override only if the prompt specifies different lanes):
20
33
 
21
34
  1. **`<Sender Name> - Post Engagers`** — one per sender (warm lane, highest priority)
22
- 2. **`<Workspace/Team> - Shared Signal Discovery`** — one shared across senders
23
- 3. **`<Workspace/Team> - Shared Cold Fallback`** — one shared across senders
35
+ 2. **`<Workspace/Team> - Shared Signal Discovery`** — one shared source lane across senders
36
+ 3. **`<Workspace/Team> - Shared Cold Fallback`** — one shared source/fallback lane across senders
24
37
  </inputs>
25
38
 
26
39
  <objective>
27
- 1. **Inventory first**: `get_campaigns` + `list_tables` in the active workspace. Match existing campaigns to the plan by name (case-insensitive, ignore suffixes like "(Copy)"). A matching non-archived campaign = REUSE; record it and move on. Never create a second campaign for a slot that already has one.
40
+ 1. **Inventory first**: `get_campaigns` + `list_tables` + `get_campaign_waterfall` in the active workspace. Treat `list_tables` and the managed waterfall as authoritative for older managed slots; `get_campaigns` is a recent campaign page and may miss canonical evergreen lanes. Match existing campaigns/tables/waterfall slots to the plan by name (case-insensitive, ignore suffixes like "(Copy)") and stored slot identity. A matching non-archived campaign/table/waterfall slot = REUSE; record it and move on. Never create a second campaign for a slot that already has one in any of those inventories.
28
41
  2. **Create only the missing slots** with `create_on_demand_campaign({ name, senderIds, campaignBrief })`:
29
42
  - Post Engagers lanes: that sender's ID only. Shared lanes: all the senders' IDs.
30
43
  - The brief must include the warm post-engager first-message template style — short, casual, references the post they engaged with, closed question, **no internal vocabulary, no pitch, no meeting ask** in message one:
@@ -42,23 +55,27 @@ Default evergreen plan per workspace (override only if the prompt specifies diff
42
55
  - **InMail lanes can never be multiline**: an InMail is one message and the recipient must reply before anything else can be sent. InMail-bound templates must read as one cohesive message — declare `Delivery format: single message (InMail — no follow-up until reply)` and never structure the copy to depend on multi-message pacing.
43
56
 
44
57
  - The sequence is auto-selected by sender tier; do not hand-author sequence templates here. Never select a paid-InMail template.
45
- 3. **Verify each slot** after create/reuse: `get_campaign` shows the table exists and a sequence is attached (`list_tables({ hasSequence: true })` as a cross-check).
46
- 4. **Confirm the message template with the user and check it reads chat-native.** Show the exact first-message template each created campaign's brief carries and ask the user to confirm or adjust it before moving on. Because DM copy may send paragraph-by-paragraph (each blank-line block becomes its own message), every paragraph must read like something a human literally typed as a separate chat message:
58
+ 3. **Verify each slot** after create/reuse/repair:
59
+ - `get_campaign` shows the campaign exists, remains unlaunched, and has the expected workflow table.
60
+ - Send/action lanes such as Post Engagers have a sequence attached (`list_tables({ hasSequence: true })` as a cross-check).
61
+ - Source-only shared lanes may legitimately have `hasSequence:false` when they only supply rows through the managed waterfall. For those, verify the active waterfall linkage, table ID, campaign ID, source type, and priority; report `source-only/no sequence expected` instead of repairing or duplicating.
62
+ 4. **Interactive polish mode only: confirm the message template with the user — and check it reads chat-native.** Show the exact first-message template each created campaign's brief carries and ask the user to confirm or adjust it before moving on. Because DM copy may send paragraph-by-paragraph (each blank-line block becomes its own message), every paragraph must read like something a human literally typed as a separate chat message:
47
63
  - **No letter punctuation.** `Hey {{first_name}}` — never `Hey {{first_name}},` (nobody types a trailing comma and hits send). No `Dear`, no sign-offs, no `Best,`.
48
64
  - Each paragraph stands alone as a message — short, lowercase-casual is fine, sentence fragments are fine.
49
65
  - No paragraph should depend on letter formatting (no "As I mentioned above" referencing layout).
50
66
  If the template violates these, propose the chat-native version and ask; on approval, update the brief via `update_campaign_brief` and show the final version.
51
67
 
52
68
  **Also confirm the delivery format and keep config in sync.** Ask the user whether DM lanes should send multiline (paragraph-per-message) or as a single message. Record the answer as the brief's `Delivery format:` line, and when multiline is chosen, set `actionConfig.sendEachParagraphAsMessage: true` on that campaign's `send_dm` column via `update_column` (config edits run no cells and nothing sends from unlaunched campaigns). Never set the paragraph-split flag on InMail columns — the option does not apply to InMail.
53
- 5. **Prove the template on one real row.** For ONE campaign that has at least one lead row (add one via `add_on_demand_leads` if every lane is empty and the user provides/approves a test lead), generate a message for exactly one row (`queue_campaign_cells` with `columnRole: "generateMessage"`, `rowSelector: { type: "reviewBatch", limit: 1 }` or the single row's ID), wait for it, then show the user the generated message next to the template — AND show how it would split if paragraph-per-message sending is enabled (list each paragraph as `msg 1:`, `msg 2:`, …) so the user confirms each one reads like a real typed message. Check alignment: tone, structure, no internal vocabulary, correct token substitution, no letter punctuation. Do NOT approve the row or generate for more rows — one sample only.
54
- 6. **Report the reconcile plan and result** every slot tagged `reused` or `created`, plus the confirmed template and the sample message:
69
+ 5. **Interactive polish mode only: prove the template on one real row.** For ONE campaign that has at least one lead row (add one via `add_on_demand_leads` if every lane is empty and the user provides/approves a test lead), generate a message for exactly one row (`queue_campaign_cells` with `columnRole: "generateMessage"`, `rowSelector: { type: "reviewBatch", limit: 1 }` or the single row's ID), wait for it, then show the user the generated message next to the template — AND show how it would split if paragraph-per-message sending is enabled (list each paragraph as `msg 1:`, `msg 2:`, …) so the user confirms each one reads like a real typed message. Check alignment: tone, structure, no internal vocabulary, correct token substitution, no letter punctuation. Do NOT approve the row or generate for more rows — one sample only.
70
+ 6. **Automation mode template/config audit**: inspect existing send-lane briefs for a `Delivery format:` line and chat-native warm template. If a send lane is missing the line and the prompt allows cleanup, update only the campaign brief metadata with the safest default for that lane (`Delivery format: single message (LinkedIn DM; line breaks remain inside one generated message; no follow-up until reply)`). If cleanup is not allowed, flag it. Do not generate a sample row in automation mode unless the invocation explicitly asks for sample proof.
71
+ 7. **Report the reconcile plan and result** — every slot tagged `reused`, `created`, `repaired`, `flagged`, or `blocked`, plus template/sample details only when that mode ran:
55
72
 
56
73
  ```
57
74
  Evergreen Reconcile — {date}
58
- • Christian Reyes - Post Engagers: reused (18 rows)
59
- • Thomas Nobbs - Post Engagers: reused (8 rows)
60
- • Sellable.dev - Shared Signal Discovery: reused
61
- • Sellable.dev - Shared Cold Fallback: created (empty needs leads)
75
+ • Christian Reyes - Post Engagers: reused (18 rows, sequence attached)
76
+ • Thomas Nobbs - Post Engagers: reused (8 rows, sequence attached)
77
+ • Sellable.dev - Shared Signal Discovery: reused (source-only/no sequence expected)
78
+ • Sellable.dev - Shared Cold Fallback: created (source-only/no sequence expected)
62
79
  All campaigns remain unlaunched. Next: refresh-sender-engagement to supply the warm lanes, then fill-send-horizon.
63
80
  ```
64
81
  </objective>