linkedin-toolkit-mcp 2.0.0

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.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +80 -0
  3. package/dist/bridge.d.ts +95 -0
  4. package/dist/bridge.js +259 -0
  5. package/dist/bridge.js.map +1 -0
  6. package/dist/cli.d.ts +83 -0
  7. package/dist/cli.js +898 -0
  8. package/dist/cli.js.map +1 -0
  9. package/dist/config.d.ts +47 -0
  10. package/dist/config.js +149 -0
  11. package/dist/config.js.map +1 -0
  12. package/dist/contract.d.ts +6770 -0
  13. package/dist/contract.js +926 -0
  14. package/dist/contract.js.map +1 -0
  15. package/dist/db.d.ts +64 -0
  16. package/dist/db.js +542 -0
  17. package/dist/db.js.map +1 -0
  18. package/dist/fake-data.d.ts +277 -0
  19. package/dist/fake-data.js +789 -0
  20. package/dist/fake-data.js.map +1 -0
  21. package/dist/fake-extension.d.ts +51 -0
  22. package/dist/fake-extension.js +151 -0
  23. package/dist/fake-extension.js.map +1 -0
  24. package/dist/gen.d.ts +5 -0
  25. package/dist/gen.js +22 -0
  26. package/dist/gen.js.map +1 -0
  27. package/dist/http.d.ts +27 -0
  28. package/dist/http.js +329 -0
  29. package/dist/http.js.map +1 -0
  30. package/dist/openapi.d.ts +30 -0
  31. package/dist/openapi.js +306 -0
  32. package/dist/openapi.js.map +1 -0
  33. package/dist/prompts.d.ts +9 -0
  34. package/dist/prompts.js +65 -0
  35. package/dist/prompts.js.map +1 -0
  36. package/dist/resources.d.ts +7 -0
  37. package/dist/resources.js +73 -0
  38. package/dist/resources.js.map +1 -0
  39. package/dist/server.d.ts +3 -0
  40. package/dist/server.js +38 -0
  41. package/dist/server.js.map +1 -0
  42. package/dist/toolkit.d.ts +82 -0
  43. package/dist/toolkit.js +171 -0
  44. package/dist/toolkit.js.map +1 -0
  45. package/dist/tools.d.ts +31 -0
  46. package/dist/tools.js +103 -0
  47. package/dist/tools.js.map +1 -0
  48. package/dist/webhooks.d.ts +52 -0
  49. package/dist/webhooks.js +112 -0
  50. package/dist/webhooks.js.map +1 -0
  51. package/openapi.json +8334 -0
  52. package/package.json +45 -0
  53. package/tools.json +1205 -0
@@ -0,0 +1,789 @@
1
+ /** What `status.get { verify: true }` reports in fake mode. */
2
+ export const FAKE_CLIENT_VERSION = '1.13.35548';
3
+ export const FAKE_VERIFIED_ENDPOINTS = [
4
+ 'me',
5
+ 'profile',
6
+ 'profileExperience',
7
+ 'search',
8
+ 'company',
9
+ 'companyEmployees',
10
+ 'connections',
11
+ 'sentInvitations',
12
+ 'conversations',
13
+ 'followers',
14
+ 'memberPosts',
15
+ ];
16
+ export const FAKE_UNVERIFIED_ENDPOINTS = [
17
+ 'comments',
18
+ 'groupMembers',
19
+ 'eventAttendees',
20
+ 'salesNavSearch',
21
+ 'recruiterSearch',
22
+ 'follow',
23
+ 'invite',
24
+ 'message',
25
+ ];
26
+ export const FAKE_BANNER = 'FAKE MODE — no LinkedIn calls are made; demo data only';
27
+ const CAPTURED_AT = Date.UTC(2026, 8, 1, 9, 0, 0);
28
+ const DAY = 86_400_000;
29
+ /** 25 invented people at invented companies. */
30
+ const SEEDS = [
31
+ ['Marisol', 'Adeyemi', 'VP Platform Engineering', 'Northwind Robotics', 'London, United Kingdom', 2],
32
+ ['Tobias', 'Reinhardt', 'Head of Infrastructure', 'Bellhaven Analytics', 'Berlin, Germany', 2],
33
+ ['Priya', 'Ranganathan', 'Director of Engineering', 'Quillstone Systems', 'Manchester, United Kingdom', 1],
34
+ ['Émile', 'Ducharme', 'Staff Site Reliability Engineer', 'Fernwood Labs', 'Montréal, Canada', 3],
35
+ ['Nadia', 'Karimov', 'CTO', 'Marrowgate Health', 'Amsterdam, Netherlands', 2],
36
+ ['Callum', 'Whitfield', 'Engineering Manager, Payments', 'Tidewater Logistics', 'Bristol, United Kingdom', 2],
37
+ ['Ingrid', 'Halvorsen', 'Principal Engineer', 'Ashgrove Bioscience', 'Oslo, Norway', 3],
38
+ ['Rafael', 'Sotomayor', 'Head of Data Platform', 'Calderon Freight', 'Madrid, Spain', 2],
39
+ ['Wenjun', 'Xiao', 'Director, Developer Experience', 'Pinemark Software', 'Dublin, Ireland', 1],
40
+ ['Anneke', 'Vos', 'VP Engineering', 'Verity Loop', 'Rotterdam, Netherlands', 2],
41
+ ['Dmitri', 'Barinov', 'Lead Backend Engineer', 'Northwind Robotics', 'Warsaw, Poland', 3],
42
+ ['Sofia', 'Marchetti', 'Head of Security Engineering', 'Bellhaven Analytics', 'Milan, Italy', 2],
43
+ ['Oluwaseun', 'Bakare', 'Engineering Director', 'Quillstone Systems', 'Lagos, Nigeria', 2],
44
+ ['Hana', 'Kobayashi', 'Staff Engineer, Platform', 'Fernwood Labs', 'Copenhagen, Denmark', 3],
45
+ ['Lucas', 'Fenwick', 'Chief Architect', 'Marrowgate Health', 'Edinburgh, United Kingdom', 1],
46
+ ['Amara', 'Nwosu', 'Head of SRE', 'Tidewater Logistics', 'Lisbon, Portugal', 2],
47
+ ['Bjorn', 'Lindqvist', 'VP Technology', 'Ashgrove Bioscience', 'Stockholm, Sweden', 2],
48
+ ['Yasmin', 'Haddad', 'Director of Cloud Platform', 'Calderon Freight', 'Paris, France', 3],
49
+ ['Gareth', 'Pemberton', 'Engineering Lead, Core Services', 'Pinemark Software', 'Cardiff, United Kingdom', 2],
50
+ ['Elena', 'Vasquez', 'Head of Infrastructure', 'Verity Loop', 'Barcelona, Spain', 2],
51
+ ['Kofi', 'Mensah', 'Principal Platform Engineer', 'Northwind Robotics', 'Accra, Ghana', 3],
52
+ ['Astrid', 'Bergström', 'CTO', 'Bellhaven Analytics', 'Helsinki, Finland', 2],
53
+ ['Ravi', 'Chandrasekaran', 'Director of Engineering', 'Quillstone Systems', 'Bengaluru, India', 2],
54
+ ['Freya', 'Ashworth', 'Head of Developer Platform', 'Fernwood Labs', 'Leeds, United Kingdom', 1],
55
+ ['Mateo', 'Iglesias', 'VP Infrastructure', 'Marrowgate Health', 'Valencia, Spain', 2],
56
+ ];
57
+ const slug = (first, last) => `${first}-${last}`
58
+ .toLowerCase()
59
+ .normalize('NFKD')
60
+ .replace(/[̀-ͯ]/g, '')
61
+ .replace(/[^a-z0-9]+/g, '-');
62
+ const companySlug = (name) => name.toLowerCase().replace(/[^a-z0-9]+/g, '-');
63
+ function makeProfile(seed, index) {
64
+ const [firstName, lastName, title, company, location, connectionDegree] = seed;
65
+ const publicId = slug(firstName, lastName);
66
+ return {
67
+ publicId,
68
+ url: `https://www.linkedin.com/in/${publicId}/`,
69
+ firstName,
70
+ lastName,
71
+ fullName: `${firstName} ${lastName}`,
72
+ headline: `${title} at ${company}`,
73
+ title,
74
+ company,
75
+ companyUrn: `urn:li:company:${companySlug(company)}`,
76
+ location,
77
+ industry: 'Software Development',
78
+ connectionDegree,
79
+ capturedAt: CAPTURED_AT + index * 1000,
80
+ source: 'search',
81
+ };
82
+ }
83
+ export const FAKE_PROFILES = SEEDS.map(makeProfile);
84
+ const COMPANY_NAMES = [...new Set(SEEDS.map((seed) => seed[3]))];
85
+ export const FAKE_COMPANIES = COMPANY_NAMES.map((name, index) => ({
86
+ universalName: companySlug(name),
87
+ name,
88
+ url: `https://www.linkedin.com/company/${companySlug(name)}/`,
89
+ industry: 'Software Development',
90
+ size: ['51-200', '201-500', '501-1,000', '1,001-5,000'][index % 4],
91
+ hq: FAKE_PROFILES.find((profile) => profile.company === name)?.location ?? 'London, United Kingdom',
92
+ website: `https://${companySlug(name)}.example`,
93
+ description: `${name} is a fictional company used for the LinkedIn Toolkit demo dataset.`,
94
+ followerCount: 1200 + index * 830,
95
+ capturedAt: CAPTURED_AT,
96
+ }));
97
+ const profileFor = (publicId) => FAKE_PROFILES.find((profile) => profile.publicId === publicId);
98
+ export function createDemoState() {
99
+ const lists = [
100
+ {
101
+ listId: 'list_platform_leads',
102
+ name: 'Platform engineering leads',
103
+ tags: ['platform', 'q3'],
104
+ createdAt: CAPTURED_AT - 14 * DAY,
105
+ members: FAKE_PROFILES.slice(0, 10).map((profile, index) => ({
106
+ publicId: profile.publicId,
107
+ profile,
108
+ addedAt: CAPTURED_AT - (14 - index) * DAY,
109
+ tags: ['platform'],
110
+ contactedBefore: index < 3,
111
+ signals: index % 3 === 0 ? ['posted in the last 30 days'] : [],
112
+ })),
113
+ },
114
+ {
115
+ listId: 'list_dach_outbound',
116
+ name: 'Q3 outbound — DACH',
117
+ tags: ['dach'],
118
+ createdAt: CAPTURED_AT - 9 * DAY,
119
+ members: FAKE_PROFILES.filter((profile) => ['Germany', 'Netherlands', 'Poland', 'Sweden'].some((country) => profile.location.endsWith(country))).map((profile, index) => ({
120
+ publicId: profile.publicId,
121
+ profile,
122
+ addedAt: CAPTURED_AT - (9 - index) * DAY,
123
+ tags: ['dach'],
124
+ contactedBefore: false,
125
+ signals: [],
126
+ })),
127
+ },
128
+ {
129
+ listId: 'list_event_warm',
130
+ name: 'Warm intros from PlatformConf',
131
+ tags: ['event', 'warm'],
132
+ createdAt: CAPTURED_AT - 4 * DAY,
133
+ members: FAKE_PROFILES.slice(18, 25).map((profile, index) => ({
134
+ publicId: profile.publicId,
135
+ profile,
136
+ addedAt: CAPTURED_AT - (4 - index * 0.2) * DAY,
137
+ tags: ['event'],
138
+ contactedBefore: false,
139
+ signals: ['attended PlatformConf'],
140
+ })),
141
+ },
142
+ ];
143
+ const campaigns = [
144
+ {
145
+ campaignId: 'camp_platform_intro',
146
+ name: 'Platform leads — warm intro',
147
+ steps: [
148
+ { type: 'view' },
149
+ { type: 'wait', waitMs: 2 * DAY },
150
+ { type: 'invite', note: 'Hi {{firstName}}, enjoyed your take on platform tooling.' },
151
+ { type: 'wait', waitMs: 3 * DAY },
152
+ { type: 'message', body: 'Thanks for connecting, {{firstName}} — worth a chat?' },
153
+ ],
154
+ status: 'active',
155
+ createdAt: CAPTURED_AT - 12 * DAY,
156
+ settings: { stopOnReply: true, autopilot: false },
157
+ stats: {
158
+ enrolled: 10,
159
+ sent: 24,
160
+ accepted: 6,
161
+ replied: 3,
162
+ positive: 2,
163
+ byStep: { '0': { sent: 10 }, '2': { sent: 10, accepted: 6 }, '4': { sent: 4, replied: 3 } },
164
+ },
165
+ },
166
+ {
167
+ campaignId: 'camp_dach_q3',
168
+ name: 'Q3 outbound — DACH',
169
+ steps: [
170
+ { type: 'follow' },
171
+ { type: 'wait', waitMs: DAY },
172
+ { type: 'invite', note: 'Hallo {{firstName}}, kurze Frage zu eurem Platform-Team.' },
173
+ ],
174
+ status: 'paused',
175
+ createdAt: CAPTURED_AT - 6 * DAY,
176
+ settings: { stopOnReply: true, autopilot: false },
177
+ stats: {
178
+ enrolled: 6,
179
+ sent: 9,
180
+ accepted: 2,
181
+ replied: 1,
182
+ positive: 0,
183
+ byStep: { '0': { sent: 6 }, '2': { sent: 3, accepted: 2 } },
184
+ },
185
+ },
186
+ ];
187
+ const queue = [
188
+ {
189
+ id: 'q_pending_1',
190
+ action: 'outreach.invite',
191
+ params: {
192
+ publicId: 'ingrid-halvorsen',
193
+ note: 'Hi Ingrid, your write-up on build caching was excellent — worth connecting.',
194
+ },
195
+ origin: 'campaign',
196
+ profile: profileFor('ingrid-halvorsen'),
197
+ createdAt: CAPTURED_AT - 2 * 3_600_000,
198
+ status: 'pending',
199
+ },
200
+ {
201
+ id: 'q_pending_2',
202
+ action: 'outreach.message',
203
+ params: {
204
+ publicId: 'priya-ranganathan',
205
+ body: 'Thanks for connecting, Priya — are you still hiring on the platform side?',
206
+ },
207
+ origin: 'mcp',
208
+ profile: profileFor('priya-ranganathan'),
209
+ createdAt: CAPTURED_AT - 3_600_000,
210
+ status: 'pending',
211
+ },
212
+ {
213
+ id: 'q_pending_3',
214
+ action: 'outreach.comment',
215
+ params: {
216
+ postUrl: 'https://www.linkedin.com/feed/update/demo-3/',
217
+ body: 'The point about golden paths over golden cages lands well.',
218
+ },
219
+ origin: 'mcp',
220
+ profile: profileFor('anneke-vos'),
221
+ createdAt: CAPTURED_AT - 1_800_000,
222
+ status: 'pending',
223
+ },
224
+ ];
225
+ const threads = [
226
+ {
227
+ threadId: 'thread_marisol',
228
+ participants: [{ publicId: 'marisol-adeyemi', fullName: 'Marisol Adeyemi' }],
229
+ lastMessageAt: CAPTURED_AT - 3 * 3_600_000,
230
+ unread: true,
231
+ snippet: 'That sounds useful — can you send over a short summary?',
232
+ sentiment: 'positive',
233
+ },
234
+ {
235
+ threadId: 'thread_tobias',
236
+ participants: [{ publicId: 'tobias-reinhardt', fullName: 'Tobias Reinhardt' }],
237
+ lastMessageAt: CAPTURED_AT - 8 * 3_600_000,
238
+ unread: true,
239
+ snippet: 'We are mid-migration right now, ask me again in Q4.',
240
+ sentiment: 'neutral',
241
+ },
242
+ {
243
+ threadId: 'thread_callum',
244
+ participants: [{ publicId: 'callum-whitfield', fullName: 'Callum Whitfield' }],
245
+ lastMessageAt: CAPTURED_AT - 26 * 3_600_000,
246
+ unread: false,
247
+ snippet: 'Not for us, thanks — please take me off the list.',
248
+ sentiment: 'negative',
249
+ },
250
+ {
251
+ threadId: 'thread_freya',
252
+ participants: [{ publicId: 'freya-ashworth', fullName: 'Freya Ashworth' }],
253
+ lastMessageAt: CAPTURED_AT - 50 * 3_600_000,
254
+ unread: false,
255
+ snippet: 'Happy to chat. Tuesday afternoon any good?',
256
+ sentiment: 'positive',
257
+ },
258
+ ];
259
+ const messages = [
260
+ {
261
+ messageId: 'msg_marisol_1',
262
+ threadId: 'thread_marisol',
263
+ fromPublicId: 'me',
264
+ body: 'Hi Marisol — we build a local-first LinkedIn automation layer for agents.',
265
+ sentAt: CAPTURED_AT - 5 * 3_600_000,
266
+ },
267
+ {
268
+ messageId: 'msg_marisol_2',
269
+ threadId: 'thread_marisol',
270
+ fromPublicId: 'marisol-adeyemi',
271
+ body: 'That sounds useful — can you send over a short summary?',
272
+ sentAt: CAPTURED_AT - 3 * 3_600_000,
273
+ },
274
+ {
275
+ messageId: 'msg_tobias_1',
276
+ threadId: 'thread_tobias',
277
+ fromPublicId: 'tobias-reinhardt',
278
+ body: 'We are mid-migration right now, ask me again in Q4.',
279
+ sentAt: CAPTURED_AT - 8 * 3_600_000,
280
+ },
281
+ {
282
+ messageId: 'msg_callum_1',
283
+ threadId: 'thread_callum',
284
+ fromPublicId: 'callum-whitfield',
285
+ body: 'Not for us, thanks — please take me off the list.',
286
+ sentAt: CAPTURED_AT - 26 * 3_600_000,
287
+ },
288
+ {
289
+ messageId: 'msg_freya_1',
290
+ threadId: 'thread_freya',
291
+ fromPublicId: 'freya-ashworth',
292
+ body: 'Happy to chat. Tuesday afternoon any good?',
293
+ sentAt: CAPTURED_AT - 50 * 3_600_000,
294
+ },
295
+ ];
296
+ return { lists, campaigns, queue, threads, messages, nextId: { queue: 4, list: 1, campaign: 1 } };
297
+ }
298
+ /** The three rows `lit research` demonstrates against. */
299
+ export const FAKE_RESEARCH_ROWS = [
300
+ { name: 'Marisol Adeyemi', company: 'Northwind Robotics' },
301
+ { name: 'Nadia Karimov', domain: 'marrowgate-health.example' },
302
+ { name: 'Anneke Vos', linkedinUrl: 'https://www.linkedin.com/in/anneke-vos/' },
303
+ ];
304
+ function packFor(row, index) {
305
+ const profile = FAKE_PROFILES[[0, 4, 9][index] ?? 0];
306
+ const company = FAKE_COMPANIES.find((c) => c.name === profile.company);
307
+ const signals = [
308
+ 'posted in the last 30 days',
309
+ `${2 + index} shared connections`,
310
+ 'company hiring on the platform team',
311
+ ];
312
+ const markdown = [
313
+ `# ${profile.fullName}`,
314
+ '',
315
+ `**${profile.title}** at **${profile.company}** — ${profile.location}`,
316
+ '',
317
+ '## Why now',
318
+ ...signals.map((signal) => `- ${signal}`),
319
+ '',
320
+ '## Company',
321
+ `${company.name} (${company.size} employees, ${company.industry}). ${company.description}`,
322
+ '',
323
+ '## Suggested opener',
324
+ `Hi ${profile.firstName}, your recent post on platform tooling matched something we are working on — worth a short chat?`,
325
+ '',
326
+ ].join('\n');
327
+ return {
328
+ row,
329
+ resolved: { row, kind: 'person', publicId: profile.publicId, confidence: 0.93 - index * 0.04 },
330
+ profile,
331
+ company,
332
+ recentPosts: [
333
+ {
334
+ url: `https://www.linkedin.com/feed/update/demo-${index + 1}/`,
335
+ text: 'Golden paths beat golden cages. Notes from our platform rebuild.',
336
+ likes: 84 + index * 31,
337
+ comments: 12 + index * 4,
338
+ postedAt: CAPTURED_AT - (3 + index) * DAY,
339
+ },
340
+ ],
341
+ mutualConnections: 2 + index,
342
+ connectionStatus: 'none',
343
+ signals,
344
+ markdown,
345
+ csvRow: {
346
+ name: profile.fullName,
347
+ publicId: profile.publicId,
348
+ title: profile.title,
349
+ company: profile.company,
350
+ location: profile.location,
351
+ linkedinUrl: profile.url,
352
+ signals: signals.join('; '),
353
+ },
354
+ };
355
+ }
356
+ /**
357
+ * Handlers for every action, over the demo dataset. Writes never send
358
+ * anything: they land in the approval queue exactly as Copilot mode does.
359
+ */
360
+ export function createDemoHandlers(emit) {
361
+ const state = createDemoState();
362
+ const jobs = new Map();
363
+ const listSummary = (list) => ({
364
+ listId: list.listId,
365
+ name: list.name,
366
+ tags: list.tags,
367
+ createdAt: list.createdAt,
368
+ count: list.members.length,
369
+ });
370
+ const findList = (listId) => state.lists.find((list) => list.listId === listId);
371
+ const findCampaign = (campaignId) => state.campaigns.find((campaign) => campaign.campaignId === campaignId);
372
+ const page = (items, params) => {
373
+ const start = Number(params?.start ?? 0);
374
+ const count = Number(params?.count ?? 25);
375
+ const slice = items.slice(start, start + count);
376
+ const nextStart = start + count < items.length ? start + count : undefined;
377
+ return { slice, nextStart };
378
+ };
379
+ /** Every write queues rather than sending, and shows up in `queue.list`. */
380
+ const enqueue = (action, params) => {
381
+ if (params?.dry_run)
382
+ return { status: 'dryRun', wouldSend: params };
383
+ const id = `q_demo_${state.nextId.queue++}`;
384
+ state.queue.push({
385
+ id,
386
+ action: action,
387
+ params,
388
+ origin: 'mcp',
389
+ profile: typeof params?.publicId === 'string' ? profileFor(params.publicId) : undefined,
390
+ createdAt: Date.now(),
391
+ status: 'pending',
392
+ });
393
+ return { status: 'queued', queueId: id };
394
+ };
395
+ const search = (params) => {
396
+ const keywords = String(params?.keywords ?? '').toLowerCase();
397
+ const matches = FAKE_PROFILES.filter((profile) => {
398
+ if (!keywords)
399
+ return true;
400
+ const haystack = `${profile.fullName} ${profile.headline} ${profile.company} ${profile.location}`.toLowerCase();
401
+ return keywords.split(/\s+/).some((word) => haystack.includes(word));
402
+ });
403
+ // A demo search should never come back empty just because of the keywords.
404
+ const results = matches.length > 0 ? matches : FAKE_PROFILES;
405
+ const filtered = results.filter((profile) => {
406
+ const title = String(params?.title ?? '').toLowerCase();
407
+ const company = String(params?.company ?? '').toLowerCase();
408
+ const location = String(params?.location ?? '').toLowerCase();
409
+ return ((!title || profile.title.toLowerCase().includes(title)) &&
410
+ (!company || profile.company.toLowerCase().includes(company)) &&
411
+ (!location || profile.location.toLowerCase().includes(location)));
412
+ });
413
+ const { slice, nextStart } = page(filtered, params);
414
+ return { profiles: slice, total: filtered.length, nextStart };
415
+ };
416
+ return {
417
+ 'status.get': (params) => ({
418
+ ...(params?.verify
419
+ ? {
420
+ endpoints: {
421
+ ...Object.fromEntries(FAKE_VERIFIED_ENDPOINTS.map((name) => [name, 'ok'])),
422
+ // Without a post to count reactions on there is nothing to check.
423
+ reactions: params?.postUrl ? 'ok' : 'skipped',
424
+ ...Object.fromEntries(FAKE_UNVERIFIED_ENDPOINTS.map((name) => [name, 'unverified'])),
425
+ },
426
+ clientVersionCaptured: FAKE_CLIENT_VERSION,
427
+ endpointsCapturedAt: '2026-09-08',
428
+ }
429
+ : {}),
430
+ connected: true,
431
+ extensionVersion: '2.0.0 (fake)',
432
+ loggedIn: true,
433
+ autopilot: false,
434
+ businessHours: true,
435
+ quotas: {
436
+ invite: { hourlyUsed: 3, hourlyCap: 20, dailyUsed: 18, dailyCap: 100, nextAllowedAt: 0 },
437
+ message: { hourlyUsed: 1, hourlyCap: 20, dailyUsed: 7, dailyCap: 150, nextAllowedAt: 0 },
438
+ visit: { hourlyUsed: 9, hourlyCap: 50, dailyUsed: 64, dailyCap: 500, nextAllowedAt: 0 },
439
+ search: { hourlyUsed: 2, hourlyCap: 50, dailyUsed: 120, dailyCap: 1000, nextAllowedAt: 0 },
440
+ },
441
+ queue: { pending: state.queue.filter((item) => item.status === 'pending').length },
442
+ campaigns: {
443
+ active: state.campaigns.filter((c) => c.status === 'active').length,
444
+ paused: state.campaigns.filter((c) => c.status === 'paused').length,
445
+ },
446
+ }),
447
+ 'config.get': () => FAKE_CONFIG,
448
+ 'config.set': (params) => ({ ...FAKE_CONFIG, ...params }),
449
+ 'search.people': search,
450
+ 'profile.get': (params) => {
451
+ const publicId = String(params?.publicId ?? '') || String(params?.url ?? '');
452
+ const profile = profileFor(publicId.replace(/.*\/in\//, '').replace(/\/$/, ''));
453
+ if (!profile)
454
+ return FAKE_PROFILES[0];
455
+ return params?.full
456
+ ? {
457
+ ...profile,
458
+ pageText: `${profile.fullName}\n${profile.headline}\n${profile.location}\n\nDemo profile text.`,
459
+ skills: ['Kubernetes', 'Platform engineering', 'Go'],
460
+ experience: [
461
+ { title: profile.title, company: profile.company, start: '2023' },
462
+ { title: 'Senior Engineer', company: 'Hollowmere Systems', start: '2019', end: '2023' },
463
+ ],
464
+ education: [{ school: 'Ashcombe University', degree: 'BSc', field: 'Computer Science' }],
465
+ }
466
+ : profile;
467
+ },
468
+ 'profile.export': (params) => {
469
+ const urls = params?.urls ?? [];
470
+ const profiles = urls
471
+ .map((url) => profileFor(String(url).replace(/.*\/in\//, '').replace(/\/$/, '')))
472
+ .filter(Boolean);
473
+ return {
474
+ profiles,
475
+ failed: urls
476
+ .filter((url) => !profileFor(String(url).replace(/.*\/in\//, '').replace(/\/$/, '')))
477
+ .map((url) => ({ url, error: 'not in the demo dataset' })),
478
+ };
479
+ },
480
+ 'company.get': (params) => {
481
+ const name = String(params?.universalName ?? params?.url ?? '')
482
+ .replace(/.*\/company\//, '')
483
+ .replace(/\/$/, '');
484
+ return FAKE_COMPANIES.find((company) => company.universalName === name) ?? FAKE_COMPANIES[0];
485
+ },
486
+ 'company.employees': (params) => {
487
+ const company = FAKE_COMPANIES.find((c) => c.universalName === params?.universalName);
488
+ const staff = FAKE_PROFILES.filter((profile) => profile.company === company?.name);
489
+ const { slice, nextStart } = page(staff, params);
490
+ return { profiles: slice, nextStart };
491
+ },
492
+ 'post.engagers': (params) => {
493
+ const engagers = FAKE_PROFILES.slice(0, 8).map((profile, index) => ({
494
+ ...profile,
495
+ ...(index % 2 === 0
496
+ ? { reaction: 'like' }
497
+ : { commentText: 'Strongly agree with the golden paths point.' }),
498
+ engagedAt: CAPTURED_AT - index * 3_600_000,
499
+ }));
500
+ const kind = params?.kind ?? 'both';
501
+ const filtered = kind === 'likes'
502
+ ? engagers.filter((e) => 'reaction' in e)
503
+ : kind === 'comments'
504
+ ? engagers.filter((e) => 'commentText' in e)
505
+ : engagers;
506
+ const { slice, nextStart } = page(filtered, params);
507
+ return { engagers: slice, nextStart };
508
+ },
509
+ 'group.members': (params) => {
510
+ const { slice, nextStart } = page(FAKE_PROFILES.slice(5, 20), params);
511
+ return { profiles: slice, nextStart };
512
+ },
513
+ 'event.attendees': (params) => {
514
+ const { slice, nextStart } = page(FAKE_PROFILES.slice(18), params);
515
+ return { profiles: slice, nextStart };
516
+ },
517
+ 'network.connections': (params) => {
518
+ const first = FAKE_PROFILES.filter((profile) => profile.connectionDegree === 1);
519
+ const { slice, nextStart } = page(first, params);
520
+ return { profiles: slice, nextStart };
521
+ },
522
+ 'network.followers': (params) => {
523
+ const { slice, nextStart } = page(FAKE_PROFILES.slice(10), params);
524
+ return { profiles: slice, nextStart };
525
+ },
526
+ 'network.status': (params) => ({
527
+ statuses: Object.fromEntries((params?.publicIds ?? []).map((publicId) => {
528
+ const profile = profileFor(publicId);
529
+ return [
530
+ publicId,
531
+ profile?.connectionDegree === 1 ? 'connected' : profile ? 'none' : 'none',
532
+ ];
533
+ })),
534
+ }),
535
+ 'network.unfollowCount': () => ({ count: 7 }),
536
+ 'network.unfollowAll': () => ({ unfollowed: 7 }),
537
+ 'outreach.view': (params) => params?.dry_run ? { status: 'dryRun', wouldSend: params } : enqueue('outreach.view', params),
538
+ 'outreach.follow': (params) => enqueue('outreach.follow', params),
539
+ 'outreach.invite': (params) => enqueue('outreach.invite', params),
540
+ 'outreach.message': (params) => enqueue('outreach.message', params),
541
+ 'outreach.inmail': (params) => enqueue('outreach.inmail', params),
542
+ 'outreach.like': (params) => enqueue('outreach.like', params),
543
+ 'outreach.comment': (params) => enqueue('outreach.comment', params),
544
+ 'inbox.threads': (params) => {
545
+ const since = Number(params?.since ?? 0);
546
+ let threads = state.threads.filter((thread) => thread.lastMessageAt >= since);
547
+ if (params?.unreadOnly)
548
+ threads = threads.filter((thread) => thread.unread);
549
+ return { threads: threads.slice(0, Number(params?.count ?? threads.length)) };
550
+ },
551
+ 'inbox.messages': (params) => ({
552
+ messages: state.messages.filter((message) => message.threadId === params?.threadId),
553
+ }),
554
+ 'inbox.export': () => ({ threads: state.threads, messages: state.messages }),
555
+ 'list.create': (params) => {
556
+ const list = {
557
+ listId: `list_demo_${state.nextId.list++}`,
558
+ name: String(params?.name ?? 'Untitled list'),
559
+ tags: params?.tags ?? [],
560
+ createdAt: Date.now(),
561
+ members: [],
562
+ };
563
+ state.lists.push(list);
564
+ return listSummary(list);
565
+ },
566
+ 'list.getAll': () => ({ lists: state.lists.map(listSummary) }),
567
+ 'list.get': (params) => {
568
+ const list = findList(params?.listId);
569
+ if (!list)
570
+ throw new Error(`no such list ${params?.listId}`);
571
+ return listSummary(list);
572
+ },
573
+ 'list.add': (params) => {
574
+ const list = findList(params?.listId);
575
+ if (!list)
576
+ throw new Error(`no such list ${params?.listId}`);
577
+ const incoming = params?.profiles ?? (params?.publicIds ?? []).map((id) => profileFor(id)).filter(Boolean);
578
+ let added = 0;
579
+ let duplicates = 0;
580
+ for (const profile of incoming) {
581
+ if (list.members.some((member) => member.publicId === profile.publicId)) {
582
+ duplicates++;
583
+ continue;
584
+ }
585
+ list.members.push({
586
+ publicId: profile.publicId,
587
+ profile,
588
+ addedAt: Date.now(),
589
+ tags: [],
590
+ contactedBefore: false,
591
+ signals: [],
592
+ });
593
+ added++;
594
+ }
595
+ return { added, duplicates };
596
+ },
597
+ 'list.remove': (params) => {
598
+ const list = findList(params?.listId);
599
+ if (!list)
600
+ throw new Error(`no such list ${params?.listId}`);
601
+ const before = list.members.length;
602
+ list.members = list.members.filter((member) => !(params?.publicIds ?? []).includes(member.publicId));
603
+ return { removed: before - list.members.length };
604
+ },
605
+ 'list.members': (params) => {
606
+ const list = findList(params?.listId);
607
+ if (!list)
608
+ throw new Error(`no such list ${params?.listId}`);
609
+ const { slice } = page(list.members, params);
610
+ return { members: slice, total: list.members.length };
611
+ },
612
+ 'list.delete': (params) => {
613
+ state.lists = state.lists.filter((list) => list.listId !== params?.listId);
614
+ return { ok: true };
615
+ },
616
+ 'list.importCsv': (params) => {
617
+ const rows = String(params?.csv ?? '').trim().split('\n').slice(1);
618
+ return { added: rows.length, duplicates: 0, invalid: 0 };
619
+ },
620
+ 'campaign.create': (params) => {
621
+ const campaign = {
622
+ campaignId: `camp_demo_${state.nextId.campaign++}`,
623
+ name: String(params?.name ?? 'Untitled campaign'),
624
+ steps: params?.steps ?? [],
625
+ status: 'active',
626
+ createdAt: Date.now(),
627
+ settings: { stopOnReply: true, autopilot: false, ...(params?.settings ?? {}) },
628
+ stats: {
629
+ enrolled: findList(params?.listId)?.members.length ?? (params?.publicIds ?? []).length,
630
+ sent: 0,
631
+ accepted: 0,
632
+ replied: 0,
633
+ positive: 0,
634
+ byStep: {},
635
+ },
636
+ };
637
+ state.campaigns.push(campaign);
638
+ return campaign;
639
+ },
640
+ 'campaign.getAll': () => ({ campaigns: state.campaigns }),
641
+ 'campaign.get': (params) => {
642
+ const campaign = findCampaign(params?.campaignId);
643
+ if (!campaign)
644
+ throw new Error(`no such campaign ${params?.campaignId}`);
645
+ return campaign;
646
+ },
647
+ 'campaign.enroll': (params) => {
648
+ const campaign = findCampaign(params?.campaignId);
649
+ if (!campaign)
650
+ throw new Error(`no such campaign ${params?.campaignId}`);
651
+ const enrolled = (params?.publicIds ?? []).length;
652
+ campaign.stats.enrolled += enrolled;
653
+ return { enrolled, skipped: 0 };
654
+ },
655
+ 'campaign.pause': (params) => {
656
+ const campaign = findCampaign(params?.campaignId);
657
+ if (!campaign)
658
+ throw new Error(`no such campaign ${params?.campaignId}`);
659
+ campaign.status = 'paused';
660
+ return campaign;
661
+ },
662
+ 'campaign.resume': (params) => {
663
+ const campaign = findCampaign(params?.campaignId);
664
+ if (!campaign)
665
+ throw new Error(`no such campaign ${params?.campaignId}`);
666
+ campaign.status = 'active';
667
+ return campaign;
668
+ },
669
+ 'campaign.delete': (params) => {
670
+ const campaign = findCampaign(params?.campaignId);
671
+ state.campaigns = state.campaigns.filter((c) => c.campaignId !== params?.campaignId);
672
+ return { ...(campaign ?? {}), status: 'completed' };
673
+ },
674
+ 'campaign.tick': () => ({ executed: 0, queued: 0 }),
675
+ 'queue.list': (params) => ({
676
+ items: params?.status
677
+ ? state.queue.filter((item) => item.status === params.status)
678
+ : state.queue,
679
+ }),
680
+ 'queue.approve': (params) => {
681
+ const ids = params?.ids ?? [];
682
+ let approved = 0;
683
+ for (const item of state.queue) {
684
+ if (ids.includes(item.id) && item.status === 'pending') {
685
+ item.status = 'sent';
686
+ approved++;
687
+ }
688
+ }
689
+ return { approved };
690
+ },
691
+ 'queue.reject': (params) => {
692
+ const ids = params?.ids ?? [];
693
+ let rejected = 0;
694
+ for (const item of state.queue) {
695
+ if (ids.includes(item.id) && item.status === 'pending') {
696
+ item.status = 'rejected';
697
+ rejected++;
698
+ }
699
+ }
700
+ return { rejected };
701
+ },
702
+ 'ai.complete': (params) => ({
703
+ output: params?.task === 'sentiment'
704
+ ? 'positive'
705
+ : 'Hi there — your recent post on platform tooling lined up with something we are building. Worth a short chat?',
706
+ provider: 'fake',
707
+ model: 'demo',
708
+ }),
709
+ 'export.csv': () => ({
710
+ csv: ['publicId,fullName,title,company,location']
711
+ .concat(FAKE_PROFILES.map((p) => [p.publicId, p.fullName, p.title, p.company, p.location].join(',')))
712
+ .join('\n'),
713
+ filename: 'demo-profiles.csv',
714
+ }),
715
+ 'research.resolve': (params) => ({
716
+ resolved: (params?.rows ?? []).map((row, index) => ({
717
+ row,
718
+ kind: 'person',
719
+ publicId: FAKE_PROFILES[index % FAKE_PROFILES.length].publicId,
720
+ confidence: 0.9 - index * 0.05,
721
+ candidates: [FAKE_PROFILES[index % FAKE_PROFILES.length]],
722
+ })),
723
+ }),
724
+ 'research.pack': (params) => {
725
+ const rows = params?.rows ?? [];
726
+ const jobId = `job_demo_${jobs.size + 1}`;
727
+ const packs = rows.map((row, index) => packFor(row, index));
728
+ jobs.set(jobId, { total: rows.length, packs });
729
+ // Completion arrives on the event channel, exactly as the real engine does.
730
+ setTimeout(() => emit('research_completed', { jobId, listId: params?.listName }), 150);
731
+ return { jobId, total: rows.length, etaMs: 150 };
732
+ },
733
+ 'research.get': (params) => {
734
+ const job = jobs.get(params?.jobId);
735
+ if (!job)
736
+ throw new Error(`no such job ${params?.jobId}`);
737
+ return {
738
+ jobId: params.jobId,
739
+ status: 'completed',
740
+ done: job.packs.length,
741
+ total: job.total,
742
+ packs: job.packs,
743
+ };
744
+ },
745
+ 'sync.pull': () => ({
746
+ profiles: FAKE_PROFILES,
747
+ companies: FAKE_COMPANIES,
748
+ lists: state.lists.map(listSummary),
749
+ listMembers: state.lists.flatMap((list) => list.members.map((member) => ({ ...member, listId: list.listId }))),
750
+ campaigns: state.campaigns,
751
+ enrollments: state.lists[0].members.map((member) => ({
752
+ campaignId: 'camp_platform_intro',
753
+ publicId: member.publicId,
754
+ stepIndex: 2,
755
+ status: 'active',
756
+ enrolledAt: CAPTURED_AT - 10 * DAY,
757
+ })),
758
+ actions: state.queue.map((item) => ({
759
+ id: `action_${item.id}`,
760
+ action: item.action,
761
+ publicId: item.params?.publicId,
762
+ status: item.status,
763
+ at: item.createdAt,
764
+ })),
765
+ threads: state.threads,
766
+ messages: state.messages,
767
+ events: [],
768
+ }),
769
+ };
770
+ }
771
+ const FAKE_CONFIG = {
772
+ minDelayMs: 5000,
773
+ maxDelayMs: 15000,
774
+ hourlyCap: 20,
775
+ dailyInviteCap: 100,
776
+ dailyMessageCap: 150,
777
+ dailyVisitCap: 500,
778
+ dailySearchCap: 1000,
779
+ businessHoursOnly: true,
780
+ businessStart: 9,
781
+ businessEnd: 18,
782
+ weekdaysOnly: true,
783
+ autopilot: false,
784
+ accountPreset: 'free',
785
+ warmup: { enabled: true, days: 14 },
786
+ ai: { provider: 'none' },
787
+ bridge: { enabled: true, port: 47829 },
788
+ };
789
+ //# sourceMappingURL=fake-data.js.map