@sellable/mcp 0.1.236 → 0.1.238

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/README.md CHANGED
@@ -20,7 +20,7 @@ There are three public Sellable entrypoints shared across hosts:
20
20
 
21
21
  - `sellable:create-campaign`
22
22
  - `sellable:interview`
23
- - `sellable:load-voice`
23
+ - `sellable:create-post`
24
24
 
25
25
  The create-campaign public wrapper at
26
26
  `mcp/sellable/skills/create-campaign/SKILL.md` handles auth/bootstrap and loads
@@ -33,12 +33,15 @@ from:
33
33
 
34
34
  - `mcp/sellable/skills/interview/SKILL.md`
35
35
 
36
- The load-voice public wrapper loads the current voice/company memory for use in
37
- writing, application answers, posts, replies, and reviews from:
36
+ The create-post public wrapper captures raw ideas, loads voice internally,
37
+ researches hooks, validates proof/AI tells, and saves content artifacts under
38
+ `~/.sellable/content/linkedin/**` from:
38
39
 
39
- - `mcp/sellable/skills/load-voice/SKILL.md`
40
+ - `mcp/sellable/skills/create-post/SKILL.md`
40
41
 
41
- Keep `create-campaign-v2` internal. Do not advertise it as a public command.
42
+ Keep `create-campaign-v2` and `load-voice` internal. Do not advertise either as
43
+ a public command. `load-voice` remains a direct MCP utility for generic writing
44
+ surfaces that are not posts or campaigns.
42
45
 
43
46
  ### 1. One-Command Agent Install
44
47
 
@@ -134,8 +137,8 @@ The installer does the full local setup:
134
137
 
135
138
  After the installer passes, fully quit and reopen Codex Desktop. Start a new
136
139
  thread and select `Sellable Create Campaign`, `Sellable Identity Interview`, or
137
- `Sellable Load Voice`; or invoke `$sellable:create-campaign`,
138
- `$sellable:interview`, or `$sellable:load-voice`. If the app still says
140
+ `Sellable Create Post`; or invoke `$sellable:create-campaign`,
141
+ `$sellable:interview`, or `$sellable:create-post`. If the app still says
139
142
  `mcp__sellable__*` tools are missing after the installer passes, check that
140
143
  `~/.codex/config.toml` contains both `[marketplaces.sellable]` and
141
144
  `[plugins."sellable@sellable"]`.
@@ -148,24 +151,26 @@ Use these names consistently:
148
151
 
149
152
  - Claude Code command: `/sellable:create-campaign`
150
153
  - Claude Code command: `/sellable:interview`
151
- - Claude Code command: `/sellable:load-voice`
154
+ - Claude Code command: `/sellable:create-post`
152
155
  - Codex command: `$sellable:create-campaign`
153
156
  - Codex command: `$sellable:interview`
154
- - Codex command: `$sellable:load-voice`
157
+ - Codex command: `$sellable:create-post`
155
158
  - Codex Desktop plugin: `sellable@sellable`
156
159
  - Codex visible skill: `Sellable Create Campaign`
157
160
  - Codex visible skill: `Sellable Identity Interview`
158
- - Codex visible skill: `Sellable Load Voice`
161
+ - Codex visible skill: `Sellable Create Post`
159
162
  - Codex skill frontmatter name: `create-campaign`
160
163
  - Codex skill frontmatter name: `interview`
161
- - Codex skill frontmatter name: `load-voice`
164
+ - Codex skill frontmatter name: `create-post`
162
165
  - MCP server name: `sellable`
163
166
  - Internal workflow prompt: `create-campaign-v2`
164
167
 
165
168
  Never tell users to run `/sellable:create-campaign-v2`,
166
- `$sellable:create-campaign-v2`, or `$sellable:sellable:create-campaign`.
167
- `create-campaign-v2` is an internal MCP subskill loaded by
168
- `get_subskill_prompt({ subskillName: "create-campaign-v2" })`.
169
+ `$sellable:create-campaign-v2`, `$sellable:load-voice`, or
170
+ `$sellable:sellable:create-campaign`. `create-campaign-v2` is an internal MCP
171
+ subskill loaded by `get_subskill_prompt({ subskillName: "create-campaign-v2" })`.
172
+ `load-voice` is an internal direct MCP utility; `create-post` loads voice
173
+ silently for post drafting.
169
174
 
170
175
  ## Structured Question Parity
171
176
 
@@ -19,6 +19,7 @@ export interface TrackedPerson {
19
19
  }
20
20
  export interface EngageMemory {
21
21
  styleGuide?: EngageStyleGuide;
22
+ postWritingRules?: EngageStyleGuide;
22
23
  provenSearches: ProvenSearch[];
23
24
  trackedPeople: TrackedPerson[];
24
25
  identity?: IdentityMemoryChunk;
@@ -116,8 +116,18 @@ function buildMarkdownTable(headers, rows) {
116
116
  // ── Style guide ────────────────────────────────────────────────────────────
117
117
  const FLAT_STYLE_CORE_PATH = "writing/styleguide-core.md";
118
118
  const STYLE_COMMENTS_PATH = "writing/comments.md";
119
+ const STYLE_POSTS_PATH = "writing/posts.md";
119
120
  const LEGACY_AUDIENCE_ICP_PATH = "audience/icp.md";
120
121
  const LEGACY_FAQS_CORE_PATH = "faqs/core.md";
122
+ function readPostWritingRules() {
123
+ const markdown = readFile(STYLE_POSTS_PATH);
124
+ if (!markdown)
125
+ return undefined;
126
+ return {
127
+ markdown,
128
+ updatedAt: latestUpdatedAt([STYLE_POSTS_PATH]),
129
+ };
130
+ }
121
131
  function readStyleGuide(senderId, identityMemory) {
122
132
  const corePath = senderPath(senderId, "styleguide-core.md", FLAT_STYLE_CORE_PATH);
123
133
  const core = readFile(corePath);
@@ -200,7 +210,8 @@ function hasCoreIdentityMemory(identityMemory) {
200
210
  identityMemory.decisionRules ||
201
211
  identityMemory.changeLog ||
202
212
  identityMemory.transcripts?.index ||
203
- identityMemory.references?.index);
213
+ identityMemory.references?.index ||
214
+ (identityMemory.references?.childIndexes?.length ?? 0) > 0);
204
215
  }
205
216
  function appendCoreSection(parts, sourcePaths, title, chunk) {
206
217
  if (!chunk)
@@ -209,9 +220,12 @@ function appendCoreSection(parts, sourcePaths, title, chunk) {
209
220
  sourcePaths.push(chunk.source.relativePath);
210
221
  }
211
222
  function appendCoreDirectoryIndex(parts, sourcePaths, title, directory) {
212
- if (!directory?.index)
223
+ if (!directory)
213
224
  return;
214
225
  appendCoreSection(parts, sourcePaths, title, directory.index);
226
+ for (const childIndex of directory.childIndexes ?? []) {
227
+ appendCoreSection(parts, sourcePaths, title, childIndex);
228
+ }
215
229
  }
216
230
  function appendCompatibilitySection(parts, sourcePaths, section) {
217
231
  parts.push(`## ${section.title} (${section.relativePath})\n\n${section.markdown.trim()}`);
@@ -229,11 +243,13 @@ function latestUpdatedAt(relativePaths) {
229
243
  export function getEngageMemory(senderId) {
230
244
  const identityMemory = readIdentityMemory({ connectedSenderId: senderId });
231
245
  const styleGuide = readStyleGuide(senderId, identityMemory);
246
+ const postWritingRules = readPostWritingRules();
232
247
  const provenSearches = readProvenSearches(senderId);
233
248
  const trackedPeople = readTrackedPeople(senderId);
234
249
  return {
235
250
  memory: {
236
251
  styleGuide,
252
+ postWritingRules,
237
253
  provenSearches,
238
254
  trackedPeople,
239
255
  ...identityMemory,
@@ -23,6 +23,7 @@ export interface IdentityMemoryChunk {
23
23
  export interface IdentityMemoryDirectory {
24
24
  source: IdentityMemorySource;
25
25
  index?: IdentityMemoryChunk;
26
+ childIndexes?: IdentityMemoryChunk[];
26
27
  }
27
28
  export interface IdentityMemory {
28
29
  identity?: IdentityMemoryChunk;
@@ -81,5 +81,32 @@ function readDirectory(configsDir, relativePath) {
81
81
  if (index) {
82
82
  directory.index = index;
83
83
  }
84
+ const childIndexes = readChildIndexes(configsDir, relativePath);
85
+ if (childIndexes.length > 0) {
86
+ directory.childIndexes = childIndexes;
87
+ }
84
88
  return directory;
85
89
  }
90
+ function readChildIndexes(configsDir, relativePath) {
91
+ const root = path.join(configsDir, relativePath);
92
+ const indexes = [];
93
+ function walk(currentFullPath, currentRelativePath) {
94
+ for (const entry of fs.readdirSync(currentFullPath).sort()) {
95
+ const entryFullPath = path.join(currentFullPath, entry);
96
+ const entryRelativePath = `${currentRelativePath}/${entry}`;
97
+ const stat = fs.statSync(entryFullPath);
98
+ if (stat.isDirectory()) {
99
+ walk(entryFullPath, entryRelativePath);
100
+ }
101
+ else if (stat.isFile() &&
102
+ entry === "INDEX.md" &&
103
+ entryRelativePath !== `${relativePath}/INDEX.md`) {
104
+ const chunk = readMarkdownChunk(configsDir, entryRelativePath);
105
+ if (chunk)
106
+ indexes.push(chunk);
107
+ }
108
+ }
109
+ }
110
+ walk(root, relativePath);
111
+ return indexes;
112
+ }
package/dist/server.js CHANGED
@@ -10,6 +10,7 @@ import { getCampaignTableSchema, queueCampaignCells, reviseMessageTemplateAndRer
10
10
  import { queueCells, updateCell } from "./tools/cells.js";
11
11
  import { handleStartCliLogin, handleWaitForCliLogin, } from "./tools/cli-login.js";
12
12
  import { getCampaignContext, hydrateCampaignContextFromCampaign, markCampaignContextDirty, } from "./tools/context.js";
13
+ import { capturePostIdeaTool, getPostDraftTool, getPostIdeaTool, listPostDraftsTool, listPostIdeasTool, listPublishedPostsTool, markPostPublishedTool, saveHookResearchTool, savePostDraftTool, } from "./tools/content-posts.js";
13
14
  import { addToCommentCampaign, addToConnectionCampaign, addToInmailCampaign, getEngagedPosts, getOrCreateDirectCampaignTable, pauseDirectCampaign, startDirectCampaign, } from "./tools/direct-campaigns.js";
14
15
  import { bootstrapEngage, bootstrapEngageMulti, } from "./tools/engage-bootstrap.js";
15
16
  import { searchEngagementPosts } from "./tools/engage-discovery.js";
@@ -222,6 +223,33 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
222
223
  case "get_campaign_navigation_state":
223
224
  result = await getCampaignNavigationState(args);
224
225
  break;
226
+ case "capture_post_idea":
227
+ result = capturePostIdeaTool(args);
228
+ break;
229
+ case "list_post_ideas":
230
+ result = listPostIdeasTool(args);
231
+ break;
232
+ case "get_post_idea":
233
+ result = getPostIdeaTool(args);
234
+ break;
235
+ case "save_hook_research":
236
+ result = saveHookResearchTool(args);
237
+ break;
238
+ case "save_post_draft":
239
+ result = savePostDraftTool(args);
240
+ break;
241
+ case "list_post_drafts":
242
+ result = listPostDraftsTool(args);
243
+ break;
244
+ case "get_post_draft":
245
+ result = getPostDraftTool(args);
246
+ break;
247
+ case "mark_post_published":
248
+ result = markPostPublishedTool(args);
249
+ break;
250
+ case "list_published_posts":
251
+ result = listPublishedPostsTool(args);
252
+ break;
225
253
  case "get_table_rows":
226
254
  result = await getTableRows(args?.tableId, {
227
255
  limit: args?.limit,
@@ -49,24 +49,27 @@ function getCampaignBuilderWatchModeFromUrl(watchUrl) {
49
49
  export function getCampaignBuilderWatchModeDriverLabel(mode = getCampaignBuilderWatchModeParam()) {
50
50
  return mode === "codex" ? "Codex" : "Claude Code";
51
51
  }
52
- function centerWatchHandoffHeadline(headline) {
53
- const width = 60;
52
+ function buildWatchHandoffHeadlineBox(headline) {
53
+ const width = 52;
54
54
  const visibleHeadline = headline.length > width ? headline.slice(0, width) : headline;
55
55
  const padding = Math.max(width - visibleHeadline.length, 0);
56
56
  const left = Math.floor(padding / 2);
57
57
  const right = padding - left;
58
+ const border = `+${"-".repeat(width + 2)}+`;
58
59
  return [
59
- `╔${"".repeat(width + 2)}╗`,
60
- `║ ${" ".repeat(left)}${visibleHeadline}${" ".repeat(right)} ║`,
61
- `╚${"".repeat(width + 2)}╝`,
60
+ "```text",
61
+ border,
62
+ `| ${" ".repeat(left)}${visibleHeadline}${" ".repeat(right)} |`,
63
+ border,
64
+ "```",
62
65
  ].join("\n");
63
66
  }
64
67
  export function buildCampaignWatchHandoffMarkdown(watchUrl, mode = getCampaignBuilderWatchModeFromUrl(watchUrl) ??
65
68
  getCampaignBuilderWatchModeParam()) {
66
69
  const driverLabel = getCampaignBuilderWatchModeDriverLabel(mode);
67
- const headline = `OPEN THIS LINK TO WATCH ${driverLabel.toUpperCase()} BUILD THE CAMPAIGN LIVE`;
70
+ const headline = `WATCH ${driverLabel.toUpperCase()} BUILD THE CAMPAIGN LIVE`;
68
71
  return [
69
- centerWatchHandoffHeadline(headline),
72
+ buildWatchHandoffHeadlineBox(headline),
70
73
  "",
71
74
  `[Open live campaign builder](${watchUrl})`,
72
75
  "",
@@ -0,0 +1,308 @@
1
+ type ContentKind = "idea" | "hook_research" | "draft" | "published_post";
2
+ type ArtifactMetadata = {
3
+ id: string;
4
+ type: ContentKind;
5
+ status: string;
6
+ createdAt: string;
7
+ updatedAt: string;
8
+ title?: string;
9
+ ideaId?: string;
10
+ hookResearchId?: string;
11
+ publishUrl?: string;
12
+ publishedAt?: string;
13
+ sourceType?: string;
14
+ sourceUrl?: string;
15
+ [key: string]: unknown;
16
+ };
17
+ export type ContentPostSummary = {
18
+ id: string;
19
+ type: ContentKind;
20
+ status: string;
21
+ path: string;
22
+ title?: string;
23
+ updatedAt: string;
24
+ preview: string;
25
+ };
26
+ export declare const contentPostToolDefinitions: ({
27
+ name: string;
28
+ description: string;
29
+ inputSchema: {
30
+ type: string;
31
+ properties: {
32
+ rawSource: {
33
+ type: string;
34
+ description: string;
35
+ };
36
+ title: {
37
+ type: string;
38
+ };
39
+ distilledBrief: {
40
+ type: string;
41
+ description: string;
42
+ };
43
+ ideaId: {
44
+ type: string;
45
+ description: string;
46
+ };
47
+ sourceType: {
48
+ type: string;
49
+ };
50
+ sourceUrl: {
51
+ type: string;
52
+ };
53
+ capturedAt: {
54
+ type: string;
55
+ };
56
+ researchId?: undefined;
57
+ topic?: undefined;
58
+ keywords?: undefined;
59
+ sourcePosts?: undefined;
60
+ selectedPatterns?: undefined;
61
+ notes?: undefined;
62
+ createdAt?: undefined;
63
+ draftId?: undefined;
64
+ hookResearchId?: undefined;
65
+ body?: undefined;
66
+ validationReceipt?: undefined;
67
+ status?: undefined;
68
+ };
69
+ required: string[];
70
+ additionalProperties: boolean;
71
+ };
72
+ } | {
73
+ name: string;
74
+ description: string;
75
+ inputSchema: {
76
+ type: string;
77
+ properties: {
78
+ limit: {
79
+ type: string;
80
+ };
81
+ };
82
+ required: never[];
83
+ additionalProperties: boolean;
84
+ };
85
+ } | {
86
+ name: string;
87
+ description: string;
88
+ inputSchema: {
89
+ type: string;
90
+ properties: {
91
+ [x: string]: {
92
+ type: string;
93
+ };
94
+ };
95
+ required: string[];
96
+ additionalProperties: boolean;
97
+ };
98
+ } | {
99
+ name: string;
100
+ description: string;
101
+ inputSchema: {
102
+ type: string;
103
+ properties: {
104
+ researchId: {
105
+ type: string;
106
+ };
107
+ ideaId: {
108
+ type: string;
109
+ description?: undefined;
110
+ };
111
+ topic: {
112
+ type: string;
113
+ };
114
+ keywords: {
115
+ type: string;
116
+ items: {
117
+ type: string;
118
+ };
119
+ };
120
+ sourcePosts: {
121
+ type: string;
122
+ items: {
123
+ type: string;
124
+ additionalProperties: boolean;
125
+ };
126
+ };
127
+ selectedPatterns: {
128
+ type: string;
129
+ items: {
130
+ type: string;
131
+ };
132
+ };
133
+ notes: {
134
+ type: string;
135
+ };
136
+ createdAt: {
137
+ type: string;
138
+ };
139
+ rawSource?: undefined;
140
+ title?: undefined;
141
+ distilledBrief?: undefined;
142
+ sourceType?: undefined;
143
+ sourceUrl?: undefined;
144
+ capturedAt?: undefined;
145
+ draftId?: undefined;
146
+ hookResearchId?: undefined;
147
+ body?: undefined;
148
+ validationReceipt?: undefined;
149
+ status?: undefined;
150
+ };
151
+ required: never[];
152
+ additionalProperties: boolean;
153
+ };
154
+ } | {
155
+ name: string;
156
+ description: string;
157
+ inputSchema: {
158
+ type: string;
159
+ properties: {
160
+ draftId: {
161
+ type: string;
162
+ };
163
+ ideaId: {
164
+ type: string;
165
+ description?: undefined;
166
+ };
167
+ hookResearchId: {
168
+ type: string;
169
+ };
170
+ title: {
171
+ type: string;
172
+ };
173
+ body: {
174
+ type: string;
175
+ };
176
+ validationReceipt: {
177
+ description: string;
178
+ };
179
+ createdAt: {
180
+ type: string;
181
+ };
182
+ status: {
183
+ type: string;
184
+ };
185
+ rawSource?: undefined;
186
+ distilledBrief?: undefined;
187
+ sourceType?: undefined;
188
+ sourceUrl?: undefined;
189
+ capturedAt?: undefined;
190
+ researchId?: undefined;
191
+ topic?: undefined;
192
+ keywords?: undefined;
193
+ sourcePosts?: undefined;
194
+ selectedPatterns?: undefined;
195
+ notes?: undefined;
196
+ };
197
+ required: string[];
198
+ additionalProperties: boolean;
199
+ };
200
+ })[];
201
+ export declare function resolveContentRoot(): string;
202
+ export declare function ensureContentLayout(root?: string): {
203
+ root: string;
204
+ directories: {
205
+ ideas: "linkedin/ideas";
206
+ hookResearch: "linkedin/research/hooks";
207
+ drafts: "linkedin/drafts";
208
+ published: "linkedin/published";
209
+ commentLibrary: "linkedin/comments/library";
210
+ };
211
+ };
212
+ export declare function capturePostIdeaTool(input: {
213
+ rawSource: string;
214
+ title?: string;
215
+ distilledBrief?: string;
216
+ ideaId?: string;
217
+ sourceType?: string;
218
+ sourceUrl?: string;
219
+ capturedAt?: string;
220
+ }): {
221
+ id: string;
222
+ path: string;
223
+ status: string;
224
+ createdAt: string;
225
+ updatedAt: string;
226
+ preview: string;
227
+ rawSourceExact: boolean;
228
+ };
229
+ export declare function listPostIdeasTool(input?: {
230
+ limit?: number;
231
+ }): ContentPostSummary[];
232
+ export declare function getPostIdeaTool(input: {
233
+ ideaId: string;
234
+ }): {
235
+ id: string;
236
+ path: string;
237
+ metadata: ArtifactMetadata;
238
+ markdown: string;
239
+ rawSource: string | undefined;
240
+ };
241
+ export declare function saveHookResearchTool(input: {
242
+ researchId?: string;
243
+ ideaId?: string;
244
+ topic?: string;
245
+ keywords?: string[];
246
+ sourcePosts?: Array<Record<string, unknown>>;
247
+ selectedPatterns?: string[];
248
+ notes?: string;
249
+ createdAt?: string;
250
+ }): {
251
+ id: string;
252
+ path: string;
253
+ status: string;
254
+ ideaId: string | undefined;
255
+ updatedAt: string;
256
+ preview: string;
257
+ };
258
+ export declare function savePostDraftTool(input: {
259
+ draftId?: string;
260
+ ideaId: string;
261
+ hookResearchId?: string;
262
+ title?: string;
263
+ body: string;
264
+ validationReceipt: unknown;
265
+ createdAt?: string;
266
+ status?: string;
267
+ }): {
268
+ id: string;
269
+ path: string;
270
+ status: string;
271
+ ideaId: string;
272
+ hookResearchId: string | undefined;
273
+ updatedAt: string;
274
+ preview: string;
275
+ };
276
+ export declare function listPostDraftsTool(input?: {
277
+ limit?: number;
278
+ }): ContentPostSummary[];
279
+ export declare function getPostDraftTool(input: {
280
+ draftId: string;
281
+ }): {
282
+ id: string;
283
+ path: string;
284
+ metadata: ArtifactMetadata;
285
+ markdown: string;
286
+ rawSource: string | undefined;
287
+ };
288
+ export declare function markPostPublishedTool(input: {
289
+ draftId?: string;
290
+ publishUrl: string;
291
+ activityId?: string;
292
+ publishedAt?: string;
293
+ finalText?: string;
294
+ title?: string;
295
+ }): {
296
+ id: string;
297
+ path: string;
298
+ status: string;
299
+ draftId: string | undefined;
300
+ publishUrl: string;
301
+ publishedAt: string;
302
+ updatedAt: string;
303
+ preview: string;
304
+ };
305
+ export declare function listPublishedPostsTool(input?: {
306
+ limit?: number;
307
+ }): ContentPostSummary[];
308
+ export {};