@sellable/mcp 0.1.240 → 0.1.243

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,101 @@ 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 ??
412
+ existing.metadata.iteration, input.priorDraftId ?? existing.metadata.priorDraftId);
413
+ const status = input.status || existing.metadata.status || "draft";
414
+ assertDraftStatus(status);
415
+ const metadata = {
416
+ ...existing.metadata,
417
+ status,
418
+ title: input.title ?? existing.metadata.title,
419
+ hookResearchId: safeResearchId,
420
+ draftVersion: iteration?.version ||
421
+ existing.metadata.draftVersion ||
422
+ inferDraftVersion(safeDraftId),
423
+ priorDraftId: iteration?.priorDraftId,
424
+ iteration,
425
+ updatedAt: now,
426
+ };
427
+ const markdown = buildMarkdown(metadata, [
428
+ ["Draft Body", body],
429
+ ["Validation Receipt", validationReceipt],
430
+ ]);
431
+ writeArtifact(relativePath, markdown);
432
+ return {
433
+ id: safeDraftId,
434
+ path: relativePath,
435
+ status,
436
+ ideaId: metadata.ideaId,
437
+ hookResearchId: safeResearchId,
438
+ draftVersion: metadata.draftVersion,
439
+ priorDraftId: metadata.priorDraftId,
440
+ iterationScore: iteration?.score,
441
+ iterationVerdict: iteration?.verdict,
442
+ updatedAt: now,
443
+ preview: sanitizedPreview(body),
444
+ };
445
+ }
311
446
  export function listPostDraftsTool(input) {
312
447
  return listArtifacts(RELATIVE_DIRS.drafts, "draft", input?.limit);
313
448
  }
449
+ export function listPostDraftIterationsTool(input) {
450
+ const safeIdeaId = normalizeArtifactId(input.ideaId, "ideaId");
451
+ const drafts = readArtifactsFromDir(RELATIVE_DIRS.drafts)
452
+ .filter((artifact) => artifact.metadata.type === "draft" &&
453
+ artifact.metadata.ideaId === safeIdeaId)
454
+ .map((artifact) => {
455
+ const iteration = artifact.metadata.iteration ||
456
+ extractIterationFromReceipt(extractMarkdownSection(artifact.markdown, "Validation Receipt"));
457
+ return {
458
+ id: artifact.metadata.id,
459
+ type: artifact.metadata.type,
460
+ status: artifact.metadata.status,
461
+ title: artifact.metadata.title,
462
+ path: artifact.relativePath,
463
+ ideaId: artifact.metadata.ideaId,
464
+ hookResearchId: artifact.metadata.hookResearchId,
465
+ draftVersion: artifact.metadata.draftVersion ||
466
+ iteration?.version ||
467
+ inferDraftVersion(artifact.metadata.id),
468
+ priorDraftId: artifact.metadata.priorDraftId || iteration?.priorDraftId,
469
+ iterationScore: iteration?.score,
470
+ iterationVerdict: iteration?.verdict,
471
+ publishedPostId: artifact.metadata.publishedPostId,
472
+ publishedAt: artifact.metadata.publishedAt,
473
+ updatedAt: artifact.metadata.updatedAt,
474
+ preview: sanitizedPreview(previewBasis(artifact.markdown)),
475
+ };
476
+ });
477
+ return drafts.sort(compareDraftIterations);
478
+ }
314
479
  export function getPostDraftTool(input) {
315
480
  return getArtifact(RELATIVE_DIRS.drafts, input.draftId, "draftId");
316
481
  }
@@ -327,6 +492,15 @@ export function markPostPublishedTool(input) {
327
492
  const relativePath = `${RELATIVE_DIRS.published}/${year}/${safeId}.md`;
328
493
  const existing = readArtifactIfExists(relativePath);
329
494
  const createdAt = existing?.metadata.createdAt || publishedAt;
495
+ const finalText = input.finalText ??
496
+ extractMarkdownSection(existing?.markdown || "", "Final Text");
497
+ const publishMetadata = readJsonSection(existing?.markdown || "", "Publish Metadata", {});
498
+ const futureMetrics = readJsonSection(existing?.markdown || "", "Future Metrics", {
499
+ impressions: null,
500
+ reactions: null,
501
+ comments: null,
502
+ snapshots: [],
503
+ });
330
504
  const metadata = {
331
505
  id: safeId,
332
506
  type: "published_post",
@@ -340,27 +514,23 @@ export function markPostPublishedTool(input) {
340
514
  updatedAt: publishedAt,
341
515
  };
342
516
  const markdown = buildMarkdown(metadata, [
343
- ["Final Text", input.finalText || ""],
517
+ ["Final Text", finalText],
344
518
  [
345
519
  "Publish Metadata",
346
520
  jsonBlock(stripUndefined({
521
+ ...publishMetadata,
347
522
  draftId: safeDraftId,
348
523
  publishUrl: input.publishUrl,
349
524
  activityId: activityId || undefined,
350
525
  publishedAt,
351
526
  })),
352
527
  ],
353
- [
354
- "Future Metrics",
355
- jsonBlock({
356
- impressions: null,
357
- reactions: null,
358
- comments: null,
359
- snapshots: [],
360
- }),
361
- ],
528
+ ["Future Metrics", jsonBlock(futureMetrics)],
362
529
  ]);
363
530
  writeArtifact(relativePath, markdown);
531
+ if (safeDraftId && input.updateDraftStatus !== false) {
532
+ markDraftAsPublished(safeDraftId, safeId, publishedAt);
533
+ }
364
534
  return {
365
535
  id: safeId,
366
536
  path: relativePath,
@@ -369,7 +539,65 @@ export function markPostPublishedTool(input) {
369
539
  publishUrl: input.publishUrl,
370
540
  publishedAt,
371
541
  updatedAt: publishedAt,
372
- preview: sanitizedPreview(input.finalText || input.publishUrl),
542
+ preview: sanitizedPreview(finalText || input.publishUrl),
543
+ };
544
+ }
545
+ export function getPublishedPostTool(input) {
546
+ const found = findPublishedArtifact(input.publishedPostId, input.year);
547
+ if (!found) {
548
+ throw new Error(`No publishedPostId found for ID: ${normalizeArtifactId(input.publishedPostId, "publishedPostId")}`);
549
+ }
550
+ return {
551
+ id: found.artifact.metadata.id,
552
+ path: found.relativePath,
553
+ metadata: found.artifact.metadata,
554
+ markdown: found.artifact.markdown,
555
+ };
556
+ }
557
+ export function updatePublishedPostMetricsTool(input) {
558
+ if (!input.metrics || typeof input.metrics !== "object") {
559
+ throw new Error("metrics is required.");
560
+ }
561
+ const found = findPublishedArtifact(input.publishedPostId, input.year);
562
+ if (!found) {
563
+ throw new Error(`No publishedPostId found for ID: ${normalizeArtifactId(input.publishedPostId, "publishedPostId")}`);
564
+ }
565
+ const capturedAt = normalizeDate(input.capturedAt);
566
+ const finalText = extractMarkdownSection(found.artifact.markdown, "Final Text");
567
+ const publishMetadata = readJsonSection(found.artifact.markdown, "Publish Metadata", {});
568
+ const futureMetrics = readJsonSection(found.artifact.markdown, "Future Metrics", {});
569
+ const existingSnapshots = Array.isArray(futureMetrics.snapshots)
570
+ ? futureMetrics.snapshots
571
+ : [];
572
+ const snapshot = stripUndefined({
573
+ capturedAt,
574
+ metrics: input.metrics,
575
+ note: input.note,
576
+ });
577
+ const nextMetrics = {
578
+ ...futureMetrics,
579
+ ...input.metrics,
580
+ lastCapturedAt: capturedAt,
581
+ snapshots: [...existingSnapshots, snapshot],
582
+ };
583
+ const metadata = {
584
+ ...found.artifact.metadata,
585
+ updatedAt: capturedAt,
586
+ };
587
+ const markdown = buildMarkdown(metadata, [
588
+ ["Final Text", finalText],
589
+ ["Publish Metadata", jsonBlock(publishMetadata)],
590
+ ["Future Metrics", jsonBlock(nextMetrics)],
591
+ ]);
592
+ writeArtifact(found.relativePath, markdown);
593
+ return {
594
+ id: metadata.id,
595
+ path: found.relativePath,
596
+ status: metadata.status,
597
+ updatedAt: capturedAt,
598
+ metrics: input.metrics,
599
+ snapshotsCount: existingSnapshots.length + 1,
600
+ preview: sanitizedPreview(finalText || String(metadata.publishUrl || "")),
373
601
  };
374
602
  }
375
603
  export function listPublishedPostsTool(input) {
@@ -423,8 +651,135 @@ function summarizeArtifacts(artifacts, limit) {
423
651
  path: artifact.relativePath,
424
652
  updatedAt: artifact.metadata.updatedAt,
425
653
  preview: sanitizedPreview(previewBasis(artifact.markdown)),
654
+ ideaId: artifact.metadata.ideaId,
655
+ hookResearchId: artifact.metadata.hookResearchId,
656
+ draftVersion: artifact.metadata.draftVersion,
657
+ priorDraftId: artifact.metadata.priorDraftId,
658
+ iterationScore: artifact.metadata.iteration?.score,
659
+ iterationVerdict: artifact.metadata.iteration?.verdict,
660
+ publishedPostId: artifact.metadata.publishedPostId,
661
+ publishedAt: artifact.metadata.publishedAt,
426
662
  }));
427
663
  }
664
+ function assertDraftStatus(status) {
665
+ if (!["draft", "ready", "needs_revision", "published", "archived"].includes(status)) {
666
+ throw new Error(`Unsupported draft status: ${status}`);
667
+ }
668
+ }
669
+ function normalizeDraftIteration(iteration, priorDraftId) {
670
+ if (!iteration && !priorDraftId)
671
+ return undefined;
672
+ const safePriorDraftId = priorDraftId
673
+ ? normalizeArtifactId(priorDraftId, "priorDraftId")
674
+ : iteration?.priorDraftId
675
+ ? normalizeArtifactId(iteration.priorDraftId, "priorDraftId")
676
+ : undefined;
677
+ return stripUndefined({
678
+ ...iteration,
679
+ priorDraftId: safePriorDraftId,
680
+ });
681
+ }
682
+ function inferDraftVersion(draftId) {
683
+ const match = draftId.match(/(?:^|_)v(\d+)(?:$|_)/i);
684
+ return match ? `v${match[1]}` : undefined;
685
+ }
686
+ function compareDraftIterations(a, b) {
687
+ const aVersion = numericVersion(a.draftVersion);
688
+ const bVersion = numericVersion(b.draftVersion);
689
+ if (aVersion !== bVersion)
690
+ return aVersion - bVersion;
691
+ const updated = String(a.updatedAt).localeCompare(String(b.updatedAt));
692
+ if (updated !== 0)
693
+ return updated;
694
+ return a.id.localeCompare(b.id);
695
+ }
696
+ function numericVersion(version) {
697
+ const match = version?.match(/^v(\d+)$/i);
698
+ return match ? Number(match[1]) : Number.MAX_SAFE_INTEGER;
699
+ }
700
+ function extractIterationFromReceipt(receipt) {
701
+ if (!receipt.includes("iteration:"))
702
+ return undefined;
703
+ const version = receipt.match(/^\s*version:\s*(\S+)/m)?.[1];
704
+ const priorDraftId = receipt.match(/^\s*priorDraftId:\s*(\S+)/m)?.[1];
705
+ const verdict = receipt.match(/^\s*verdict:\s*(\S+)/m)?.[1];
706
+ const scoreBlock = receipt.match(/^\s*score:\s*\n((?:\s{4}[A-Za-z][A-Za-z0-9_-]*:\s*[-\d.]+\n?)+)/m)?.[1];
707
+ const score = scoreBlock
708
+ ? Object.fromEntries(scoreBlock
709
+ .trim()
710
+ .split(/\n/)
711
+ .map((line) => line.trim().split(/:\s*/))
712
+ .filter(([key, value]) => key && value)
713
+ .map(([key, value]) => [key, Number(value)]))
714
+ : undefined;
715
+ return stripUndefined({
716
+ version,
717
+ priorDraftId: priorDraftId && priorDraftId !== "none" ? priorDraftId : undefined,
718
+ verdict,
719
+ score,
720
+ });
721
+ }
722
+ function extractMarkdownSection(markdown, heading) {
723
+ const escaped = heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
724
+ const match = markdown.match(new RegExp(`(?:^|\\n)## ${escaped}\\n\\n([\\s\\S]*?)(?=\\n## |$)`));
725
+ return match?.[1]?.trimEnd() ?? "";
726
+ }
727
+ function readJsonSection(markdown, heading, fallback) {
728
+ const section = extractMarkdownSection(markdown, heading);
729
+ const match = section.match(/```json\n([\s\S]*?)\n```/);
730
+ if (!match)
731
+ return fallback;
732
+ try {
733
+ return JSON.parse(match[1]);
734
+ }
735
+ catch {
736
+ return fallback;
737
+ }
738
+ }
739
+ function findPublishedArtifact(publishedPostId, year) {
740
+ const safeId = normalizeArtifactId(publishedPostId, "publishedPostId");
741
+ if (year) {
742
+ if (!/^\d{4}$/.test(year))
743
+ throw new Error("year must be YYYY.");
744
+ const relativePath = `${RELATIVE_DIRS.published}/${year}/${safeId}.md`;
745
+ const artifact = readArtifactIfExists(relativePath);
746
+ return artifact ? { artifact, relativePath } : null;
747
+ }
748
+ const root = resolveContentRoot();
749
+ ensureContentLayout(root);
750
+ const publishedRoot = safePath(root, RELATIVE_DIRS.published);
751
+ if (!fs.existsSync(publishedRoot))
752
+ return null;
753
+ for (const yearDir of fs.readdirSync(publishedRoot).sort()) {
754
+ if (!/^\d{4}$/.test(yearDir))
755
+ continue;
756
+ const relativePath = `${RELATIVE_DIRS.published}/${yearDir}/${safeId}.md`;
757
+ const artifact = readArtifactIfExists(relativePath);
758
+ if (artifact)
759
+ return { artifact, relativePath };
760
+ }
761
+ return null;
762
+ }
763
+ function markDraftAsPublished(draftId, publishedPostId, publishedAt) {
764
+ const relativePath = `${RELATIVE_DIRS.drafts}/${draftId}.md`;
765
+ const existing = readArtifactIfExists(relativePath);
766
+ if (!existing)
767
+ return;
768
+ const body = extractMarkdownSection(existing.markdown, "Draft Body");
769
+ const receipt = extractMarkdownSection(existing.markdown, "Validation Receipt");
770
+ const metadata = {
771
+ ...existing.metadata,
772
+ status: "published",
773
+ publishedPostId,
774
+ publishedAt,
775
+ updatedAt: publishedAt,
776
+ };
777
+ const markdown = buildMarkdown(metadata, [
778
+ ["Draft Body", body],
779
+ ["Validation Receipt", receipt],
780
+ ]);
781
+ writeArtifact(relativePath, markdown);
782
+ }
428
783
  function readArtifactsFromDir(relativeDir) {
429
784
  const root = resolveContentRoot();
430
785
  ensureContentLayout(root);
@@ -2,7 +2,7 @@ import { copySenderConfig, getEngageMemory, migrateFlatConfigs, recordProvenSear
2
2
  export const engageMemoryToolDefinitions = [
3
3
  {
4
4
  name: "get_engage_memory",
5
- description: "Load backward-compatible engage memory from ~/.sellable/configs/: style guide, post writing rules, proven search keywords, tracked people, plus optional core identity/company memory. All data lives in readable markdown files the user can also edit directly. When senderId is provided, reads compatibility overrides from senders/{senderId}/ with flat-path fallback.",
5
+ description: "Load unified Sellable memory from ~/.sellable/configs/: core identity/company/proof/story memory, transcript/content-memory clusters, references, style and post writing rules, proven search keywords, and tracked people. The get_engage_memory name is backward-compatible; all data lives in readable markdown files the user can edit directly. When senderId is provided, reads compatibility overrides from senders/{senderId}/ with flat-path fallback.",
6
6
  inputSchema: {
7
7
  type: "object",
8
8
  properties: {