@hasna/microservices 0.0.9 → 0.0.11

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 (100) hide show
  1. package/bin/index.js +236 -36
  2. package/bin/mcp.js +153 -4
  3. package/dist/index.js +120 -3
  4. package/microservices/microservice-analytics/package.json +27 -0
  5. package/microservices/microservice-analytics/src/cli/index.ts +373 -0
  6. package/microservices/microservice-analytics/src/db/analytics.ts +564 -0
  7. package/microservices/microservice-analytics/src/db/database.ts +93 -0
  8. package/microservices/microservice-analytics/src/db/migrations.ts +50 -0
  9. package/microservices/microservice-analytics/src/index.ts +37 -0
  10. package/microservices/microservice-analytics/src/mcp/index.ts +334 -0
  11. package/microservices/microservice-assets/package.json +27 -0
  12. package/microservices/microservice-assets/src/cli/index.ts +375 -0
  13. package/microservices/microservice-assets/src/db/assets.ts +370 -0
  14. package/microservices/microservice-assets/src/db/database.ts +93 -0
  15. package/microservices/microservice-assets/src/db/migrations.ts +51 -0
  16. package/microservices/microservice-assets/src/index.ts +32 -0
  17. package/microservices/microservice-assets/src/mcp/index.ts +346 -0
  18. package/microservices/microservice-compliance/package.json +27 -0
  19. package/microservices/microservice-compliance/src/cli/index.ts +467 -0
  20. package/microservices/microservice-compliance/src/db/compliance.ts +633 -0
  21. package/microservices/microservice-compliance/src/db/database.ts +93 -0
  22. package/microservices/microservice-compliance/src/db/migrations.ts +63 -0
  23. package/microservices/microservice-compliance/src/index.ts +46 -0
  24. package/microservices/microservice-compliance/src/mcp/index.ts +438 -0
  25. package/microservices/microservice-habits/package.json +27 -0
  26. package/microservices/microservice-habits/src/cli/index.ts +315 -0
  27. package/microservices/microservice-habits/src/db/database.ts +93 -0
  28. package/microservices/microservice-habits/src/db/habits.ts +451 -0
  29. package/microservices/microservice-habits/src/db/migrations.ts +46 -0
  30. package/microservices/microservice-habits/src/index.ts +31 -0
  31. package/microservices/microservice-habits/src/mcp/index.ts +313 -0
  32. package/microservices/microservice-health/package.json +27 -0
  33. package/microservices/microservice-health/src/cli/index.ts +484 -0
  34. package/microservices/microservice-health/src/db/database.ts +93 -0
  35. package/microservices/microservice-health/src/db/health.ts +708 -0
  36. package/microservices/microservice-health/src/db/migrations.ts +70 -0
  37. package/microservices/microservice-health/src/index.ts +63 -0
  38. package/microservices/microservice-health/src/mcp/index.ts +437 -0
  39. package/microservices/microservice-leads/package.json +27 -0
  40. package/microservices/microservice-leads/src/cli/index.ts +596 -0
  41. package/microservices/microservice-leads/src/db/database.ts +93 -0
  42. package/microservices/microservice-leads/src/db/leads.ts +520 -0
  43. package/microservices/microservice-leads/src/db/lists.ts +151 -0
  44. package/microservices/microservice-leads/src/db/migrations.ts +93 -0
  45. package/microservices/microservice-leads/src/index.ts +65 -0
  46. package/microservices/microservice-leads/src/lib/enrichment.ts +202 -0
  47. package/microservices/microservice-leads/src/lib/scoring.ts +134 -0
  48. package/microservices/microservice-leads/src/mcp/index.ts +533 -0
  49. package/microservices/microservice-notifications/package.json +27 -0
  50. package/microservices/microservice-notifications/src/cli/index.ts +349 -0
  51. package/microservices/microservice-notifications/src/db/database.ts +93 -0
  52. package/microservices/microservice-notifications/src/db/migrations.ts +62 -0
  53. package/microservices/microservice-notifications/src/db/notifications.ts +509 -0
  54. package/microservices/microservice-notifications/src/index.ts +41 -0
  55. package/microservices/microservice-notifications/src/mcp/index.ts +422 -0
  56. package/microservices/microservice-products/package.json +27 -0
  57. package/microservices/microservice-products/src/cli/index.ts +416 -0
  58. package/microservices/microservice-products/src/db/categories.ts +154 -0
  59. package/microservices/microservice-products/src/db/database.ts +93 -0
  60. package/microservices/microservice-products/src/db/migrations.ts +58 -0
  61. package/microservices/microservice-products/src/db/pricing-tiers.ts +66 -0
  62. package/microservices/microservice-products/src/db/products.ts +452 -0
  63. package/microservices/microservice-products/src/index.ts +53 -0
  64. package/microservices/microservice-products/src/mcp/index.ts +453 -0
  65. package/microservices/microservice-projects/package.json +27 -0
  66. package/microservices/microservice-projects/src/cli/index.ts +480 -0
  67. package/microservices/microservice-projects/src/db/database.ts +93 -0
  68. package/microservices/microservice-projects/src/db/migrations.ts +65 -0
  69. package/microservices/microservice-projects/src/db/projects.ts +715 -0
  70. package/microservices/microservice-projects/src/index.ts +57 -0
  71. package/microservices/microservice-projects/src/mcp/index.ts +501 -0
  72. package/microservices/microservice-proposals/package.json +27 -0
  73. package/microservices/microservice-proposals/src/cli/index.ts +400 -0
  74. package/microservices/microservice-proposals/src/db/database.ts +93 -0
  75. package/microservices/microservice-proposals/src/db/migrations.ts +52 -0
  76. package/microservices/microservice-proposals/src/db/proposals.ts +532 -0
  77. package/microservices/microservice-proposals/src/index.ts +37 -0
  78. package/microservices/microservice-proposals/src/mcp/index.ts +375 -0
  79. package/microservices/microservice-reading/package.json +27 -0
  80. package/microservices/microservice-reading/src/cli/index.ts +464 -0
  81. package/microservices/microservice-reading/src/db/database.ts +93 -0
  82. package/microservices/microservice-reading/src/db/migrations.ts +59 -0
  83. package/microservices/microservice-reading/src/db/reading.ts +524 -0
  84. package/microservices/microservice-reading/src/index.ts +51 -0
  85. package/microservices/microservice-reading/src/mcp/index.ts +368 -0
  86. package/microservices/microservice-travel/package.json +27 -0
  87. package/microservices/microservice-travel/src/cli/index.ts +505 -0
  88. package/microservices/microservice-travel/src/db/database.ts +93 -0
  89. package/microservices/microservice-travel/src/db/migrations.ts +77 -0
  90. package/microservices/microservice-travel/src/db/travel.ts +802 -0
  91. package/microservices/microservice-travel/src/index.ts +60 -0
  92. package/microservices/microservice-travel/src/mcp/index.ts +495 -0
  93. package/microservices/microservice-wiki/package.json +27 -0
  94. package/microservices/microservice-wiki/src/cli/index.ts +345 -0
  95. package/microservices/microservice-wiki/src/db/database.ts +93 -0
  96. package/microservices/microservice-wiki/src/db/migrations.ts +55 -0
  97. package/microservices/microservice-wiki/src/db/wiki.ts +395 -0
  98. package/microservices/microservice-wiki/src/index.ts +32 -0
  99. package/microservices/microservice-wiki/src/mcp/index.ts +344 -0
  100. package/package.json +1 -1
@@ -0,0 +1,520 @@
1
+ /**
2
+ * Lead CRUD operations
3
+ */
4
+
5
+ import { getDatabase } from "./database.js";
6
+
7
+ export interface Lead {
8
+ id: string;
9
+ name: string | null;
10
+ email: string | null;
11
+ phone: string | null;
12
+ company: string | null;
13
+ title: string | null;
14
+ website: string | null;
15
+ linkedin_url: string | null;
16
+ source: string;
17
+ status: string;
18
+ score: number;
19
+ score_reason: string | null;
20
+ tags: string[];
21
+ notes: string | null;
22
+ metadata: Record<string, unknown>;
23
+ enriched: boolean;
24
+ enriched_at: string | null;
25
+ created_at: string;
26
+ updated_at: string;
27
+ }
28
+
29
+ interface LeadRow {
30
+ id: string;
31
+ name: string | null;
32
+ email: string | null;
33
+ phone: string | null;
34
+ company: string | null;
35
+ title: string | null;
36
+ website: string | null;
37
+ linkedin_url: string | null;
38
+ source: string;
39
+ status: string;
40
+ score: number;
41
+ score_reason: string | null;
42
+ tags: string;
43
+ notes: string | null;
44
+ metadata: string;
45
+ enriched: number;
46
+ enriched_at: string | null;
47
+ created_at: string;
48
+ updated_at: string;
49
+ }
50
+
51
+ function rowToLead(row: LeadRow): Lead {
52
+ return {
53
+ ...row,
54
+ tags: JSON.parse(row.tags || "[]"),
55
+ metadata: JSON.parse(row.metadata || "{}"),
56
+ enriched: row.enriched === 1,
57
+ };
58
+ }
59
+
60
+ export interface CreateLeadInput {
61
+ name?: string;
62
+ email?: string;
63
+ phone?: string;
64
+ company?: string;
65
+ title?: string;
66
+ website?: string;
67
+ linkedin_url?: string;
68
+ source?: string;
69
+ status?: string;
70
+ score?: number;
71
+ score_reason?: string;
72
+ tags?: string[];
73
+ notes?: string;
74
+ metadata?: Record<string, unknown>;
75
+ }
76
+
77
+ export function createLead(input: CreateLeadInput): Lead {
78
+ const db = getDatabase();
79
+ const id = crypto.randomUUID();
80
+ const tags = JSON.stringify(input.tags || []);
81
+ const metadata = JSON.stringify(input.metadata || {});
82
+
83
+ db.prepare(
84
+ `INSERT INTO leads (id, name, email, phone, company, title, website, linkedin_url, source, status, score, score_reason, tags, notes, metadata)
85
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
86
+ ).run(
87
+ id,
88
+ input.name || null,
89
+ input.email || null,
90
+ input.phone || null,
91
+ input.company || null,
92
+ input.title || null,
93
+ input.website || null,
94
+ input.linkedin_url || null,
95
+ input.source || "manual",
96
+ input.status || "new",
97
+ input.score || 0,
98
+ input.score_reason || null,
99
+ tags,
100
+ input.notes || null,
101
+ metadata
102
+ );
103
+
104
+ return getLead(id)!;
105
+ }
106
+
107
+ export function getLead(id: string): Lead | null {
108
+ const db = getDatabase();
109
+ const row = db.prepare("SELECT * FROM leads WHERE id = ?").get(id) as LeadRow | null;
110
+ return row ? rowToLead(row) : null;
111
+ }
112
+
113
+ export interface UpdateLeadInput {
114
+ name?: string;
115
+ email?: string;
116
+ phone?: string;
117
+ company?: string;
118
+ title?: string;
119
+ website?: string;
120
+ linkedin_url?: string;
121
+ source?: string;
122
+ status?: string;
123
+ score?: number;
124
+ score_reason?: string;
125
+ tags?: string[];
126
+ notes?: string;
127
+ metadata?: Record<string, unknown>;
128
+ enriched?: boolean;
129
+ enriched_at?: string;
130
+ }
131
+
132
+ export function updateLead(id: string, input: UpdateLeadInput): Lead | null {
133
+ const db = getDatabase();
134
+ const existing = getLead(id);
135
+ if (!existing) return null;
136
+
137
+ const sets: string[] = [];
138
+ const params: unknown[] = [];
139
+
140
+ if (input.name !== undefined) { sets.push("name = ?"); params.push(input.name); }
141
+ if (input.email !== undefined) { sets.push("email = ?"); params.push(input.email); }
142
+ if (input.phone !== undefined) { sets.push("phone = ?"); params.push(input.phone); }
143
+ if (input.company !== undefined) { sets.push("company = ?"); params.push(input.company); }
144
+ if (input.title !== undefined) { sets.push("title = ?"); params.push(input.title); }
145
+ if (input.website !== undefined) { sets.push("website = ?"); params.push(input.website); }
146
+ if (input.linkedin_url !== undefined) { sets.push("linkedin_url = ?"); params.push(input.linkedin_url); }
147
+ if (input.source !== undefined) { sets.push("source = ?"); params.push(input.source); }
148
+ if (input.status !== undefined) { sets.push("status = ?"); params.push(input.status); }
149
+ if (input.score !== undefined) { sets.push("score = ?"); params.push(input.score); }
150
+ if (input.score_reason !== undefined) { sets.push("score_reason = ?"); params.push(input.score_reason); }
151
+ if (input.tags !== undefined) { sets.push("tags = ?"); params.push(JSON.stringify(input.tags)); }
152
+ if (input.notes !== undefined) { sets.push("notes = ?"); params.push(input.notes); }
153
+ if (input.metadata !== undefined) { sets.push("metadata = ?"); params.push(JSON.stringify(input.metadata)); }
154
+ if (input.enriched !== undefined) { sets.push("enriched = ?"); params.push(input.enriched ? 1 : 0); }
155
+ if (input.enriched_at !== undefined) { sets.push("enriched_at = ?"); params.push(input.enriched_at); }
156
+
157
+ if (sets.length === 0) return existing;
158
+
159
+ sets.push("updated_at = datetime('now')");
160
+ params.push(id);
161
+
162
+ db.prepare(
163
+ `UPDATE leads SET ${sets.join(", ")} WHERE id = ?`
164
+ ).run(...params);
165
+
166
+ return getLead(id);
167
+ }
168
+
169
+ export function deleteLead(id: string): boolean {
170
+ const db = getDatabase();
171
+ const result = db.prepare("DELETE FROM leads WHERE id = ?").run(id);
172
+ return result.changes > 0;
173
+ }
174
+
175
+ export interface ListLeadsOptions {
176
+ status?: string;
177
+ source?: string;
178
+ score_min?: number;
179
+ score_max?: number;
180
+ enriched?: boolean;
181
+ limit?: number;
182
+ offset?: number;
183
+ }
184
+
185
+ export function listLeads(options: ListLeadsOptions = {}): Lead[] {
186
+ const db = getDatabase();
187
+ const conditions: string[] = [];
188
+ const params: unknown[] = [];
189
+
190
+ if (options.status) {
191
+ conditions.push("status = ?");
192
+ params.push(options.status);
193
+ }
194
+ if (options.source) {
195
+ conditions.push("source = ?");
196
+ params.push(options.source);
197
+ }
198
+ if (options.score_min !== undefined) {
199
+ conditions.push("score >= ?");
200
+ params.push(options.score_min);
201
+ }
202
+ if (options.score_max !== undefined) {
203
+ conditions.push("score <= ?");
204
+ params.push(options.score_max);
205
+ }
206
+ if (options.enriched !== undefined) {
207
+ conditions.push("enriched = ?");
208
+ params.push(options.enriched ? 1 : 0);
209
+ }
210
+
211
+ let sql = "SELECT * FROM leads";
212
+ if (conditions.length > 0) {
213
+ sql += " WHERE " + conditions.join(" AND ");
214
+ }
215
+ sql += " ORDER BY created_at DESC";
216
+
217
+ if (options.limit) {
218
+ sql += " LIMIT ?";
219
+ params.push(options.limit);
220
+ }
221
+ if (options.offset) {
222
+ sql += " OFFSET ?";
223
+ params.push(options.offset);
224
+ }
225
+
226
+ const rows = db.prepare(sql).all(...params) as LeadRow[];
227
+ return rows.map(rowToLead);
228
+ }
229
+
230
+ export function searchLeads(query: string): Lead[] {
231
+ const db = getDatabase();
232
+ const q = `%${query}%`;
233
+ const rows = db
234
+ .prepare(
235
+ "SELECT * FROM leads WHERE name LIKE ? OR email LIKE ? OR company LIKE ? ORDER BY created_at DESC"
236
+ )
237
+ .all(q, q, q) as LeadRow[];
238
+ return rows.map(rowToLead);
239
+ }
240
+
241
+ export function findByEmail(email: string): Lead | null {
242
+ const db = getDatabase();
243
+ const row = db.prepare("SELECT * FROM leads WHERE email = ?").get(email) as LeadRow | null;
244
+ return row ? rowToLead(row) : null;
245
+ }
246
+
247
+ export interface BulkImportResult {
248
+ imported: number;
249
+ skipped: number;
250
+ errors: string[];
251
+ }
252
+
253
+ export function bulkImportLeads(data: CreateLeadInput[]): BulkImportResult {
254
+ const result: BulkImportResult = { imported: 0, skipped: 0, errors: [] };
255
+
256
+ for (const item of data) {
257
+ try {
258
+ // Dedup by email
259
+ if (item.email) {
260
+ const existing = findByEmail(item.email);
261
+ if (existing) {
262
+ result.skipped++;
263
+ continue;
264
+ }
265
+ }
266
+ createLead(item);
267
+ result.imported++;
268
+ } catch (error) {
269
+ result.errors.push(
270
+ `Failed to import ${item.email || item.name || "unknown"}: ${error instanceof Error ? error.message : String(error)}`
271
+ );
272
+ }
273
+ }
274
+
275
+ return result;
276
+ }
277
+
278
+ export function exportLeads(format: "csv" | "json", filters?: ListLeadsOptions): string {
279
+ const leads = listLeads(filters || {});
280
+
281
+ if (format === "json") {
282
+ return JSON.stringify(leads, null, 2);
283
+ }
284
+
285
+ // CSV format
286
+ const headers = [
287
+ "id", "name", "email", "phone", "company", "title", "website",
288
+ "linkedin_url", "source", "status", "score", "tags", "notes", "created_at",
289
+ ];
290
+ const rows = leads.map((lead) =>
291
+ headers.map((h) => {
292
+ const value = (lead as Record<string, unknown>)[h];
293
+ if (Array.isArray(value)) return value.join(";");
294
+ if (value === null || value === undefined) return "";
295
+ const str = String(value);
296
+ return str.includes(",") ? `"${str}"` : str;
297
+ }).join(",")
298
+ );
299
+
300
+ return [headers.join(","), ...rows].join("\n");
301
+ }
302
+
303
+ export interface LeadActivity {
304
+ id: string;
305
+ lead_id: string;
306
+ type: string;
307
+ description: string | null;
308
+ metadata: Record<string, unknown>;
309
+ created_at: string;
310
+ }
311
+
312
+ interface ActivityRow {
313
+ id: string;
314
+ lead_id: string;
315
+ type: string;
316
+ description: string | null;
317
+ metadata: string;
318
+ created_at: string;
319
+ }
320
+
321
+ function rowToActivity(row: ActivityRow): LeadActivity {
322
+ return {
323
+ ...row,
324
+ metadata: JSON.parse(row.metadata || "{}"),
325
+ };
326
+ }
327
+
328
+ export function addActivity(
329
+ leadId: string,
330
+ type: string,
331
+ description?: string,
332
+ metadata?: Record<string, unknown>
333
+ ): LeadActivity {
334
+ const db = getDatabase();
335
+ const id = crypto.randomUUID();
336
+
337
+ db.prepare(
338
+ `INSERT INTO lead_activities (id, lead_id, type, description, metadata)
339
+ VALUES (?, ?, ?, ?, ?)`
340
+ ).run(id, leadId, type, description || null, JSON.stringify(metadata || {}));
341
+
342
+ const row = db.prepare("SELECT * FROM lead_activities WHERE id = ?").get(id) as ActivityRow;
343
+ return rowToActivity(row);
344
+ }
345
+
346
+ export function getActivities(leadId: string, limit?: number): LeadActivity[] {
347
+ const db = getDatabase();
348
+ let sql = "SELECT * FROM lead_activities WHERE lead_id = ? ORDER BY created_at DESC";
349
+ const params: unknown[] = [leadId];
350
+
351
+ if (limit) {
352
+ sql += " LIMIT ?";
353
+ params.push(limit);
354
+ }
355
+
356
+ const rows = db.prepare(sql).all(...params) as ActivityRow[];
357
+ return rows.map(rowToActivity);
358
+ }
359
+
360
+ export function getLeadTimeline(leadId: string): LeadActivity[] {
361
+ const db = getDatabase();
362
+ const rows = db
363
+ .prepare(
364
+ "SELECT * FROM lead_activities WHERE lead_id = ? ORDER BY created_at ASC"
365
+ )
366
+ .all(leadId) as ActivityRow[];
367
+ return rows.map(rowToActivity);
368
+ }
369
+
370
+ export interface LeadStats {
371
+ total: number;
372
+ by_status: Record<string, number>;
373
+ by_source: Record<string, number>;
374
+ avg_score: number;
375
+ conversion_rate: number;
376
+ }
377
+
378
+ export function getLeadStats(): LeadStats {
379
+ const db = getDatabase();
380
+
381
+ const total = (db.prepare("SELECT COUNT(*) as count FROM leads").get() as { count: number }).count;
382
+
383
+ const statusRows = db
384
+ .prepare("SELECT status, COUNT(*) as count FROM leads GROUP BY status")
385
+ .all() as { status: string; count: number }[];
386
+ const by_status: Record<string, number> = {};
387
+ for (const row of statusRows) {
388
+ by_status[row.status] = row.count;
389
+ }
390
+
391
+ const sourceRows = db
392
+ .prepare("SELECT source, COUNT(*) as count FROM leads GROUP BY source")
393
+ .all() as { source: string; count: number }[];
394
+ const by_source: Record<string, number> = {};
395
+ for (const row of sourceRows) {
396
+ by_source[row.source] = row.count;
397
+ }
398
+
399
+ const avgRow = db
400
+ .prepare("SELECT AVG(score) as avg_score FROM leads")
401
+ .get() as { avg_score: number | null };
402
+ const avg_score = Math.round(avgRow.avg_score || 0);
403
+
404
+ const converted = by_status["converted"] || 0;
405
+ const conversion_rate = total > 0 ? Math.round((converted / total) * 100 * 100) / 100 : 0;
406
+
407
+ return { total, by_status, by_source, avg_score, conversion_rate };
408
+ }
409
+
410
+ export interface PipelineStage {
411
+ status: string;
412
+ count: number;
413
+ pct: number;
414
+ }
415
+
416
+ export function getPipeline(): PipelineStage[] {
417
+ const db = getDatabase();
418
+ const total = (db.prepare("SELECT COUNT(*) as count FROM leads").get() as { count: number }).count;
419
+
420
+ const statuses = ["new", "contacted", "qualified", "unqualified", "converted", "lost"];
421
+ const rows = db
422
+ .prepare("SELECT status, COUNT(*) as count FROM leads GROUP BY status")
423
+ .all() as { status: string; count: number }[];
424
+
425
+ const countMap: Record<string, number> = {};
426
+ for (const row of rows) {
427
+ countMap[row.status] = row.count;
428
+ }
429
+
430
+ return statuses.map((status) => ({
431
+ status,
432
+ count: countMap[status] || 0,
433
+ pct: total > 0 ? Math.round(((countMap[status] || 0) / total) * 100 * 100) / 100 : 0,
434
+ }));
435
+ }
436
+
437
+ export interface DuplicatePair {
438
+ lead1: Lead;
439
+ lead2: Lead;
440
+ email: string;
441
+ }
442
+
443
+ export function deduplicateLeads(): DuplicatePair[] {
444
+ const db = getDatabase();
445
+ // Find emails that appear more than once
446
+ const dupes = db
447
+ .prepare(
448
+ "SELECT email FROM leads WHERE email IS NOT NULL GROUP BY email HAVING COUNT(*) > 1"
449
+ )
450
+ .all() as { email: string }[];
451
+
452
+ const pairs: DuplicatePair[] = [];
453
+ for (const { email } of dupes) {
454
+ const rows = db
455
+ .prepare("SELECT * FROM leads WHERE email = ? ORDER BY created_at ASC")
456
+ .all(email) as LeadRow[];
457
+ const leads = rows.map(rowToLead);
458
+ // Create pairs from first lead with each subsequent
459
+ for (let i = 1; i < leads.length; i++) {
460
+ pairs.push({ lead1: leads[0], lead2: leads[i], email });
461
+ }
462
+ }
463
+
464
+ return pairs;
465
+ }
466
+
467
+ export function mergeLeads(keepId: string, mergeId: string): Lead | null {
468
+ const db = getDatabase();
469
+ const keep = getLead(keepId);
470
+ const merge = getLead(mergeId);
471
+ if (!keep || !merge) return null;
472
+
473
+ // Merge data: fill in blanks from mergeId into keepId
474
+ const updates: UpdateLeadInput = {};
475
+ if (!keep.name && merge.name) updates.name = merge.name;
476
+ if (!keep.email && merge.email) updates.email = merge.email;
477
+ if (!keep.phone && merge.phone) updates.phone = merge.phone;
478
+ if (!keep.company && merge.company) updates.company = merge.company;
479
+ if (!keep.title && merge.title) updates.title = merge.title;
480
+ if (!keep.website && merge.website) updates.website = merge.website;
481
+ if (!keep.linkedin_url && merge.linkedin_url) updates.linkedin_url = merge.linkedin_url;
482
+
483
+ // Merge tags
484
+ const mergedTags = [...new Set([...keep.tags, ...merge.tags])];
485
+ if (mergedTags.length > keep.tags.length) {
486
+ updates.tags = mergedTags;
487
+ }
488
+
489
+ // Take higher score
490
+ if (merge.score > keep.score) {
491
+ updates.score = merge.score;
492
+ updates.score_reason = merge.score_reason || keep.score_reason || undefined;
493
+ }
494
+
495
+ // Merge notes
496
+ if (merge.notes && merge.notes !== keep.notes) {
497
+ updates.notes = [keep.notes, merge.notes].filter(Boolean).join("\n---\n");
498
+ }
499
+
500
+ // Apply updates
501
+ if (Object.keys(updates).length > 0) {
502
+ updateLead(keepId, updates);
503
+ }
504
+
505
+ // Move activities from merge to keep
506
+ db.prepare("UPDATE lead_activities SET lead_id = ? WHERE lead_id = ?").run(keepId, mergeId);
507
+
508
+ // Move list memberships
509
+ db.prepare(
510
+ "UPDATE OR IGNORE lead_list_members SET lead_id = ? WHERE lead_id = ?"
511
+ ).run(keepId, mergeId);
512
+
513
+ // Delete the merged lead
514
+ deleteLead(mergeId);
515
+
516
+ // Log the merge
517
+ addActivity(keepId, "note", `Merged with lead ${mergeId}`);
518
+
519
+ return getLead(keepId);
520
+ }
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Lead list operations
3
+ */
4
+
5
+ import { getDatabase } from "./database.js";
6
+ import type { Lead } from "./leads.js";
7
+
8
+ export interface LeadList {
9
+ id: string;
10
+ name: string;
11
+ description: string | null;
12
+ filter_query: string | null;
13
+ created_at: string;
14
+ }
15
+
16
+ export interface CreateListInput {
17
+ name: string;
18
+ description?: string;
19
+ filter_query?: string;
20
+ }
21
+
22
+ export function createList(input: CreateListInput): LeadList {
23
+ const db = getDatabase();
24
+ const id = crypto.randomUUID();
25
+
26
+ db.prepare(
27
+ `INSERT INTO lead_lists (id, name, description, filter_query) VALUES (?, ?, ?, ?)`
28
+ ).run(id, input.name, input.description || null, input.filter_query || null);
29
+
30
+ return getList(id)!;
31
+ }
32
+
33
+ export function getList(id: string): LeadList | null {
34
+ const db = getDatabase();
35
+ return db.prepare("SELECT * FROM lead_lists WHERE id = ?").get(id) as LeadList | null;
36
+ }
37
+
38
+ export function listLists(): LeadList[] {
39
+ const db = getDatabase();
40
+ return db.prepare("SELECT * FROM lead_lists ORDER BY created_at DESC").all() as LeadList[];
41
+ }
42
+
43
+ export function deleteList(id: string): boolean {
44
+ const db = getDatabase();
45
+ const result = db.prepare("DELETE FROM lead_lists WHERE id = ?").run(id);
46
+ return result.changes > 0;
47
+ }
48
+
49
+ export function addToList(listId: string, leadId: string): boolean {
50
+ const db = getDatabase();
51
+ try {
52
+ db.prepare(
53
+ "INSERT OR IGNORE INTO lead_list_members (lead_list_id, lead_id) VALUES (?, ?)"
54
+ ).run(listId, leadId);
55
+ return true;
56
+ } catch {
57
+ return false;
58
+ }
59
+ }
60
+
61
+ export function removeFromList(listId: string, leadId: string): boolean {
62
+ const db = getDatabase();
63
+ const result = db
64
+ .prepare("DELETE FROM lead_list_members WHERE lead_list_id = ? AND lead_id = ?")
65
+ .run(listId, leadId);
66
+ return result.changes > 0;
67
+ }
68
+
69
+ interface LeadRow {
70
+ id: string;
71
+ name: string | null;
72
+ email: string | null;
73
+ phone: string | null;
74
+ company: string | null;
75
+ title: string | null;
76
+ website: string | null;
77
+ linkedin_url: string | null;
78
+ source: string;
79
+ status: string;
80
+ score: number;
81
+ score_reason: string | null;
82
+ tags: string;
83
+ notes: string | null;
84
+ metadata: string;
85
+ enriched: number;
86
+ enriched_at: string | null;
87
+ created_at: string;
88
+ updated_at: string;
89
+ }
90
+
91
+ function rowToLead(row: LeadRow): Lead {
92
+ return {
93
+ ...row,
94
+ tags: JSON.parse(row.tags || "[]"),
95
+ metadata: JSON.parse(row.metadata || "{}"),
96
+ enriched: row.enriched === 1,
97
+ };
98
+ }
99
+
100
+ export function getListMembers(listId: string): Lead[] {
101
+ const db = getDatabase();
102
+ const rows = db
103
+ .prepare(
104
+ `SELECT l.* FROM leads l
105
+ JOIN lead_list_members m ON l.id = m.lead_id
106
+ WHERE m.lead_list_id = ?
107
+ ORDER BY m.added_at DESC`
108
+ )
109
+ .all(listId) as LeadRow[];
110
+ return rows.map(rowToLead);
111
+ }
112
+
113
+ export function getSmartListMembers(listId: string): Lead[] {
114
+ const db = getDatabase();
115
+ const list = getList(listId);
116
+ if (!list) return [];
117
+
118
+ // If no filter_query, return regular members
119
+ if (!list.filter_query) {
120
+ return getListMembers(listId);
121
+ }
122
+
123
+ // Parse simple filter queries like "status=qualified AND score>=50"
124
+ const conditions: string[] = [];
125
+ const params: unknown[] = [];
126
+
127
+ const filters = list.filter_query.split(/\s+AND\s+/i);
128
+ for (const filter of filters) {
129
+ const match = filter.match(/^(\w+)\s*(=|>=|<=|>|<|!=)\s*(.+)$/);
130
+ if (match) {
131
+ const [, field, op, value] = match;
132
+ conditions.push(`${field} ${op} ?`);
133
+ // Try numeric parse
134
+ const num = Number(value);
135
+ params.push(isNaN(num) ? value : num);
136
+ }
137
+ }
138
+
139
+ if (conditions.length === 0) {
140
+ return getListMembers(listId);
141
+ }
142
+
143
+ const sql = `SELECT * FROM leads WHERE ${conditions.join(" AND ")} ORDER BY created_at DESC`;
144
+ try {
145
+ const rows = db.prepare(sql).all(...params) as LeadRow[];
146
+ return rows.map(rowToLead);
147
+ } catch {
148
+ // If filter query is invalid, fall back to regular members
149
+ return getListMembers(listId);
150
+ }
151
+ }