@sellable/mcp 0.1.255 → 0.1.257

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.
@@ -14,7 +14,13 @@ export interface ListTablesInput {
14
14
  limit?: number;
15
15
  hasSequence?: boolean;
16
16
  }
17
- export declare const tableToolDefinitions: {
17
+ export interface ExportTableCsvInput {
18
+ tableId: string;
19
+ filters?: unknown[];
20
+ sort?: Record<string, unknown>;
21
+ splitRows?: number;
22
+ }
23
+ export declare const tableToolDefinitions: ({
18
24
  name: string;
19
25
  description: string;
20
26
  inputSchema: {
@@ -28,8 +34,53 @@ export declare const tableToolDefinitions: {
28
34
  type: string;
29
35
  description: string;
30
36
  };
37
+ tableId?: undefined;
38
+ filters?: undefined;
39
+ sort?: undefined;
40
+ splitRows?: undefined;
31
41
  };
32
42
  required: never[];
43
+ additionalProperties?: undefined;
44
+ };
45
+ } | {
46
+ name: string;
47
+ description: string;
48
+ inputSchema: {
49
+ type: string;
50
+ properties: {
51
+ tableId: {
52
+ type: string;
53
+ };
54
+ filters: {
55
+ type: string;
56
+ description: string;
57
+ items: {
58
+ type: string;
59
+ };
60
+ };
61
+ sort: {
62
+ type: string;
63
+ description: string;
64
+ };
65
+ splitRows: {
66
+ type: string;
67
+ description: string;
68
+ };
69
+ limit?: undefined;
70
+ hasSequence?: undefined;
71
+ };
72
+ required: string[];
73
+ additionalProperties: boolean;
33
74
  };
34
- }[];
75
+ })[];
35
76
  export declare function listTables(input: ListTablesInput): Promise<WorkflowTableListItem[]>;
77
+ export declare function exportTableCsv(input: ExportTableCsvInput): Promise<{
78
+ tableId: string;
79
+ endpoint: string;
80
+ totalRows: number;
81
+ files: Array<{
82
+ path: string;
83
+ rows: number;
84
+ }>;
85
+ splitRows: number | null;
86
+ }>;
@@ -1,3 +1,7 @@
1
+ import { parse } from "csv-parse/sync";
2
+ import { mkdir, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import path from "node:path";
1
5
  import { getApi } from "../api.js";
2
6
  export const tableToolDefinitions = [
3
7
  {
@@ -19,6 +23,31 @@ export const tableToolDefinitions = [
19
23
  required: [],
20
24
  },
21
25
  },
26
+ {
27
+ name: "export_table_csv",
28
+ description: "Export a workflow table using the existing Sellable Export CSV endpoint. Optional filters/sort are passed through and splitRows writes deterministic CSV batches.",
29
+ inputSchema: {
30
+ type: "object",
31
+ properties: {
32
+ tableId: { type: "string" },
33
+ filters: {
34
+ type: "array",
35
+ description: "Optional workflow-table filters to pass through.",
36
+ items: { type: "object" },
37
+ },
38
+ sort: {
39
+ type: "object",
40
+ description: "Optional workflow-table sort object to pass through.",
41
+ },
42
+ splitRows: {
43
+ type: "number",
44
+ description: "Optional data rows per split file.",
45
+ },
46
+ },
47
+ required: ["tableId"],
48
+ additionalProperties: false,
49
+ },
50
+ },
22
51
  ];
23
52
  export async function listTables(input) {
24
53
  const api = getApi();
@@ -34,3 +63,52 @@ export async function listTables(input) {
34
63
  const result = await api.get(path);
35
64
  return result.tables;
36
65
  }
66
+ const csvEscape = (value) => {
67
+ const text = value == null ? "" : String(value);
68
+ if (/[",\n\r]/.test(text)) {
69
+ return `"${text.replace(/"/g, '""')}"`;
70
+ }
71
+ return text;
72
+ };
73
+ const toCsv = (records) => records.map((row) => row.map(csvEscape).join(",")).join("\n") + "\n";
74
+ export async function exportTableCsv(input) {
75
+ if (!input.tableId) {
76
+ throw new Error("tableId is required");
77
+ }
78
+ const params = new URLSearchParams();
79
+ if (input.filters)
80
+ params.set("filters", JSON.stringify(input.filters));
81
+ if (input.sort)
82
+ params.set("sort", JSON.stringify(input.sort));
83
+ const endpoint = `/api/v3/workflow-tables/${input.tableId}/export-csv${params.toString() ? `?${params.toString()}` : ""}`;
84
+ const api = getApi();
85
+ const csv = await api.getText(endpoint);
86
+ const records = parse(csv, {
87
+ bom: true,
88
+ relax_column_count: true,
89
+ });
90
+ const header = records[0] ?? [];
91
+ const dataRows = records.slice(1);
92
+ const splitRows = Number.isInteger(input.splitRows) && input.splitRows > 0
93
+ ? input.splitRows
94
+ : null;
95
+ const outputDir = path.join(tmpdir(), "sellable-mcp-exports", `${input.tableId}-${Date.now()}`);
96
+ await mkdir(outputDir, { recursive: true });
97
+ const chunks = splitRows && dataRows.length > 0
98
+ ? Array.from({ length: Math.ceil(dataRows.length / splitRows) }, (_, index) => dataRows.slice(index * splitRows, index * splitRows + splitRows))
99
+ : [dataRows];
100
+ const files = [];
101
+ for (let index = 0; index < chunks.length; index++) {
102
+ const chunk = chunks[index];
103
+ const filePath = path.join(outputDir, chunks.length === 1 ? "export.csv" : `export-${index + 1}.csv`);
104
+ await writeFile(filePath, toCsv([header, ...chunk]), "utf8");
105
+ files.push({ path: filePath, rows: chunk.length });
106
+ }
107
+ return {
108
+ tableId: input.tableId,
109
+ endpoint,
110
+ totalRows: dataRows.length,
111
+ files,
112
+ splitRows,
113
+ };
114
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/mcp",
3
- "version": "0.1.255",
3
+ "version": "0.1.257",
4
4
  "type": "module",
5
5
  "description": "Sellable MCP server for Claude Code and Codex campaign workflows",
6
6
  "main": "dist/index.js",
@@ -58,7 +58,6 @@ allowed-tools:
58
58
  - mcp__sellable__get_rows
59
59
  - mcp__sellable__get_rows_minimal
60
60
  - mcp__sellable__get_table_rows
61
- - mcp__sellable__list_dnc_entries
62
61
  - mcp__sellable__load_csv_dnc_entries
63
62
  - mcp__sellable__load_csv_linkedin_leads
64
63
  - mcp__sellable__load_csv_domains
@@ -102,11 +101,6 @@ are for known-account targeting only, not DNC. Campaign creation already
102
101
  includes a `DNC Check` column that checks domain and LinkedIn profile before
103
102
  message generation.
104
103
 
105
- If the user asks to show/check the current DNC list, count, list names, or first
106
- page before importing, call `list_dnc_entries`. Confirm the active workspace
107
- name and ID in the response before any write. This is Sellable's workspace DNC
108
- list used by DNC Check.
109
-
110
104
  ## Opening Turn Contract
111
105
 
112
106
  On the first visible response after this skill is invoked, do not narrate
@@ -219,8 +219,6 @@ belong in the recommendation as DNC/suppression instructions outside the rubric
219
219
  DNC imports/suppression lists are handled through `load_csv_dnc_entries` outside
220
220
  `leadScoringRubrics`. The import previews the exact Sellable workspace name and
221
221
  ID, then writes to Sellable's workspace-level DNC list only after confirmation.
222
- If the user asks to show the current DNC count, list names, or first page before
223
- import, call `list_dnc_entries` and report the active workspace name and ID.
224
222
  Campaign creation already includes `DNC Check`, which checks domain/profile
225
223
  before message generation. Do not describe provider workarounds, provider
226
224
  search-time limitations, or Prospeo domain filters as the DNC mechanism.
@@ -50,8 +50,6 @@ Supplied-source preview:
50
50
  preview. Preview the exact Sellable workspace name and ID and confirm before
51
51
  writing. This updates Sellable's workspace-level DNC list; it is not a lead
52
52
  source, Prospeo domain filter, or provider workaround.
53
- If the user asks for the existing DNC count, list names, or first page first,
54
- call `list_dnc_entries` and report the active workspace name and ID.
55
53
  - `supplied-linkedin-profiles` uses `load_csv_linkedin_leads` before the
56
54
  15-lead import batch only as preview/source attachment. Pre-import calls
57
55
  must omit provider-import parameters. Use `campaignOfferId` only to attach the
@@ -61,9 +61,7 @@ Supported branches:
61
61
  Sellable workspace name and ID, and confirmation writes only to that
62
62
  workspace's Sellable DNC list. This is not a lead source and does not create a
63
63
  `domainFilterId`; campaign rows are protected later by the existing
64
- `DNC Check` column before message generation. If the user asks for the
65
- current DNC count, list names, or first page before import, call
66
- `list_dnc_entries` first.
64
+ `DNC Check` column before message generation.
67
65
  - **Supplied LinkedIn profile CSV** — confirm `load_csv_linkedin_leads` only
68
66
  after the user approves that supplied-list source. Batch/materialize the
69
67
  uploaded CSV into a Sellable lead-list table. Persist the returned
@@ -84,6 +84,7 @@ Before drafting, load all required assets with `mcp__sellable__get_subskill_asse
84
84
  3. `subskillName: "create-post", assetPath: "references/premise-development.md"`
85
85
  4. `subskillName: "create-post", assetPath: "references/post-validation.md"`
86
86
  5. `subskillName: "create-post", assetPath: "references/gold-standard-post-pack.md"`
87
+ 6. `subskillName: "create-post", assetPath: "references/linkedin-preview-rendering.md"`
87
88
 
88
89
  If any required asset is missing, unreadable, truncated without continuation, or internally inconsistent, return:
89
90
 
@@ -304,7 +305,8 @@ If local idea capture succeeds but auth/workspace is missing, keep the idea and
304
305
 
305
306
  ## Step 1: Hook Research
306
307
 
307
- Use `references/hook-research-playbook.md`.
308
+ Use `references/hook-research-playbook.md` and
309
+ `references/linkedin-preview-rendering.md`.
308
310
 
309
311
  If the host supports background agents, delegate the search/fetch/autopsy work
310
312
  to one bounded `research-worker` before hook candidate generation. The worker
@@ -319,9 +321,10 @@ The research worker must return a compact packet only:
319
321
  - full adapted hook blocks
320
322
  - market belief map: resonating ideas, implicit beliefs, audience wants, resentments, fears, and credible controversy angles
321
323
  - premise inputs: real scenes, observed tensions, reader value openings, and proof gaps
324
+ - reach-normalized signal notes, including follower-band fit when available
322
325
  - exact phrase patterns and sentence shapes
323
326
  - body structures and exact body language moves
324
- - preview measurements
327
+ - rendered mobile and desktop preview records
325
328
  - track-person and gold-standard recommendations
326
329
  - blocked states or confidence gaps
327
330
 
@@ -332,18 +335,20 @@ the exact extracted phrase shapes.
332
335
  Default flow:
333
336
 
334
337
  1. Convert the idea into 3-8 search keywords.
335
- 2. Call `mcp__sellable__search_engagement_posts` with an explicit multi-month window. Default to `maxAgeDays: 120`, tightening to 30-60 days only when the topic is trend-sensitive.
336
- 3. Shortlist high-engagement posts by topic fit, hook strength, creator repeat evidence, and weighted engagement quality.
337
- 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.
338
+ 2. Call `mcp__sellable__search_engagement_posts` with an explicit multi-month window. Default to `maxAgeDays: 120`, tightening to 30-60 days only when the topic is trend-sensitive. When the user gives a follower range, pass it as `targetFollowerMin` and `targetFollowerMax`; for example, "8k-20k followers" means `targetFollowerMin: 8000` and `targetFollowerMax: 20000`.
339
+ 3. Shortlist posts by topic fit, rendered hook strength, content pattern replicability, creator repeat evidence, weighted engagement quality, and reach-normalized signal quality. Do not sort by raw engagement alone, and do not let the numeric reach score choose the final hook by itself.
340
+ 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. If a promising source is missing follower count and the user requested reach normalization, call `mcp__sellable__fetch_linkedin_profile` on a bounded shortlist before treating the source as a keeper.
338
341
  5. If full text cannot be matched, record `full_text_unavailable` and use only the preview. Do not invent missing body details.
339
- 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`.
342
+ 6. Weigh shares/reposts above comments, comments above reactions, and reactions as weak reach unless paired with stronger signals. Normalize engagement against follower count when available: record `authorFollowerCount`, `targetFollowerBand`, `followerBandFit`, `engagementPer1kFollowers`, `weightedEngagementPer1kFollowers`, `reachPenaltyMultiplier`, `reachAdjustedScore`, and `baselineLift` when repeated creator posts make it possible. If follower count is unavailable, record `follower_count_unavailable`; if shares/reposts are unavailable, record `repost_data_unavailable`.
340
343
  7. Penalize lead-magnet or giveaway mechanics unless the user explicitly asks for a lead magnet post.
341
344
  8. Build a market belief map before hook generation: what the space is rewarding, what the audience implicitly believes, what they want permission to say, what they resent or fear, what they will argue with, and which controversial angle the user can credibly own. If the user's raw idea is internally coherent but not attached to a live market tension, do not draft from the internal idea alone; present stronger directions or rewrite the draft angle around the external tension.
342
345
  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.
343
346
  10. For story posts, extract the story mechanism that made the post work, not just the first line.
344
347
  11. Extract hook structures plus specific reusable words, phrases, sentence
345
348
  shapes, transitions, and body language patterns.
346
- 12. Save the research with `mcp__sellable__save_hook_research`.
349
+ 12. Render each kept source hook and adapted hook block through the LinkedIn
350
+ preview rendering contract. Character counts alone are not enough.
351
+ 13. Save the research with `mcp__sellable__save_hook_research`.
347
352
 
348
353
  Record provenance:
349
354
 
@@ -352,18 +357,26 @@ Record provenance:
352
357
  - search window
353
358
  - source post URLs
354
359
  - authors/profile URLs
360
+ - author follower counts and target follower-band fit when available
355
361
  - engagement totals and available likes/comments/shares breakdown
362
+ - reach-normalized scoring fields and confidence notes
356
363
  - creator repeat evidence
357
364
  - lead-magnet or engagement-bait penalties
358
365
  - story mechanism when relevant
359
366
  - full-text match status
360
- - source hook preview measurements and whether they came from full text or a search preview
367
+ - source hook rendered preview records and whether they came from full text,
368
+ authenticated LinkedIn screenshots, or a search preview
361
369
  - selected hook patterns
362
370
  - market belief map and selected controversy
363
371
  - premise cards and selected premise
364
372
  - exact phrase patterns and sentence shapes
365
373
  - body structures and body language patterns
366
374
  - why each pattern fits the user's idea and voice
375
+ - `whyTheHookCarries`: why the selected hook works from the words, tension, and
376
+ content pattern independent of the source creator's reach
377
+ - `whyTheReachEvidenceIsTrustworthy`: follower-band fit, reach-adjusted score,
378
+ baseline lift, share/comment quality, or why a large-account example is only
379
+ secondary pattern evidence
367
380
 
368
381
  ## Step 1.5: Research Learning Report
369
382
 
@@ -389,6 +402,9 @@ Research status:
389
402
  - keywords:
390
403
  - full-text coverage:
391
404
  - repost/share data:
405
+ - target follower band:
406
+ - reach-normalized winners:
407
+ - big-account examples used only as secondary pattern evidence:
392
408
 
393
409
  Best source examples:
394
410
  1. author, URL, engagement, why kept, why not copied
@@ -409,7 +425,10 @@ Premise cards:
409
425
  Hook patterns learned:
410
426
  1. full adapted hook block
411
427
  - source mechanism:
412
- - preview budget:
428
+ - rendered preview:
429
+ - mobile visible block:
430
+ - desktop visible block:
431
+ - verdict:
413
432
  - internal question:
414
433
  - why it fits / why it does not:
415
434
 
@@ -484,6 +503,15 @@ Each hook must include:
484
503
  - reader value implied
485
504
  - source hook pattern
486
505
  - why it fits this idea
506
+ - `renderedPreview` using `references/linkedin-preview-rendering.md`
507
+ - `mobileRenderedPreviewBlock`
508
+ - `desktopRenderedPreviewBlock`
509
+ - `mobileRenderedLines`
510
+ - `desktopRenderedLines`
511
+ - `firstScreenPromise`
512
+ - `corePainProofOrCuriosityVisibleMobile`
513
+ - `corePointVisibleMobile`
514
+ - `pointAfterMobileClamp`
487
515
  - `charCount`
488
516
  - `charCountIncludingNewlines`
489
517
  - `firstLineChars`
@@ -504,22 +532,39 @@ Each hook must include:
504
532
 
505
533
  Do not copy source wording. Copy only the structure.
506
534
 
507
- Use this conservative mobile-first LinkedIn preview gate. LinkedIn does not
508
- publish exact "see more" cutoff rules, and rendering varies by device, app
509
- version, font, media, and line break. These are v1 safety budgets, not claims
510
- about an official LinkedIn limit:
511
-
512
- - `pass`: hook is <= 110 chars including newlines, every nonblank line is <= 45 chars, and the hook's core point lands before likely truncation.
513
- - `warn`: hook is 111-140 chars including newlines, any nonblank line is 46-55 chars, or blank lines create visual-line risk. Blank lines are allowed, but they count as physical lines.
514
- - `fail`: hook is > 140 chars including newlines, any nonblank line is > 55 chars, or the hook's point depends on text after likely truncation.
535
+ Render every candidate before scoring it. The LinkedIn preview rendering
536
+ contract is the gate; character counts are secondary diagnostics only. Use the
537
+ deterministic CSS contract when no authenticated LinkedIn screenshot is
538
+ available:
539
+
540
+ - mobile text width: `308px`
541
+ - desktop text width: `582px`
542
+ - font size: `14px`
543
+ - line height: `21px`
544
+ - white space: `pre-wrap`
545
+ - overflow wrap: `break-word`
546
+ - review clamp: first 3 rendered text lines
547
+
548
+ Use the rendered gates from `references/linkedin-preview-rendering.md`:
549
+
550
+ - `pass`: the mobile rendered preview shows the pain, proof, or curiosity by
551
+ the end of the first 3 rendered lines, and the core point is understandable
552
+ without opening "see more".
553
+ - `warn`: the mobile rendered preview creates useful curiosity but the core
554
+ point is slightly softened by wrapping, blank-line rhythm, or one missing
555
+ context word. A compact fallback is required.
556
+ - `fail`: the hook's real point appears after the first 3 mobile rendered
557
+ lines, the first rendered line is generic setup, blank lines consume the
558
+ preview before the reader sees the point, or desktop fit is the only reason it
559
+ looks good.
515
560
 
516
561
  Desktop preview usually has more room. Still record `desktopPreviewFit`, but
517
562
  never let desktop fit compensate for a mobile `fail`.
518
563
 
519
- If a hook's point depends on text after the likely preview, rewrite it before
520
- selecting it. A selected hook may carry a `warn` only when the warning is about
521
- intentional blank-line rhythm or a slight line-length overage; include a compact
522
- fallback in the validation receipt.
564
+ If a hook's point depends on text after the rendered mobile preview clamp,
565
+ rewrite it before selecting it. A selected hook may carry a `warn` only when
566
+ the warning is explicit and the validation receipt includes a compact fallback.
567
+ A hook with no rendered mobile and desktop preview record cannot be selected.
523
568
 
524
569
  ## Step 3: Draft
525
570
 
@@ -553,6 +598,7 @@ Every saved draft needs a validation receipt with:
553
598
  - story/proof files consulted
554
599
  - gold standards consulted
555
600
  - LinkedIn preview audit
601
+ - rendered mobile and desktop hook preview blocks
556
602
  - premise/value audit findings
557
603
  - simplifier/concrete-language audit findings
558
604
  - voice audit findings
@@ -119,6 +119,8 @@ 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
+ - rendered mobile preview block and verdict
123
+ - rendered desktop preview block and verdict
122
124
  - content/body mechanism
123
125
  - rhythm notes
124
126
  - sentence-structure notes
@@ -194,6 +196,15 @@ Tags:
194
196
  - longest nonblank line chars:
195
197
  - blank-line visual risk:
196
198
  - source text basis:
199
+ - render basis:
200
+ - css contract version:
201
+ - mobile rendered preview block:
202
+ - desktop rendered preview block:
203
+ - mobile rendered lines:
204
+ - desktop rendered lines:
205
+ - core pain/proof/curiosity visible on mobile:
206
+ - core point visible on mobile:
207
+ - point after mobile clamp:
197
208
  - preview budget status:
198
209
  - mobile preview fit:
199
210
  - desktop preview fit: