@sellable/mcp 0.1.235 → 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.
@@ -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
+ }
@@ -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" | "keyword" | "profile" | "post" | null;
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" | "keyword" | "profile" | "post";
3944
+ searchType: "company" | "post" | "keyword" | "profile";
3945
3945
  displayLabel: string;
3946
3946
  tabId: string | null;
3947
3947
  postCount: number;