@sellable/mcp 0.1.259 → 0.1.260

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/index-dev.js CHANGED
File without changes
package/dist/index.js CHANGED
File without changes
@@ -1,3 +1,14 @@
1
+ type FollowerBandFit = "in_target_band" | "below_target_band" | "above_target_band" | "unknown";
2
+ type ReachSignals = {
3
+ targetFollowerMin?: number;
4
+ targetFollowerMax?: number;
5
+ followerBandFit: FollowerBandFit;
6
+ engagementPer1kFollowers: number | null;
7
+ weightedEngagementPer1kFollowers: number | null;
8
+ reachPenaltyMultiplier: number;
9
+ reachAdjustedScore: number;
10
+ confidence: "high" | "medium" | "low";
11
+ };
1
12
  export type EngagementPost = {
2
13
  postId: string;
3
14
  url: string;
@@ -7,6 +18,7 @@ export type EngagementPost = {
7
18
  name: string;
8
19
  headline: string;
9
20
  profileUrl: string;
21
+ followerCount?: number;
10
22
  };
11
23
  engagement: {
12
24
  likes: number;
@@ -14,6 +26,7 @@ export type EngagementPost = {
14
26
  shares: number;
15
27
  total: number;
16
28
  };
29
+ reachSignals?: ReachSignals;
17
30
  contentPreview: string;
18
31
  };
19
32
  export type SearchEngagementPostsInput = {
@@ -23,6 +36,8 @@ export type SearchEngagementPostsInput = {
23
36
  minTotalEngagement?: number;
24
37
  maxPosts?: number;
25
38
  excludePostUrls?: string[];
39
+ targetFollowerMin?: number;
40
+ targetFollowerMax?: number;
26
41
  };
27
42
  export type SearchEngagementPostsResponse = {
28
43
  success: boolean;
@@ -70,9 +85,18 @@ export declare const engageDiscoveryToolDefinitions: {
70
85
  };
71
86
  description: string;
72
87
  };
88
+ targetFollowerMin: {
89
+ type: string;
90
+ description: string;
91
+ };
92
+ targetFollowerMax: {
93
+ type: string;
94
+ description: string;
95
+ };
73
96
  };
74
97
  required: string[];
75
98
  additionalProperties: boolean;
76
99
  };
77
100
  }[];
78
101
  export declare function searchEngagementPosts(input: SearchEngagementPostsInput): Promise<SearchEngagementPostsResponse>;
102
+ export {};
@@ -32,6 +32,14 @@ export const engageDiscoveryToolDefinitions = [
32
32
  items: { type: "string" },
33
33
  description: "Optional list of post URLs to exclude (e.g. already engaged/scheduled).",
34
34
  },
35
+ targetFollowerMin: {
36
+ type: "number",
37
+ description: "Optional lower bound for creator follower count when reach-normalizing hook/source quality.",
38
+ },
39
+ targetFollowerMax: {
40
+ type: "number",
41
+ description: "Optional upper bound for creator follower count when reach-normalizing hook/source quality.",
42
+ },
35
43
  },
36
44
  required: ["keywords"],
37
45
  additionalProperties: false,
@@ -63,6 +71,79 @@ function safeNumber(value) {
63
71
  const n = typeof value === "number" ? value : Number(value);
64
72
  return Number.isFinite(n) ? n : 0;
65
73
  }
74
+ function round3(value) {
75
+ return Number(value.toFixed(3));
76
+ }
77
+ function parseFollowerCount(author) {
78
+ const direct = safeNumber(author?.followerCount);
79
+ if (direct > 0)
80
+ return direct;
81
+ const text = String(author?.followers || author?.followerCountText || "");
82
+ const match = text.match(/([\d,.]+)\s*([kKmM])?/);
83
+ if (!match)
84
+ return undefined;
85
+ const base = Number(match[1].replace(/,/g, ""));
86
+ if (!Number.isFinite(base) || base <= 0)
87
+ return undefined;
88
+ const suffix = match[2]?.toLowerCase();
89
+ if (suffix === "m")
90
+ return Math.round(base * 1_000_000);
91
+ if (suffix === "k")
92
+ return Math.round(base * 1_000);
93
+ return Math.round(base);
94
+ }
95
+ function reachPenaltyMultiplier(followerCount, targetFollowerMin, targetFollowerMax) {
96
+ if (!followerCount || !targetFollowerMin || !targetFollowerMax)
97
+ return 0.4;
98
+ if (followerCount >= targetFollowerMin && followerCount <= targetFollowerMax) {
99
+ return 1;
100
+ }
101
+ if (followerCount < targetFollowerMin)
102
+ return 0.75;
103
+ if (followerCount <= targetFollowerMax * 2)
104
+ return 0.65;
105
+ if (followerCount <= targetFollowerMax * 5)
106
+ return 0.35;
107
+ return 0.15;
108
+ }
109
+ function followerBandFit(followerCount, targetFollowerMin, targetFollowerMax) {
110
+ if (!followerCount || !targetFollowerMin || !targetFollowerMax) {
111
+ return "unknown";
112
+ }
113
+ if (followerCount >= targetFollowerMin && followerCount <= targetFollowerMax) {
114
+ return "in_target_band";
115
+ }
116
+ if (followerCount < targetFollowerMin)
117
+ return "below_target_band";
118
+ return "above_target_band";
119
+ }
120
+ function buildReachSignals(params) {
121
+ const { followerCount, likes, comments, shares, total } = params;
122
+ const hasTarget = Boolean(params.targetFollowerMin && params.targetFollowerMax);
123
+ if (!hasTarget && !followerCount)
124
+ return undefined;
125
+ const weightedEngagement = likes + comments * 4 + shares * 12;
126
+ const penalty = reachPenaltyMultiplier(followerCount, params.targetFollowerMin, params.targetFollowerMax);
127
+ const engagementPer1kFollowers = followerCount
128
+ ? round3((total / followerCount) * 1000)
129
+ : null;
130
+ const weightedEngagementPer1kFollowers = followerCount
131
+ ? round3((weightedEngagement / followerCount) * 1000)
132
+ : null;
133
+ const reachAdjustedScore = weightedEngagementPer1kFollowers === null
134
+ ? 0
135
+ : round3(weightedEngagementPer1kFollowers * penalty);
136
+ return {
137
+ targetFollowerMin: params.targetFollowerMin,
138
+ targetFollowerMax: params.targetFollowerMax,
139
+ followerBandFit: followerBandFit(followerCount, params.targetFollowerMin, params.targetFollowerMax),
140
+ engagementPer1kFollowers,
141
+ weightedEngagementPer1kFollowers,
142
+ reachPenaltyMultiplier: penalty,
143
+ reachAdjustedScore,
144
+ confidence: followerCount ? (hasTarget ? "high" : "medium") : "low",
145
+ };
146
+ }
66
147
  export async function searchEngagementPosts(input) {
67
148
  const api = getApi();
68
149
  const keywords = (input.keywords || [])
@@ -79,6 +160,12 @@ export async function searchEngagementPosts(input) {
79
160
  const maxPosts = typeof input.maxPosts === "number" && input.maxPosts > 0
80
161
  ? Math.min(50, input.maxPosts)
81
162
  : 25;
163
+ const targetFollowerMin = typeof input.targetFollowerMin === "number" && input.targetFollowerMin > 0
164
+ ? input.targetFollowerMin
165
+ : undefined;
166
+ const targetFollowerMax = typeof input.targetFollowerMax === "number" && input.targetFollowerMax > 0
167
+ ? input.targetFollowerMax
168
+ : undefined;
82
169
  const exclude = new Set((input.excludePostUrls || [])
83
170
  .map((u) => normalizePostUrl(u))
84
171
  .filter(Boolean));
@@ -87,10 +174,10 @@ export async function searchEngagementPosts(input) {
87
174
  keywords: keywords.map((keyword) => ({ keyword })),
88
175
  page,
89
176
  });
90
- const rawPosts = Array.isArray(response?.topPostsForLLM)
91
- ? response.topPostsForLLM
92
- : Array.isArray(response?.posts)
93
- ? response.posts
177
+ const rawPosts = Array.isArray(response?.posts)
178
+ ? response.posts
179
+ : Array.isArray(response?.topPostsForLLM)
180
+ ? response.topPostsForLLM
94
181
  : [];
95
182
  const now = Date.now();
96
183
  const oldestMs = now - maxAgeDays * 24 * 60 * 60 * 1000;
@@ -121,6 +208,16 @@ export async function searchEngagementPosts(input) {
121
208
  tooOld += 1;
122
209
  continue;
123
210
  }
211
+ const followerCount = parseFollowerCount(p?.author);
212
+ const reachSignals = buildReachSignals({
213
+ followerCount,
214
+ likes,
215
+ comments,
216
+ shares,
217
+ total,
218
+ targetFollowerMin,
219
+ targetFollowerMax,
220
+ });
124
221
  kept.push({
125
222
  postId: String(p?.id || ""),
126
223
  url,
@@ -132,18 +229,26 @@ export async function searchEngagementPosts(input) {
132
229
  name: String(p?.author?.name || ""),
133
230
  headline: String(p?.author?.headline || ""),
134
231
  profileUrl: String(p?.author?.profileUrl || ""),
232
+ ...(followerCount ? { followerCount } : {}),
135
233
  },
136
234
  engagement: { likes, comments, shares, total },
235
+ ...(reachSignals ? { reachSignals } : {}),
137
236
  contentPreview: previewText(String(p?.content || ""), 220),
138
237
  });
139
- if (kept.length >= maxPosts)
140
- break;
141
238
  }
142
- // Sort by total engagement desc for a predictable shortlist.
143
- kept.sort((a, b) => b.engagement.total - a.engagement.total);
239
+ const hasReachTarget = Boolean(targetFollowerMin && targetFollowerMax);
240
+ kept.sort((a, b) => {
241
+ if (hasReachTarget) {
242
+ const reachDelta = (b.reachSignals?.reachAdjustedScore || 0) -
243
+ (a.reachSignals?.reachAdjustedScore || 0);
244
+ if (reachDelta !== 0)
245
+ return reachDelta;
246
+ }
247
+ return b.engagement.total - a.engagement.total;
248
+ });
144
249
  return {
145
250
  success: true,
146
- posts: kept,
251
+ posts: kept.slice(0, maxPosts),
147
252
  totalReturned: rawPosts.length,
148
253
  filteredOut: { tooOld, excluded, tooLowEngagement },
149
254
  };
@@ -1749,6 +1749,14 @@ export declare const allTools: ({
1749
1749
  };
1750
1750
  description: string;
1751
1751
  };
1752
+ targetFollowerMin: {
1753
+ type: string;
1754
+ description: string;
1755
+ };
1756
+ targetFollowerMax: {
1757
+ type: string;
1758
+ description: string;
1759
+ };
1752
1760
  };
1753
1761
  required: string[];
1754
1762
  additionalProperties: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/mcp",
3
- "version": "0.1.259",
3
+ "version": "0.1.260",
4
4
  "type": "module",
5
5
  "description": "Sellable MCP server for Claude Code and Codex campaign workflows",
6
6
  "main": "dist/index.js",
@@ -84,6 +84,7 @@ Before drafting, load all required assets with `mcp__sellable__get_subskill_asse
84
84
  3. `subskillName: "create-post", assetPath: "references/premise-development.md"`
85
85
  4. `subskillName: "create-post", assetPath: "references/post-validation.md"`
86
86
  5. `subskillName: "create-post", assetPath: "references/gold-standard-post-pack.md"`
87
+ 6. `subskillName: "create-post", assetPath: "references/linkedin-preview-rendering.md"`
87
88
 
88
89
  If any required asset is missing, unreadable, truncated without continuation, or internally inconsistent, return:
89
90
 
@@ -320,6 +321,12 @@ The research worker must return a compact packet only:
320
321
  - market belief map: resonating ideas, implicit beliefs, audience wants, resentments, fears, and credible controversy angles
321
322
  - premise inputs: real scenes, observed tensions, reader value openings, and proof gaps
322
323
  - exact phrase patterns and sentence shapes
324
+ - post positioning breakdowns that label each meaningful line or phrase by
325
+ narrative job and category
326
+ - viral-post outlines for the best source posts
327
+ - line-to-template conversions that turn source structures into reusable
328
+ templates without source wording
329
+ - hook-to-body promise maps that show how each hook tells the body
323
330
  - body structures and exact body language moves
324
331
  - preview measurements
325
332
  - track-person and gold-standard recommendations
@@ -343,7 +350,15 @@ Default flow:
343
350
  10. For story posts, extract the story mechanism that made the post work, not just the first line.
344
351
  11. Extract hook structures plus specific reusable words, phrases, sentence
345
352
  shapes, transitions, and body language patterns.
346
- 12. Save the research with `mcp__sellable__save_hook_research`.
353
+ 12. Create a post positioning breakdown for each keeper post: line/phrase,
354
+ category, narrative technique, tension created, reader question opened,
355
+ proof dependency, and reusable template line.
356
+ 13. Convert each keeper into a viral-post outline: hook job, see-more trigger,
357
+ body payoff, close job, and beat-by-beat narrative structure.
358
+ 14. Convert the best outlines into reusable post templates with positioning
359
+ sequences, required story/proof inputs, forbidden borrowing, and
360
+ Sellable-specific adaptation instructions.
361
+ 15. Save the research with `mcp__sellable__save_hook_research`.
347
362
 
348
363
  Record provenance:
349
364
 
@@ -362,6 +377,10 @@ Record provenance:
362
377
  - market belief map and selected controversy
363
378
  - premise cards and selected premise
364
379
  - exact phrase patterns and sentence shapes
380
+ - post positioning breakdowns
381
+ - viral-post outlines
382
+ - reusable post templates
383
+ - hook-to-body promise maps
365
384
  - body structures and body language patterns
366
385
  - why each pattern fits the user's idea and voice
367
386
 
@@ -410,7 +429,10 @@ Hook patterns learned:
410
429
  1. full adapted hook block
411
430
  - source mechanism:
412
431
  - preview budget:
432
+ - see-more tension:
433
+ - curiosity debt:
413
434
  - internal question:
435
+ - hook-to-body promise:
414
436
  - why it fits / why it does not:
415
437
 
416
438
  Specific words and phrase shapes:
@@ -421,9 +443,35 @@ Specific words and phrase shapes:
421
443
  - adapted Sellable form:
422
444
  - do not copy:
423
445
 
446
+ Post positioning breakdown templates:
447
+ 1. source + template name
448
+ - positioning sequence:
449
+ - line-level narrative techniques:
450
+ - tension created:
451
+ - reader questions opened:
452
+ - reusable template lines:
453
+ - Sellable adaptation:
454
+
455
+ Viral-post outlines:
456
+ 1. outline name
457
+ - hook job:
458
+ - see-more trigger:
459
+ - body payoff:
460
+ - close job:
461
+ - beat sequence:
462
+
463
+ Line-to-template conversion:
464
+ 1. source line/beat
465
+ - narrative job:
466
+ - template line shape:
467
+ - required user story/proof:
468
+ - forbidden borrowing:
469
+ - adapted Sellable expression:
470
+
424
471
  Body structures learned:
425
472
  1. structure name
426
473
  - source:
474
+ - positioning sequence:
427
475
  - sequence:
428
476
  - exact language moves:
429
477
  - adapted Sellable body move:
@@ -437,9 +485,9 @@ Save recommendations:
437
485
  - gold-standard candidates:
438
486
 
439
487
  Recommended draft directions:
440
- 1. premise card + hook block + body structure
441
- 2. premise card + hook block + body structure
442
- 3. premise card + hook block + body structure
488
+ 1. premise card + source template + hook block + viral outline + body structure
489
+ 2. premise card + source template + hook block + viral outline + body structure
490
+ 3. premise card + source template + hook block + viral outline + body structure
443
491
  ```
444
492
 
445
493
  Keep this report concise enough to read, but concrete enough that another agent
@@ -453,7 +501,9 @@ Generate 3-5 `Premise Card` candidates from the raw idea, market research, core
453
501
  memory, story/proof files, and current-session user feedback. Each card must
454
502
  include a real story/scene or observed pattern, target reader, common belief,
455
503
  contrarian truth, tension, reader value, proof available, proof missing, and a
456
- score.
504
+ score. Each card must also evaluate which source template or no-template path
505
+ fits, which positioning sequence to test, and how the hook promise will be
506
+ repaid in the body.
457
507
 
458
508
  Select the strongest premise before hook generation. The selected premise must
459
509
  pass:
@@ -465,12 +515,63 @@ pass:
465
515
  - `credible_speaker`
466
516
  - `proof_safety`
467
517
  - `market_heat`
518
+ - `template_fit`
519
+ - `hook_to_body_repayment`
468
520
 
469
521
  If the idea has market heat but no real scene, ask for the missing scene unless
470
522
  the user explicitly requested an immediate draft. For immediate draft mode, use
471
523
  only source-backed observed patterns and save the draft as `needs_revision`
472
524
  unless the premise still has concrete reader value.
473
525
 
526
+ ## Step 1.9: Pre-Draft Structure Brief
527
+
528
+ Before generating final draft prose, create and show a compact `Pre-Draft
529
+ Structure Brief`. In other words, show a compact Pre-Draft Structure Brief
530
+ before the final draft body. This is the checkpoint that prevents the agent from
531
+ jumping from a good hook into a messy body.
532
+
533
+ The brief must be concise, mobile-scanable, and concrete enough for the user to
534
+ approve or correct. Do not hide it inside the validation receipt after the
535
+ draft. Show it before writing the draft body unless the user explicitly says to
536
+ skip outline/structure and write immediately. Even when the user skips the
537
+ visible checkpoint, still include the brief in the validation receipt.
538
+
539
+ The `Pre-Draft Structure Brief` must include:
540
+
541
+ ```text
542
+ Pre-Draft Structure Brief
543
+ - hook:
544
+ - thesis:
545
+ - reader being taught:
546
+ - why this reader cares now:
547
+ - core equation or mechanism:
548
+ - key definitions:
549
+ - proof claims:
550
+ - proof source / risk:
551
+ - source template or no-template rationale:
552
+ - mobile scan path:
553
+ - section outline:
554
+ - body promise after see more:
555
+ - concrete examples to include:
556
+ - abstractions to remove:
557
+ - draft risks:
558
+ ```
559
+
560
+ Rules:
561
+
562
+ - The hook must already have a rendered mobile and desktop preview record.
563
+ - The thesis must be one sentence the post can defend.
564
+ - The reader must be specific enough to guide what gets cut.
565
+ - Key definitions must name concrete examples when the post teaches an
566
+ operating concept. For example, define `lead source` as how the list was
567
+ built, not as a persona label.
568
+ - `mobile scan path` must say what a reader understands if they read only the
569
+ hook, separators, section labels, numbers, and final line.
570
+ - `abstractions to remove` must list abstract phrases and the concrete words
571
+ that will replace them.
572
+ - If the user corrects the brief, update the brief first, then draft. Do not
573
+ patch the final prose while leaving the brief stale.
574
+
474
575
  ## Step 2: Hook Candidates
475
576
 
476
577
  Generate at least 12 hook candidates from the selected premise unless the user
@@ -528,7 +629,12 @@ Draft from:
528
629
  - exact raw idea
529
630
  - selected premise card
530
631
  - selected hook
632
+ - approved or latest `Pre-Draft Structure Brief`
531
633
  - hook research artifact
634
+ - selected source template or no-template rationale
635
+ - viral-post outline
636
+ - post positioning breakdown template
637
+ - body expression candidates and combined body plan
532
638
  - user's core memory
533
639
  - story/proof files
534
640
  - post writing rules
@@ -549,11 +655,20 @@ Every saved draft needs a validation receipt with:
549
655
  - selected premise card
550
656
  - candidate hooks considered
551
657
  - selected hook and why
658
+ - pre-draft structure brief
659
+ - selected source template and no-copy adaptation rationale
660
+ - post positioning breakdown
661
+ - viral-post outline
662
+ - hook-to-body promise map
663
+ - body expression candidates and combined body plan
552
664
  - proof claims used and source
553
665
  - story/proof files consulted
554
666
  - gold standards consulted
555
667
  - LinkedIn preview audit
556
668
  - premise/value audit findings
669
+ - mobile scanability audit findings
670
+ - template-adaptation audit findings
671
+ - abstraction-to-concrete rewrite audit findings
557
672
  - simplifier/concrete-language audit findings
558
673
  - voice audit findings
559
674
  - anti-AI audit findings
@@ -648,8 +763,14 @@ iteration:
648
763
  score: <compact score object>
649
764
  selected_premise: <premise or none>
650
765
  selected_hook: <hook>
766
+ pre_draft_structure_brief: <compact structure summary or none>
767
+ selected_source_template: <template name/source or none>
651
768
  validation_summary:
652
769
  premise_value: pass | needs_user_input | needs_revision
770
+ mobile_scanability: pass | needs_revision
771
+ template_adaptation: pass | needs_revision | blocked
772
+ abstraction_to_concrete: pass | needs_revision
773
+ hook_to_body_repayment: pass | needs_revision
653
774
  proof: pass | needs_user_input | blocked
654
775
  voice: pass | needs_revision
655
776
  anti_ai: pass | needs_revision