@sellable/mcp 0.1.293 → 0.1.294

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
@@ -9,6 +9,14 @@ type ReachSignals = {
9
9
  reachAdjustedScore: number;
10
10
  confidence: "high" | "medium" | "low";
11
11
  };
12
+ type EngagementPostMedia = {
13
+ imageUrls: string[];
14
+ videoThumbnailUrl?: string;
15
+ videoUrl?: string;
16
+ articleImageUrl?: string;
17
+ articleUrl?: string;
18
+ mediaTypes: string[];
19
+ };
12
20
  export type EngagementPost = {
13
21
  postId: string;
14
22
  url: string;
@@ -27,6 +35,7 @@ export type EngagementPost = {
27
35
  total: number;
28
36
  };
29
37
  reachSignals?: ReachSignals;
38
+ media?: EngagementPostMedia;
30
39
  contentPreview: string;
31
40
  };
32
41
  export type SearchEngagementPostsInput = {
@@ -2,7 +2,7 @@ import { getApi } from "../api.js";
2
2
  export const engageDiscoveryToolDefinitions = [
3
3
  {
4
4
  name: "search_engagement_posts",
5
- description: "Search for high-signal LinkedIn posts by keyword for engagement/comment drafting. Designed for the engage skill (no campaignOfferId required). Filters by recency and engagement, and can exclude already-engaged URLs.",
5
+ description: "Search for high-signal LinkedIn posts by keyword for engagement/comment drafting. Designed for the engage skill (no campaignOfferId required). Filters by recency and engagement, can exclude already-engaged URLs, and returns source media hints when the scraper provides images, video covers, or article images.",
6
6
  inputSchema: {
7
7
  type: "object",
8
8
  properties: {
@@ -74,6 +74,50 @@ function safeNumber(value) {
74
74
  function round3(value) {
75
75
  return Number(value.toFixed(3));
76
76
  }
77
+ function normalizeMedia(raw) {
78
+ if (!raw || typeof raw !== "object")
79
+ return undefined;
80
+ const imageUrls = Array.isArray(raw.imageUrls)
81
+ ? raw.imageUrls
82
+ .map((url) => (typeof url === "string" ? url.trim() : ""))
83
+ .filter(Boolean)
84
+ : [];
85
+ const videoThumbnailUrl = typeof raw.videoThumbnailUrl === "string" && raw.videoThumbnailUrl.trim()
86
+ ? raw.videoThumbnailUrl.trim()
87
+ : undefined;
88
+ const videoUrl = typeof raw.videoUrl === "string" && raw.videoUrl.trim()
89
+ ? raw.videoUrl.trim()
90
+ : undefined;
91
+ const articleImageUrl = typeof raw.articleImageUrl === "string" && raw.articleImageUrl.trim()
92
+ ? raw.articleImageUrl.trim()
93
+ : undefined;
94
+ const articleUrl = typeof raw.articleUrl === "string" && raw.articleUrl.trim()
95
+ ? raw.articleUrl.trim()
96
+ : undefined;
97
+ const mediaTypes = Array.from(new Set([
98
+ ...(Array.isArray(raw.mediaTypes)
99
+ ? raw.mediaTypes.map((type) => typeof type === "string" ? type.trim() : "")
100
+ : []),
101
+ imageUrls.length > 0 ? "image" : "",
102
+ videoThumbnailUrl || videoUrl ? "video" : "",
103
+ articleImageUrl || articleUrl ? "article" : "",
104
+ ].filter(Boolean)));
105
+ if (imageUrls.length === 0 &&
106
+ !videoThumbnailUrl &&
107
+ !videoUrl &&
108
+ !articleImageUrl &&
109
+ !articleUrl) {
110
+ return undefined;
111
+ }
112
+ return {
113
+ imageUrls,
114
+ ...(videoThumbnailUrl ? { videoThumbnailUrl } : {}),
115
+ ...(videoUrl ? { videoUrl } : {}),
116
+ ...(articleImageUrl ? { articleImageUrl } : {}),
117
+ ...(articleUrl ? { articleUrl } : {}),
118
+ mediaTypes,
119
+ };
120
+ }
77
121
  function parseFollowerCount(author) {
78
122
  const direct = safeNumber(author?.followerCount);
79
123
  if (direct > 0)
@@ -218,6 +262,7 @@ export async function searchEngagementPosts(input) {
218
262
  targetFollowerMin,
219
263
  targetFollowerMax,
220
264
  });
265
+ const media = normalizeMedia(p?.media);
221
266
  kept.push({
222
267
  postId: String(p?.id || ""),
223
268
  url,
@@ -233,6 +278,7 @@ export async function searchEngagementPosts(input) {
233
278
  },
234
279
  engagement: { likes, comments, shares, total },
235
280
  ...(reachSignals ? { reachSignals } : {}),
281
+ ...(media ? { media } : {}),
236
282
  contentPreview: previewText(String(p?.content || ""), 220),
237
283
  });
238
284
  }
@@ -5,6 +5,14 @@ type SignalPostForImportSelection = {
5
5
  likes: number;
6
6
  comments: number;
7
7
  };
8
+ type SignalPostMedia = {
9
+ imageUrls: string[];
10
+ videoThumbnailUrl?: string;
11
+ videoUrl?: string;
12
+ articleImageUrl?: string;
13
+ articleUrl?: string;
14
+ mediaTypes: string[];
15
+ };
8
16
  export declare function selectSignalPostsForImport<T extends SignalPostForImportSelection>(posts: T[], options: {
9
17
  targetEngagerCount?: number;
10
18
  maxPostsToScrape?: number;
@@ -4406,6 +4414,7 @@ export declare function searchSignals(input: SignalSearchInput): Promise<{
4406
4414
  selectionTarget: number;
4407
4415
  recommendedPostIds: string[];
4408
4416
  topPosts: {
4417
+ media?: SignalPostMedia | undefined;
4409
4418
  id: string;
4410
4419
  url: string | undefined;
4411
4420
  matchedKeyword: string | null;
@@ -3,8 +3,8 @@ import { dirname, join, resolve } from "path";
3
3
  import { getApi, SellableApiError } from "../api.js";
4
4
  import { resolveSkillsDir } from "../skills.js";
5
5
  import { resolveWorkspaceRoot } from "../utils/workspace-root.js";
6
- import { buildCsvDomainPreview, matchesConfirmationToken, parseConfirmationToken, projectCsvCarryRows, } from "./csv-domains.js";
7
6
  import { listDncEntries, loadCsvDncEntries, } from "./csv-dnc.js";
7
+ import { buildCsvDomainPreview, matchesConfirmationToken, parseConfirmationToken, projectCsvCarryRows, } from "./csv-domains.js";
8
8
  import { buildCsvLinkedinPreview, matchesLinkedinConfirmationToken, parseLinkedinConfirmationToken, uploadCsvLinkedinFile, } from "./csv-linkedin.js";
9
9
  import { assertInteractionApproval } from "./interaction-mode.js";
10
10
  import { assertProviderPromptLoaded, markProviderPromptLoaded, } from "./provider-preflight.js";
@@ -727,6 +727,52 @@ function truncate(text, max = 220) {
727
727
  return clean;
728
728
  return `${clean.slice(0, max - 1)}...`;
729
729
  }
730
+ function normalizeSignalPostMedia(raw) {
731
+ if (!raw || typeof raw !== "object")
732
+ return undefined;
733
+ const media = raw;
734
+ const imageUrls = Array.isArray(media.imageUrls)
735
+ ? media.imageUrls
736
+ .map((url) => (typeof url === "string" ? url.trim() : ""))
737
+ .filter(Boolean)
738
+ : [];
739
+ const videoThumbnailUrl = typeof media.videoThumbnailUrl === "string" &&
740
+ media.videoThumbnailUrl.trim()
741
+ ? media.videoThumbnailUrl.trim()
742
+ : undefined;
743
+ const videoUrl = typeof media.videoUrl === "string" && media.videoUrl.trim()
744
+ ? media.videoUrl.trim()
745
+ : undefined;
746
+ const articleImageUrl = typeof media.articleImageUrl === "string" && media.articleImageUrl.trim()
747
+ ? media.articleImageUrl.trim()
748
+ : undefined;
749
+ const articleUrl = typeof media.articleUrl === "string" && media.articleUrl.trim()
750
+ ? media.articleUrl.trim()
751
+ : undefined;
752
+ const mediaTypes = Array.from(new Set([
753
+ ...(Array.isArray(media.mediaTypes)
754
+ ? media.mediaTypes.map((type) => typeof type === "string" ? type.trim() : "")
755
+ : []),
756
+ imageUrls.length > 0 ? "image" : "",
757
+ videoThumbnailUrl || videoUrl ? "video" : "",
758
+ articleImageUrl || articleUrl ? "article" : "",
759
+ ].filter(Boolean)));
760
+ if (imageUrls.length === 0 &&
761
+ !videoThumbnailUrl &&
762
+ !videoUrl &&
763
+ !articleImageUrl &&
764
+ !articleUrl) {
765
+ return undefined;
766
+ }
767
+ return {
768
+ imageUrls,
769
+ ...(videoThumbnailUrl ? { videoThumbnailUrl } : {}),
770
+ ...(videoUrl ? { videoUrl } : {}),
771
+ ...(articleImageUrl ? { articleImageUrl } : {}),
772
+ ...(articleUrl ? { articleUrl } : {}),
773
+ mediaTypes,
774
+ };
775
+ }
730
776
  function normalizePostUrl(url) {
731
777
  if (!url)
732
778
  return null;
@@ -764,6 +810,7 @@ function summarizeSignalPost(post) {
764
810
  const likes = post.engagement?.likes ?? 0;
765
811
  const comments = post.engagement?.comments ?? 0;
766
812
  const shares = post.engagement?.shares ?? 0;
813
+ const media = normalizeSignalPostMedia(post.media);
767
814
  return {
768
815
  id: post.id,
769
816
  url: post.url,
@@ -784,6 +831,7 @@ function summarizeSignalPost(post) {
784
831
  },
785
832
  score: Number(post._score.toFixed(2)),
786
833
  excerpt: truncate(post.content),
834
+ ...(media ? { media } : {}),
787
835
  };
788
836
  }
789
837
  function summarizeSignalSearchResponse(response) {
@@ -2697,9 +2745,7 @@ function normalizeMcpCompanyIcp(filters) {
2697
2745
  if (!isPlainObject(icp)) {
2698
2746
  return;
2699
2747
  }
2700
- const scope = typeof icp.geographic_scope === "string"
2701
- ? icp.geographic_scope.trim()
2702
- : "";
2748
+ const scope = typeof icp.geographic_scope === "string" ? icp.geographic_scope.trim() : "";
2703
2749
  const scopeMarkets = expandMcpCompanyIcpMarket(scope);
2704
2750
  if (scopeMarkets.length > 0) {
2705
2751
  icp.geographic_scope = "multi_country";
@@ -2708,9 +2754,7 @@ function normalizeMcpCompanyIcp(filters) {
2708
2754
  ...normalizeMcpCompanyIcpMarkets(icp.geographic_markets),
2709
2755
  ]);
2710
2756
  }
2711
- else if (scope &&
2712
- scope !== "single_country" &&
2713
- scope !== "multi_country") {
2757
+ else if (scope && scope !== "single_country" && scope !== "multi_country") {
2714
2758
  delete icp.geographic_scope;
2715
2759
  }
2716
2760
  if (icp.geographic_markets !== undefined) {
@@ -2791,8 +2835,7 @@ function normalizeMcpSeedMatchAll(filters, concreteSeedCount, omittedFilters = [
2791
2835
  if (!isPlainObject(filters.company_lookalike)) {
2792
2836
  return;
2793
2837
  }
2794
- if (filters.company_lookalike.match_all === true &&
2795
- concreteSeedCount < 2) {
2838
+ if (filters.company_lookalike.match_all === true && concreteSeedCount < 2) {
2796
2839
  omittedFilters.push({
2797
2840
  field: "company_lookalike.match_all",
2798
2841
  reason: "Dropped match_all because fewer than two concrete approved lookalike seeds remained after MCP seed normalization.",
@@ -2900,7 +2943,9 @@ function normalizeStringArray(input) {
2900
2943
  .filter((value) => value.length > 0);
2901
2944
  }
2902
2945
  function normalizeMcpSeeds(input, kind) {
2903
- return uniqueStrings(normalizeStringArray(input).filter((seed) => kind === "domain" ? isLikelyConcreteDomain(seed) : isLikelyConcreteSeedCompany(seed)));
2946
+ return uniqueStrings(normalizeStringArray(input).filter((seed) => kind === "domain"
2947
+ ? isLikelyConcreteDomain(seed)
2948
+ : isLikelyConcreteSeedCompany(seed)));
2904
2949
  }
2905
2950
  function isLikelyConcreteDomain(input) {
2906
2951
  return /^[a-z0-9.-]+\.[a-z]{2,}$/i.test(input);
@@ -3111,13 +3156,7 @@ function buildProspeoPeopleSearchFallbackInput(input, error) {
3111
3156
  const fallbackFilters = {
3112
3157
  person_name_or_job_title: keyword,
3113
3158
  person_seniority: {
3114
- include: [
3115
- "C-Suite",
3116
- "Vice President",
3117
- "Head",
3118
- "Director",
3119
- "Manager",
3120
- ],
3159
+ include: ["C-Suite", "Vice President", "Head", "Director", "Manager"],
3121
3160
  },
3122
3161
  max_person_per_company: typeof filters.max_person_per_company === "number"
3123
3162
  ? filters.max_person_per_company
@@ -185,7 +185,15 @@ interface RawLinkedInPost {
185
185
  authorPublicIdentifier?: string;
186
186
  };
187
187
  text?: string;
188
- content?: string;
188
+ content?: string | {
189
+ images?: unknown;
190
+ article?: unknown;
191
+ video?: unknown;
192
+ };
193
+ media?: unknown;
194
+ postImages?: unknown;
195
+ postVideo?: unknown;
196
+ article?: unknown;
189
197
  activityDate?: string;
190
198
  postedAt?: string | {
191
199
  date?: string;
@@ -219,6 +227,15 @@ interface SerializedPost {
219
227
  comments: number;
220
228
  url: string;
221
229
  isRepost: boolean;
230
+ media?: SerializedPostMedia;
231
+ }
232
+ interface SerializedPostMedia {
233
+ imageUrls: string[];
234
+ videoThumbnailUrl?: string;
235
+ videoUrl?: string;
236
+ articleImageUrl?: string;
237
+ articleUrl?: string;
238
+ mediaTypes: string[];
222
239
  }
223
240
  export declare function serializeLinkedInPosts(rawPosts: RawLinkedInPost[] | undefined, context: string): SerializedPost[];
224
241
  export declare function fetchLinkedInPosts(linkedinUrl: string, limit?: number): Promise<{
@@ -157,14 +157,105 @@ function normalizePostUrl(post) {
157
157
  post.header?.linkedinUrl ||
158
158
  "");
159
159
  }
160
+ function collectImageUrls(value, urls = new Set()) {
161
+ if (!value)
162
+ return urls;
163
+ if (typeof value === "string" && /^https?:\/\//i.test(value)) {
164
+ urls.add(value);
165
+ return urls;
166
+ }
167
+ if (Array.isArray(value)) {
168
+ for (const item of value)
169
+ collectImageUrls(item, urls);
170
+ return urls;
171
+ }
172
+ if (typeof value !== "object")
173
+ return urls;
174
+ const objectValue = value;
175
+ for (const key of ["url", "imageUrl", "thumbnailUrl", "thumbnail_url"]) {
176
+ const candidate = objectValue[key];
177
+ if (typeof candidate === "string" && /^https?:\/\//i.test(candidate)) {
178
+ urls.add(candidate);
179
+ }
180
+ }
181
+ if (objectValue.image)
182
+ collectImageUrls(objectValue.image, urls);
183
+ if (objectValue.images)
184
+ collectImageUrls(objectValue.images, urls);
185
+ return urls;
186
+ }
187
+ function pickString(value) {
188
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
189
+ }
190
+ function serializePostMedia(post) {
191
+ const media = post.media && typeof post.media === "object"
192
+ ? post.media
193
+ : {};
194
+ const content = post.content && typeof post.content === "object" ? post.content : {};
195
+ const article = post.article && typeof post.article === "object"
196
+ ? post.article
197
+ : content.article && typeof content.article === "object"
198
+ ? content.article
199
+ : {};
200
+ const video = post.postVideo && typeof post.postVideo === "object"
201
+ ? post.postVideo
202
+ : content.video && typeof content.video === "object"
203
+ ? content.video
204
+ : {};
205
+ const imageUrls = Array.from(collectImageUrls([
206
+ media.imageUrls,
207
+ post.postImages,
208
+ content.images,
209
+ article.image,
210
+ ]));
211
+ const videoThumbnailUrl = pickString(media.videoThumbnailUrl) ||
212
+ pickString(video.thumbnailUrl) ||
213
+ pickString(video.thumbnail_url) ||
214
+ pickString(video.thumbnail);
215
+ const videoUrl = pickString(media.videoUrl) ||
216
+ pickString(video.videoUrl) ||
217
+ pickString(video.video_url);
218
+ const articleImageUrl = pickString(media.articleImageUrl) ||
219
+ Array.from(collectImageUrls(article.image))[0];
220
+ const articleUrl = pickString(media.articleUrl) ||
221
+ pickString(article.link) ||
222
+ pickString(article.article_url) ||
223
+ pickString(article.articleUrl) ||
224
+ pickString(article.url);
225
+ const mediaTypes = Array.from(new Set([
226
+ ...(Array.isArray(media.mediaTypes)
227
+ ? media.mediaTypes.map((type) => typeof type === "string" ? type.trim() : "")
228
+ : []),
229
+ imageUrls.length > 0 ? "image" : "",
230
+ videoThumbnailUrl || videoUrl ? "video" : "",
231
+ articleImageUrl || articleUrl ? "article" : "",
232
+ ].filter(Boolean)));
233
+ if (imageUrls.length === 0 &&
234
+ !videoThumbnailUrl &&
235
+ !videoUrl &&
236
+ !articleImageUrl &&
237
+ !articleUrl) {
238
+ return undefined;
239
+ }
240
+ return {
241
+ imageUrls,
242
+ ...(videoThumbnailUrl ? { videoThumbnailUrl } : {}),
243
+ ...(videoUrl ? { videoUrl } : {}),
244
+ ...(articleImageUrl ? { articleImageUrl } : {}),
245
+ ...(articleUrl ? { articleUrl } : {}),
246
+ mediaTypes,
247
+ };
248
+ }
160
249
  function serializeLinkedInPost(post) {
250
+ const media = serializePostMedia(post);
161
251
  return {
162
- text: post.text || post.content || "",
252
+ text: post.text || (typeof post.content === "string" ? post.content : "") || "",
163
253
  date: normalizePostDate(post),
164
254
  reactions: post.reactionsCount ?? post.engagement?.likes ?? 0,
165
255
  comments: post.commentsCount ?? post.engagement?.comments ?? 0,
166
256
  url: normalizePostUrl(post),
167
257
  isRepost: Boolean(post.isRepublishedPost || post.repostedBy || post.repostedAt),
258
+ ...(media ? { media } : {}),
168
259
  };
169
260
  }
170
261
  function isUsableSerializedPost(post) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/mcp",
3
- "version": "0.1.293",
3
+ "version": "0.1.294",
4
4
  "type": "module",
5
5
  "description": "Sellable MCP server for Claude Code and Codex campaign workflows",
6
6
  "main": "dist/index.js",
@@ -516,6 +516,10 @@ Visible Flow Trace
516
516
  - calculated mobile/desktop visible blocks from
517
517
  `calculate_linkedin_hook_preview` or authenticated LinkedIn screenshot:
518
518
  - optional visual artifact from `render_linkedin_post_preview`:
519
+ - source attached media inspected:
520
+ - sourceVisualBasis / mediaType / cover_only:
521
+ - visualHookMechanism / beliefActivated / proofCreated:
522
+ - visualToCopyAlignment:
519
523
  - see-more tension:
520
524
  - curiosity debt:
521
525
  - body promise:
@@ -727,6 +731,9 @@ The research worker must return a compact packet only:
727
731
  - viral-post outlines for the best source posts
728
732
  - line-to-template conversions that turn source structures into reusable
729
733
  templates without source wording
734
+ - visual/media hook analysis: source image URL or local path, visual basis,
735
+ media type, visible image text, visual hook mechanism, belief activated,
736
+ proof created, visual-to-copy alignment, and Sellable adaptation notes
730
737
  - hook-to-body promise maps that show how each hook tells the body
731
738
  - body structures and exact body language moves
732
739
  - preview measurements
@@ -746,22 +753,31 @@ Default flow:
746
753
  3. Shortlist high-engagement posts by topic fit, hook strength, creator repeat evidence, and weighted engagement quality.
747
754
  4. Because search results may only include previews, call `mcp__sellable__fetch_linkedin_posts` for shortlisted authors/profile URLs and match recent posts by URL/activity ID when full text is needed.
748
755
  5. If full text cannot be matched, record `full_text_unavailable` and use only the preview. Do not invent missing body details.
749
- 6. Weigh shares/reposts above comments, comments above reactions, and reactions as weak reach unless paired with stronger signals. If shares/reposts are unavailable, record `repost_data_unavailable`.
750
- 7. Penalize lead-magnet or giveaway mechanics unless the user explicitly asks for a lead magnet post.
751
- 8. Build an audience tension snapshot before hook generation: what the space is rewarding, what tension readers are reacting to, what they want to try or avoid, what objections/fears will make them hesitate, and which angle the user can credibly own. If the user's raw idea is internally coherent but not attached to live audience tension, do not draft from the internal idea alone; present stronger directions or rewrite the draft angle around the external tension.
752
- 9. Extract premise inputs: real story/scene possibilities, observed tensions, useful reader takeaways, and proof gaps. A good hook cannot rescue a premise with no value.
753
- 10. For story posts, extract the story mechanism that made the post work, not just the first line.
754
- 11. Extract hook structures plus specific reusable words, phrases, sentence
756
+ 6. For every shortlisted keeper or near-keeper, inspect attached media when
757
+ available. Prefer authenticated screenshots or downloaded media; otherwise
758
+ use public `og:image`, `twitter:image`, or JSON-LD `image.url` from the
759
+ source post URL. Record `sourceVisualBasis`, `sourceImageUrl`,
760
+ `sourceImageLocalPath` when created, `imageAvailability`, `mediaType`,
761
+ `cover_only` when only a first frame/cover was inspected,
762
+ `visualHookMechanism`, `beliefActivated`, `proofCreated`, and
763
+ `visualToCopyAlignment`. Do not infer hidden video or carousel frames.
764
+ 7. Weigh shares/reposts above comments, comments above reactions, and reactions as weak reach unless paired with stronger signals. If shares/reposts are unavailable, record `repost_data_unavailable`.
765
+ 8. Penalize lead-magnet or giveaway mechanics unless the user explicitly asks for a lead magnet post.
766
+ 9. Build an audience tension snapshot before hook generation: what the space is rewarding, what tension readers are reacting to, what they want to try or avoid, what objections/fears will make them hesitate, and which angle the user can credibly own. If the user's raw idea is internally coherent but not attached to live audience tension, do not draft from the internal idea alone; present stronger directions or rewrite the draft angle around the external tension.
767
+ 10. Extract premise inputs: real story/scene possibilities, observed tensions, useful reader takeaways, and proof gaps. A good hook cannot rescue a premise with no value.
768
+ 11. For story posts, extract the story mechanism that made the post work, not just the first line.
769
+ 12. Extract hook structures plus specific reusable words, phrases, sentence
755
770
  shapes, transitions, and body language patterns.
756
- 12. Create a post positioning breakdown for each keeper post: line/phrase,
771
+ 13. Create a post positioning breakdown for each keeper post: line/phrase,
757
772
  category, narrative technique, tension created, reader question opened,
758
- proof dependency, and reusable template line.
759
- 13. Convert each keeper into a viral-post outline: hook job, see-more trigger,
773
+ proof dependency, source visual basis, visual hook mechanism, and reusable
774
+ template line.
775
+ 14. Convert each keeper into a viral-post outline: hook job, see-more trigger,
760
776
  body payoff, close job, and beat-by-beat narrative structure.
761
- 14. Convert the best outlines into reusable post templates with positioning
762
- sequences, required story/proof inputs, forbidden borrowing, and
763
- Sellable-specific adaptation instructions.
764
- 15. Save the research with `mcp__sellable__save_hook_research`.
777
+ 15. Convert the best outlines into reusable post templates with positioning
778
+ sequences, visual engine, required story/proof inputs, forbidden borrowing,
779
+ and Sellable-specific adaptation instructions.
780
+ 16. Save the research with `mcp__sellable__save_hook_research`.
765
781
 
766
782
  Record provenance:
767
783
 
@@ -775,6 +791,10 @@ Record provenance:
775
791
  - lead-magnet or engagement-bait penalties
776
792
  - story mechanism when relevant
777
793
  - full-text match status
794
+ - source visual basis, source image URLs, local image paths when created,
795
+ media type, image availability, `cover_only` status, visible image text,
796
+ visual hook mechanism, belief activated, proof created, visual-to-copy
797
+ alignment, visual risk, and Sellable adaptation notes
778
798
  - transcript/content-memory match status and worldview packet
779
799
  - source hook preview measurements and whether they came from full text or a search preview
780
800
  - selected hook patterns
@@ -819,6 +839,19 @@ Research status:
819
839
  Best source examples:
820
840
  1. author, URL, engagement, why kept, why not copied
821
841
 
842
+ Visual hook analysis:
843
+ 1. source + sourceImageUrl/sourceImageLocalPath
844
+ - sourceVisualBasis:
845
+ - imageAvailability:
846
+ - mediaType:
847
+ - cover_only:
848
+ - visualHookMechanism:
849
+ - beliefActivated:
850
+ - proofCreated:
851
+ - visualToCopyAlignment:
852
+ - visualRisk:
853
+ - Sellable adaptation:
854
+
822
855
  Audience tension snapshot:
823
856
  - resonating ideas:
824
857
  - visible audience tension:
@@ -119,6 +119,9 @@ Show compact candidate cards. Include:
119
119
  - why it might belong in the pack
120
120
  - hook mechanism
121
121
  - hook preview budget status and measurement basis
122
+ - source image URL or local path when available
123
+ - source visual basis, media type, visual hook mechanism, belief activated, and
124
+ visual-to-copy alignment
122
125
  - content/body mechanism
123
126
  - rhythm notes
124
127
  - sentence-structure notes
@@ -154,6 +157,7 @@ Do not write candidates to the approved pack before the user chooses.
154
157
  - `Primary Use`
155
158
  - `Allowed Use`
156
159
  - `Hook Pattern`
160
+ - `Visual Pattern`
157
161
  - `Body Pattern`
158
162
  - `Rhythm Notes`
159
163
  - `Sentence Notes`
@@ -201,6 +205,24 @@ Tags:
201
205
  - internal question:
202
206
  - emotional trigger:
203
207
 
208
+ ## Visual / Media Breakdown
209
+
210
+ - source image URL:
211
+ - source image local path:
212
+ - source visual basis:
213
+ - image availability:
214
+ - media type:
215
+ - cover only:
216
+ - image alt text:
217
+ - visible image text:
218
+ - visual layout:
219
+ - visual hook mechanism:
220
+ - belief activated:
221
+ - proof created:
222
+ - visual-to-copy alignment:
223
+ - visual risk:
224
+ - allowed adaptation:
225
+
204
226
  ## Content / Body Breakdown
205
227
 
206
228
  - thesis:
@@ -7,7 +7,7 @@ readers click "see more," decompose each post into a reusable narrative and
7
7
  positioning template, and then adapt those templates to the user's real story
8
8
  without copying source wording.
9
9
 
10
- V2 research has six outputs:
10
+ V2 research has seven outputs:
11
11
 
12
12
  1. weighted source winners: the best recent posts to learn from
13
13
  2. hook autopsies: exact preview measurements, open loops, and tension created
@@ -16,7 +16,9 @@ V2 research has six outputs:
16
16
  4. viral-post outlines: the reusable narrative structure of each source post
17
17
  5. post positioning breakdown templates: line-level positioning and narrative
18
18
  technique maps that can be adapted into the user's draft
19
- 6. thought leader voice variants from the configured active influencer
19
+ 6. visual/media hook analysis: the attached image, video cover, carousel cover,
20
+ screenshot, diagram, or lack of media that changed how the hook landed
21
+ 7. thought leader voice variants from the configured active influencer
20
22
  list, unless the user explicitly skipped thought leaders or supplied a
21
23
  named subset
22
24
 
@@ -45,11 +47,15 @@ Worker owns:
45
47
  - broad keyword search
46
48
  - tracked-person post fetches
47
49
  - full-text matching by URL/activity ID
50
+ - public post image/media metadata extraction from shortlisted source URLs
48
51
  - duplicate removal
49
52
  - lead-magnet, giveaway, engagement-bait, and off-voice filtering
50
53
  - audience tension extraction across kept and rejected examples
51
54
  - premise input extraction: real scenes, observed tensions, reader value, proof gaps
52
55
  - hook opening measurement and "see more" tension autopsy
56
+ - visual artifact inspection: first-frame screenshot, `og:image`,
57
+ `twitter:image`, JSON-LD `image.url`, carousel cover, video cover, visual hook
58
+ mechanism, belief activated, what the image proves, and copy/image alignment
53
59
  - exact phrase-pattern extraction
54
60
  - post positioning breakdown by line and phrase
55
61
  - viral-post outline extraction
@@ -86,6 +92,9 @@ Research packet:
86
92
  - body patterns: max 8
87
93
  - source URLs and author profile URLs
88
94
  - preview measurements
95
+ - source visual records: media availability, source image URLs, local image paths
96
+ when downloaded, visual basis, media type, visible image text, visual hook
97
+ mechanism, belief activated, proof created, and visual-to-copy alignment
89
98
  - hook-to-body promise maps
90
99
  - body expression inputs
91
100
  - thought leader voice variants
@@ -261,6 +270,66 @@ When full hook/body text matters:
261
270
 
262
271
  If full text is unavailable, record `full_text_unavailable`. Use only the preview for hook analysis and do not infer missing body details.
263
272
 
273
+ ## Visual / Media Reality
274
+
275
+ For every shortlisted keeper or near-keeper source post, inspect attached media
276
+ when available. Do this before judging why the hook worked. Many high-performing
277
+ LinkedIn hooks are carried by the first image, video cover, carousel cover,
278
+ diagram, terminal screenshot, before/after output, or workflow map. If the text
279
+ is studied without the media, the research can misread the post.
280
+
281
+ Use the best available visual source in this order:
282
+
283
+ 1. authenticated LinkedIn screenshot or downloaded media when the host can
284
+ access it
285
+ 2. public `og:image` metadata from the source post URL
286
+ 3. public `twitter:image` metadata from the source post URL
287
+ 4. JSON-LD `image.url` from the source post URL
288
+ 5. no visual source available
289
+
290
+ Record `sourceVisualBasis` as one of:
291
+
292
+ - `authenticated_screenshot`
293
+ - `downloaded_image`
294
+ - `public_og_image`
295
+ - `public_twitter_image`
296
+ - `json_ld_image`
297
+ - `none_available`
298
+
299
+ If the source is a video or carousel and only the first frame or cover image is
300
+ available, record `cover_only`. Do not infer hidden video frames, carousel
301
+ slides, hover states, captions, or comments that were not inspected.
302
+
303
+ For each inspected source, preserve:
304
+
305
+ - `sourceImageUrl`
306
+ - `sourceImageLocalPath` when downloaded or screenshotted
307
+ - `imageAvailability`: `present`, `none`, or `blocked`
308
+ - `mediaType`: `image`, `carousel_cover`, `video_cover`, `text_only`, or
309
+ `unknown`
310
+ - `imageAltText` when available
311
+ - `visibleText`: exact visible words in the image when safe to quote briefly,
312
+ otherwise short anchors
313
+ - `visualLayout`: what the viewer sees first, such as file tree, stack diagram,
314
+ terminal output, checklist, before/after table, UI screenshot, founder photo,
315
+ logo grid, or generic placeholder
316
+ - `visualHookMechanism`: how the visual creates curiosity, proof, contrast,
317
+ concreteness, safety, status, or "I can picture this" belief
318
+ - `beliefActivated`: what the audience has to believe for the post to work
319
+ - `proofCreated`: what the visual proves or makes more believable that the text
320
+ alone would not
321
+ - `visualToCopyAlignment`: how the opening text and media reinforce or fight
322
+ each other
323
+ - `visualRisk`: generic stock feel, unreadable text, borrowed proof,
324
+ engagement-bait cover, misleading model/logo claims, account-safety risk, or
325
+ unclear product proof
326
+ - `sellableAdaptation`: what kind of Sellable visual could use the same
327
+ mechanism without copying the source
328
+
329
+ When a post has no media, record `imageAvailability: none` and treat the hook as
330
+ text-led. When the media is blocked, record `imageAvailability: blocked` and
331
+ lower confidence instead of inventing a visual analysis.
332
+
264
333
  ## Opening Preview Measurement
265
334
 
266
335
  Measure the visible opening for every shortlisted source post before extracting
@@ -339,6 +408,8 @@ For each shortlisted source post, record:
339
408
  - author profile URL when available
340
409
  - engagement totals and available likes/comments/shares breakdown
341
410
  - creator repeat evidence
411
+ - source visual basis, media type, image URL/local path, and visual hook
412
+ analysis
342
413
  - visible hook text or preview
343
414
  - opening preview measurement fields from the section above
344
415
  - hook mechanism
@@ -348,6 +419,11 @@ For each shortlisted source post, record:
348
419
  - exact hook language patterns: reusable words, phrase shapes, contrast forms,
349
420
  sentence shapes, and transition moves
350
421
  - story mechanism when the post is a story
422
+ - visual mechanism when an image, carousel cover, video cover, screenshot,
423
+ diagram, or generic placeholder affects the hook
424
+ - belief activated by the visual
425
+ - proof created by the visual
426
+ - visual-to-copy alignment
351
427
  - internal question created
352
428
  - emotional trigger
353
429
  - proof/story dependency
@@ -441,6 +517,14 @@ Use this format:
441
517
  Post positioning breakdown:
442
518
  source: <author + URL>
443
519
  text_basis: full_text | search_preview | manual_user_source
520
+ source_visual_basis: authenticated_screenshot | downloaded_image | public_og_image | public_twitter_image | json_ld_image | none_available
521
+ media_type: image | carousel_cover | video_cover | text_only | unknown
522
+ source_image_url:
523
+ source_image_local_path:
524
+ visual_hook_mechanism:
525
+ belief_activated:
526
+ proof_created:
527
+ visual_to_copy_alignment:
444
528
  overall_positioning_sequence:
445
529
  <Category> -> <Category> -> <Category> -> ...
446
530
 
@@ -576,6 +660,14 @@ source:
576
660
  template_name:
577
661
  positioning_sequence:
578
662
  <Category> -> <Category> -> <Category> -> ...
663
+ visual_engine:
664
+ source_visual_basis:
665
+ media_type:
666
+ visual_hook_mechanism:
667
+ belief_activated:
668
+ proof_created:
669
+ visual_to_copy_alignment:
670
+ sellable_adaptation:
579
671
  required_story_inputs:
580
672
  - <input the user must actually have>
581
673
  required_proof_inputs:
@@ -682,6 +774,8 @@ why_it_might_fail:
682
774
  positioning_sequence_to_borrow:
683
775
  hook_move_to_borrow:
684
776
  body_outline_to_borrow:
777
+ visual_engine_to_borrow:
778
+ visual_guardrails:
685
779
  line_shapes_to_test:
686
780
  required_user_inputs:
687
781
  required_proof:
@@ -845,8 +939,27 @@ Save the research with `mcp__sellable__save_hook_research` before drafting.
845
939
  The saved research must contain enough detail to support a user-visible
846
940
  `Research Learning Report`: source examples, full adapted hook blocks, exact
847
941
  phrase patterns, post positioning breakdowns, viral-post outlines, reusable
848
- post templates, hook-to-body promise maps, body structures, rejected examples,
849
- tracked-person recommendations, and gold-standard recommendations.
942
+ post templates, visual/media hook analysis, hook-to-body promise maps, body
943
+ structures, rejected examples, tracked-person recommendations, and
944
+ gold-standard recommendations.
945
+
946
+ Save source visual provenance for every keeper or near-keeper:
947
+
948
+ - `sourceVisualBasis`
949
+ - `sourceImageUrl`
950
+ - `sourceImageLocalPath` when created
951
+ - `imageAvailability`
952
+ - `mediaType`
953
+ - `imageAltText` when available
954
+ - `visibleText`
955
+ - `visualLayout`
956
+ - `visualHookMechanism`
957
+ - `beliefActivated`
958
+ - `proofCreated`
959
+ - `visualToCopyAlignment`
960
+ - `visualRisk`
961
+ - `sellableAdaptation`
962
+ - `cover_only` when only the cover or first frame was inspected
850
963
 
851
964
  If the user approves a creator/person as a recurring inspiration source, also
852
965
  call `mcp__sellable__upsert_engage_tracked_person` with:
@@ -67,6 +67,12 @@ Hook research files must preserve:
67
67
  reach-adjusted score, and normalization confidence notes when reach
68
68
  normalization was used
69
69
  - full-text availability
70
+ - source image/media URLs, source visual basis, local screenshot/image artifact
71
+ paths when created, image availability (`present`, `none`, or `blocked`),
72
+ media type (`image`, `carousel_cover`, `video_cover`, `text_only`, or
73
+ `unknown`), `cover_only` status, image alt text when available, visible image
74
+ text anchors, visual hook mechanism, visual belief activated, proof created by
75
+ the visual, visual-to-copy alignment, visual risk, and adaptation guards
70
76
  - source hook preview measurements, including text basis, char count including
71
77
  newlines, physical/content line counts, longest nonblank line, blank-line
72
78
  visual risk, and mobile/desktop preview budget status
@@ -36,6 +36,7 @@ Every saved draft needs a validation receipt. A draft without this receipt is no
36
36
  - `premiseValueAudit`
37
37
  - `mobileScanabilityAudit`
38
38
  - `templateAdaptationAudit`
39
+ - `sourceVisualAudit`
39
40
  - `abstractionToConcreteRewriteAudit`
40
41
  - `linkedinPreviewAudit`
41
42
  - `simplifierConcreteLanguageAudit`
@@ -97,6 +98,8 @@ Each candidate should include:
97
98
  - premise tension opened
98
99
  - reader value implied
99
100
  - source pattern
101
+ - source visual mechanism, source visual basis, visual-to-copy alignment, and
102
+ visual belief activated when the source used media
100
103
  - hook-to-body promise
101
104
  - see-more tension
102
105
  - curiosity debt
@@ -348,6 +351,8 @@ Record:
348
351
  - `selectedSourceTemplate`: source, template name, and why it fits
349
352
  - `positioningSequenceBorrowed`: category sequence being adapted
350
353
  - `viralPostOutlineBorrowed`: beat sequence being adapted
354
+ - `visualEngineBorrowed`: visual hook mechanism, source visual basis, and
355
+ visual-to-copy alignment being adapted when the source used media
351
356
  - `lineShapesBorrowed`: reusable line shapes, not copied wording
352
357
  - `userStoryInputsUsed`: source-backed user facts filling the template
353
358
  - `userProofInputsUsed`: source-backed user proof filling the template
@@ -361,6 +366,34 @@ Save as `needs_revision` when the draft copies outside wording, borrows outside
361
366
  proof, keeps a source template's status without equivalent user authority, or
362
367
  uses a hook whose promised body payoff is not delivered.
363
368
 
369
+ ## Source Visual Audit
370
+
371
+ Before a draft can be `ready`, validate that media from the source examples was
372
+ seen and interpreted, not guessed.
373
+
374
+ Record:
375
+
376
+ - `visual_hook_analysis`: keeper source, source visual basis, source image URL
377
+ or local path, image availability, media type, and whether the inspection was
378
+ `cover_only`
379
+ - `visibleImageText`: brief visible text anchors from the image when safe
380
+ - `visualHookMechanism`: how the source image, video cover, carousel cover,
381
+ screenshot, diagram, or lack of media helped the hook work
382
+ - `beliefActivated`: what belief the visual made easier for the audience to
383
+ accept
384
+ - `proofCreated`: what the visual made more credible than the text alone
385
+ - `visualToCopyAlignment`: how the source copy and visual reinforced or fought
386
+ each other
387
+ - `sellableVisualAdaptation`: the equivalent Sellable visual mechanism planned
388
+ for this draft, if any
389
+ - `visualRisk`: unreadable media, generic stock-like media, misleading logo or
390
+ model claims, borrowed product proof, account-safety concern, or blocked
391
+ media
392
+
393
+ Save as `needs_revision` when a selected source template relied on media but
394
+ the media was not inspected, when the draft borrows visual proof Christian does
395
+ not have, or when the planned Sellable visual contradicts the hook.
396
+
364
397
  ## Audience Tension Audit
365
398
 
366
399
  Before a draft can be `ready`, validate that it is not merely an internally