@sellable/mcp 0.1.263 → 0.1.264
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 +4 -1
- package/dist/tools/content-posts.d.ts +205 -6
- package/dist/tools/content-posts.js +315 -17
- package/dist/tools/registry.d.ts +40 -5
- package/package.json +1 -1
- package/skills/create-post/SKILL.md +12 -7
- package/skills/create-post/references/linkedin-preview-rendering.md +38 -34
package/dist/server.js
CHANGED
|
@@ -9,7 +9,7 @@ import { getCampaignTableSchema, queueCampaignCells, reviseMessageTemplateAndRer
|
|
|
9
9
|
import { createCampaign, duplicateCampaign, getCampaign, getCampaignMessagesPreview, getCampaigns, pauseCampaign, startCampaign, updateCampaign, updateCampaignBrief, } from "./tools/campaigns.js";
|
|
10
10
|
import { queueCells, updateCell } from "./tools/cells.js";
|
|
11
11
|
import { handleStartCliLogin, handleWaitForCliLogin, } from "./tools/cli-login.js";
|
|
12
|
-
import { capturePostIdeaTool, getPostDraftTool, getPostIdeaTool, getPublishedPostTool, listPostDraftIterationsTool, listPostDraftsTool, listPostIdeasTool, listPublishedPostsTool, markPostPublishedTool, renderLinkedInPostPreviewTool, saveHookResearchTool, savePostDraftTool, updatePostDraftTool, updatePublishedPostMetricsTool, } from "./tools/content-posts.js";
|
|
12
|
+
import { calculateLinkedInHookPreviewTool, capturePostIdeaTool, getPostDraftTool, getPostIdeaTool, getPublishedPostTool, listPostDraftIterationsTool, listPostDraftsTool, listPostIdeasTool, listPublishedPostsTool, markPostPublishedTool, renderLinkedInPostPreviewTool, saveHookResearchTool, savePostDraftTool, updatePostDraftTool, updatePublishedPostMetricsTool, } from "./tools/content-posts.js";
|
|
13
13
|
import { getCampaignContext, hydrateCampaignContextFromCampaign, markCampaignContextDirty, } from "./tools/context.js";
|
|
14
14
|
import { addToCommentCampaign, addToConnectionCampaign, addToInmailCampaign, getEngagedPosts, getOrCreateDirectCampaignTable, pauseDirectCampaign, startDirectCampaign, } from "./tools/direct-campaigns.js";
|
|
15
15
|
import { bootstrapEngage, bootstrapEngageMulti, } from "./tools/engage-bootstrap.js";
|
|
@@ -244,6 +244,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
244
244
|
case "save_post_draft":
|
|
245
245
|
result = savePostDraftTool(args);
|
|
246
246
|
break;
|
|
247
|
+
case "calculate_linkedin_hook_preview":
|
|
248
|
+
result = calculateLinkedInHookPreviewTool(args);
|
|
249
|
+
break;
|
|
247
250
|
case "render_linkedin_post_preview":
|
|
248
251
|
result = renderLinkedInPostPreviewTool(args);
|
|
249
252
|
break;
|
|
@@ -95,8 +95,15 @@ export declare const contentPostToolDefinitions: ({
|
|
|
95
95
|
validationReceipt?: undefined;
|
|
96
96
|
status?: undefined;
|
|
97
97
|
postText?: undefined;
|
|
98
|
-
renderId?: undefined;
|
|
99
98
|
sourceLabel?: undefined;
|
|
99
|
+
measurementMode?: undefined;
|
|
100
|
+
requireBrowserMeasurement?: undefined;
|
|
101
|
+
clampLines?: undefined;
|
|
102
|
+
mobileClampLines?: undefined;
|
|
103
|
+
desktopClampLines?: undefined;
|
|
104
|
+
mobileTextWidthPx?: undefined;
|
|
105
|
+
desktopTextWidthPx?: undefined;
|
|
106
|
+
renderId?: undefined;
|
|
100
107
|
renderScreenshots?: undefined;
|
|
101
108
|
requireScreenshots?: undefined;
|
|
102
109
|
reviewClampLines?: undefined;
|
|
@@ -200,8 +207,15 @@ export declare const contentPostToolDefinitions: ({
|
|
|
200
207
|
validationReceipt?: undefined;
|
|
201
208
|
status?: undefined;
|
|
202
209
|
postText?: undefined;
|
|
203
|
-
renderId?: undefined;
|
|
204
210
|
sourceLabel?: undefined;
|
|
211
|
+
measurementMode?: undefined;
|
|
212
|
+
requireBrowserMeasurement?: undefined;
|
|
213
|
+
clampLines?: undefined;
|
|
214
|
+
mobileClampLines?: undefined;
|
|
215
|
+
desktopClampLines?: undefined;
|
|
216
|
+
mobileTextWidthPx?: undefined;
|
|
217
|
+
desktopTextWidthPx?: undefined;
|
|
218
|
+
renderId?: undefined;
|
|
205
219
|
renderScreenshots?: undefined;
|
|
206
220
|
requireScreenshots?: undefined;
|
|
207
221
|
reviewClampLines?: undefined;
|
|
@@ -273,8 +287,15 @@ export declare const contentPostToolDefinitions: ({
|
|
|
273
287
|
previewBudget?: undefined;
|
|
274
288
|
notes?: undefined;
|
|
275
289
|
postText?: undefined;
|
|
276
|
-
renderId?: undefined;
|
|
277
290
|
sourceLabel?: undefined;
|
|
291
|
+
measurementMode?: undefined;
|
|
292
|
+
requireBrowserMeasurement?: undefined;
|
|
293
|
+
clampLines?: undefined;
|
|
294
|
+
mobileClampLines?: undefined;
|
|
295
|
+
desktopClampLines?: undefined;
|
|
296
|
+
mobileTextWidthPx?: undefined;
|
|
297
|
+
desktopTextWidthPx?: undefined;
|
|
298
|
+
renderId?: undefined;
|
|
278
299
|
renderScreenshots?: undefined;
|
|
279
300
|
requireScreenshots?: undefined;
|
|
280
301
|
reviewClampLines?: undefined;
|
|
@@ -292,6 +313,91 @@ export declare const contentPostToolDefinitions: ({
|
|
|
292
313
|
required: string[];
|
|
293
314
|
additionalProperties: boolean;
|
|
294
315
|
};
|
|
316
|
+
} | {
|
|
317
|
+
name: string;
|
|
318
|
+
description: string;
|
|
319
|
+
inputSchema: {
|
|
320
|
+
type: string;
|
|
321
|
+
properties: {
|
|
322
|
+
postText: {
|
|
323
|
+
type: string;
|
|
324
|
+
description: string;
|
|
325
|
+
};
|
|
326
|
+
draftId: {
|
|
327
|
+
type: string;
|
|
328
|
+
description: string;
|
|
329
|
+
};
|
|
330
|
+
sourceLabel: {
|
|
331
|
+
type: string;
|
|
332
|
+
};
|
|
333
|
+
measurementMode: {
|
|
334
|
+
type: string;
|
|
335
|
+
enum: string[];
|
|
336
|
+
description: string;
|
|
337
|
+
};
|
|
338
|
+
requireBrowserMeasurement: {
|
|
339
|
+
type: string;
|
|
340
|
+
description: string;
|
|
341
|
+
};
|
|
342
|
+
clampLines: {
|
|
343
|
+
type: string;
|
|
344
|
+
description: string;
|
|
345
|
+
};
|
|
346
|
+
mobileClampLines: {
|
|
347
|
+
type: string;
|
|
348
|
+
description: string;
|
|
349
|
+
};
|
|
350
|
+
desktopClampLines: {
|
|
351
|
+
type: string;
|
|
352
|
+
description: string;
|
|
353
|
+
};
|
|
354
|
+
mobileTextWidthPx: {
|
|
355
|
+
type: string;
|
|
356
|
+
description: string;
|
|
357
|
+
};
|
|
358
|
+
desktopTextWidthPx: {
|
|
359
|
+
type: string;
|
|
360
|
+
description: string;
|
|
361
|
+
};
|
|
362
|
+
rawSource?: undefined;
|
|
363
|
+
title?: undefined;
|
|
364
|
+
distilledBrief?: undefined;
|
|
365
|
+
ideaId?: undefined;
|
|
366
|
+
sourceType?: undefined;
|
|
367
|
+
sourceUrl?: undefined;
|
|
368
|
+
capturedAt?: undefined;
|
|
369
|
+
researchId?: undefined;
|
|
370
|
+
topic?: undefined;
|
|
371
|
+
keywords?: undefined;
|
|
372
|
+
sourcePosts?: undefined;
|
|
373
|
+
selectedPatterns?: undefined;
|
|
374
|
+
previewBudget?: undefined;
|
|
375
|
+
notes?: undefined;
|
|
376
|
+
createdAt?: undefined;
|
|
377
|
+
hookResearchId?: undefined;
|
|
378
|
+
priorDraftId?: undefined;
|
|
379
|
+
iteration?: undefined;
|
|
380
|
+
body?: undefined;
|
|
381
|
+
validationReceipt?: undefined;
|
|
382
|
+
status?: undefined;
|
|
383
|
+
renderId?: undefined;
|
|
384
|
+
renderScreenshots?: undefined;
|
|
385
|
+
requireScreenshots?: undefined;
|
|
386
|
+
reviewClampLines?: undefined;
|
|
387
|
+
updatedAt?: undefined;
|
|
388
|
+
publishUrl?: undefined;
|
|
389
|
+
activityId?: undefined;
|
|
390
|
+
publishedAt?: undefined;
|
|
391
|
+
finalText?: undefined;
|
|
392
|
+
updateDraftStatus?: undefined;
|
|
393
|
+
publishedPostId?: undefined;
|
|
394
|
+
year?: undefined;
|
|
395
|
+
metrics?: undefined;
|
|
396
|
+
note?: undefined;
|
|
397
|
+
};
|
|
398
|
+
required: never[];
|
|
399
|
+
additionalProperties: boolean;
|
|
400
|
+
};
|
|
295
401
|
} | {
|
|
296
402
|
name: string;
|
|
297
403
|
description: string;
|
|
@@ -348,6 +454,13 @@ export declare const contentPostToolDefinitions: ({
|
|
|
348
454
|
body?: undefined;
|
|
349
455
|
validationReceipt?: undefined;
|
|
350
456
|
status?: undefined;
|
|
457
|
+
measurementMode?: undefined;
|
|
458
|
+
requireBrowserMeasurement?: undefined;
|
|
459
|
+
clampLines?: undefined;
|
|
460
|
+
mobileClampLines?: undefined;
|
|
461
|
+
desktopClampLines?: undefined;
|
|
462
|
+
mobileTextWidthPx?: undefined;
|
|
463
|
+
desktopTextWidthPx?: undefined;
|
|
351
464
|
updatedAt?: undefined;
|
|
352
465
|
publishUrl?: undefined;
|
|
353
466
|
activityId?: undefined;
|
|
@@ -414,8 +527,15 @@ export declare const contentPostToolDefinitions: ({
|
|
|
414
527
|
notes?: undefined;
|
|
415
528
|
createdAt?: undefined;
|
|
416
529
|
postText?: undefined;
|
|
417
|
-
renderId?: undefined;
|
|
418
530
|
sourceLabel?: undefined;
|
|
531
|
+
measurementMode?: undefined;
|
|
532
|
+
requireBrowserMeasurement?: undefined;
|
|
533
|
+
clampLines?: undefined;
|
|
534
|
+
mobileClampLines?: undefined;
|
|
535
|
+
desktopClampLines?: undefined;
|
|
536
|
+
mobileTextWidthPx?: undefined;
|
|
537
|
+
desktopTextWidthPx?: undefined;
|
|
538
|
+
renderId?: undefined;
|
|
419
539
|
renderScreenshots?: undefined;
|
|
420
540
|
requireScreenshots?: undefined;
|
|
421
541
|
reviewClampLines?: undefined;
|
|
@@ -482,8 +602,15 @@ export declare const contentPostToolDefinitions: ({
|
|
|
482
602
|
validationReceipt?: undefined;
|
|
483
603
|
status?: undefined;
|
|
484
604
|
postText?: undefined;
|
|
485
|
-
renderId?: undefined;
|
|
486
605
|
sourceLabel?: undefined;
|
|
606
|
+
measurementMode?: undefined;
|
|
607
|
+
requireBrowserMeasurement?: undefined;
|
|
608
|
+
clampLines?: undefined;
|
|
609
|
+
mobileClampLines?: undefined;
|
|
610
|
+
desktopClampLines?: undefined;
|
|
611
|
+
mobileTextWidthPx?: undefined;
|
|
612
|
+
desktopTextWidthPx?: undefined;
|
|
613
|
+
renderId?: undefined;
|
|
487
614
|
renderScreenshots?: undefined;
|
|
488
615
|
requireScreenshots?: undefined;
|
|
489
616
|
reviewClampLines?: undefined;
|
|
@@ -540,8 +667,15 @@ export declare const contentPostToolDefinitions: ({
|
|
|
540
667
|
validationReceipt?: undefined;
|
|
541
668
|
status?: undefined;
|
|
542
669
|
postText?: undefined;
|
|
543
|
-
renderId?: undefined;
|
|
544
670
|
sourceLabel?: undefined;
|
|
671
|
+
measurementMode?: undefined;
|
|
672
|
+
requireBrowserMeasurement?: undefined;
|
|
673
|
+
clampLines?: undefined;
|
|
674
|
+
mobileClampLines?: undefined;
|
|
675
|
+
desktopClampLines?: undefined;
|
|
676
|
+
mobileTextWidthPx?: undefined;
|
|
677
|
+
desktopTextWidthPx?: undefined;
|
|
678
|
+
renderId?: undefined;
|
|
545
679
|
renderScreenshots?: undefined;
|
|
546
680
|
requireScreenshots?: undefined;
|
|
547
681
|
reviewClampLines?: undefined;
|
|
@@ -639,6 +773,71 @@ export declare function savePostDraftTool(input: {
|
|
|
639
773
|
updatedAt: string;
|
|
640
774
|
preview: string;
|
|
641
775
|
};
|
|
776
|
+
type LinkedInHookPreviewSurface = {
|
|
777
|
+
label: "mobile" | "desktop";
|
|
778
|
+
textWidthPx: number;
|
|
779
|
+
fontSizePx: number;
|
|
780
|
+
lineHeightPx: number;
|
|
781
|
+
clampLines: number;
|
|
782
|
+
charGuardrail: number;
|
|
783
|
+
renderedLines: string[];
|
|
784
|
+
visibleLines: string[];
|
|
785
|
+
visibleTextBlock: string;
|
|
786
|
+
visibleTextBlockCharCount: number;
|
|
787
|
+
withinCharGuardrail: boolean;
|
|
788
|
+
lineCountBeforeClamp: number;
|
|
789
|
+
blankLinesBeforeClamp: number;
|
|
790
|
+
truncated: boolean;
|
|
791
|
+
seeMoreWouldShow: boolean;
|
|
792
|
+
hiddenTextStartsWith?: string;
|
|
793
|
+
measurementBasis: "local_chrome_browser_layout" | "estimated_width_model";
|
|
794
|
+
measurementWarning?: string;
|
|
795
|
+
chromePath?: string;
|
|
796
|
+
};
|
|
797
|
+
export declare function calculateLinkedInHookPreviewTool(input: {
|
|
798
|
+
postText?: string;
|
|
799
|
+
draftId?: string;
|
|
800
|
+
sourceLabel?: string;
|
|
801
|
+
measurementMode?: "browser" | "estimated";
|
|
802
|
+
requireBrowserMeasurement?: boolean;
|
|
803
|
+
clampLines?: number;
|
|
804
|
+
mobileClampLines?: number;
|
|
805
|
+
desktopClampLines?: number;
|
|
806
|
+
mobileTextWidthPx?: number;
|
|
807
|
+
desktopTextWidthPx?: number;
|
|
808
|
+
}): {
|
|
809
|
+
basis: string;
|
|
810
|
+
ruleConfidence: string;
|
|
811
|
+
cssContractVersion: string;
|
|
812
|
+
sourceLabel: string | undefined;
|
|
813
|
+
draftId: string | undefined;
|
|
814
|
+
ruleSummary: string;
|
|
815
|
+
rules: {
|
|
816
|
+
visibleUnit: string;
|
|
817
|
+
defaultClampLines: number;
|
|
818
|
+
fontSizePx: number;
|
|
819
|
+
lineHeightPx: number;
|
|
820
|
+
whiteSpace: string;
|
|
821
|
+
overflowWrap: string;
|
|
822
|
+
desktopCharGuardrail: number;
|
|
823
|
+
mobileCharGuardrail: number;
|
|
824
|
+
characterCountsAreGuardrailsOnly: boolean;
|
|
825
|
+
notOfficialLinkedInApi: boolean;
|
|
826
|
+
strongestEvidence: string;
|
|
827
|
+
};
|
|
828
|
+
mobile: LinkedInHookPreviewSurface;
|
|
829
|
+
desktop: LinkedInHookPreviewSurface;
|
|
830
|
+
diagnostics: {
|
|
831
|
+
charCount: number;
|
|
832
|
+
charCountIncludingNewlines: number;
|
|
833
|
+
firstLineChars: number;
|
|
834
|
+
firstTwoPhysicalLinesChars: number;
|
|
835
|
+
physicalLineCount: number;
|
|
836
|
+
contentLineCount: number;
|
|
837
|
+
longestNonblankLineChars: number;
|
|
838
|
+
blankLineCount: number;
|
|
839
|
+
};
|
|
840
|
+
};
|
|
642
841
|
export declare function renderLinkedInPostPreviewTool(input: {
|
|
643
842
|
postText?: string;
|
|
644
843
|
draftId?: string;
|
|
@@ -5,6 +5,24 @@ import { execFileSync } from "node:child_process";
|
|
|
5
5
|
import { pathToFileURL } from "node:url";
|
|
6
6
|
const CONTENT_ROOT_ENV = "SELLABLE_CONTENT_DIR";
|
|
7
7
|
const DEFAULT_PREVIEW_CHARS = 220;
|
|
8
|
+
const LINKEDIN_PREVIEW_CONTRACT_VERSION = "linkedin-preview-rendering/v2";
|
|
9
|
+
const LINKEDIN_PREVIEW_FONT_SIZE_PX = 14;
|
|
10
|
+
const LINKEDIN_PREVIEW_LINE_HEIGHT_PX = 21;
|
|
11
|
+
const LINKEDIN_DEFAULT_CLAMP_LINES = 3;
|
|
12
|
+
const LINKEDIN_MOBILE_CHAR_GUARDRAIL = 140;
|
|
13
|
+
const LINKEDIN_DESKTOP_CHAR_GUARDRAIL = 210;
|
|
14
|
+
const LINKEDIN_PREVIEW_SURFACES = {
|
|
15
|
+
mobile: {
|
|
16
|
+
textWidthPx: 308,
|
|
17
|
+
viewportWidthPx: 390,
|
|
18
|
+
viewportHeightPx: 760,
|
|
19
|
+
},
|
|
20
|
+
desktop: {
|
|
21
|
+
textWidthPx: 582,
|
|
22
|
+
viewportWidthPx: 744,
|
|
23
|
+
viewportHeightPx: 760,
|
|
24
|
+
},
|
|
25
|
+
};
|
|
8
26
|
const RELATIVE_DIRS = {
|
|
9
27
|
ideas: "linkedin/ideas",
|
|
10
28
|
hookResearch: "linkedin/research/hooks",
|
|
@@ -107,6 +125,55 @@ export const contentPostToolDefinitions = [
|
|
|
107
125
|
additionalProperties: false,
|
|
108
126
|
},
|
|
109
127
|
},
|
|
128
|
+
{
|
|
129
|
+
name: "calculate_linkedin_hook_preview",
|
|
130
|
+
description: "Functionally calculate the LinkedIn pre-see-more hook for mobile and desktop. Returns rendered visible lines and rule diagnostics without writing files.",
|
|
131
|
+
inputSchema: {
|
|
132
|
+
type: "object",
|
|
133
|
+
properties: {
|
|
134
|
+
postText: {
|
|
135
|
+
type: "string",
|
|
136
|
+
description: "Full post text or hook block to calculate. Provide either postText or draftId.",
|
|
137
|
+
},
|
|
138
|
+
draftId: {
|
|
139
|
+
type: "string",
|
|
140
|
+
description: "Optional saved draft ID. When postText is omitted, the tool calculates this draft's Draft Body.",
|
|
141
|
+
},
|
|
142
|
+
sourceLabel: { type: "string" },
|
|
143
|
+
measurementMode: {
|
|
144
|
+
type: "string",
|
|
145
|
+
enum: ["browser", "estimated"],
|
|
146
|
+
description: "Defaults to browser. Browser mode uses local Chrome layout measurement when available; estimated mode uses the deterministic width model.",
|
|
147
|
+
},
|
|
148
|
+
requireBrowserMeasurement: {
|
|
149
|
+
type: "boolean",
|
|
150
|
+
description: "Defaults to false. When true, fail if local Chrome cannot measure rendered lines.",
|
|
151
|
+
},
|
|
152
|
+
clampLines: {
|
|
153
|
+
type: "number",
|
|
154
|
+
description: "Visible rendered lines before see-more for both surfaces. Defaults to 3.",
|
|
155
|
+
},
|
|
156
|
+
mobileClampLines: {
|
|
157
|
+
type: "number",
|
|
158
|
+
description: "Optional mobile-specific visible rendered line count. Defaults to clampLines.",
|
|
159
|
+
},
|
|
160
|
+
desktopClampLines: {
|
|
161
|
+
type: "number",
|
|
162
|
+
description: "Optional desktop-specific visible rendered line count. Defaults to clampLines.",
|
|
163
|
+
},
|
|
164
|
+
mobileTextWidthPx: {
|
|
165
|
+
type: "number",
|
|
166
|
+
description: "Optional mobile text width override. Defaults to 308px.",
|
|
167
|
+
},
|
|
168
|
+
desktopTextWidthPx: {
|
|
169
|
+
type: "number",
|
|
170
|
+
description: "Optional desktop text width override. Defaults to 582px.",
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
required: [],
|
|
174
|
+
additionalProperties: false,
|
|
175
|
+
},
|
|
176
|
+
},
|
|
110
177
|
{
|
|
111
178
|
name: "render_linkedin_post_preview",
|
|
112
179
|
description: "Render any LinkedIn post text through the Sellable LinkedIn preview CSS contract for mobile and desktop. Writes HTML artifacts and, when local Chrome is available, browser screenshots under ~/.sellable/content/linkedin/previews.",
|
|
@@ -429,16 +496,54 @@ export function savePostDraftTool(input) {
|
|
|
429
496
|
preview: sanitizedPreview(input.body),
|
|
430
497
|
};
|
|
431
498
|
}
|
|
499
|
+
export function calculateLinkedInHookPreviewTool(input) {
|
|
500
|
+
const { postText, draftId } = resolveLinkedInPreviewPostText(input);
|
|
501
|
+
const defaultClampLines = boundedWholeNumber(input.clampLines, LINKEDIN_DEFAULT_CLAMP_LINES, 1, 8);
|
|
502
|
+
const measurementMode = input.measurementMode ?? "browser";
|
|
503
|
+
const mobile = calculateLinkedInHookPreviewSurface({
|
|
504
|
+
label: "mobile",
|
|
505
|
+
text: postText,
|
|
506
|
+
textWidthPx: boundedWholeNumber(input.mobileTextWidthPx, LINKEDIN_PREVIEW_SURFACES.mobile.textWidthPx, 160, 900),
|
|
507
|
+
clampLines: boundedWholeNumber(input.mobileClampLines, defaultClampLines, 1, 8),
|
|
508
|
+
measurementMode,
|
|
509
|
+
requireBrowserMeasurement: input.requireBrowserMeasurement,
|
|
510
|
+
});
|
|
511
|
+
const desktop = calculateLinkedInHookPreviewSurface({
|
|
512
|
+
label: "desktop",
|
|
513
|
+
text: postText,
|
|
514
|
+
textWidthPx: boundedWholeNumber(input.desktopTextWidthPx, LINKEDIN_PREVIEW_SURFACES.desktop.textWidthPx, 260, 1200),
|
|
515
|
+
clampLines: boundedWholeNumber(input.desktopClampLines, defaultClampLines, 1, 8),
|
|
516
|
+
measurementMode,
|
|
517
|
+
requireBrowserMeasurement: input.requireBrowserMeasurement,
|
|
518
|
+
});
|
|
519
|
+
return {
|
|
520
|
+
basis: "linkedin_rendering_rule_function",
|
|
521
|
+
ruleConfidence: "calibrated_from_observed_linkedin_feed_screenshots_and_public_line_clamp_behavior",
|
|
522
|
+
cssContractVersion: LINKEDIN_PREVIEW_CONTRACT_VERSION,
|
|
523
|
+
sourceLabel: input.sourceLabel || draftId,
|
|
524
|
+
draftId,
|
|
525
|
+
ruleSummary: "The hook is the text visible before see more. The review gate is rendered line count, not a fixed character count. Blank lines consume visible lines.",
|
|
526
|
+
rules: {
|
|
527
|
+
visibleUnit: "rendered_visual_line",
|
|
528
|
+
defaultClampLines: LINKEDIN_DEFAULT_CLAMP_LINES,
|
|
529
|
+
fontSizePx: LINKEDIN_PREVIEW_FONT_SIZE_PX,
|
|
530
|
+
lineHeightPx: LINKEDIN_PREVIEW_LINE_HEIGHT_PX,
|
|
531
|
+
whiteSpace: "pre-wrap",
|
|
532
|
+
overflowWrap: "break-word",
|
|
533
|
+
desktopCharGuardrail: LINKEDIN_DESKTOP_CHAR_GUARDRAIL,
|
|
534
|
+
mobileCharGuardrail: LINKEDIN_MOBILE_CHAR_GUARDRAIL,
|
|
535
|
+
characterCountsAreGuardrailsOnly: true,
|
|
536
|
+
notOfficialLinkedInApi: true,
|
|
537
|
+
strongestEvidence: "authenticated LinkedIn feed screenshot on the target surface",
|
|
538
|
+
},
|
|
539
|
+
mobile,
|
|
540
|
+
desktop,
|
|
541
|
+
diagnostics: previewDiagnostics(postText),
|
|
542
|
+
};
|
|
543
|
+
}
|
|
432
544
|
export function renderLinkedInPostPreviewTool(input) {
|
|
433
545
|
const now = normalizeDate(input.createdAt);
|
|
434
|
-
const draftId = input
|
|
435
|
-
? normalizeArtifactId(input.draftId, "draftId")
|
|
436
|
-
: undefined;
|
|
437
|
-
const draftBody = draftId
|
|
438
|
-
? extractMarkdownSection(getArtifact(RELATIVE_DIRS.drafts, draftId, "draftId").markdown, "Draft Body")
|
|
439
|
-
: undefined;
|
|
440
|
-
const postText = input.postText ?? draftBody;
|
|
441
|
-
requireString(postText, "postText");
|
|
546
|
+
const { postText, draftId } = resolveLinkedInPreviewPostText(input);
|
|
442
547
|
const reviewClampLines = typeof input.reviewClampLines === "number" &&
|
|
443
548
|
Number.isFinite(input.reviewClampLines)
|
|
444
549
|
? Math.max(1, Math.min(Math.floor(input.reviewClampLines), 8))
|
|
@@ -453,18 +558,18 @@ export function renderLinkedInPostPreviewTool(input) {
|
|
|
453
558
|
label: "mobile",
|
|
454
559
|
text: postText,
|
|
455
560
|
baseDir,
|
|
456
|
-
textWidthPx:
|
|
457
|
-
viewportWidthPx:
|
|
458
|
-
viewportHeightPx:
|
|
561
|
+
textWidthPx: LINKEDIN_PREVIEW_SURFACES.mobile.textWidthPx,
|
|
562
|
+
viewportWidthPx: LINKEDIN_PREVIEW_SURFACES.mobile.viewportWidthPx,
|
|
563
|
+
viewportHeightPx: LINKEDIN_PREVIEW_SURFACES.mobile.viewportHeightPx,
|
|
459
564
|
reviewClampLines,
|
|
460
565
|
});
|
|
461
566
|
const desktop = buildPreviewSurface({
|
|
462
567
|
label: "desktop",
|
|
463
568
|
text: postText,
|
|
464
569
|
baseDir,
|
|
465
|
-
textWidthPx:
|
|
466
|
-
viewportWidthPx:
|
|
467
|
-
viewportHeightPx:
|
|
570
|
+
textWidthPx: LINKEDIN_PREVIEW_SURFACES.desktop.textWidthPx,
|
|
571
|
+
viewportWidthPx: LINKEDIN_PREVIEW_SURFACES.desktop.viewportWidthPx,
|
|
572
|
+
viewportHeightPx: LINKEDIN_PREVIEW_SURFACES.desktop.viewportHeightPx,
|
|
468
573
|
reviewClampLines,
|
|
469
574
|
});
|
|
470
575
|
const combinedHtmlRelativePath = `${baseDir}/preview.html`;
|
|
@@ -491,7 +596,7 @@ export function renderLinkedInPostPreviewTool(input) {
|
|
|
491
596
|
basis: screenshotResult.status === "rendered"
|
|
492
597
|
? "local_chrome_headless_screenshot"
|
|
493
598
|
: "linkedin_css_contract_html",
|
|
494
|
-
cssContractVersion:
|
|
599
|
+
cssContractVersion: LINKEDIN_PREVIEW_CONTRACT_VERSION,
|
|
495
600
|
generatedAt: now,
|
|
496
601
|
sourceLabel: title,
|
|
497
602
|
mobile: surfaceRecord(mobile),
|
|
@@ -760,6 +865,199 @@ export function listPublishedPostsTool(input) {
|
|
|
760
865
|
}
|
|
761
866
|
return summarizeArtifacts(artifacts, input?.limit);
|
|
762
867
|
}
|
|
868
|
+
function resolveLinkedInPreviewPostText(input) {
|
|
869
|
+
const draftId = input.draftId
|
|
870
|
+
? normalizeArtifactId(input.draftId, "draftId")
|
|
871
|
+
: undefined;
|
|
872
|
+
const draftBody = draftId
|
|
873
|
+
? extractMarkdownSection(getArtifact(RELATIVE_DIRS.drafts, draftId, "draftId").markdown, "Draft Body")
|
|
874
|
+
: undefined;
|
|
875
|
+
const postText = input.postText ?? draftBody;
|
|
876
|
+
requireString(postText, "postText");
|
|
877
|
+
return { postText, draftId };
|
|
878
|
+
}
|
|
879
|
+
function boundedWholeNumber(value, fallback, min, max) {
|
|
880
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
881
|
+
return fallback;
|
|
882
|
+
return Math.max(min, Math.min(Math.floor(value), max));
|
|
883
|
+
}
|
|
884
|
+
function calculateLinkedInHookPreviewSurface(input) {
|
|
885
|
+
const measurement = measureLinkedInRenderedLines({
|
|
886
|
+
text: input.text,
|
|
887
|
+
textWidthPx: input.textWidthPx,
|
|
888
|
+
measurementMode: input.measurementMode,
|
|
889
|
+
requireBrowserMeasurement: input.requireBrowserMeasurement,
|
|
890
|
+
});
|
|
891
|
+
const visibleLines = measurement.lines.slice(0, input.clampLines);
|
|
892
|
+
const hiddenLines = measurement.lines.slice(input.clampLines);
|
|
893
|
+
const visibleTextBlock = visibleLines.join("\n");
|
|
894
|
+
const charGuardrail = input.label === "mobile"
|
|
895
|
+
? LINKEDIN_MOBILE_CHAR_GUARDRAIL
|
|
896
|
+
: LINKEDIN_DESKTOP_CHAR_GUARDRAIL;
|
|
897
|
+
return {
|
|
898
|
+
label: input.label,
|
|
899
|
+
textWidthPx: input.textWidthPx,
|
|
900
|
+
fontSizePx: LINKEDIN_PREVIEW_FONT_SIZE_PX,
|
|
901
|
+
lineHeightPx: LINKEDIN_PREVIEW_LINE_HEIGHT_PX,
|
|
902
|
+
clampLines: input.clampLines,
|
|
903
|
+
charGuardrail,
|
|
904
|
+
renderedLines: measurement.lines,
|
|
905
|
+
visibleLines,
|
|
906
|
+
visibleTextBlock,
|
|
907
|
+
visibleTextBlockCharCount: visibleTextBlock.length,
|
|
908
|
+
withinCharGuardrail: visibleTextBlock.length <= charGuardrail,
|
|
909
|
+
lineCountBeforeClamp: measurement.lines.length,
|
|
910
|
+
blankLinesBeforeClamp: visibleLines.filter((line) => line.trim().length === 0).length,
|
|
911
|
+
truncated: hiddenLines.length > 0,
|
|
912
|
+
seeMoreWouldShow: hiddenLines.length > 0,
|
|
913
|
+
hiddenTextStartsWith: hiddenLines.find((line) => line.trim().length > 0),
|
|
914
|
+
measurementBasis: measurement.basis,
|
|
915
|
+
measurementWarning: measurement.warning,
|
|
916
|
+
chromePath: measurement.chromePath,
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
function measureLinkedInRenderedLines(input) {
|
|
920
|
+
if (input.measurementMode === "browser") {
|
|
921
|
+
const browserMeasurement = measureLinkedInRenderedLinesWithChrome(input.text, input.textWidthPx);
|
|
922
|
+
if (browserMeasurement)
|
|
923
|
+
return browserMeasurement;
|
|
924
|
+
if (input.requireBrowserMeasurement) {
|
|
925
|
+
throw new Error("Chrome executable not found or browser line measurement failed. Set CHROME_PATH or use measurementMode=estimated.");
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
return {
|
|
929
|
+
lines: estimateRenderedLines(input.text, input.textWidthPx),
|
|
930
|
+
basis: "estimated_width_model",
|
|
931
|
+
warning: input.measurementMode === "browser"
|
|
932
|
+
? "local Chrome measurement unavailable; used deterministic width estimate"
|
|
933
|
+
: undefined,
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
function measureLinkedInRenderedLinesWithChrome(text, textWidthPx) {
|
|
937
|
+
const chromePath = findChromeExecutable();
|
|
938
|
+
if (!chromePath)
|
|
939
|
+
return null;
|
|
940
|
+
const htmlPath = path.join(os.tmpdir(), `sellable-linkedin-hook-preview-${process.pid}-${Date.now()}-${Math.random()
|
|
941
|
+
.toString(36)
|
|
942
|
+
.slice(2)}.html`);
|
|
943
|
+
try {
|
|
944
|
+
fs.writeFileSync(htmlPath, buildLineMeasurementHtml(text, textWidthPx), "utf8");
|
|
945
|
+
const dom = execFileSync(chromePath, [
|
|
946
|
+
"--headless=new",
|
|
947
|
+
"--disable-gpu",
|
|
948
|
+
"--no-sandbox",
|
|
949
|
+
"--disable-dev-shm-usage",
|
|
950
|
+
"--hide-scrollbars",
|
|
951
|
+
"--dump-dom",
|
|
952
|
+
pathToFileURL(htmlPath).href,
|
|
953
|
+
], { encoding: "utf8", timeout: 20000, stdio: ["ignore", "pipe", "ignore"] });
|
|
954
|
+
const encoded = dom.match(/data-linkedin-lines="([^"]+)"/)?.[1];
|
|
955
|
+
if (!encoded)
|
|
956
|
+
return null;
|
|
957
|
+
const decoded = Buffer.from(encoded, "base64").toString("utf8");
|
|
958
|
+
const parsed = JSON.parse(decoded);
|
|
959
|
+
if (!Array.isArray(parsed) ||
|
|
960
|
+
parsed.some((line) => typeof line !== "string")) {
|
|
961
|
+
return null;
|
|
962
|
+
}
|
|
963
|
+
return {
|
|
964
|
+
lines: parsed,
|
|
965
|
+
basis: "local_chrome_browser_layout",
|
|
966
|
+
chromePath,
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
catch {
|
|
970
|
+
return null;
|
|
971
|
+
}
|
|
972
|
+
finally {
|
|
973
|
+
fs.rmSync(htmlPath, { force: true });
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
function buildLineMeasurementHtml(text, textWidthPx) {
|
|
977
|
+
return [
|
|
978
|
+
"<!doctype html>",
|
|
979
|
+
'<html lang="en">',
|
|
980
|
+
"<head>",
|
|
981
|
+
'<meta charset="utf-8">',
|
|
982
|
+
'<meta name="viewport" content="width=device-width, initial-scale=1">',
|
|
983
|
+
"</head>",
|
|
984
|
+
"<body>",
|
|
985
|
+
"<script>",
|
|
986
|
+
`const text = ${jsonForInlineScript(text)};`,
|
|
987
|
+
`const textWidthPx = ${textWidthPx};`,
|
|
988
|
+
`const fontSizePx = ${LINKEDIN_PREVIEW_FONT_SIZE_PX};`,
|
|
989
|
+
`const lineHeightPx = ${LINKEDIN_PREVIEW_LINE_HEIGHT_PX};`,
|
|
990
|
+
`
|
|
991
|
+
const div = document.createElement("div");
|
|
992
|
+
div.style.cssText = [
|
|
993
|
+
"position:absolute",
|
|
994
|
+
"left:0",
|
|
995
|
+
"top:0",
|
|
996
|
+
"width:" + textWidthPx + "px",
|
|
997
|
+
"font-family:-apple-system,system-ui,'Segoe UI',Roboto,'Helvetica Neue','Fira Sans',Ubuntu,'Oxygen Sans',Cantarell,'Droid Sans','Lucida Grande',Helvetica,Arial,sans-serif",
|
|
998
|
+
"font-size:" + fontSizePx + "px",
|
|
999
|
+
"line-height:" + lineHeightPx + "px",
|
|
1000
|
+
"font-weight:400",
|
|
1001
|
+
"letter-spacing:normal",
|
|
1002
|
+
"white-space:pre-wrap",
|
|
1003
|
+
"overflow-wrap:break-word",
|
|
1004
|
+
"color:#000"
|
|
1005
|
+
].join(";");
|
|
1006
|
+
Array.from(text).forEach((char, index) => {
|
|
1007
|
+
if (char === "\\n") {
|
|
1008
|
+
div.appendChild(document.createTextNode("\\n"));
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
const span = document.createElement("span");
|
|
1012
|
+
span.dataset.index = String(index);
|
|
1013
|
+
span.textContent = char;
|
|
1014
|
+
div.appendChild(span);
|
|
1015
|
+
});
|
|
1016
|
+
document.body.appendChild(div);
|
|
1017
|
+
|
|
1018
|
+
const lines = [];
|
|
1019
|
+
let currentText = null;
|
|
1020
|
+
let currentTop = null;
|
|
1021
|
+
let previousTop = null;
|
|
1022
|
+
function flushLine() {
|
|
1023
|
+
if (currentText !== null) {
|
|
1024
|
+
lines.push(currentText.trimEnd());
|
|
1025
|
+
currentText = null;
|
|
1026
|
+
currentTop = null;
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
for (const span of div.querySelectorAll("span")) {
|
|
1031
|
+
const rect = span.getClientRects()[0];
|
|
1032
|
+
if (!rect) continue;
|
|
1033
|
+
const top = Math.round(rect.top * 10) / 10;
|
|
1034
|
+
if (currentTop === null || Math.abs(top - currentTop) > 2) {
|
|
1035
|
+
flushLine();
|
|
1036
|
+
if (previousTop !== null) {
|
|
1037
|
+
const blankCount = Math.max(0, Math.round((top - previousTop) / lineHeightPx) - 1);
|
|
1038
|
+
for (let index = 0; index < blankCount; index += 1) lines.push("");
|
|
1039
|
+
}
|
|
1040
|
+
currentText = "";
|
|
1041
|
+
currentTop = top;
|
|
1042
|
+
previousTop = top;
|
|
1043
|
+
}
|
|
1044
|
+
currentText += span.textContent;
|
|
1045
|
+
}
|
|
1046
|
+
flushLine();
|
|
1047
|
+
document.documentElement.setAttribute(
|
|
1048
|
+
"data-linkedin-lines",
|
|
1049
|
+
btoa(unescape(encodeURIComponent(JSON.stringify(lines))))
|
|
1050
|
+
);
|
|
1051
|
+
`,
|
|
1052
|
+
"</script>",
|
|
1053
|
+
"</body>",
|
|
1054
|
+
"</html>",
|
|
1055
|
+
"",
|
|
1056
|
+
].join("\n");
|
|
1057
|
+
}
|
|
1058
|
+
function jsonForInlineScript(value) {
|
|
1059
|
+
return JSON.stringify(value).replace(/</g, "\\u003c");
|
|
1060
|
+
}
|
|
763
1061
|
function buildPreviewSurface(input) {
|
|
764
1062
|
const estimatedLines = estimateRenderedLines(input.text, input.textWidthPx);
|
|
765
1063
|
const htmlPath = `${input.baseDir}/${input.label}.html`;
|
|
@@ -768,8 +1066,8 @@ function buildPreviewSurface(input) {
|
|
|
768
1066
|
textWidthPx: input.textWidthPx,
|
|
769
1067
|
viewportWidthPx: input.viewportWidthPx,
|
|
770
1068
|
viewportHeightPx: input.viewportHeightPx,
|
|
771
|
-
fontSizePx:
|
|
772
|
-
lineHeightPx:
|
|
1069
|
+
fontSizePx: LINKEDIN_PREVIEW_FONT_SIZE_PX,
|
|
1070
|
+
lineHeightPx: LINKEDIN_PREVIEW_LINE_HEIGHT_PX,
|
|
773
1071
|
reviewClampLines: input.reviewClampLines,
|
|
774
1072
|
estimatedLines,
|
|
775
1073
|
estimatedVisibleTextBlock: estimatedLines
|
package/dist/tools/registry.d.ts
CHANGED
|
@@ -1261,8 +1261,15 @@ export declare const allTools: ({
|
|
|
1261
1261
|
validationReceipt?: undefined;
|
|
1262
1262
|
status?: undefined;
|
|
1263
1263
|
postText?: undefined;
|
|
1264
|
-
renderId?: undefined;
|
|
1265
1264
|
sourceLabel?: undefined;
|
|
1265
|
+
measurementMode?: undefined;
|
|
1266
|
+
requireBrowserMeasurement?: undefined;
|
|
1267
|
+
clampLines?: undefined;
|
|
1268
|
+
mobileClampLines?: undefined;
|
|
1269
|
+
desktopClampLines?: undefined;
|
|
1270
|
+
mobileTextWidthPx?: undefined;
|
|
1271
|
+
desktopTextWidthPx?: undefined;
|
|
1272
|
+
renderId?: undefined;
|
|
1266
1273
|
renderScreenshots?: undefined;
|
|
1267
1274
|
requireScreenshots?: undefined;
|
|
1268
1275
|
reviewClampLines?: undefined;
|
|
@@ -1347,8 +1354,15 @@ export declare const allTools: ({
|
|
|
1347
1354
|
previewBudget?: undefined;
|
|
1348
1355
|
notes?: undefined;
|
|
1349
1356
|
postText?: undefined;
|
|
1350
|
-
renderId?: undefined;
|
|
1351
1357
|
sourceLabel?: undefined;
|
|
1358
|
+
measurementMode?: undefined;
|
|
1359
|
+
requireBrowserMeasurement?: undefined;
|
|
1360
|
+
clampLines?: undefined;
|
|
1361
|
+
mobileClampLines?: undefined;
|
|
1362
|
+
desktopClampLines?: undefined;
|
|
1363
|
+
mobileTextWidthPx?: undefined;
|
|
1364
|
+
desktopTextWidthPx?: undefined;
|
|
1365
|
+
renderId?: undefined;
|
|
1352
1366
|
renderScreenshots?: undefined;
|
|
1353
1367
|
requireScreenshots?: undefined;
|
|
1354
1368
|
reviewClampLines?: undefined;
|
|
@@ -1418,8 +1432,15 @@ export declare const allTools: ({
|
|
|
1418
1432
|
notes?: undefined;
|
|
1419
1433
|
createdAt?: undefined;
|
|
1420
1434
|
postText?: undefined;
|
|
1421
|
-
renderId?: undefined;
|
|
1422
1435
|
sourceLabel?: undefined;
|
|
1436
|
+
measurementMode?: undefined;
|
|
1437
|
+
requireBrowserMeasurement?: undefined;
|
|
1438
|
+
clampLines?: undefined;
|
|
1439
|
+
mobileClampLines?: undefined;
|
|
1440
|
+
desktopClampLines?: undefined;
|
|
1441
|
+
mobileTextWidthPx?: undefined;
|
|
1442
|
+
desktopTextWidthPx?: undefined;
|
|
1443
|
+
renderId?: undefined;
|
|
1423
1444
|
renderScreenshots?: undefined;
|
|
1424
1445
|
requireScreenshots?: undefined;
|
|
1425
1446
|
reviewClampLines?: undefined;
|
|
@@ -1486,8 +1507,15 @@ export declare const allTools: ({
|
|
|
1486
1507
|
validationReceipt?: undefined;
|
|
1487
1508
|
status?: undefined;
|
|
1488
1509
|
postText?: undefined;
|
|
1489
|
-
renderId?: undefined;
|
|
1490
1510
|
sourceLabel?: undefined;
|
|
1511
|
+
measurementMode?: undefined;
|
|
1512
|
+
requireBrowserMeasurement?: undefined;
|
|
1513
|
+
clampLines?: undefined;
|
|
1514
|
+
mobileClampLines?: undefined;
|
|
1515
|
+
desktopClampLines?: undefined;
|
|
1516
|
+
mobileTextWidthPx?: undefined;
|
|
1517
|
+
desktopTextWidthPx?: undefined;
|
|
1518
|
+
renderId?: undefined;
|
|
1491
1519
|
renderScreenshots?: undefined;
|
|
1492
1520
|
requireScreenshots?: undefined;
|
|
1493
1521
|
reviewClampLines?: undefined;
|
|
@@ -1544,8 +1572,15 @@ export declare const allTools: ({
|
|
|
1544
1572
|
validationReceipt?: undefined;
|
|
1545
1573
|
status?: undefined;
|
|
1546
1574
|
postText?: undefined;
|
|
1547
|
-
renderId?: undefined;
|
|
1548
1575
|
sourceLabel?: undefined;
|
|
1576
|
+
measurementMode?: undefined;
|
|
1577
|
+
requireBrowserMeasurement?: undefined;
|
|
1578
|
+
clampLines?: undefined;
|
|
1579
|
+
mobileClampLines?: undefined;
|
|
1580
|
+
desktopClampLines?: undefined;
|
|
1581
|
+
mobileTextWidthPx?: undefined;
|
|
1582
|
+
desktopTextWidthPx?: undefined;
|
|
1583
|
+
renderId?: undefined;
|
|
1549
1584
|
renderScreenshots?: undefined;
|
|
1550
1585
|
requireScreenshots?: undefined;
|
|
1551
1586
|
reviewClampLines?: undefined;
|
package/package.json
CHANGED
|
@@ -108,6 +108,7 @@ Use these MCP tools when available:
|
|
|
108
108
|
- `mcp__sellable__get_post_idea`
|
|
109
109
|
- `mcp__sellable__list_post_ideas`
|
|
110
110
|
- `mcp__sellable__save_hook_research`
|
|
111
|
+
- `mcp__sellable__calculate_linkedin_hook_preview`
|
|
111
112
|
- `mcp__sellable__render_linkedin_post_preview`
|
|
112
113
|
- `mcp__sellable__save_post_draft`
|
|
113
114
|
- `mcp__sellable__update_post_draft`
|
|
@@ -343,8 +344,9 @@ Visible Flow Trace
|
|
|
343
344
|
|
|
344
345
|
4. Hook Autopsies
|
|
345
346
|
- source hook:
|
|
346
|
-
-
|
|
347
|
-
authenticated LinkedIn screenshot:
|
|
347
|
+
- calculated mobile/desktop visible blocks from
|
|
348
|
+
`calculate_linkedin_hook_preview` or authenticated LinkedIn screenshot:
|
|
349
|
+
- optional visual artifact from `render_linkedin_post_preview`:
|
|
348
350
|
- see-more tension:
|
|
349
351
|
- curiosity debt:
|
|
350
352
|
- body promise:
|
|
@@ -404,7 +406,8 @@ Visible Flow Trace
|
|
|
404
406
|
|
|
405
407
|
11. Hook Lab
|
|
406
408
|
- at least 12 hooks:
|
|
407
|
-
-
|
|
409
|
+
- calculated preview from `calculate_linkedin_hook_preview`:
|
|
410
|
+
- optional visual artifact from `render_linkedin_post_preview` for finalists:
|
|
408
411
|
- hook-to-body promise:
|
|
409
412
|
- score:
|
|
410
413
|
- selected hook:
|
|
@@ -838,10 +841,12 @@ media, and line break. Treat character counts as diagnostics only, not as proof
|
|
|
838
841
|
that the hook will render before "see more."
|
|
839
842
|
|
|
840
843
|
The selected hook and top candidates must include literal mobile and desktop
|
|
841
|
-
visible blocks
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
844
|
+
visible blocks from `mcp__sellable__calculate_linkedin_hook_preview`.
|
|
845
|
+
Observed LinkedIn screenshots and current third-party preview tools support a
|
|
846
|
+
line-count model: review the first 3 rendered visual lines, not the first 210
|
|
847
|
+
characters. Blank lines and `--` separators consume visible preview lines.
|
|
848
|
+
Use `mcp__sellable__render_linkedin_post_preview` only when a visual QA artifact
|
|
849
|
+
is useful.
|
|
845
850
|
|
|
846
851
|
Use:
|
|
847
852
|
|
|
@@ -5,45 +5,49 @@ research, hook candidate generation, gold-standard decomposition, and draft
|
|
|
5
5
|
validation.
|
|
6
6
|
|
|
7
7
|
Character budgets are only diagnostics. They are not the preview gate. A hook is
|
|
8
|
-
not studied, selected, or ready until
|
|
9
|
-
`
|
|
8
|
+
not studied, selected, or ready until its visible mobile and desktop blocks come
|
|
9
|
+
from `mcp__sellable__calculate_linkedin_hook_preview` or from a stricter
|
|
10
10
|
authenticated LinkedIn screenshot.
|
|
11
11
|
|
|
12
|
-
Do not let the LLM imagine wrapping.
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
Do not let the LLM imagine wrapping. The normal path is a function call that
|
|
13
|
+
returns rendered mobile and desktop lines. HTML and PNG outputs from
|
|
14
|
+
`mcp__sellable__render_linkedin_post_preview` are optional visual QA artifacts,
|
|
15
|
+
not the primary interface.
|
|
16
16
|
|
|
17
17
|
## Rendering Basis
|
|
18
18
|
|
|
19
19
|
LinkedIn does not publish exact feed truncation rules, and rendering can vary by
|
|
20
20
|
surface, app version, device, media attachment, and account state. Use this
|
|
21
|
-
deterministic
|
|
21
|
+
deterministic function as the MCP review gate for unpublished drafts.
|
|
22
|
+
|
|
23
|
+
Simple operating policy: judge the hook by the first 3 rendered lines. Use ~140
|
|
24
|
+
characters on mobile and ~210 characters on desktop as guardrails only. If the
|
|
25
|
+
3-line preview and the character guardrail disagree, trust the rendered lines.
|
|
22
26
|
|
|
23
27
|
Call:
|
|
24
28
|
|
|
25
29
|
```json
|
|
26
|
-
|
|
30
|
+
mcp__sellable__calculate_linkedin_hook_preview({
|
|
27
31
|
"postText": "<full post text or candidate hook block>",
|
|
28
32
|
"sourceLabel": "<draft id, hook id, or source label>",
|
|
29
|
-
"
|
|
33
|
+
"measurementMode": "browser"
|
|
30
34
|
})
|
|
31
35
|
```
|
|
32
36
|
|
|
33
|
-
When
|
|
34
|
-
|
|
35
|
-
|
|
37
|
+
When local Chrome is available, browser measurement is the preferred functional
|
|
38
|
+
basis because it asks the browser layout engine to wrap the text under the
|
|
39
|
+
contract. When Chrome is unavailable, the tool falls back to the deterministic
|
|
40
|
+
width model and must mark that basis.
|
|
36
41
|
|
|
37
42
|
Treat "see more" as a rendered-line problem first and a character-count problem
|
|
38
|
-
second.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
-
|
|
44
|
-
|
|
45
|
-
- posts with media may show fewer text lines before truncation
|
|
46
|
-
1-2 lines
|
|
43
|
+
second. Observed LinkedIn screenshots show the current desktop feed behaving
|
|
44
|
+
like a first-3-rendered-lines clamp: a first line, a blank line, and a third
|
|
45
|
+
line can be all that appears before `... more`.
|
|
46
|
+
|
|
47
|
+
- review gate: first 3 rendered visual lines
|
|
48
|
+
- mobile feed: tighter because the text column is narrower
|
|
49
|
+
- desktop feed: more characters can fit per line, but still review line count
|
|
50
|
+
- posts with media may show fewer text lines before truncation
|
|
47
51
|
- blank lines and intentional separators consume visible preview lines
|
|
48
52
|
- device width, font scaling, app version, browser, and profile/page context can
|
|
49
53
|
move the cutoff
|
|
@@ -86,13 +90,16 @@ selected hook must include a `renderedPreview` record:
|
|
|
86
90
|
|
|
87
91
|
```text
|
|
88
92
|
renderedPreview:
|
|
89
|
-
basis:
|
|
90
|
-
cssContractVersion: linkedin-preview-rendering/
|
|
93
|
+
basis: linkedin_rendering_rule_function | authenticated_linkedin_screenshot | manual_user_source
|
|
94
|
+
cssContractVersion: linkedin-preview-rendering/v2
|
|
91
95
|
mobile:
|
|
92
96
|
textWidthPx: 308
|
|
93
97
|
fontSizePx: 14
|
|
94
98
|
lineHeightPx: 21
|
|
95
|
-
|
|
99
|
+
clampLines: 3
|
|
100
|
+
charGuardrail: 140
|
|
101
|
+
measurementBasis: local_chrome_browser_layout | estimated_width_model
|
|
102
|
+
visibleTextBlock: <literal first 3 rendered lines>
|
|
96
103
|
renderedLines:
|
|
97
104
|
- <line 1 exactly as wrapped>
|
|
98
105
|
- <line 2 exactly as wrapped>
|
|
@@ -106,13 +113,14 @@ renderedPreview:
|
|
|
106
113
|
payoffPlannedImmediatelyAfterClamp: true | false
|
|
107
114
|
seeMoreClickReason: <why a reader would click see more>
|
|
108
115
|
seeMoreRisk: pass | warn | fail
|
|
109
|
-
htmlPath: <local path>
|
|
110
|
-
screenshotPath: <local path when Chrome rendered PNG>
|
|
111
116
|
desktop:
|
|
112
117
|
textWidthPx: 582
|
|
113
118
|
fontSizePx: 14
|
|
114
119
|
lineHeightPx: 21
|
|
115
|
-
|
|
120
|
+
clampLines: 3
|
|
121
|
+
charGuardrail: 210
|
|
122
|
+
measurementBasis: local_chrome_browser_layout | estimated_width_model
|
|
123
|
+
visibleTextBlock: <literal first 3 rendered lines>
|
|
116
124
|
renderedLines:
|
|
117
125
|
- <line 1 exactly as wrapped>
|
|
118
126
|
- <line 2 exactly as wrapped>
|
|
@@ -122,8 +130,6 @@ renderedPreview:
|
|
|
122
130
|
corePainProofOrCuriosityVisible: true | false
|
|
123
131
|
corePointVisible: true | false
|
|
124
132
|
seeMoreRisk: pass | warn | fail
|
|
125
|
-
htmlPath: <local path>
|
|
126
|
-
screenshotPath: <local path when Chrome rendered PNG>
|
|
127
133
|
diagnostics:
|
|
128
134
|
charCount: <number>
|
|
129
135
|
charCountIncludingNewlines: <number>
|
|
@@ -136,11 +142,9 @@ renderedPreview:
|
|
|
136
142
|
rewriteIfTruncated: <short fallback>
|
|
137
143
|
```
|
|
138
144
|
|
|
139
|
-
If a host cannot produce
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
`needs_revision`; do not claim the hook passed preview validation from character
|
|
143
|
-
counts alone.
|
|
145
|
+
If a host cannot produce literal wrapped line blocks from
|
|
146
|
+
`calculate_linkedin_hook_preview`, return `blocked` or `needs_revision`; do not
|
|
147
|
+
claim the hook passed preview validation from character counts alone.
|
|
144
148
|
|
|
145
149
|
## Study Rules
|
|
146
150
|
|