@sellable/mcp 0.1.263 → 0.1.265
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 +155 -25
- package/skills/create-post/references/hook-research-playbook.md +90 -14
- package/skills/create-post/references/linkedin-preview-rendering.md +38 -34
- package/skills/create-post/references/post-file-contract.md +8 -0
- package/skills/create-post/references/post-validation.md +38 -11
- package/skills/create-post/references/premise-development.md +3 -3
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
|