@sellable/mcp 0.1.239 → 0.1.242

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.
@@ -80,13 +80,18 @@ export const contentPostToolDefinitions = [
80
80
  },
81
81
  {
82
82
  name: "save_post_draft",
83
- description: "Save a validated LinkedIn post draft under ~/.sellable/content/linkedin/drafts.",
83
+ description: "Save a validated LinkedIn post draft under ~/.sellable/content/linkedin/drafts. Use versioned draft IDs for multiple drafts of the same idea.",
84
84
  inputSchema: {
85
85
  type: "object",
86
86
  properties: {
87
87
  draftId: { type: "string" },
88
88
  ideaId: { type: "string" },
89
89
  hookResearchId: { type: "string" },
90
+ priorDraftId: {
91
+ type: "string",
92
+ description: "Optional previous draft ID this draft is trying to improve on.",
93
+ },
94
+ iteration: draftIterationSchema(),
90
95
  title: { type: "string" },
91
96
  body: { type: "string" },
92
97
  validationReceipt: {
@@ -99,11 +104,38 @@ export const contentPostToolDefinitions = [
99
104
  additionalProperties: false,
100
105
  },
101
106
  },
107
+ {
108
+ name: "update_post_draft",
109
+ description: "Update an existing LinkedIn post draft in place while preserving omitted fields. Useful for adding iteration receipts, status changes, or final copy edits without rewriting the whole artifact.",
110
+ inputSchema: {
111
+ type: "object",
112
+ properties: {
113
+ draftId: { type: "string" },
114
+ hookResearchId: { type: "string" },
115
+ priorDraftId: { type: "string" },
116
+ iteration: draftIterationSchema(),
117
+ title: { type: "string" },
118
+ body: { type: "string" },
119
+ validationReceipt: {
120
+ description: "Markdown string or structured object to replace the saved validation receipt.",
121
+ },
122
+ status: { type: "string" },
123
+ updatedAt: { type: "string" },
124
+ },
125
+ required: ["draftId"],
126
+ additionalProperties: false,
127
+ },
128
+ },
102
129
  {
103
130
  name: "list_post_drafts",
104
131
  description: "List saved LinkedIn post drafts with compact sanitized previews. Use get_post_draft for full Markdown.",
105
132
  inputSchema: listInputSchema(),
106
133
  },
134
+ {
135
+ name: "list_post_draft_iterations",
136
+ description: "List all draft versions for one idea, including iteration scores, verdicts, prior draft links, status, and sanitized previews.",
137
+ inputSchema: idInputSchema("ideaId"),
138
+ },
107
139
  {
108
140
  name: "get_post_draft",
109
141
  description: "Read one saved LinkedIn post draft by ID.",
@@ -121,11 +153,47 @@ export const contentPostToolDefinitions = [
121
153
  publishedAt: { type: "string" },
122
154
  finalText: { type: "string" },
123
155
  title: { type: "string" },
156
+ updateDraftStatus: {
157
+ type: "boolean",
158
+ description: "Defaults to true when draftId is supplied. Marks the source draft as published and links it to the published record.",
159
+ },
124
160
  },
125
161
  required: ["publishUrl"],
126
162
  additionalProperties: false,
127
163
  },
128
164
  },
165
+ {
166
+ name: "get_published_post",
167
+ description: "Read one published LinkedIn post record by ID. If year is omitted, searches all published year folders.",
168
+ inputSchema: {
169
+ type: "object",
170
+ properties: {
171
+ publishedPostId: { type: "string" },
172
+ year: { type: "string" },
173
+ },
174
+ required: ["publishedPostId"],
175
+ additionalProperties: false,
176
+ },
177
+ },
178
+ {
179
+ name: "update_published_post_metrics",
180
+ description: "Append a metrics snapshot to a published LinkedIn post record and update latest metric fields for post-performance learning.",
181
+ inputSchema: {
182
+ type: "object",
183
+ properties: {
184
+ publishedPostId: { type: "string" },
185
+ year: { type: "string" },
186
+ capturedAt: { type: "string" },
187
+ metrics: {
188
+ type: "object",
189
+ additionalProperties: true,
190
+ },
191
+ note: { type: "string" },
192
+ },
193
+ required: ["publishedPostId", "metrics"],
194
+ additionalProperties: false,
195
+ },
196
+ },
129
197
  {
130
198
  name: "list_published_posts",
131
199
  description: "List published LinkedIn post records with compact sanitized previews.",
@@ -152,6 +220,13 @@ function idInputSchema(propertyName) {
152
220
  additionalProperties: false,
153
221
  };
154
222
  }
223
+ function draftIterationSchema() {
224
+ return {
225
+ type: "object",
226
+ description: "Optional draft iteration metadata: version, priorDraftId, changeIntent, whatChanged, whatImproved, whatGotWorse, score, and verdict.",
227
+ additionalProperties: true,
228
+ };
229
+ }
155
230
  export function resolveContentRoot() {
156
231
  const envValue = process.env[CONTENT_ROOT_ENV]?.trim();
157
232
  if (envValue) {
@@ -274,14 +349,13 @@ export function savePostDraftTool(input) {
274
349
  const safeResearchId = input.hookResearchId
275
350
  ? normalizeArtifactId(input.hookResearchId, "hookResearchId")
276
351
  : undefined;
352
+ const iteration = normalizeDraftIteration(input.iteration, input.priorDraftId);
277
353
  requireString(input.body, "body");
278
354
  const id = input.draftId ??
279
355
  `draft_${dateStamp(now)}_${slugify(input.title || safeIdeaId)}_v1`;
280
356
  const safeId = normalizeArtifactId(id, "draftId");
281
357
  const status = input.status || "draft";
282
- if (!["draft", "ready", "needs_revision"].includes(status)) {
283
- throw new Error(`Unsupported draft status: ${status}`);
284
- }
358
+ assertDraftStatus(status);
285
359
  const metadata = {
286
360
  id: safeId,
287
361
  type: "draft",
@@ -289,6 +363,9 @@ export function savePostDraftTool(input) {
289
363
  title: input.title,
290
364
  ideaId: safeIdeaId,
291
365
  hookResearchId: safeResearchId,
366
+ draftVersion: iteration?.version || inferDraftVersion(safeId),
367
+ priorDraftId: iteration?.priorDraftId,
368
+ iteration,
292
369
  createdAt: now,
293
370
  updatedAt: now,
294
371
  };
@@ -304,13 +381,98 @@ export function savePostDraftTool(input) {
304
381
  status,
305
382
  ideaId: safeIdeaId,
306
383
  hookResearchId: safeResearchId,
384
+ draftVersion: metadata.draftVersion,
385
+ priorDraftId: metadata.priorDraftId,
386
+ iterationScore: iteration?.score,
387
+ iterationVerdict: iteration?.verdict,
307
388
  updatedAt: now,
308
389
  preview: sanitizedPreview(input.body),
309
390
  };
310
391
  }
392
+ export function updatePostDraftTool(input) {
393
+ const safeDraftId = normalizeArtifactId(input.draftId, "draftId");
394
+ const relativePath = `${RELATIVE_DIRS.drafts}/${safeDraftId}.md`;
395
+ const existing = readArtifactIfExists(relativePath);
396
+ if (!existing) {
397
+ throw new Error(`No draftId found for ID: ${safeDraftId}`);
398
+ }
399
+ const now = normalizeDate(input.updatedAt);
400
+ const existingBody = extractMarkdownSection(existing.markdown, "Draft Body");
401
+ const existingReceipt = extractMarkdownSection(existing.markdown, "Validation Receipt");
402
+ const body = input.body ?? existingBody;
403
+ requireString(body, "body");
404
+ const validationReceipt = input.validationReceipt === undefined
405
+ ? existingReceipt
406
+ : formatReceipt(input.validationReceipt);
407
+ requireString(validationReceipt, "validationReceipt");
408
+ const safeResearchId = input.hookResearchId
409
+ ? normalizeArtifactId(input.hookResearchId, "hookResearchId")
410
+ : existing.metadata.hookResearchId;
411
+ const iteration = normalizeDraftIteration(input.iteration ?? existing.metadata.iteration, input.priorDraftId ?? existing.metadata.priorDraftId);
412
+ const status = input.status || existing.metadata.status || "draft";
413
+ assertDraftStatus(status);
414
+ const metadata = {
415
+ ...existing.metadata,
416
+ status,
417
+ title: input.title ?? existing.metadata.title,
418
+ hookResearchId: safeResearchId,
419
+ draftVersion: iteration?.version || existing.metadata.draftVersion || inferDraftVersion(safeDraftId),
420
+ priorDraftId: iteration?.priorDraftId,
421
+ iteration,
422
+ updatedAt: now,
423
+ };
424
+ const markdown = buildMarkdown(metadata, [
425
+ ["Draft Body", body],
426
+ ["Validation Receipt", validationReceipt],
427
+ ]);
428
+ writeArtifact(relativePath, markdown);
429
+ return {
430
+ id: safeDraftId,
431
+ path: relativePath,
432
+ status,
433
+ ideaId: metadata.ideaId,
434
+ hookResearchId: safeResearchId,
435
+ draftVersion: metadata.draftVersion,
436
+ priorDraftId: metadata.priorDraftId,
437
+ iterationScore: iteration?.score,
438
+ iterationVerdict: iteration?.verdict,
439
+ updatedAt: now,
440
+ preview: sanitizedPreview(body),
441
+ };
442
+ }
311
443
  export function listPostDraftsTool(input) {
312
444
  return listArtifacts(RELATIVE_DIRS.drafts, "draft", input?.limit);
313
445
  }
446
+ export function listPostDraftIterationsTool(input) {
447
+ const safeIdeaId = normalizeArtifactId(input.ideaId, "ideaId");
448
+ const drafts = readArtifactsFromDir(RELATIVE_DIRS.drafts)
449
+ .filter((artifact) => artifact.metadata.type === "draft" &&
450
+ artifact.metadata.ideaId === safeIdeaId)
451
+ .map((artifact) => {
452
+ const iteration = artifact.metadata.iteration ||
453
+ extractIterationFromReceipt(extractMarkdownSection(artifact.markdown, "Validation Receipt"));
454
+ return {
455
+ id: artifact.metadata.id,
456
+ type: artifact.metadata.type,
457
+ status: artifact.metadata.status,
458
+ title: artifact.metadata.title,
459
+ path: artifact.relativePath,
460
+ ideaId: artifact.metadata.ideaId,
461
+ hookResearchId: artifact.metadata.hookResearchId,
462
+ draftVersion: artifact.metadata.draftVersion ||
463
+ iteration?.version ||
464
+ inferDraftVersion(artifact.metadata.id),
465
+ priorDraftId: artifact.metadata.priorDraftId || iteration?.priorDraftId,
466
+ iterationScore: iteration?.score,
467
+ iterationVerdict: iteration?.verdict,
468
+ publishedPostId: artifact.metadata.publishedPostId,
469
+ publishedAt: artifact.metadata.publishedAt,
470
+ updatedAt: artifact.metadata.updatedAt,
471
+ preview: sanitizedPreview(previewBasis(artifact.markdown)),
472
+ };
473
+ });
474
+ return drafts.sort(compareDraftIterations);
475
+ }
314
476
  export function getPostDraftTool(input) {
315
477
  return getArtifact(RELATIVE_DIRS.drafts, input.draftId, "draftId");
316
478
  }
@@ -327,6 +489,14 @@ export function markPostPublishedTool(input) {
327
489
  const relativePath = `${RELATIVE_DIRS.published}/${year}/${safeId}.md`;
328
490
  const existing = readArtifactIfExists(relativePath);
329
491
  const createdAt = existing?.metadata.createdAt || publishedAt;
492
+ const finalText = input.finalText ?? extractMarkdownSection(existing?.markdown || "", "Final Text");
493
+ const publishMetadata = readJsonSection(existing?.markdown || "", "Publish Metadata", {});
494
+ const futureMetrics = readJsonSection(existing?.markdown || "", "Future Metrics", {
495
+ impressions: null,
496
+ reactions: null,
497
+ comments: null,
498
+ snapshots: [],
499
+ });
330
500
  const metadata = {
331
501
  id: safeId,
332
502
  type: "published_post",
@@ -340,27 +510,23 @@ export function markPostPublishedTool(input) {
340
510
  updatedAt: publishedAt,
341
511
  };
342
512
  const markdown = buildMarkdown(metadata, [
343
- ["Final Text", input.finalText || ""],
513
+ ["Final Text", finalText],
344
514
  [
345
515
  "Publish Metadata",
346
516
  jsonBlock(stripUndefined({
517
+ ...publishMetadata,
347
518
  draftId: safeDraftId,
348
519
  publishUrl: input.publishUrl,
349
520
  activityId: activityId || undefined,
350
521
  publishedAt,
351
522
  })),
352
523
  ],
353
- [
354
- "Future Metrics",
355
- jsonBlock({
356
- impressions: null,
357
- reactions: null,
358
- comments: null,
359
- snapshots: [],
360
- }),
361
- ],
524
+ ["Future Metrics", jsonBlock(futureMetrics)],
362
525
  ]);
363
526
  writeArtifact(relativePath, markdown);
527
+ if (safeDraftId && input.updateDraftStatus !== false) {
528
+ markDraftAsPublished(safeDraftId, safeId, publishedAt);
529
+ }
364
530
  return {
365
531
  id: safeId,
366
532
  path: relativePath,
@@ -369,7 +535,65 @@ export function markPostPublishedTool(input) {
369
535
  publishUrl: input.publishUrl,
370
536
  publishedAt,
371
537
  updatedAt: publishedAt,
372
- preview: sanitizedPreview(input.finalText || input.publishUrl),
538
+ preview: sanitizedPreview(finalText || input.publishUrl),
539
+ };
540
+ }
541
+ export function getPublishedPostTool(input) {
542
+ const found = findPublishedArtifact(input.publishedPostId, input.year);
543
+ if (!found) {
544
+ throw new Error(`No publishedPostId found for ID: ${normalizeArtifactId(input.publishedPostId, "publishedPostId")}`);
545
+ }
546
+ return {
547
+ id: found.artifact.metadata.id,
548
+ path: found.relativePath,
549
+ metadata: found.artifact.metadata,
550
+ markdown: found.artifact.markdown,
551
+ };
552
+ }
553
+ export function updatePublishedPostMetricsTool(input) {
554
+ if (!input.metrics || typeof input.metrics !== "object") {
555
+ throw new Error("metrics is required.");
556
+ }
557
+ const found = findPublishedArtifact(input.publishedPostId, input.year);
558
+ if (!found) {
559
+ throw new Error(`No publishedPostId found for ID: ${normalizeArtifactId(input.publishedPostId, "publishedPostId")}`);
560
+ }
561
+ const capturedAt = normalizeDate(input.capturedAt);
562
+ const finalText = extractMarkdownSection(found.artifact.markdown, "Final Text");
563
+ const publishMetadata = readJsonSection(found.artifact.markdown, "Publish Metadata", {});
564
+ const futureMetrics = readJsonSection(found.artifact.markdown, "Future Metrics", {});
565
+ const existingSnapshots = Array.isArray(futureMetrics.snapshots)
566
+ ? futureMetrics.snapshots
567
+ : [];
568
+ const snapshot = stripUndefined({
569
+ capturedAt,
570
+ metrics: input.metrics,
571
+ note: input.note,
572
+ });
573
+ const nextMetrics = {
574
+ ...futureMetrics,
575
+ ...input.metrics,
576
+ lastCapturedAt: capturedAt,
577
+ snapshots: [...existingSnapshots, snapshot],
578
+ };
579
+ const metadata = {
580
+ ...found.artifact.metadata,
581
+ updatedAt: capturedAt,
582
+ };
583
+ const markdown = buildMarkdown(metadata, [
584
+ ["Final Text", finalText],
585
+ ["Publish Metadata", jsonBlock(publishMetadata)],
586
+ ["Future Metrics", jsonBlock(nextMetrics)],
587
+ ]);
588
+ writeArtifact(found.relativePath, markdown);
589
+ return {
590
+ id: metadata.id,
591
+ path: found.relativePath,
592
+ status: metadata.status,
593
+ updatedAt: capturedAt,
594
+ metrics: input.metrics,
595
+ snapshotsCount: existingSnapshots.length + 1,
596
+ preview: sanitizedPreview(finalText || String(metadata.publishUrl || "")),
373
597
  };
374
598
  }
375
599
  export function listPublishedPostsTool(input) {
@@ -423,8 +647,135 @@ function summarizeArtifacts(artifacts, limit) {
423
647
  path: artifact.relativePath,
424
648
  updatedAt: artifact.metadata.updatedAt,
425
649
  preview: sanitizedPreview(previewBasis(artifact.markdown)),
650
+ ideaId: artifact.metadata.ideaId,
651
+ hookResearchId: artifact.metadata.hookResearchId,
652
+ draftVersion: artifact.metadata.draftVersion,
653
+ priorDraftId: artifact.metadata.priorDraftId,
654
+ iterationScore: artifact.metadata.iteration?.score,
655
+ iterationVerdict: artifact.metadata.iteration?.verdict,
656
+ publishedPostId: artifact.metadata.publishedPostId,
657
+ publishedAt: artifact.metadata.publishedAt,
426
658
  }));
427
659
  }
660
+ function assertDraftStatus(status) {
661
+ if (!["draft", "ready", "needs_revision", "published", "archived"].includes(status)) {
662
+ throw new Error(`Unsupported draft status: ${status}`);
663
+ }
664
+ }
665
+ function normalizeDraftIteration(iteration, priorDraftId) {
666
+ if (!iteration && !priorDraftId)
667
+ return undefined;
668
+ const safePriorDraftId = priorDraftId
669
+ ? normalizeArtifactId(priorDraftId, "priorDraftId")
670
+ : iteration?.priorDraftId
671
+ ? normalizeArtifactId(iteration.priorDraftId, "priorDraftId")
672
+ : undefined;
673
+ return stripUndefined({
674
+ ...iteration,
675
+ priorDraftId: safePriorDraftId,
676
+ });
677
+ }
678
+ function inferDraftVersion(draftId) {
679
+ const match = draftId.match(/(?:^|_)v(\d+)(?:$|_)/i);
680
+ return match ? `v${match[1]}` : undefined;
681
+ }
682
+ function compareDraftIterations(a, b) {
683
+ const aVersion = numericVersion(a.draftVersion);
684
+ const bVersion = numericVersion(b.draftVersion);
685
+ if (aVersion !== bVersion)
686
+ return aVersion - bVersion;
687
+ const updated = String(a.updatedAt).localeCompare(String(b.updatedAt));
688
+ if (updated !== 0)
689
+ return updated;
690
+ return a.id.localeCompare(b.id);
691
+ }
692
+ function numericVersion(version) {
693
+ const match = version?.match(/^v(\d+)$/i);
694
+ return match ? Number(match[1]) : Number.MAX_SAFE_INTEGER;
695
+ }
696
+ function extractIterationFromReceipt(receipt) {
697
+ if (!receipt.includes("iteration:"))
698
+ return undefined;
699
+ const version = receipt.match(/^\s*version:\s*(\S+)/m)?.[1];
700
+ const priorDraftId = receipt.match(/^\s*priorDraftId:\s*(\S+)/m)?.[1];
701
+ const verdict = receipt.match(/^\s*verdict:\s*(\S+)/m)?.[1];
702
+ const scoreBlock = receipt.match(/^\s*score:\s*\n((?:\s{4}[A-Za-z][A-Za-z0-9_-]*:\s*[-\d.]+\n?)+)/m)?.[1];
703
+ const score = scoreBlock
704
+ ? Object.fromEntries(scoreBlock
705
+ .trim()
706
+ .split(/\n/)
707
+ .map((line) => line.trim().split(/:\s*/))
708
+ .filter(([key, value]) => key && value)
709
+ .map(([key, value]) => [key, Number(value)]))
710
+ : undefined;
711
+ return stripUndefined({
712
+ version,
713
+ priorDraftId: priorDraftId && priorDraftId !== "none" ? priorDraftId : undefined,
714
+ verdict,
715
+ score,
716
+ });
717
+ }
718
+ function extractMarkdownSection(markdown, heading) {
719
+ const escaped = heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
720
+ const match = markdown.match(new RegExp(`(?:^|\\n)## ${escaped}\\n\\n([\\s\\S]*?)(?=\\n## |$)`));
721
+ return match?.[1]?.trimEnd() ?? "";
722
+ }
723
+ function readJsonSection(markdown, heading, fallback) {
724
+ const section = extractMarkdownSection(markdown, heading);
725
+ const match = section.match(/```json\n([\s\S]*?)\n```/);
726
+ if (!match)
727
+ return fallback;
728
+ try {
729
+ return JSON.parse(match[1]);
730
+ }
731
+ catch {
732
+ return fallback;
733
+ }
734
+ }
735
+ function findPublishedArtifact(publishedPostId, year) {
736
+ const safeId = normalizeArtifactId(publishedPostId, "publishedPostId");
737
+ if (year) {
738
+ if (!/^\d{4}$/.test(year))
739
+ throw new Error("year must be YYYY.");
740
+ const relativePath = `${RELATIVE_DIRS.published}/${year}/${safeId}.md`;
741
+ const artifact = readArtifactIfExists(relativePath);
742
+ return artifact ? { artifact, relativePath } : null;
743
+ }
744
+ const root = resolveContentRoot();
745
+ ensureContentLayout(root);
746
+ const publishedRoot = safePath(root, RELATIVE_DIRS.published);
747
+ if (!fs.existsSync(publishedRoot))
748
+ return null;
749
+ for (const yearDir of fs.readdirSync(publishedRoot).sort()) {
750
+ if (!/^\d{4}$/.test(yearDir))
751
+ continue;
752
+ const relativePath = `${RELATIVE_DIRS.published}/${yearDir}/${safeId}.md`;
753
+ const artifact = readArtifactIfExists(relativePath);
754
+ if (artifact)
755
+ return { artifact, relativePath };
756
+ }
757
+ return null;
758
+ }
759
+ function markDraftAsPublished(draftId, publishedPostId, publishedAt) {
760
+ const relativePath = `${RELATIVE_DIRS.drafts}/${draftId}.md`;
761
+ const existing = readArtifactIfExists(relativePath);
762
+ if (!existing)
763
+ return;
764
+ const body = extractMarkdownSection(existing.markdown, "Draft Body");
765
+ const receipt = extractMarkdownSection(existing.markdown, "Validation Receipt");
766
+ const metadata = {
767
+ ...existing.metadata,
768
+ status: "published",
769
+ publishedPostId,
770
+ publishedAt,
771
+ updatedAt: publishedAt,
772
+ };
773
+ const markdown = buildMarkdown(metadata, [
774
+ ["Draft Body", body],
775
+ ["Validation Receipt", receipt],
776
+ ]);
777
+ writeArtifact(relativePath, markdown);
778
+ }
428
779
  function readArtifactsFromDir(relativeDir) {
429
780
  const root = resolveContentRoot();
430
781
  ensureContentLayout(root);