@sellable/mcp 0.1.236 → 0.1.237
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 +19 -14
- package/dist/server.js +28 -0
- package/dist/tools/campaigns.js +10 -7
- package/dist/tools/content-posts.d.ts +308 -0
- package/dist/tools/content-posts.js +611 -0
- package/dist/tools/leads.d.ts +2 -2
- package/dist/tools/registry.d.ts +105 -0
- package/dist/tools/registry.js +2 -0
- package/package.json +1 -1
- package/skills/create-campaign/SKILL.md +6 -4
- package/skills/create-campaign-v2/SOUL.md +6 -4
- package/skills/create-campaign-v2/core/flow.v2.json +1 -1
- package/skills/create-campaign-v2/references/watch-link-handoff.md +6 -4
- package/skills/create-post/SKILL.md +204 -1273
- package/skills/create-post/references/hook-research-playbook.md +68 -0
- package/skills/create-post/references/post-file-contract.md +88 -0
- package/skills/create-post/references/post-validation.md +95 -0
- package/skills/interview/SKILL.md +19 -0
- package/skills/load-voice/SKILL.md +8 -2
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
const CONTENT_ROOT_ENV = "SELLABLE_CONTENT_DIR";
|
|
5
|
+
const DEFAULT_PREVIEW_CHARS = 220;
|
|
6
|
+
const RELATIVE_DIRS = {
|
|
7
|
+
ideas: "linkedin/ideas",
|
|
8
|
+
hookResearch: "linkedin/research/hooks",
|
|
9
|
+
drafts: "linkedin/drafts",
|
|
10
|
+
published: "linkedin/published",
|
|
11
|
+
commentLibrary: "linkedin/comments/library",
|
|
12
|
+
};
|
|
13
|
+
export const contentPostToolDefinitions = [
|
|
14
|
+
{
|
|
15
|
+
name: "capture_post_idea",
|
|
16
|
+
description: "Capture a raw LinkedIn post idea into ~/.sellable/content/linkedin/ideas. Preserves the raw source exactly and returns an ID for later drafting.",
|
|
17
|
+
inputSchema: {
|
|
18
|
+
type: "object",
|
|
19
|
+
properties: {
|
|
20
|
+
rawSource: {
|
|
21
|
+
type: "string",
|
|
22
|
+
description: "The user's original idea, transcript, or freestyle note. This is preserved exactly.",
|
|
23
|
+
},
|
|
24
|
+
title: { type: "string" },
|
|
25
|
+
distilledBrief: {
|
|
26
|
+
type: "string",
|
|
27
|
+
description: "Optional short brief. Must not add claims beyond rawSource.",
|
|
28
|
+
},
|
|
29
|
+
ideaId: {
|
|
30
|
+
type: "string",
|
|
31
|
+
description: "Optional stable ID. Must be a safe filename without slashes.",
|
|
32
|
+
},
|
|
33
|
+
sourceType: { type: "string" },
|
|
34
|
+
sourceUrl: { type: "string" },
|
|
35
|
+
capturedAt: { type: "string" },
|
|
36
|
+
},
|
|
37
|
+
required: ["rawSource"],
|
|
38
|
+
additionalProperties: false,
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: "list_post_ideas",
|
|
43
|
+
description: "List captured LinkedIn post ideas with compact sanitized previews. Use get_post_idea for full Markdown.",
|
|
44
|
+
inputSchema: listInputSchema(),
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: "get_post_idea",
|
|
48
|
+
description: "Read one captured LinkedIn post idea by ID, including its full Markdown and exact raw source section.",
|
|
49
|
+
inputSchema: idInputSchema("ideaId"),
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: "save_hook_research",
|
|
53
|
+
description: "Save hook research for a LinkedIn post idea under ~/.sellable/content/linkedin/research/hooks.",
|
|
54
|
+
inputSchema: {
|
|
55
|
+
type: "object",
|
|
56
|
+
properties: {
|
|
57
|
+
researchId: { type: "string" },
|
|
58
|
+
ideaId: { type: "string" },
|
|
59
|
+
topic: { type: "string" },
|
|
60
|
+
keywords: { type: "array", items: { type: "string" } },
|
|
61
|
+
sourcePosts: {
|
|
62
|
+
type: "array",
|
|
63
|
+
items: { type: "object", additionalProperties: true },
|
|
64
|
+
},
|
|
65
|
+
selectedPatterns: {
|
|
66
|
+
type: "array",
|
|
67
|
+
items: { type: "string" },
|
|
68
|
+
},
|
|
69
|
+
notes: { type: "string" },
|
|
70
|
+
createdAt: { type: "string" },
|
|
71
|
+
},
|
|
72
|
+
required: [],
|
|
73
|
+
additionalProperties: false,
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: "save_post_draft",
|
|
78
|
+
description: "Save a validated LinkedIn post draft under ~/.sellable/content/linkedin/drafts.",
|
|
79
|
+
inputSchema: {
|
|
80
|
+
type: "object",
|
|
81
|
+
properties: {
|
|
82
|
+
draftId: { type: "string" },
|
|
83
|
+
ideaId: { type: "string" },
|
|
84
|
+
hookResearchId: { type: "string" },
|
|
85
|
+
title: { type: "string" },
|
|
86
|
+
body: { type: "string" },
|
|
87
|
+
validationReceipt: {
|
|
88
|
+
description: "Markdown string or structured object with hook, proof, voice, anti-AI, concrete-language, finalizer, and blocked/retry checks.",
|
|
89
|
+
},
|
|
90
|
+
createdAt: { type: "string" },
|
|
91
|
+
status: { type: "string" },
|
|
92
|
+
},
|
|
93
|
+
required: ["ideaId", "body", "validationReceipt"],
|
|
94
|
+
additionalProperties: false,
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
name: "list_post_drafts",
|
|
99
|
+
description: "List saved LinkedIn post drafts with compact sanitized previews. Use get_post_draft for full Markdown.",
|
|
100
|
+
inputSchema: listInputSchema(),
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: "get_post_draft",
|
|
104
|
+
description: "Read one saved LinkedIn post draft by ID.",
|
|
105
|
+
inputSchema: idInputSchema("draftId"),
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
name: "mark_post_published",
|
|
109
|
+
description: "Record a published LinkedIn post under ~/.sellable/content/linkedin/published/{year}. Drafts and published posts remain separate files.",
|
|
110
|
+
inputSchema: {
|
|
111
|
+
type: "object",
|
|
112
|
+
properties: {
|
|
113
|
+
draftId: { type: "string" },
|
|
114
|
+
publishUrl: { type: "string" },
|
|
115
|
+
activityId: { type: "string" },
|
|
116
|
+
publishedAt: { type: "string" },
|
|
117
|
+
finalText: { type: "string" },
|
|
118
|
+
title: { type: "string" },
|
|
119
|
+
},
|
|
120
|
+
required: ["publishUrl"],
|
|
121
|
+
additionalProperties: false,
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
name: "list_published_posts",
|
|
126
|
+
description: "List published LinkedIn post records with compact sanitized previews.",
|
|
127
|
+
inputSchema: listInputSchema(),
|
|
128
|
+
},
|
|
129
|
+
];
|
|
130
|
+
function listInputSchema() {
|
|
131
|
+
return {
|
|
132
|
+
type: "object",
|
|
133
|
+
properties: {
|
|
134
|
+
limit: { type: "number" },
|
|
135
|
+
},
|
|
136
|
+
required: [],
|
|
137
|
+
additionalProperties: false,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function idInputSchema(propertyName) {
|
|
141
|
+
return {
|
|
142
|
+
type: "object",
|
|
143
|
+
properties: {
|
|
144
|
+
[propertyName]: { type: "string" },
|
|
145
|
+
},
|
|
146
|
+
required: [propertyName],
|
|
147
|
+
additionalProperties: false,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
export function resolveContentRoot() {
|
|
151
|
+
const envValue = process.env[CONTENT_ROOT_ENV]?.trim();
|
|
152
|
+
if (envValue) {
|
|
153
|
+
return normalizeConfiguredRoot(envValue, CONTENT_ROOT_ENV);
|
|
154
|
+
}
|
|
155
|
+
const home = os.homedir();
|
|
156
|
+
if (!home || !path.isAbsolute(home)) {
|
|
157
|
+
throw new Error(`Unable to resolve Sellable content root: ${CONTENT_ROOT_ENV} is unset and home directory is unavailable.`);
|
|
158
|
+
}
|
|
159
|
+
return path.resolve(home, ".sellable/content");
|
|
160
|
+
}
|
|
161
|
+
function normalizeConfiguredRoot(value, label) {
|
|
162
|
+
const expanded = expandLeadingTilde(value);
|
|
163
|
+
if (!path.isAbsolute(expanded)) {
|
|
164
|
+
throw new Error(`${label} must be an absolute path.`);
|
|
165
|
+
}
|
|
166
|
+
return path.resolve(expanded);
|
|
167
|
+
}
|
|
168
|
+
function expandLeadingTilde(value) {
|
|
169
|
+
if (value === "~")
|
|
170
|
+
return os.homedir();
|
|
171
|
+
if (value.startsWith("~/") || value.startsWith("~\\")) {
|
|
172
|
+
return path.join(os.homedir(), value.slice(2));
|
|
173
|
+
}
|
|
174
|
+
return value;
|
|
175
|
+
}
|
|
176
|
+
export function ensureContentLayout(root = resolveContentRoot()) {
|
|
177
|
+
for (const relativeDir of Object.values(RELATIVE_DIRS)) {
|
|
178
|
+
fs.mkdirSync(safePath(root, relativeDir), { recursive: true });
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
root,
|
|
182
|
+
directories: { ...RELATIVE_DIRS },
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
export function capturePostIdeaTool(input) {
|
|
186
|
+
requireString(input.rawSource, "rawSource");
|
|
187
|
+
const now = normalizeDate(input.capturedAt);
|
|
188
|
+
const id = input.ideaId ??
|
|
189
|
+
`idea_${dateStamp(now)}_${slugify(input.title || input.rawSource)}`;
|
|
190
|
+
const safeId = normalizeArtifactId(id, "ideaId");
|
|
191
|
+
const metadata = {
|
|
192
|
+
id: safeId,
|
|
193
|
+
type: "idea",
|
|
194
|
+
status: "captured",
|
|
195
|
+
title: input.title,
|
|
196
|
+
sourceType: input.sourceType,
|
|
197
|
+
sourceUrl: input.sourceUrl,
|
|
198
|
+
createdAt: now,
|
|
199
|
+
updatedAt: now,
|
|
200
|
+
};
|
|
201
|
+
const relativePath = `${RELATIVE_DIRS.ideas}/${safeId}.md`;
|
|
202
|
+
const markdown = buildMarkdown(metadata, [
|
|
203
|
+
["Distilled Brief", input.distilledBrief || ""],
|
|
204
|
+
["Raw Source", rawBlock(input.rawSource)],
|
|
205
|
+
["Source Metadata", jsonBlock(stripUndefined({
|
|
206
|
+
sourceType: input.sourceType,
|
|
207
|
+
sourceUrl: input.sourceUrl,
|
|
208
|
+
capturedAt: now,
|
|
209
|
+
}))],
|
|
210
|
+
]);
|
|
211
|
+
writeArtifact(relativePath, markdown);
|
|
212
|
+
return {
|
|
213
|
+
id: safeId,
|
|
214
|
+
path: relativePath,
|
|
215
|
+
status: metadata.status,
|
|
216
|
+
createdAt: now,
|
|
217
|
+
updatedAt: now,
|
|
218
|
+
preview: sanitizedPreview(input.rawSource),
|
|
219
|
+
rawSourceExact: true,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
export function listPostIdeasTool(input) {
|
|
223
|
+
return listArtifacts(RELATIVE_DIRS.ideas, "idea", input?.limit);
|
|
224
|
+
}
|
|
225
|
+
export function getPostIdeaTool(input) {
|
|
226
|
+
return getArtifact(RELATIVE_DIRS.ideas, input.ideaId, "ideaId");
|
|
227
|
+
}
|
|
228
|
+
export function saveHookResearchTool(input) {
|
|
229
|
+
const now = normalizeDate(input.createdAt);
|
|
230
|
+
const id = input.researchId ??
|
|
231
|
+
`research_${dateStamp(now)}_${slugify(input.topic || input.ideaId || "hooks")}`;
|
|
232
|
+
const safeId = normalizeArtifactId(id, "researchId");
|
|
233
|
+
const safeIdeaId = input.ideaId
|
|
234
|
+
? normalizeArtifactId(input.ideaId, "ideaId")
|
|
235
|
+
: undefined;
|
|
236
|
+
const metadata = {
|
|
237
|
+
id: safeId,
|
|
238
|
+
type: "hook_research",
|
|
239
|
+
status: "researched",
|
|
240
|
+
title: input.topic,
|
|
241
|
+
ideaId: safeIdeaId,
|
|
242
|
+
createdAt: now,
|
|
243
|
+
updatedAt: now,
|
|
244
|
+
};
|
|
245
|
+
const relativePath = `${RELATIVE_DIRS.hookResearch}/${safeId}.md`;
|
|
246
|
+
const markdown = buildMarkdown(metadata, [
|
|
247
|
+
["Keywords", listBlock(input.keywords ?? [])],
|
|
248
|
+
["Selected Patterns", listBlock(input.selectedPatterns ?? [])],
|
|
249
|
+
["Source Posts", jsonBlock(input.sourcePosts ?? [])],
|
|
250
|
+
["Notes", input.notes || ""],
|
|
251
|
+
]);
|
|
252
|
+
writeArtifact(relativePath, markdown);
|
|
253
|
+
return {
|
|
254
|
+
id: safeId,
|
|
255
|
+
path: relativePath,
|
|
256
|
+
status: metadata.status,
|
|
257
|
+
ideaId: safeIdeaId,
|
|
258
|
+
updatedAt: now,
|
|
259
|
+
preview: sanitizedPreview(input.notes || input.topic || ""),
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
export function savePostDraftTool(input) {
|
|
263
|
+
const now = normalizeDate(input.createdAt);
|
|
264
|
+
const safeIdeaId = normalizeArtifactId(input.ideaId, "ideaId");
|
|
265
|
+
const safeResearchId = input.hookResearchId
|
|
266
|
+
? normalizeArtifactId(input.hookResearchId, "hookResearchId")
|
|
267
|
+
: undefined;
|
|
268
|
+
requireString(input.body, "body");
|
|
269
|
+
const id = input.draftId ??
|
|
270
|
+
`draft_${dateStamp(now)}_${slugify(input.title || safeIdeaId)}_v1`;
|
|
271
|
+
const safeId = normalizeArtifactId(id, "draftId");
|
|
272
|
+
const status = input.status || "draft";
|
|
273
|
+
if (!["draft", "ready", "needs_revision"].includes(status)) {
|
|
274
|
+
throw new Error(`Unsupported draft status: ${status}`);
|
|
275
|
+
}
|
|
276
|
+
const metadata = {
|
|
277
|
+
id: safeId,
|
|
278
|
+
type: "draft",
|
|
279
|
+
status,
|
|
280
|
+
title: input.title,
|
|
281
|
+
ideaId: safeIdeaId,
|
|
282
|
+
hookResearchId: safeResearchId,
|
|
283
|
+
createdAt: now,
|
|
284
|
+
updatedAt: now,
|
|
285
|
+
};
|
|
286
|
+
const relativePath = `${RELATIVE_DIRS.drafts}/${safeId}.md`;
|
|
287
|
+
const markdown = buildMarkdown(metadata, [
|
|
288
|
+
["Draft Body", input.body],
|
|
289
|
+
["Validation Receipt", formatReceipt(input.validationReceipt)],
|
|
290
|
+
]);
|
|
291
|
+
writeArtifact(relativePath, markdown);
|
|
292
|
+
return {
|
|
293
|
+
id: safeId,
|
|
294
|
+
path: relativePath,
|
|
295
|
+
status,
|
|
296
|
+
ideaId: safeIdeaId,
|
|
297
|
+
hookResearchId: safeResearchId,
|
|
298
|
+
updatedAt: now,
|
|
299
|
+
preview: sanitizedPreview(input.body),
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
export function listPostDraftsTool(input) {
|
|
303
|
+
return listArtifacts(RELATIVE_DIRS.drafts, "draft", input?.limit);
|
|
304
|
+
}
|
|
305
|
+
export function getPostDraftTool(input) {
|
|
306
|
+
return getArtifact(RELATIVE_DIRS.drafts, input.draftId, "draftId");
|
|
307
|
+
}
|
|
308
|
+
export function markPostPublishedTool(input) {
|
|
309
|
+
requireString(input.publishUrl, "publishUrl");
|
|
310
|
+
const publishedAt = normalizeDate(input.publishedAt);
|
|
311
|
+
const activityId = input.activityId || extractLinkedInActivityId(input.publishUrl);
|
|
312
|
+
const id = `post_${activityId ? normalizeSlug(activityId) : slugify(input.publishUrl)}`;
|
|
313
|
+
const safeId = normalizeArtifactId(id, "publishedPostId");
|
|
314
|
+
const safeDraftId = input.draftId
|
|
315
|
+
? normalizeArtifactId(input.draftId, "draftId")
|
|
316
|
+
: undefined;
|
|
317
|
+
const year = new Date(publishedAt).getUTCFullYear().toString();
|
|
318
|
+
const relativePath = `${RELATIVE_DIRS.published}/${year}/${safeId}.md`;
|
|
319
|
+
const existing = readArtifactIfExists(relativePath);
|
|
320
|
+
const createdAt = existing?.metadata.createdAt || publishedAt;
|
|
321
|
+
const metadata = {
|
|
322
|
+
id: safeId,
|
|
323
|
+
type: "published_post",
|
|
324
|
+
status: "published",
|
|
325
|
+
title: input.title || existing?.metadata.title,
|
|
326
|
+
draftId: safeDraftId,
|
|
327
|
+
publishUrl: input.publishUrl,
|
|
328
|
+
activityId: activityId || undefined,
|
|
329
|
+
publishedAt,
|
|
330
|
+
createdAt,
|
|
331
|
+
updatedAt: publishedAt,
|
|
332
|
+
};
|
|
333
|
+
const markdown = buildMarkdown(metadata, [
|
|
334
|
+
["Final Text", input.finalText || ""],
|
|
335
|
+
["Publish Metadata", jsonBlock(stripUndefined({
|
|
336
|
+
draftId: safeDraftId,
|
|
337
|
+
publishUrl: input.publishUrl,
|
|
338
|
+
activityId: activityId || undefined,
|
|
339
|
+
publishedAt,
|
|
340
|
+
}))],
|
|
341
|
+
["Future Metrics", jsonBlock({
|
|
342
|
+
impressions: null,
|
|
343
|
+
reactions: null,
|
|
344
|
+
comments: null,
|
|
345
|
+
snapshots: [],
|
|
346
|
+
})],
|
|
347
|
+
]);
|
|
348
|
+
writeArtifact(relativePath, markdown);
|
|
349
|
+
return {
|
|
350
|
+
id: safeId,
|
|
351
|
+
path: relativePath,
|
|
352
|
+
status: metadata.status,
|
|
353
|
+
draftId: safeDraftId,
|
|
354
|
+
publishUrl: input.publishUrl,
|
|
355
|
+
publishedAt,
|
|
356
|
+
updatedAt: publishedAt,
|
|
357
|
+
preview: sanitizedPreview(input.finalText || input.publishUrl),
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
export function listPublishedPostsTool(input) {
|
|
361
|
+
const root = resolveContentRoot();
|
|
362
|
+
ensureContentLayout(root);
|
|
363
|
+
const publishedRoot = safePath(root, RELATIVE_DIRS.published);
|
|
364
|
+
if (!fs.existsSync(publishedRoot))
|
|
365
|
+
return [];
|
|
366
|
+
const artifacts = [];
|
|
367
|
+
for (const yearDir of fs.readdirSync(publishedRoot)) {
|
|
368
|
+
if (!/^\d{4}$/.test(yearDir))
|
|
369
|
+
continue;
|
|
370
|
+
const fullYearDir = path.join(publishedRoot, yearDir);
|
|
371
|
+
if (!fs.statSync(fullYearDir).isDirectory())
|
|
372
|
+
continue;
|
|
373
|
+
artifacts.push(...readArtifactsFromDir(`${RELATIVE_DIRS.published}/${yearDir}`));
|
|
374
|
+
}
|
|
375
|
+
return summarizeArtifacts(artifacts, input?.limit);
|
|
376
|
+
}
|
|
377
|
+
function getArtifact(relativeDir, id, label) {
|
|
378
|
+
const safeId = normalizeArtifactId(id, label);
|
|
379
|
+
const relativePath = `${relativeDir}/${safeId}.md`;
|
|
380
|
+
const artifact = readArtifactIfExists(relativePath);
|
|
381
|
+
if (!artifact) {
|
|
382
|
+
throw new Error(`No ${label} found for ID: ${safeId}`);
|
|
383
|
+
}
|
|
384
|
+
return {
|
|
385
|
+
id: artifact.metadata.id,
|
|
386
|
+
path: relativePath,
|
|
387
|
+
metadata: artifact.metadata,
|
|
388
|
+
markdown: artifact.markdown,
|
|
389
|
+
rawSource: extractRawSource(artifact.markdown),
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
function listArtifacts(relativeDir, kind, limit) {
|
|
393
|
+
const artifacts = readArtifactsFromDir(relativeDir).filter((artifact) => artifact.metadata.type === kind);
|
|
394
|
+
return summarizeArtifacts(artifacts, limit);
|
|
395
|
+
}
|
|
396
|
+
function summarizeArtifacts(artifacts, limit) {
|
|
397
|
+
const boundedLimit = typeof limit === "number" && Number.isFinite(limit)
|
|
398
|
+
? Math.max(0, Math.min(Math.floor(limit), 100))
|
|
399
|
+
: 25;
|
|
400
|
+
return artifacts
|
|
401
|
+
.sort((a, b) => String(b.metadata.updatedAt).localeCompare(String(a.metadata.updatedAt)))
|
|
402
|
+
.slice(0, boundedLimit)
|
|
403
|
+
.map((artifact) => ({
|
|
404
|
+
id: artifact.metadata.id,
|
|
405
|
+
type: artifact.metadata.type,
|
|
406
|
+
status: artifact.metadata.status,
|
|
407
|
+
title: artifact.metadata.title,
|
|
408
|
+
path: artifact.relativePath,
|
|
409
|
+
updatedAt: artifact.metadata.updatedAt,
|
|
410
|
+
preview: sanitizedPreview(previewBasis(artifact.markdown)),
|
|
411
|
+
}));
|
|
412
|
+
}
|
|
413
|
+
function readArtifactsFromDir(relativeDir) {
|
|
414
|
+
const root = resolveContentRoot();
|
|
415
|
+
ensureContentLayout(root);
|
|
416
|
+
const dir = safePath(root, relativeDir);
|
|
417
|
+
if (!fs.existsSync(dir))
|
|
418
|
+
return [];
|
|
419
|
+
return fs
|
|
420
|
+
.readdirSync(dir)
|
|
421
|
+
.filter((entry) => entry.endsWith(".md"))
|
|
422
|
+
.map((entry) => `${relativeDir}/${entry}`)
|
|
423
|
+
.map(readArtifactIfExists)
|
|
424
|
+
.filter((artifact) => artifact !== null);
|
|
425
|
+
}
|
|
426
|
+
function readArtifactIfExists(relativePath) {
|
|
427
|
+
const root = resolveContentRoot();
|
|
428
|
+
const filePath = safePath(root, relativePath);
|
|
429
|
+
if (!fs.existsSync(filePath))
|
|
430
|
+
return null;
|
|
431
|
+
const markdown = fs.readFileSync(filePath, "utf8");
|
|
432
|
+
return {
|
|
433
|
+
markdown,
|
|
434
|
+
metadata: parseMetadata(markdown),
|
|
435
|
+
relativePath,
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
function writeArtifact(relativePath, markdown) {
|
|
439
|
+
const root = resolveContentRoot();
|
|
440
|
+
const filePath = safeWritablePath(root, relativePath);
|
|
441
|
+
fs.writeFileSync(filePath, markdown);
|
|
442
|
+
}
|
|
443
|
+
function safePath(root, relativePath) {
|
|
444
|
+
validateRelativePath(relativePath);
|
|
445
|
+
const fullPath = path.resolve(root, ...relativePath.split("/"));
|
|
446
|
+
const normalizedRoot = path.resolve(root);
|
|
447
|
+
if (!isPathInside(fullPath, normalizedRoot)) {
|
|
448
|
+
throw new Error(`Resolved path escapes content root: ${relativePath}`);
|
|
449
|
+
}
|
|
450
|
+
return fullPath;
|
|
451
|
+
}
|
|
452
|
+
function safeWritablePath(root, relativePath) {
|
|
453
|
+
fs.mkdirSync(root, { recursive: true });
|
|
454
|
+
const rootReal = fs.realpathSync(root);
|
|
455
|
+
const fullPath = safePath(rootReal, relativePath);
|
|
456
|
+
const parent = path.dirname(fullPath);
|
|
457
|
+
fs.mkdirSync(parent, { recursive: true });
|
|
458
|
+
const parentReal = fs.realpathSync(parent);
|
|
459
|
+
if (!isPathInside(parentReal, rootReal)) {
|
|
460
|
+
throw new Error(`Resolved parent path escapes content root: ${relativePath}`);
|
|
461
|
+
}
|
|
462
|
+
if (fs.existsSync(fullPath) && fs.lstatSync(fullPath).isSymbolicLink()) {
|
|
463
|
+
throw new Error(`Refusing to write through symlink: ${relativePath}`);
|
|
464
|
+
}
|
|
465
|
+
return path.join(parentReal, path.basename(fullPath));
|
|
466
|
+
}
|
|
467
|
+
function validateRelativePath(relativePath) {
|
|
468
|
+
if (!relativePath || path.isAbsolute(relativePath)) {
|
|
469
|
+
throw new Error(`Invalid relative path: ${relativePath}`);
|
|
470
|
+
}
|
|
471
|
+
if (relativePath.includes("\\") || relativePath.includes("\0")) {
|
|
472
|
+
throw new Error(`Invalid relative path: ${relativePath}`);
|
|
473
|
+
}
|
|
474
|
+
const decoded = decodeMaybe(relativePath);
|
|
475
|
+
if (decoded !== relativePath) {
|
|
476
|
+
validateRelativePath(decoded);
|
|
477
|
+
}
|
|
478
|
+
for (const segment of relativePath.split("/")) {
|
|
479
|
+
if (!segment || segment === "." || segment === "..") {
|
|
480
|
+
throw new Error(`Invalid relative path segment: ${relativePath}`);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
function isPathInside(candidate, root) {
|
|
485
|
+
const relative = path.relative(root, candidate);
|
|
486
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
487
|
+
}
|
|
488
|
+
function normalizeArtifactId(id, label) {
|
|
489
|
+
requireString(id, label);
|
|
490
|
+
const trimmed = id.trim();
|
|
491
|
+
const decoded = decodeMaybe(trimmed);
|
|
492
|
+
if (trimmed !== id ||
|
|
493
|
+
path.isAbsolute(trimmed) ||
|
|
494
|
+
trimmed.includes("/") ||
|
|
495
|
+
trimmed.includes("\\") ||
|
|
496
|
+
trimmed.includes("\0") ||
|
|
497
|
+
decoded.includes("/") ||
|
|
498
|
+
decoded.includes("\\") ||
|
|
499
|
+
trimmed.includes("..") ||
|
|
500
|
+
decoded.includes("..") ||
|
|
501
|
+
!/^[A-Za-z0-9._-]+$/.test(trimmed)) {
|
|
502
|
+
throw new Error(`${label} must be a safe filename ID.`);
|
|
503
|
+
}
|
|
504
|
+
return trimmed;
|
|
505
|
+
}
|
|
506
|
+
function requireString(value, label) {
|
|
507
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
508
|
+
throw new Error(`${label} is required.`);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
function normalizeDate(value) {
|
|
512
|
+
if (!value)
|
|
513
|
+
return new Date().toISOString();
|
|
514
|
+
const parsed = new Date(value);
|
|
515
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
516
|
+
throw new Error(`Invalid date: ${value}`);
|
|
517
|
+
}
|
|
518
|
+
return parsed.toISOString();
|
|
519
|
+
}
|
|
520
|
+
function dateStamp(isoDate) {
|
|
521
|
+
return isoDate.slice(0, 10).replace(/-/g, "");
|
|
522
|
+
}
|
|
523
|
+
function slugify(value) {
|
|
524
|
+
return normalizeSlug(value).slice(0, 60) || "post";
|
|
525
|
+
}
|
|
526
|
+
function normalizeSlug(value) {
|
|
527
|
+
return value
|
|
528
|
+
.toLowerCase()
|
|
529
|
+
.replace(/https?:\/\//g, "")
|
|
530
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
531
|
+
.replace(/^-+|-+$/g, "");
|
|
532
|
+
}
|
|
533
|
+
function decodeMaybe(value) {
|
|
534
|
+
try {
|
|
535
|
+
return decodeURIComponent(value);
|
|
536
|
+
}
|
|
537
|
+
catch {
|
|
538
|
+
return value;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
function buildMarkdown(metadata, sections) {
|
|
542
|
+
const title = metadata.title || metadata.id;
|
|
543
|
+
const body = sections
|
|
544
|
+
.map(([heading, content]) => `## ${heading}\n\n${content}`.trimEnd())
|
|
545
|
+
.join("\n\n");
|
|
546
|
+
return [
|
|
547
|
+
"<!-- sellable:metadata",
|
|
548
|
+
JSON.stringify(stripUndefined(metadata), null, 2),
|
|
549
|
+
"-->",
|
|
550
|
+
"",
|
|
551
|
+
`# ${title}`,
|
|
552
|
+
"",
|
|
553
|
+
body,
|
|
554
|
+
"",
|
|
555
|
+
].join("\n");
|
|
556
|
+
}
|
|
557
|
+
function parseMetadata(markdown) {
|
|
558
|
+
const match = markdown.match(/^<!-- sellable:metadata\n([\s\S]*?)\n-->/m);
|
|
559
|
+
if (!match) {
|
|
560
|
+
throw new Error("Missing Sellable content metadata block.");
|
|
561
|
+
}
|
|
562
|
+
return JSON.parse(match[1]);
|
|
563
|
+
}
|
|
564
|
+
function rawBlock(rawSource) {
|
|
565
|
+
return [
|
|
566
|
+
"<!-- sellable:raw-source:start -->",
|
|
567
|
+
rawSource,
|
|
568
|
+
"<!-- sellable:raw-source:end -->",
|
|
569
|
+
].join("\n");
|
|
570
|
+
}
|
|
571
|
+
function extractRawSource(markdown) {
|
|
572
|
+
const match = markdown.match(/<!-- sellable:raw-source:start -->\n([\s\S]*?)\n<!-- sellable:raw-source:end -->/);
|
|
573
|
+
return match?.[1];
|
|
574
|
+
}
|
|
575
|
+
function previewBasis(markdown) {
|
|
576
|
+
return (extractRawSource(markdown) ||
|
|
577
|
+
markdown.replace(/^<!-- sellable:metadata\n[\s\S]*?\n-->\n*/, ""));
|
|
578
|
+
}
|
|
579
|
+
function jsonBlock(value) {
|
|
580
|
+
return ["```json", JSON.stringify(value, null, 2), "```"].join("\n");
|
|
581
|
+
}
|
|
582
|
+
function listBlock(values) {
|
|
583
|
+
if (values.length === 0)
|
|
584
|
+
return "";
|
|
585
|
+
return values.map((value) => `- ${value}`).join("\n");
|
|
586
|
+
}
|
|
587
|
+
function formatReceipt(value) {
|
|
588
|
+
if (typeof value === "string")
|
|
589
|
+
return value;
|
|
590
|
+
return jsonBlock(value);
|
|
591
|
+
}
|
|
592
|
+
function stripUndefined(value) {
|
|
593
|
+
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
|
|
594
|
+
}
|
|
595
|
+
function sanitizedPreview(value) {
|
|
596
|
+
const redacted = value
|
|
597
|
+
.replace(/\bsk-[A-Za-z0-9_-]{10,}\b/g, "[redacted]")
|
|
598
|
+
.replace(/\b(?:api[_-]?key|token|secret|password)\b\s*[:=]\s*\S+/gi, "[redacted]")
|
|
599
|
+
.replace(/\s+/g, " ")
|
|
600
|
+
.trim();
|
|
601
|
+
return redacted.length > DEFAULT_PREVIEW_CHARS
|
|
602
|
+
? `${redacted.slice(0, DEFAULT_PREVIEW_CHARS - 3)}...`
|
|
603
|
+
: redacted;
|
|
604
|
+
}
|
|
605
|
+
function extractLinkedInActivityId(url) {
|
|
606
|
+
const decoded = decodeMaybe(url);
|
|
607
|
+
const match = decoded.match(/urn:li:activity:(\d+)/) ||
|
|
608
|
+
decoded.match(/activity[-_:](\d+)/i) ||
|
|
609
|
+
decoded.match(/activityId=(\d+)/i);
|
|
610
|
+
return match?.[1] ?? null;
|
|
611
|
+
}
|
package/dist/tools/leads.d.ts
CHANGED
|
@@ -3922,7 +3922,7 @@ export declare function searchSignals(input: SignalSearchInput): Promise<{
|
|
|
3922
3922
|
id: string;
|
|
3923
3923
|
url: string | undefined;
|
|
3924
3924
|
matchedKeyword: string | null;
|
|
3925
|
-
searchType: "company" | "
|
|
3925
|
+
searchType: "company" | "post" | "keyword" | "profile" | null;
|
|
3926
3926
|
displayLabel: string | null;
|
|
3927
3927
|
authorName: string;
|
|
3928
3928
|
authorHeadline: string;
|
|
@@ -3941,7 +3941,7 @@ export declare function searchSignals(input: SignalSearchInput): Promise<{
|
|
|
3941
3941
|
}[];
|
|
3942
3942
|
keywordResults: {
|
|
3943
3943
|
keyword: string;
|
|
3944
|
-
searchType: "company" | "
|
|
3944
|
+
searchType: "company" | "post" | "keyword" | "profile";
|
|
3945
3945
|
displayLabel: string;
|
|
3946
3946
|
tabId: string | null;
|
|
3947
3947
|
postCount: number;
|