@hasna/contacts 0.3.2 → 0.4.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.
- package/dist/cli/index.js +1568 -206
- package/dist/db/companies.d.ts.map +1 -1
- package/dist/db/contacts.d.ts +1 -0
- package/dist/db/contacts.d.ts.map +1 -1
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/deals.d.ts +14 -0
- package/dist/db/deals.d.ts.map +1 -0
- package/dist/db/events.d.ts +14 -0
- package/dist/db/events.d.ts.map +1 -0
- package/dist/db/tags.d.ts.map +1 -1
- package/dist/index.d.ts +18 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1190 -81
- package/dist/lib/apple-contacts.d.ts +3 -0
- package/dist/lib/apple-contacts.d.ts.map +1 -0
- package/dist/lib/audit.d.ts +12 -0
- package/dist/lib/audit.d.ts.map +1 -0
- package/dist/lib/brief.d.ts +3 -0
- package/dist/lib/brief.d.ts.map +1 -0
- package/dist/lib/import.d.ts +1 -0
- package/dist/lib/import.d.ts.map +1 -1
- package/dist/lib/stats.d.ts +24 -0
- package/dist/lib/stats.d.ts.map +1 -0
- package/dist/lib/timeline.d.ts +11 -0
- package/dist/lib/timeline.d.ts.map +1 -0
- package/dist/lib/upcoming.d.ts +14 -0
- package/dist/lib/upcoming.d.ts.map +1 -0
- package/dist/mcp/index.js +1263 -173
- package/dist/server/index.js +125 -4
- package/dist/types/index.d.ts +65 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
// @bun
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __export = (target, all) => {
|
|
4
|
+
for (var name in all)
|
|
5
|
+
__defProp(target, name, {
|
|
6
|
+
get: all[name],
|
|
7
|
+
enumerable: true,
|
|
8
|
+
configurable: true,
|
|
9
|
+
set: (newValue) => all[name] = () => newValue
|
|
10
|
+
});
|
|
11
|
+
};
|
|
12
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
13
|
+
|
|
2
14
|
// src/db/database.ts
|
|
3
15
|
import { Database } from "bun:sqlite";
|
|
4
16
|
import { existsSync, mkdirSync } from "fs";
|
|
@@ -16,8 +28,50 @@ function ensureDir(filePath) {
|
|
|
16
28
|
if (!existsSync(dir))
|
|
17
29
|
mkdirSync(dir, { recursive: true });
|
|
18
30
|
}
|
|
19
|
-
|
|
20
|
-
|
|
31
|
+
function getDatabase(path) {
|
|
32
|
+
if (_db)
|
|
33
|
+
return _db;
|
|
34
|
+
const dbPath = path || getDbPath();
|
|
35
|
+
ensureDir(dbPath);
|
|
36
|
+
const db = new Database(dbPath, { create: true });
|
|
37
|
+
db.exec("PRAGMA journal_mode=WAL");
|
|
38
|
+
db.exec("PRAGMA foreign_keys=ON");
|
|
39
|
+
runMigrations(db);
|
|
40
|
+
_db = db;
|
|
41
|
+
return db;
|
|
42
|
+
}
|
|
43
|
+
function resetDatabase() {
|
|
44
|
+
_db = null;
|
|
45
|
+
}
|
|
46
|
+
function uuid() {
|
|
47
|
+
return crypto.randomUUID();
|
|
48
|
+
}
|
|
49
|
+
function now() {
|
|
50
|
+
return new Date().toISOString();
|
|
51
|
+
}
|
|
52
|
+
function runMigrations(db) {
|
|
53
|
+
try {
|
|
54
|
+
const row = db.query("SELECT MAX(version) as v FROM _migrations").get();
|
|
55
|
+
const current = row?.v ?? -1;
|
|
56
|
+
for (let i = current + 1;i < MIGRATIONS.length; i++) {
|
|
57
|
+
db.exec(MIGRATIONS[i]);
|
|
58
|
+
db.exec(`INSERT OR REPLACE INTO _migrations(version) VALUES(${i})`);
|
|
59
|
+
}
|
|
60
|
+
} catch {
|
|
61
|
+
for (const m of MIGRATIONS) {
|
|
62
|
+
try {
|
|
63
|
+
db.exec(m);
|
|
64
|
+
} catch {}
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
db.exec(`INSERT OR REPLACE INTO _migrations(version) VALUES(${MIGRATIONS.length - 1})`);
|
|
68
|
+
} catch {}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
var MIGRATIONS, _db = null;
|
|
72
|
+
var init_database = __esm(() => {
|
|
73
|
+
MIGRATIONS = [
|
|
74
|
+
`
|
|
21
75
|
CREATE TABLE IF NOT EXISTS companies (
|
|
22
76
|
id TEXT PRIMARY KEY,
|
|
23
77
|
name TEXT NOT NULL,
|
|
@@ -170,7 +224,7 @@ var MIGRATIONS = [
|
|
|
170
224
|
|
|
171
225
|
CREATE TABLE IF NOT EXISTS _migrations (version INTEGER PRIMARY KEY);
|
|
172
226
|
`,
|
|
173
|
-
|
|
227
|
+
`
|
|
174
228
|
ALTER TABLE contacts ADD COLUMN last_contacted_at TEXT;
|
|
175
229
|
ALTER TABLE contacts ADD COLUMN website TEXT;
|
|
176
230
|
ALTER TABLE contacts ADD COLUMN preferred_contact_method TEXT;
|
|
@@ -189,7 +243,7 @@ var MIGRATIONS = [
|
|
|
189
243
|
PRIMARY KEY (contact_id, group_id)
|
|
190
244
|
);
|
|
191
245
|
`,
|
|
192
|
-
|
|
246
|
+
`
|
|
193
247
|
ALTER TABLE contacts ADD COLUMN status TEXT DEFAULT 'active';
|
|
194
248
|
ALTER TABLE contacts ADD COLUMN follow_up_at TEXT;
|
|
195
249
|
ALTER TABLE contacts ADD COLUMN archived INTEGER NOT NULL DEFAULT 0;
|
|
@@ -203,7 +257,7 @@ var MIGRATIONS = [
|
|
|
203
257
|
PRIMARY KEY (company_id, group_id)
|
|
204
258
|
);
|
|
205
259
|
`,
|
|
206
|
-
|
|
260
|
+
`
|
|
207
261
|
CREATE TABLE IF NOT EXISTS company_relationships (
|
|
208
262
|
id TEXT PRIMARY KEY,
|
|
209
263
|
contact_id TEXT NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
|
|
@@ -216,7 +270,7 @@ var MIGRATIONS = [
|
|
|
216
270
|
CREATE INDEX IF NOT EXISTS idx_company_relationships_contact ON company_relationships(contact_id);
|
|
217
271
|
CREATE INDEX IF NOT EXISTS idx_company_relationships_company ON company_relationships(company_id);
|
|
218
272
|
`,
|
|
219
|
-
|
|
273
|
+
`
|
|
220
274
|
CREATE TABLE IF NOT EXISTS contact_notes (
|
|
221
275
|
id TEXT PRIMARY KEY,
|
|
222
276
|
contact_id TEXT NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
|
|
@@ -227,7 +281,7 @@ var MIGRATIONS = [
|
|
|
227
281
|
|
|
228
282
|
CREATE INDEX IF NOT EXISTS idx_contact_notes_contact ON contact_notes(contact_id);
|
|
229
283
|
`,
|
|
230
|
-
|
|
284
|
+
`
|
|
231
285
|
ALTER TABLE companies ADD COLUMN is_owned_entity INTEGER NOT NULL DEFAULT 0;
|
|
232
286
|
ALTER TABLE companies ADD COLUMN entity_type TEXT CHECK(entity_type IN ('operating','holding','dissolved','nonprofit','trust','branch','other'));
|
|
233
287
|
|
|
@@ -305,77 +359,71 @@ var MIGRATIONS = [
|
|
|
305
359
|
);
|
|
306
360
|
|
|
307
361
|
ALTER TABLE contact_notes ADD COLUMN company_id TEXT REFERENCES companies(id) ON DELETE SET NULL;
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
return _db;
|
|
314
|
-
const dbPath = path || getDbPath();
|
|
315
|
-
ensureDir(dbPath);
|
|
316
|
-
const db = new Database(dbPath, { create: true });
|
|
317
|
-
db.exec("PRAGMA journal_mode=WAL");
|
|
318
|
-
db.exec("PRAGMA foreign_keys=ON");
|
|
319
|
-
runMigrations(db);
|
|
320
|
-
_db = db;
|
|
321
|
-
return db;
|
|
322
|
-
}
|
|
323
|
-
function resetDatabase() {
|
|
324
|
-
_db = null;
|
|
325
|
-
}
|
|
326
|
-
function uuid() {
|
|
327
|
-
return crypto.randomUUID();
|
|
328
|
-
}
|
|
329
|
-
function now() {
|
|
330
|
-
return new Date().toISOString();
|
|
331
|
-
}
|
|
332
|
-
function runMigrations(db) {
|
|
333
|
-
try {
|
|
334
|
-
const row = db.query("SELECT MAX(version) as v FROM _migrations").get();
|
|
335
|
-
const current = row?.v ?? -1;
|
|
336
|
-
for (let i = current + 1;i < MIGRATIONS.length; i++) {
|
|
337
|
-
db.exec(MIGRATIONS[i]);
|
|
338
|
-
db.exec(`INSERT OR REPLACE INTO _migrations(version) VALUES(${i})`);
|
|
339
|
-
}
|
|
340
|
-
} catch {
|
|
341
|
-
for (const m of MIGRATIONS) {
|
|
342
|
-
try {
|
|
343
|
-
db.exec(m);
|
|
344
|
-
} catch {}
|
|
345
|
-
}
|
|
346
|
-
try {
|
|
347
|
-
db.exec(`INSERT OR REPLACE INTO _migrations(version) VALUES(${MIGRATIONS.length - 1})`);
|
|
348
|
-
} catch {}
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
// src/types/index.ts
|
|
352
|
-
class ContactNotFoundError extends Error {
|
|
353
|
-
constructor(id) {
|
|
354
|
-
super(`Contact not found: ${id}`);
|
|
355
|
-
this.name = "ContactNotFoundError";
|
|
356
|
-
}
|
|
357
|
-
}
|
|
362
|
+
`,
|
|
363
|
+
`
|
|
364
|
+
ALTER TABLE contacts ADD COLUMN do_not_contact INTEGER NOT NULL DEFAULT 0;
|
|
365
|
+
ALTER TABLE contacts ADD COLUMN priority INTEGER NOT NULL DEFAULT 3 CHECK(priority BETWEEN 1 AND 5);
|
|
366
|
+
ALTER TABLE contacts ADD COLUMN timezone TEXT;
|
|
358
367
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
368
|
+
CREATE TABLE IF NOT EXISTS deals (
|
|
369
|
+
id TEXT PRIMARY KEY,
|
|
370
|
+
title TEXT NOT NULL,
|
|
371
|
+
contact_id TEXT REFERENCES contacts(id) ON DELETE SET NULL,
|
|
372
|
+
company_id TEXT REFERENCES companies(id) ON DELETE SET NULL,
|
|
373
|
+
stage TEXT NOT NULL DEFAULT 'lead' CHECK(stage IN ('lead','qualified','proposal','negotiation','won','lost','cancelled')),
|
|
374
|
+
value_usd REAL,
|
|
375
|
+
currency TEXT NOT NULL DEFAULT 'USD',
|
|
376
|
+
close_date TEXT,
|
|
377
|
+
notes TEXT,
|
|
378
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
379
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
380
|
+
);
|
|
365
381
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
382
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
383
|
+
id TEXT PRIMARY KEY,
|
|
384
|
+
title TEXT NOT NULL,
|
|
385
|
+
type TEXT NOT NULL DEFAULT 'meeting' CHECK(type IN ('meeting','call','lunch','email','demo','conference','intro','other')),
|
|
386
|
+
event_date TEXT NOT NULL,
|
|
387
|
+
duration_min INTEGER,
|
|
388
|
+
contact_ids TEXT NOT NULL DEFAULT '[]',
|
|
389
|
+
company_id TEXT REFERENCES companies(id) ON DELETE SET NULL,
|
|
390
|
+
notes TEXT,
|
|
391
|
+
outcome TEXT,
|
|
392
|
+
deal_id TEXT REFERENCES deals(id) ON DELETE SET NULL,
|
|
393
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
394
|
+
);
|
|
395
|
+
`
|
|
396
|
+
];
|
|
397
|
+
});
|
|
372
398
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
}
|
|
399
|
+
// src/types/index.ts
|
|
400
|
+
var ContactNotFoundError, CompanyNotFoundError, TagNotFoundError, DuplicateTagNameError;
|
|
401
|
+
var init_types = __esm(() => {
|
|
402
|
+
ContactNotFoundError = class ContactNotFoundError extends Error {
|
|
403
|
+
constructor(id) {
|
|
404
|
+
super(`Contact not found: ${id}`);
|
|
405
|
+
this.name = "ContactNotFoundError";
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
CompanyNotFoundError = class CompanyNotFoundError extends Error {
|
|
409
|
+
constructor(id) {
|
|
410
|
+
super(`Company not found: ${id}`);
|
|
411
|
+
this.name = "CompanyNotFoundError";
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
TagNotFoundError = class TagNotFoundError extends Error {
|
|
415
|
+
constructor(id) {
|
|
416
|
+
super(`Tag not found: ${id}`);
|
|
417
|
+
this.name = "TagNotFoundError";
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
DuplicateTagNameError = class DuplicateTagNameError extends Error {
|
|
421
|
+
constructor(name) {
|
|
422
|
+
super(`Tag with name already exists: ${name}`);
|
|
423
|
+
this.name = "DuplicateTagNameError";
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
});
|
|
379
427
|
|
|
380
428
|
// src/db/activity.ts
|
|
381
429
|
function rowToActivity(row) {
|
|
@@ -409,8 +457,29 @@ function getActivity(id, db) {
|
|
|
409
457
|
const row = d.query(`SELECT * FROM activity_log WHERE id = ?`).get(id);
|
|
410
458
|
return row ? rowToActivity(row) : null;
|
|
411
459
|
}
|
|
460
|
+
var init_activity = __esm(() => {
|
|
461
|
+
init_database();
|
|
462
|
+
});
|
|
412
463
|
|
|
413
464
|
// src/db/contacts.ts
|
|
465
|
+
var exports_contacts = {};
|
|
466
|
+
__export(exports_contacts, {
|
|
467
|
+
updateContact: () => updateContact,
|
|
468
|
+
unarchiveContact: () => unarchiveContact,
|
|
469
|
+
searchContacts: () => searchContacts,
|
|
470
|
+
mergeContacts: () => mergeContacts,
|
|
471
|
+
listRecentContacts: () => listRecentContacts,
|
|
472
|
+
listContacts: () => listContacts,
|
|
473
|
+
listColdContacts: () => listColdContacts,
|
|
474
|
+
getContactByEmail: () => getContactByEmail,
|
|
475
|
+
getContact: () => getContact,
|
|
476
|
+
deleteContact: () => deleteContact,
|
|
477
|
+
createContact: () => createContact,
|
|
478
|
+
autoLinkContactToCompany: () => autoLinkContactToCompany,
|
|
479
|
+
archiveContact: () => archiveContact,
|
|
480
|
+
addPhoneToContact: () => addPhoneToContact,
|
|
481
|
+
addEmailToContact: () => addEmailToContact
|
|
482
|
+
});
|
|
414
483
|
function rowToContact(row) {
|
|
415
484
|
return {
|
|
416
485
|
...row,
|
|
@@ -420,7 +489,10 @@ function rowToContact(row) {
|
|
|
420
489
|
status: row.status ?? "active",
|
|
421
490
|
follow_up_at: row.follow_up_at ?? null,
|
|
422
491
|
archived: !!row.archived,
|
|
423
|
-
project_id: row.project_id ?? null
|
|
492
|
+
project_id: row.project_id ?? null,
|
|
493
|
+
do_not_contact: !!row.do_not_contact,
|
|
494
|
+
priority: row.priority ?? 3,
|
|
495
|
+
timezone: row.timezone ?? null
|
|
424
496
|
};
|
|
425
497
|
}
|
|
426
498
|
function rowToEmail(row) {
|
|
@@ -505,8 +577,8 @@ function createContact(input, db) {
|
|
|
505
577
|
const firstName = input.first_name ?? "";
|
|
506
578
|
const lastName = input.last_name ?? "";
|
|
507
579
|
const displayName = input.display_name ?? (firstName || lastName ? `${firstName} ${lastName}`.trim() : "Unnamed Contact");
|
|
508
|
-
d.run(`INSERT INTO contacts (id, first_name, last_name, display_name, nickname, avatar_url, notes, birthday, company_id, job_title, source, custom_fields, last_contacted_at, website, preferred_contact_method, status, follow_up_at, project_id, created_at, updated_at)
|
|
509
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
580
|
+
d.run(`INSERT INTO contacts (id, first_name, last_name, display_name, nickname, avatar_url, notes, birthday, company_id, job_title, source, custom_fields, last_contacted_at, website, preferred_contact_method, status, follow_up_at, project_id, do_not_contact, priority, timezone, created_at, updated_at)
|
|
581
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
510
582
|
id,
|
|
511
583
|
firstName,
|
|
512
584
|
lastName,
|
|
@@ -525,6 +597,9 @@ function createContact(input, db) {
|
|
|
525
597
|
input.status ?? "active",
|
|
526
598
|
input.follow_up_at ?? null,
|
|
527
599
|
input.project_id ?? null,
|
|
600
|
+
input.do_not_contact ? 1 : 0,
|
|
601
|
+
input.priority ?? 3,
|
|
602
|
+
input.timezone ?? null,
|
|
528
603
|
timestamp,
|
|
529
604
|
timestamp
|
|
530
605
|
]);
|
|
@@ -568,12 +643,18 @@ function listContacts(opts = {}, db) {
|
|
|
568
643
|
last_contacted_after,
|
|
569
644
|
last_contacted_before,
|
|
570
645
|
order_by = "display_name",
|
|
571
|
-
order_dir = "asc"
|
|
646
|
+
order_dir = "asc",
|
|
647
|
+
include_dnc = false,
|
|
648
|
+
priority_min,
|
|
649
|
+
updated_since
|
|
572
650
|
} = opts;
|
|
573
651
|
const conditions = [];
|
|
574
652
|
const params = [];
|
|
575
653
|
conditions.push("c.archived = ?");
|
|
576
654
|
params.push(archived ? 1 : 0);
|
|
655
|
+
if (!include_dnc) {
|
|
656
|
+
conditions.push("c.do_not_contact = 0");
|
|
657
|
+
}
|
|
577
658
|
if (company_id) {
|
|
578
659
|
conditions.push("c.company_id = ?");
|
|
579
660
|
params.push(company_id);
|
|
@@ -611,6 +692,14 @@ function listContacts(opts = {}, db) {
|
|
|
611
692
|
conditions.push("c.last_contacted_at <= ?");
|
|
612
693
|
params.push(last_contacted_before);
|
|
613
694
|
}
|
|
695
|
+
if (priority_min !== undefined) {
|
|
696
|
+
conditions.push("c.priority >= ?");
|
|
697
|
+
params.push(priority_min);
|
|
698
|
+
}
|
|
699
|
+
if (updated_since) {
|
|
700
|
+
conditions.push("c.updated_at >= ?");
|
|
701
|
+
params.push(updated_since);
|
|
702
|
+
}
|
|
614
703
|
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
615
704
|
const validOrderBy = ["display_name", "created_at", "updated_at", "last_contacted_at", "follow_up_at"].includes(order_by) ? order_by : "display_name";
|
|
616
705
|
const validOrderDir = order_dir === "desc" ? "DESC" : "ASC";
|
|
@@ -694,6 +783,18 @@ function updateContact(id, input, db) {
|
|
|
694
783
|
setClauses.push("project_id = ?");
|
|
695
784
|
params.push(input.project_id);
|
|
696
785
|
}
|
|
786
|
+
if (input.do_not_contact !== undefined) {
|
|
787
|
+
setClauses.push("do_not_contact = ?");
|
|
788
|
+
params.push(input.do_not_contact ? 1 : 0);
|
|
789
|
+
}
|
|
790
|
+
if (input.priority !== undefined) {
|
|
791
|
+
setClauses.push("priority = ?");
|
|
792
|
+
params.push(input.priority ?? 3);
|
|
793
|
+
}
|
|
794
|
+
if (input.timezone !== undefined) {
|
|
795
|
+
setClauses.push("timezone = ?");
|
|
796
|
+
params.push(input.timezone);
|
|
797
|
+
}
|
|
697
798
|
params.push(id);
|
|
698
799
|
d.run(`UPDATE contacts SET ${setClauses.join(", ")} WHERE id = ?`, params);
|
|
699
800
|
if (input.emails_add?.length) {
|
|
@@ -892,6 +993,15 @@ function unarchiveContact(id, db) {
|
|
|
892
993
|
const updated = d.query(`SELECT * FROM contacts WHERE id = ?`).get(id);
|
|
893
994
|
return loadContactDetails(d, rowToContact(updated));
|
|
894
995
|
}
|
|
996
|
+
function listColdContacts(days, db) {
|
|
997
|
+
const d = db || getDatabase();
|
|
998
|
+
const rows = d.query(`SELECT c.* FROM contacts c
|
|
999
|
+
WHERE c.archived = 0 AND c.do_not_contact = 0
|
|
1000
|
+
AND (c.last_contacted_at IS NULL OR c.last_contacted_at < datetime('now', ? || ' days'))
|
|
1001
|
+
ORDER BY c.last_contacted_at ASC NULLS FIRST
|
|
1002
|
+
LIMIT 100`).all(`-${days}`);
|
|
1003
|
+
return rows.map((row) => loadContactDetails(d, rowToContact(row)));
|
|
1004
|
+
}
|
|
895
1005
|
function autoLinkContactToCompany(contactId, db) {
|
|
896
1006
|
const d = db || getDatabase();
|
|
897
1007
|
const row = d.query(`SELECT * FROM contacts WHERE id = ?`).get(contactId);
|
|
@@ -911,7 +1021,20 @@ function autoLinkContactToCompany(contactId, db) {
|
|
|
911
1021
|
const updated = d.query(`SELECT * FROM contacts WHERE id = ?`).get(contactId);
|
|
912
1022
|
return loadContactDetails(d, rowToContact(updated));
|
|
913
1023
|
}
|
|
1024
|
+
var init_contacts = __esm(() => {
|
|
1025
|
+
init_types();
|
|
1026
|
+
init_database();
|
|
1027
|
+
init_activity();
|
|
1028
|
+
});
|
|
1029
|
+
|
|
1030
|
+
// src/index.ts
|
|
1031
|
+
init_database();
|
|
1032
|
+
init_contacts();
|
|
1033
|
+
|
|
914
1034
|
// src/db/companies.ts
|
|
1035
|
+
init_types();
|
|
1036
|
+
init_database();
|
|
1037
|
+
init_activity();
|
|
915
1038
|
function rowToCompany2(row) {
|
|
916
1039
|
return {
|
|
917
1040
|
...row,
|
|
@@ -1157,7 +1280,10 @@ function listCompanyEmployees(companyId, db) {
|
|
|
1157
1280
|
status: r.status ?? "active",
|
|
1158
1281
|
follow_up_at: r.follow_up_at ?? null,
|
|
1159
1282
|
archived: !!r.archived,
|
|
1160
|
-
project_id: r.project_id ?? null
|
|
1283
|
+
project_id: r.project_id ?? null,
|
|
1284
|
+
do_not_contact: !!r.do_not_contact,
|
|
1285
|
+
priority: r.priority ?? 3,
|
|
1286
|
+
timezone: r.timezone ?? null
|
|
1161
1287
|
}));
|
|
1162
1288
|
}
|
|
1163
1289
|
function archiveCompany(id, db) {
|
|
@@ -1184,6 +1310,8 @@ function listOwnedEntities(db) {
|
|
|
1184
1310
|
return listCompanies({ is_owned_entity: true }, db);
|
|
1185
1311
|
}
|
|
1186
1312
|
// src/db/tags.ts
|
|
1313
|
+
init_types();
|
|
1314
|
+
init_database();
|
|
1187
1315
|
function rowToTag2(row) {
|
|
1188
1316
|
return { ...row };
|
|
1189
1317
|
}
|
|
@@ -1282,7 +1410,10 @@ function listContactsByTag(tagId, db) {
|
|
|
1282
1410
|
status: r.status ?? "active",
|
|
1283
1411
|
follow_up_at: r.follow_up_at ?? null,
|
|
1284
1412
|
archived: !!r.archived,
|
|
1285
|
-
project_id: r.project_id ?? null
|
|
1413
|
+
project_id: r.project_id ?? null,
|
|
1414
|
+
do_not_contact: !!r.do_not_contact,
|
|
1415
|
+
priority: r.priority ?? 3,
|
|
1416
|
+
timezone: r.timezone ?? null
|
|
1286
1417
|
}));
|
|
1287
1418
|
}
|
|
1288
1419
|
function addTagToCompany(companyId, tagId, db) {
|
|
@@ -1300,6 +1431,8 @@ function removeTagFromCompany(companyId, tagId, db) {
|
|
|
1300
1431
|
d.run(`DELETE FROM company_tags WHERE company_id = ? AND tag_id = ?`, [companyId, tagId]);
|
|
1301
1432
|
}
|
|
1302
1433
|
// src/db/relationships.ts
|
|
1434
|
+
init_types();
|
|
1435
|
+
init_database();
|
|
1303
1436
|
function rowToRelationship(row) {
|
|
1304
1437
|
return {
|
|
1305
1438
|
...row,
|
|
@@ -1416,6 +1549,7 @@ function getEntityTeam(companyId, db) {
|
|
|
1416
1549
|
}));
|
|
1417
1550
|
}
|
|
1418
1551
|
// src/db/org-members.ts
|
|
1552
|
+
init_database();
|
|
1419
1553
|
function rowToOrgMember(row) {
|
|
1420
1554
|
return {
|
|
1421
1555
|
id: row.id,
|
|
@@ -1497,6 +1631,7 @@ function listOrgMembersForContact(contactId, db) {
|
|
|
1497
1631
|
return rows.map(rowToOrgMember);
|
|
1498
1632
|
}
|
|
1499
1633
|
// src/db/vendor-comms.ts
|
|
1634
|
+
init_database();
|
|
1500
1635
|
function rowToVendorComm(row) {
|
|
1501
1636
|
return {
|
|
1502
1637
|
id: row.id,
|
|
@@ -1643,6 +1778,7 @@ function markFollowUpDone(id, db) {
|
|
|
1643
1778
|
return rowToVendorComm(d.query(`SELECT * FROM vendor_communications WHERE id = ?`).get(id));
|
|
1644
1779
|
}
|
|
1645
1780
|
// src/db/contact-tasks.ts
|
|
1781
|
+
init_database();
|
|
1646
1782
|
function rowToContactTask(row) {
|
|
1647
1783
|
return {
|
|
1648
1784
|
id: row.id,
|
|
@@ -1786,6 +1922,7 @@ function checkEscalations(db) {
|
|
|
1786
1922
|
return results;
|
|
1787
1923
|
}
|
|
1788
1924
|
// src/db/applications.ts
|
|
1925
|
+
init_database();
|
|
1789
1926
|
function rowToApplication(row) {
|
|
1790
1927
|
return {
|
|
1791
1928
|
id: row.id,
|
|
@@ -1947,7 +2084,209 @@ function listPendingApplications(db) {
|
|
|
1947
2084
|
ORDER BY created_at DESC`).all();
|
|
1948
2085
|
return rows.map(rowToApplication);
|
|
1949
2086
|
}
|
|
2087
|
+
// src/db/deals.ts
|
|
2088
|
+
init_database();
|
|
2089
|
+
function rowToDeal(row) {
|
|
2090
|
+
return {
|
|
2091
|
+
id: row.id,
|
|
2092
|
+
title: row.title,
|
|
2093
|
+
contact_id: row.contact_id,
|
|
2094
|
+
company_id: row.company_id,
|
|
2095
|
+
stage: row.stage,
|
|
2096
|
+
value_usd: row.value_usd,
|
|
2097
|
+
currency: row.currency,
|
|
2098
|
+
close_date: row.close_date,
|
|
2099
|
+
notes: row.notes,
|
|
2100
|
+
created_at: row.created_at,
|
|
2101
|
+
updated_at: row.updated_at
|
|
2102
|
+
};
|
|
2103
|
+
}
|
|
2104
|
+
function createDeal(input, db) {
|
|
2105
|
+
const d = db || getDatabase();
|
|
2106
|
+
const id = uuid();
|
|
2107
|
+
const timestamp = now();
|
|
2108
|
+
d.run(`INSERT INTO deals (id, title, contact_id, company_id, stage, value_usd, currency, close_date, notes, created_at, updated_at)
|
|
2109
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
2110
|
+
id,
|
|
2111
|
+
input.title,
|
|
2112
|
+
input.contact_id ?? null,
|
|
2113
|
+
input.company_id ?? null,
|
|
2114
|
+
input.stage ?? "lead",
|
|
2115
|
+
input.value_usd ?? null,
|
|
2116
|
+
input.currency ?? "USD",
|
|
2117
|
+
input.close_date ?? null,
|
|
2118
|
+
input.notes ?? null,
|
|
2119
|
+
timestamp,
|
|
2120
|
+
timestamp
|
|
2121
|
+
]);
|
|
2122
|
+
return rowToDeal(d.query(`SELECT * FROM deals WHERE id = ?`).get(id));
|
|
2123
|
+
}
|
|
2124
|
+
function getDeal(id, db) {
|
|
2125
|
+
const d = db || getDatabase();
|
|
2126
|
+
const row = d.query(`SELECT * FROM deals WHERE id = ?`).get(id);
|
|
2127
|
+
return row ? rowToDeal(row) : null;
|
|
2128
|
+
}
|
|
2129
|
+
function listDeals(opts = {}, db) {
|
|
2130
|
+
const d = db || getDatabase();
|
|
2131
|
+
const conditions = [];
|
|
2132
|
+
const params = [];
|
|
2133
|
+
if (opts.stage) {
|
|
2134
|
+
conditions.push("stage = ?");
|
|
2135
|
+
params.push(opts.stage);
|
|
2136
|
+
}
|
|
2137
|
+
if (opts.contact_id) {
|
|
2138
|
+
conditions.push("contact_id = ?");
|
|
2139
|
+
params.push(opts.contact_id);
|
|
2140
|
+
}
|
|
2141
|
+
if (opts.company_id) {
|
|
2142
|
+
conditions.push("company_id = ?");
|
|
2143
|
+
params.push(opts.company_id);
|
|
2144
|
+
}
|
|
2145
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
2146
|
+
const rows = d.query(`SELECT * FROM deals ${where} ORDER BY created_at DESC`).all(...params);
|
|
2147
|
+
return rows.map(rowToDeal);
|
|
2148
|
+
}
|
|
2149
|
+
function updateDeal(id, input, db) {
|
|
2150
|
+
const d = db || getDatabase();
|
|
2151
|
+
const existing = d.query(`SELECT * FROM deals WHERE id = ?`).get(id);
|
|
2152
|
+
if (!existing)
|
|
2153
|
+
return null;
|
|
2154
|
+
const setClauses = ["updated_at = ?"];
|
|
2155
|
+
const params = [now()];
|
|
2156
|
+
if (input.title !== undefined) {
|
|
2157
|
+
setClauses.push("title = ?");
|
|
2158
|
+
params.push(input.title);
|
|
2159
|
+
}
|
|
2160
|
+
if (input.contact_id !== undefined) {
|
|
2161
|
+
setClauses.push("contact_id = ?");
|
|
2162
|
+
params.push(input.contact_id ?? null);
|
|
2163
|
+
}
|
|
2164
|
+
if (input.company_id !== undefined) {
|
|
2165
|
+
setClauses.push("company_id = ?");
|
|
2166
|
+
params.push(input.company_id ?? null);
|
|
2167
|
+
}
|
|
2168
|
+
if (input.stage !== undefined) {
|
|
2169
|
+
setClauses.push("stage = ?");
|
|
2170
|
+
params.push(input.stage);
|
|
2171
|
+
}
|
|
2172
|
+
if (input.value_usd !== undefined) {
|
|
2173
|
+
setClauses.push("value_usd = ?");
|
|
2174
|
+
params.push(input.value_usd ?? null);
|
|
2175
|
+
}
|
|
2176
|
+
if (input.currency !== undefined) {
|
|
2177
|
+
setClauses.push("currency = ?");
|
|
2178
|
+
params.push(input.currency);
|
|
2179
|
+
}
|
|
2180
|
+
if (input.close_date !== undefined) {
|
|
2181
|
+
setClauses.push("close_date = ?");
|
|
2182
|
+
params.push(input.close_date ?? null);
|
|
2183
|
+
}
|
|
2184
|
+
if (input.notes !== undefined) {
|
|
2185
|
+
setClauses.push("notes = ?");
|
|
2186
|
+
params.push(input.notes ?? null);
|
|
2187
|
+
}
|
|
2188
|
+
params.push(id);
|
|
2189
|
+
d.run(`UPDATE deals SET ${setClauses.join(", ")} WHERE id = ?`, params);
|
|
2190
|
+
return rowToDeal(d.query(`SELECT * FROM deals WHERE id = ?`).get(id));
|
|
2191
|
+
}
|
|
2192
|
+
function deleteDeal(id, db) {
|
|
2193
|
+
const d = db || getDatabase();
|
|
2194
|
+
d.run(`DELETE FROM deals WHERE id = ?`, [id]);
|
|
2195
|
+
}
|
|
2196
|
+
function getDealsByStage(db) {
|
|
2197
|
+
const d = db || getDatabase();
|
|
2198
|
+
const rows = d.query(`SELECT * FROM deals ORDER BY stage, created_at DESC`).all();
|
|
2199
|
+
const result = {};
|
|
2200
|
+
for (const row of rows) {
|
|
2201
|
+
if (!result[row.stage])
|
|
2202
|
+
result[row.stage] = [];
|
|
2203
|
+
result[row.stage].push(rowToDeal(row));
|
|
2204
|
+
}
|
|
2205
|
+
return result;
|
|
2206
|
+
}
|
|
2207
|
+
// src/db/events.ts
|
|
2208
|
+
init_database();
|
|
2209
|
+
function rowToEvent(row) {
|
|
2210
|
+
let contact_ids = [];
|
|
2211
|
+
try {
|
|
2212
|
+
contact_ids = JSON.parse(row.contact_ids);
|
|
2213
|
+
} catch {
|
|
2214
|
+
contact_ids = [];
|
|
2215
|
+
}
|
|
2216
|
+
return {
|
|
2217
|
+
id: row.id,
|
|
2218
|
+
title: row.title,
|
|
2219
|
+
type: row.type,
|
|
2220
|
+
event_date: row.event_date,
|
|
2221
|
+
duration_min: row.duration_min,
|
|
2222
|
+
contact_ids,
|
|
2223
|
+
company_id: row.company_id,
|
|
2224
|
+
notes: row.notes,
|
|
2225
|
+
outcome: row.outcome,
|
|
2226
|
+
deal_id: row.deal_id,
|
|
2227
|
+
created_at: row.created_at
|
|
2228
|
+
};
|
|
2229
|
+
}
|
|
2230
|
+
function logEvent(input, db) {
|
|
2231
|
+
const d = db || getDatabase();
|
|
2232
|
+
const id = uuid();
|
|
2233
|
+
const timestamp = now();
|
|
2234
|
+
d.run(`INSERT INTO events (id, title, type, event_date, duration_min, contact_ids, company_id, notes, outcome, deal_id, created_at)
|
|
2235
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
2236
|
+
id,
|
|
2237
|
+
input.title,
|
|
2238
|
+
input.type ?? "meeting",
|
|
2239
|
+
input.event_date,
|
|
2240
|
+
input.duration_min ?? null,
|
|
2241
|
+
JSON.stringify(input.contact_ids ?? []),
|
|
2242
|
+
input.company_id ?? null,
|
|
2243
|
+
input.notes ?? null,
|
|
2244
|
+
input.outcome ?? null,
|
|
2245
|
+
input.deal_id ?? null,
|
|
2246
|
+
timestamp
|
|
2247
|
+
]);
|
|
2248
|
+
return rowToEvent(d.query(`SELECT * FROM events WHERE id = ?`).get(id));
|
|
2249
|
+
}
|
|
2250
|
+
function getEvent(id, db) {
|
|
2251
|
+
const d = db || getDatabase();
|
|
2252
|
+
const row = d.query(`SELECT * FROM events WHERE id = ?`).get(id);
|
|
2253
|
+
return row ? rowToEvent(row) : null;
|
|
2254
|
+
}
|
|
2255
|
+
function listEvents(opts = {}, db) {
|
|
2256
|
+
const d = db || getDatabase();
|
|
2257
|
+
const conditions = [];
|
|
2258
|
+
const params = [];
|
|
2259
|
+
if (opts.contact_id) {
|
|
2260
|
+
conditions.push("contact_ids LIKE ?");
|
|
2261
|
+
params.push(`%${opts.contact_id}%`);
|
|
2262
|
+
}
|
|
2263
|
+
if (opts.company_id) {
|
|
2264
|
+
conditions.push("company_id = ?");
|
|
2265
|
+
params.push(opts.company_id);
|
|
2266
|
+
}
|
|
2267
|
+
if (opts.type) {
|
|
2268
|
+
conditions.push("type = ?");
|
|
2269
|
+
params.push(opts.type);
|
|
2270
|
+
}
|
|
2271
|
+
if (opts.date_from) {
|
|
2272
|
+
conditions.push("event_date >= ?");
|
|
2273
|
+
params.push(opts.date_from);
|
|
2274
|
+
}
|
|
2275
|
+
if (opts.date_to) {
|
|
2276
|
+
conditions.push("event_date <= ?");
|
|
2277
|
+
params.push(opts.date_to);
|
|
2278
|
+
}
|
|
2279
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
2280
|
+
const rows = d.query(`SELECT * FROM events ${where} ORDER BY event_date DESC`).all(...params);
|
|
2281
|
+
return rows.map(rowToEvent);
|
|
2282
|
+
}
|
|
2283
|
+
function deleteEvent(id, db) {
|
|
2284
|
+
const d = db || getDatabase();
|
|
2285
|
+
d.run(`DELETE FROM events WHERE id = ?`, [id]);
|
|
2286
|
+
}
|
|
1950
2287
|
// src/db/notes.ts
|
|
2288
|
+
init_types();
|
|
2289
|
+
init_database();
|
|
1951
2290
|
function addNote(contactId, body, createdBy, db, companyId) {
|
|
1952
2291
|
const d = db || getDatabase();
|
|
1953
2292
|
const contact = d.query(`SELECT id FROM contacts WHERE id = ?`).get(contactId);
|
|
@@ -1974,6 +2313,7 @@ function getNote(noteId, db) {
|
|
|
1974
2313
|
return d.query(`SELECT * FROM contact_notes WHERE id = ?`).get(noteId);
|
|
1975
2314
|
}
|
|
1976
2315
|
// src/db/groups.ts
|
|
2316
|
+
init_database();
|
|
1977
2317
|
function createGroup(db, input) {
|
|
1978
2318
|
const id = uuid();
|
|
1979
2319
|
db.query(`INSERT INTO groups(id, name, description, created_at, updated_at) VALUES(?,?,?,?,?)`).run(id, input.name, input.description ?? null, now(), now());
|
|
@@ -2042,6 +2382,250 @@ function listCompaniesInGroup(db, groupId) {
|
|
|
2042
2382
|
function listGroupsForCompany(db, companyId) {
|
|
2043
2383
|
return db.query(`SELECT g.* FROM groups g JOIN company_groups cog ON g.id = cog.group_id WHERE cog.company_id = ? ORDER BY g.name`).all(companyId);
|
|
2044
2384
|
}
|
|
2385
|
+
|
|
2386
|
+
// src/index.ts
|
|
2387
|
+
init_activity();
|
|
2388
|
+
|
|
2389
|
+
// src/lib/upcoming.ts
|
|
2390
|
+
init_database();
|
|
2391
|
+
function getUpcomingItems(days = 7, db) {
|
|
2392
|
+
const _db2 = db || getDatabase();
|
|
2393
|
+
const items = [];
|
|
2394
|
+
const now3 = new Date;
|
|
2395
|
+
const future = new Date(now3.getTime() + days * 86400000);
|
|
2396
|
+
const todayStr = now3.toISOString().slice(0, 10);
|
|
2397
|
+
const futureStr = future.toISOString().slice(0, 10);
|
|
2398
|
+
const followUps = _db2.query(`SELECT c.id, c.display_name, c.follow_up_at FROM contacts c WHERE c.follow_up_at IS NOT NULL AND c.follow_up_at <= ? AND c.do_not_contact = 0`).all(futureStr);
|
|
2399
|
+
for (const r of followUps) {
|
|
2400
|
+
items.push({ date: r.follow_up_at, type: "follow_up", contact_id: r.id, contact_name: r.display_name, title: `Follow up with ${r.display_name}`, urgency: r.follow_up_at < todayStr ? "overdue" : r.follow_up_at === todayStr ? "today" : "upcoming" });
|
|
2401
|
+
}
|
|
2402
|
+
const tasks = _db2.query(`SELECT ct.*, c.display_name FROM contact_tasks ct JOIN contacts c ON ct.contact_id = c.id WHERE ct.deadline IS NOT NULL AND ct.deadline <= ? AND ct.status NOT IN ('completed','cancelled')`).all(futureStr);
|
|
2403
|
+
for (const t of tasks) {
|
|
2404
|
+
items.push({ date: t.deadline, type: "task_deadline", contact_id: t.contact_id, contact_name: t.display_name, title: t.title, urgency: t.deadline < todayStr ? "overdue" : t.deadline === todayStr ? "today" : "upcoming" });
|
|
2405
|
+
}
|
|
2406
|
+
const apps = _db2.query(`SELECT a.*, c.display_name as contact_name FROM applications a LEFT JOIN contacts c ON a.primary_contact_id = c.id WHERE a.follow_up_date IS NOT NULL AND a.follow_up_date <= ?`).all(futureStr);
|
|
2407
|
+
for (const a of apps) {
|
|
2408
|
+
items.push({ date: a.follow_up_date, type: "application_followup", contact_name: a.contact_name ?? undefined, title: `Follow up: ${a.program_name}`, urgency: a.follow_up_date < todayStr ? "overdue" : a.follow_up_date === todayStr ? "today" : "upcoming" });
|
|
2409
|
+
}
|
|
2410
|
+
const vendorFU = _db2.query(`SELECT vc.*, co.name as company_name FROM vendor_communications vc JOIN companies co ON vc.company_id = co.id WHERE vc.follow_up_date IS NOT NULL AND vc.follow_up_date <= ? AND vc.follow_up_done = 0`).all(futureStr);
|
|
2411
|
+
for (const v of vendorFU) {
|
|
2412
|
+
items.push({ date: v.follow_up_date, type: "vendor_followup", company_id: v.company_id, company_name: v.company_name, title: `Follow up with ${v.company_name}: ${v.subject || v.type}`, urgency: v.follow_up_date < todayStr ? "overdue" : v.follow_up_date === todayStr ? "today" : "upcoming" });
|
|
2413
|
+
}
|
|
2414
|
+
const contacts = _db2.query(`SELECT id, display_name, birthday FROM contacts WHERE birthday IS NOT NULL AND do_not_contact = 0`).all();
|
|
2415
|
+
for (const c of contacts) {
|
|
2416
|
+
const bday = new Date(c.birthday);
|
|
2417
|
+
const thisYear = new Date(now3.getFullYear(), bday.getMonth(), bday.getDate());
|
|
2418
|
+
const nextBday = thisYear >= now3 ? thisYear : new Date(now3.getFullYear() + 1, bday.getMonth(), bday.getDate());
|
|
2419
|
+
const nextStr = nextBday.toISOString().slice(0, 10);
|
|
2420
|
+
if (nextStr <= futureStr) {
|
|
2421
|
+
items.push({ date: nextStr, type: "birthday", contact_id: c.id, contact_name: c.display_name, title: `Birthday: ${c.display_name}`, urgency: nextStr === todayStr ? "today" : "upcoming" });
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
return items.sort((a, b) => a.date.localeCompare(b.date));
|
|
2425
|
+
}
|
|
2426
|
+
// src/lib/stats.ts
|
|
2427
|
+
init_database();
|
|
2428
|
+
function getNetworkStats(db) {
|
|
2429
|
+
const _db2 = db || getDatabase();
|
|
2430
|
+
const q = (sql) => _db2.query(sql).get();
|
|
2431
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
2432
|
+
const d30 = new Date(Date.now() - 30 * 86400000).toISOString().slice(0, 10);
|
|
2433
|
+
const d60 = new Date(Date.now() - 60 * 86400000).toISOString().slice(0, 10);
|
|
2434
|
+
return {
|
|
2435
|
+
total_contacts: q(`SELECT COUNT(*) c FROM contacts WHERE archived=0`).c,
|
|
2436
|
+
total_companies: q(`SELECT COUNT(*) c FROM companies WHERE archived=0`).c,
|
|
2437
|
+
owned_entities: q(`SELECT COUNT(*) c FROM companies WHERE is_owned_entity=1`).c,
|
|
2438
|
+
total_tags: q(`SELECT COUNT(*) c FROM tags`).c,
|
|
2439
|
+
total_groups: q(`SELECT COUNT(*) c FROM groups`).c,
|
|
2440
|
+
total_deals: q(`SELECT COUNT(*) c FROM deals WHERE stage NOT IN ('won','lost','cancelled')`).c,
|
|
2441
|
+
total_events: q(`SELECT COUNT(*) c FROM events`).c,
|
|
2442
|
+
cold_30d: q(`SELECT COUNT(*) c FROM contacts WHERE archived=0 AND do_not_contact=0 AND (last_contacted_at IS NULL OR last_contacted_at < '${d30}')`).c,
|
|
2443
|
+
cold_60d: q(`SELECT COUNT(*) c FROM contacts WHERE archived=0 AND do_not_contact=0 AND (last_contacted_at IS NULL OR last_contacted_at < '${d60}')`).c,
|
|
2444
|
+
cold_never: q(`SELECT COUNT(*) c FROM contacts WHERE archived=0 AND do_not_contact=0 AND last_contacted_at IS NULL`).c,
|
|
2445
|
+
contacts_with_email: q(`SELECT COUNT(DISTINCT contact_id) c FROM emails WHERE contact_id IS NOT NULL`).c,
|
|
2446
|
+
contacts_with_phone: q(`SELECT COUNT(DISTINCT contact_id) c FROM phones WHERE contact_id IS NOT NULL`).c,
|
|
2447
|
+
contacts_no_company: q(`SELECT COUNT(*) c FROM contacts WHERE archived=0 AND company_id IS NULL`).c,
|
|
2448
|
+
overdue_tasks: q(`SELECT COUNT(*) c FROM contact_tasks WHERE deadline < '${today}' AND status NOT IN ('completed','cancelled')`).c,
|
|
2449
|
+
pending_applications: q(`SELECT COUNT(*) c FROM applications WHERE status IN ('submitted','pending','follow_up_needed')`).c,
|
|
2450
|
+
missing_invoices: q(`SELECT COUNT(*) c FROM vendor_communications WHERE type='invoice_request' AND status IN ('awaiting_response','no_response')`).c,
|
|
2451
|
+
upcoming_7d: q(`SELECT COUNT(*) c FROM contacts WHERE follow_up_at BETWEEN '${today}' AND date('${today}','+7 days')`).c,
|
|
2452
|
+
notes_count: q(`SELECT COUNT(*) c FROM contact_notes`).c,
|
|
2453
|
+
active_deals_value: q(`SELECT COALESCE(SUM(value_usd),0) c FROM deals WHERE stage NOT IN ('won','lost','cancelled') AND currency='USD'`).c
|
|
2454
|
+
};
|
|
2455
|
+
}
|
|
2456
|
+
// src/lib/audit.ts
|
|
2457
|
+
init_database();
|
|
2458
|
+
function auditContact(contact) {
|
|
2459
|
+
const missing = [];
|
|
2460
|
+
const suggestions = [];
|
|
2461
|
+
let score = 0;
|
|
2462
|
+
if (contact.emails?.length)
|
|
2463
|
+
score += 20;
|
|
2464
|
+
else {
|
|
2465
|
+
missing.push("email");
|
|
2466
|
+
suggestions.push("Add an email address");
|
|
2467
|
+
}
|
|
2468
|
+
if (contact.phones?.length)
|
|
2469
|
+
score += 15;
|
|
2470
|
+
else {
|
|
2471
|
+
missing.push("phone");
|
|
2472
|
+
suggestions.push("Add a phone number");
|
|
2473
|
+
}
|
|
2474
|
+
if (contact.company_id)
|
|
2475
|
+
score += 15;
|
|
2476
|
+
else {
|
|
2477
|
+
missing.push("company");
|
|
2478
|
+
suggestions.push("Link to a company");
|
|
2479
|
+
}
|
|
2480
|
+
if (contact.last_contacted_at)
|
|
2481
|
+
score += 20;
|
|
2482
|
+
else {
|
|
2483
|
+
missing.push("last_contacted_at");
|
|
2484
|
+
suggestions.push("Log a contact interaction");
|
|
2485
|
+
}
|
|
2486
|
+
if (contact.tags?.length)
|
|
2487
|
+
score += 10;
|
|
2488
|
+
else {
|
|
2489
|
+
missing.push("tags");
|
|
2490
|
+
suggestions.push("Add at least one tag");
|
|
2491
|
+
}
|
|
2492
|
+
if (contact.notes)
|
|
2493
|
+
score += 10;
|
|
2494
|
+
else {
|
|
2495
|
+
missing.push("notes");
|
|
2496
|
+
suggestions.push("Add notes");
|
|
2497
|
+
}
|
|
2498
|
+
if (contact.job_title)
|
|
2499
|
+
score += 10;
|
|
2500
|
+
else {
|
|
2501
|
+
missing.push("job_title");
|
|
2502
|
+
suggestions.push("Add a job title");
|
|
2503
|
+
}
|
|
2504
|
+
return { contact_id: contact.id, display_name: contact.display_name, score, missing, suggestions };
|
|
2505
|
+
}
|
|
2506
|
+
async function listContactAudit(db) {
|
|
2507
|
+
const _db2 = db || getDatabase();
|
|
2508
|
+
const { listContacts: listContacts2 } = await Promise.resolve().then(() => (init_contacts(), exports_contacts));
|
|
2509
|
+
const { contacts } = listContacts2({ limit: 500, include_dnc: true }, _db2);
|
|
2510
|
+
return contacts.map(auditContact).sort((a, b) => a.score - b.score);
|
|
2511
|
+
}
|
|
2512
|
+
// src/lib/timeline.ts
|
|
2513
|
+
init_database();
|
|
2514
|
+
function getContactTimeline(contactId, limit = 50, db) {
|
|
2515
|
+
const _db2 = db || getDatabase();
|
|
2516
|
+
const items = [];
|
|
2517
|
+
const notes = _db2.query(`SELECT * FROM contact_notes WHERE contact_id = ? ORDER BY created_at DESC LIMIT 50`).all(contactId);
|
|
2518
|
+
for (const n of notes) {
|
|
2519
|
+
items.push({ date: n.created_at, type: "note", title: "Note", body: n.body });
|
|
2520
|
+
}
|
|
2521
|
+
const events = _db2.query(`SELECT * FROM events WHERE contact_ids LIKE ? ORDER BY event_date DESC LIMIT 50`).all(`%${contactId}%`);
|
|
2522
|
+
for (const e of events) {
|
|
2523
|
+
items.push({ date: e.event_date, type: "event", title: `${e.type}: ${e.title}`, body: e.notes ?? undefined, metadata: { outcome: e.outcome, duration_min: e.duration_min } });
|
|
2524
|
+
}
|
|
2525
|
+
const tasks = _db2.query(`SELECT * FROM contact_tasks WHERE contact_id = ? ORDER BY created_at DESC LIMIT 30`).all(contactId);
|
|
2526
|
+
for (const t of tasks) {
|
|
2527
|
+
items.push({ date: t.created_at, type: "task_created", title: `Task created: ${t.title}`, metadata: { deadline: t.deadline, priority: t.priority } });
|
|
2528
|
+
if (t.status === "completed") {
|
|
2529
|
+
items.push({ date: t.updated_at, type: "task_completed", title: `Task completed: ${t.title}` });
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
const comms = _db2.query(`SELECT vc.*, co.name as company_name FROM vendor_communications vc JOIN companies co ON vc.company_id = co.id WHERE vc.contact_id = ? ORDER BY vc.comm_date DESC LIMIT 20`).all(contactId);
|
|
2533
|
+
for (const c of comms) {
|
|
2534
|
+
items.push({ date: c.comm_date, type: "vendor_comm", title: `${c.type} \u2014 ${c.company_name}`, body: c.subject ?? undefined });
|
|
2535
|
+
}
|
|
2536
|
+
const activity = _db2.query(`SELECT * FROM activity_log WHERE contact_id = ? ORDER BY created_at DESC LIMIT 30`).all(contactId);
|
|
2537
|
+
for (const a of activity) {
|
|
2538
|
+
items.push({ date: a.created_at, type: "interaction", title: a.action, body: a.details ?? undefined });
|
|
2539
|
+
}
|
|
2540
|
+
return items.sort((a, b) => b.date.localeCompare(a.date)).slice(0, limit);
|
|
2541
|
+
}
|
|
2542
|
+
// src/lib/brief.ts
|
|
2543
|
+
init_database();
|
|
2544
|
+
init_contacts();
|
|
2545
|
+
function generateBrief(contactId, db) {
|
|
2546
|
+
const _db2 = db || getDatabase();
|
|
2547
|
+
const contact = getContact(contactId, _db2);
|
|
2548
|
+
const notes = listNotes(contactId, _db2);
|
|
2549
|
+
const allTasks = listContactTasks({ contact_id: contactId }, _db2);
|
|
2550
|
+
const tasks = allTasks.filter((t) => !["completed", "cancelled"].includes(t.status));
|
|
2551
|
+
const overdueTasks = allTasks.filter((t) => t.deadline && t.deadline < new Date().toISOString() && !["completed", "cancelled"].includes(t.status));
|
|
2552
|
+
const companyRels = listCompanyRelationships({ contact_id: contactId }, _db2);
|
|
2553
|
+
const recentTimeline = getContactTimeline(contactId, 5, _db2);
|
|
2554
|
+
const daysSince = contact.last_contacted_at ? Math.floor((Date.now() - new Date(contact.last_contacted_at).getTime()) / 86400000) : null;
|
|
2555
|
+
const lines = [];
|
|
2556
|
+
lines.push(`# ${contact.display_name}`);
|
|
2557
|
+
if (contact.job_title)
|
|
2558
|
+
lines.push(`**Role:** ${contact.job_title}${contact.company_id ? ` (linked to company)` : ""}`);
|
|
2559
|
+
if (contact.emails?.length) {
|
|
2560
|
+
const primary = contact.emails.find((e) => e.is_primary) || contact.emails[0];
|
|
2561
|
+
if (primary)
|
|
2562
|
+
lines.push(`**Email:** ${primary.address}`);
|
|
2563
|
+
}
|
|
2564
|
+
if (contact.phones?.length) {
|
|
2565
|
+
const primary = contact.phones.find((p) => p.is_primary) || contact.phones[0];
|
|
2566
|
+
if (primary)
|
|
2567
|
+
lines.push(`**Phone:** ${primary.number}`);
|
|
2568
|
+
}
|
|
2569
|
+
if (contact.preferred_contact_method)
|
|
2570
|
+
lines.push(`**Preferred contact:** ${contact.preferred_contact_method}`);
|
|
2571
|
+
lines.push("");
|
|
2572
|
+
lines.push(`## Status`);
|
|
2573
|
+
lines.push(`- Last contacted: ${daysSince !== null ? `${daysSince} days ago` : "never"}`);
|
|
2574
|
+
lines.push(`- Status: ${contact.status || "active"}`);
|
|
2575
|
+
if (contact.follow_up_at)
|
|
2576
|
+
lines.push(`- Follow-up scheduled: ${contact.follow_up_at}`);
|
|
2577
|
+
if (overdueTasks.length)
|
|
2578
|
+
lines.push(`- OVERDUE TASKS: ${overdueTasks.length}`);
|
|
2579
|
+
if (companyRels.length) {
|
|
2580
|
+
lines.push("");
|
|
2581
|
+
lines.push(`## Entity Relationships`);
|
|
2582
|
+
for (const r of companyRels)
|
|
2583
|
+
lines.push(`- ${r.relationship_type} \u2014 ${r.notes || ""}`);
|
|
2584
|
+
}
|
|
2585
|
+
if (tasks.length) {
|
|
2586
|
+
lines.push("");
|
|
2587
|
+
lines.push(`## Open Tasks`);
|
|
2588
|
+
for (const t of tasks)
|
|
2589
|
+
lines.push(`- [${t.priority}] ${t.title}${t.deadline ? ` (due ${t.deadline})` : ""}`);
|
|
2590
|
+
}
|
|
2591
|
+
if (notes.length) {
|
|
2592
|
+
lines.push("");
|
|
2593
|
+
lines.push(`## Recent Notes`);
|
|
2594
|
+
for (const n of notes.slice(0, 3))
|
|
2595
|
+
lines.push(`**${n.created_at.slice(0, 10)}:** ${n.body}`);
|
|
2596
|
+
}
|
|
2597
|
+
if (recentTimeline.length) {
|
|
2598
|
+
lines.push("");
|
|
2599
|
+
lines.push(`## Recent Activity`);
|
|
2600
|
+
for (const item of recentTimeline)
|
|
2601
|
+
lines.push(`- ${item.date.slice(0, 10)} ${item.title}`);
|
|
2602
|
+
}
|
|
2603
|
+
if (contact.notes) {
|
|
2604
|
+
lines.push("");
|
|
2605
|
+
lines.push(`## Background Notes`);
|
|
2606
|
+
lines.push(contact.notes);
|
|
2607
|
+
}
|
|
2608
|
+
return lines.join(`
|
|
2609
|
+
`);
|
|
2610
|
+
}
|
|
2611
|
+
// src/lib/apple-contacts.ts
|
|
2612
|
+
async function exportFromApple() {
|
|
2613
|
+
if (process.platform !== "darwin")
|
|
2614
|
+
throw new Error("Apple Contacts sync is only available on macOS");
|
|
2615
|
+
const script = `tell application "Contacts" to return vcard of every person`;
|
|
2616
|
+
const proc = Bun.spawn(["osascript", "-e", script], { stdout: "pipe", stderr: "pipe" });
|
|
2617
|
+
const output = await new Response(proc.stdout).text();
|
|
2618
|
+
await proc.exited;
|
|
2619
|
+
return output;
|
|
2620
|
+
}
|
|
2621
|
+
async function importToApple(vcfData) {
|
|
2622
|
+
if (process.platform !== "darwin")
|
|
2623
|
+
throw new Error("Apple Contacts sync is only available on macOS");
|
|
2624
|
+
const tmpFile = `/tmp/contacts-import-${Date.now()}.vcf`;
|
|
2625
|
+
await Bun.write(tmpFile, vcfData);
|
|
2626
|
+
const proc = Bun.spawn(["open", tmpFile], { stdout: "pipe", stderr: "pipe" });
|
|
2627
|
+
await proc.exited;
|
|
2628
|
+
}
|
|
2045
2629
|
// src/lib/connector.ts
|
|
2046
2630
|
import { join as join2 } from "path";
|
|
2047
2631
|
import { existsSync as existsSync2, readFileSync } from "fs";
|
|
@@ -2133,6 +2717,505 @@ function readConnectorTokens(name, profile = "default") {
|
|
|
2133
2717
|
throw new ConnectorAuthError(name, "tokens file unreadable");
|
|
2134
2718
|
}
|
|
2135
2719
|
}
|
|
2720
|
+
// src/lib/import.ts
|
|
2721
|
+
function parseCsv(text) {
|
|
2722
|
+
const lines = text.split(/\r?\n/);
|
|
2723
|
+
if (lines.length < 2)
|
|
2724
|
+
return [];
|
|
2725
|
+
const headers = parseCsvLine(lines[0]);
|
|
2726
|
+
const rows = [];
|
|
2727
|
+
for (let i = 1;i < lines.length; i++) {
|
|
2728
|
+
const line = lines[i].trim();
|
|
2729
|
+
if (!line)
|
|
2730
|
+
continue;
|
|
2731
|
+
const values = parseCsvLine(line);
|
|
2732
|
+
const row = {};
|
|
2733
|
+
headers.forEach((h, idx) => {
|
|
2734
|
+
row[h.trim()] = values[idx]?.trim() ?? "";
|
|
2735
|
+
});
|
|
2736
|
+
rows.push(row);
|
|
2737
|
+
}
|
|
2738
|
+
return rows;
|
|
2739
|
+
}
|
|
2740
|
+
function parseCsvLine(line) {
|
|
2741
|
+
const fields = [];
|
|
2742
|
+
let current = "";
|
|
2743
|
+
let inQuotes = false;
|
|
2744
|
+
for (let i = 0;i < line.length; i++) {
|
|
2745
|
+
const ch = line[i];
|
|
2746
|
+
if (ch === '"') {
|
|
2747
|
+
if (inQuotes && line[i + 1] === '"') {
|
|
2748
|
+
current += '"';
|
|
2749
|
+
i++;
|
|
2750
|
+
} else {
|
|
2751
|
+
inQuotes = !inQuotes;
|
|
2752
|
+
}
|
|
2753
|
+
} else if (ch === "," && !inQuotes) {
|
|
2754
|
+
fields.push(current);
|
|
2755
|
+
current = "";
|
|
2756
|
+
} else {
|
|
2757
|
+
current += ch;
|
|
2758
|
+
}
|
|
2759
|
+
}
|
|
2760
|
+
fields.push(current);
|
|
2761
|
+
return fields;
|
|
2762
|
+
}
|
|
2763
|
+
function csvRowToContact(row) {
|
|
2764
|
+
const firstName = row["First Name"] ?? row["first_name"] ?? row["Given Name"] ?? "";
|
|
2765
|
+
const lastName = row["Last Name"] ?? row["last_name"] ?? row["Family Name"] ?? "";
|
|
2766
|
+
const displayName = row["Name"] ?? row["display_name"] ?? row["Full Name"] ?? [firstName, lastName].filter(Boolean).join(" ") ?? "";
|
|
2767
|
+
if (!displayName && !firstName && !lastName)
|
|
2768
|
+
return null;
|
|
2769
|
+
const contact = {
|
|
2770
|
+
display_name: displayName || [firstName, lastName].filter(Boolean).join(" ") || "Unnamed",
|
|
2771
|
+
first_name: firstName || undefined,
|
|
2772
|
+
last_name: lastName || undefined,
|
|
2773
|
+
job_title: row["Job Title"] ?? row["job_title"] ?? row["Title"] ?? undefined,
|
|
2774
|
+
notes: row["Notes"] ?? row["notes"] ?? undefined,
|
|
2775
|
+
birthday: row["Birthday"] ?? row["birthday"] ?? undefined,
|
|
2776
|
+
source: "import"
|
|
2777
|
+
};
|
|
2778
|
+
const emails = [];
|
|
2779
|
+
for (let i = 1;i <= 5; i++) {
|
|
2780
|
+
const val = row[`Email ${i} - Value`] ?? row[`Email Address ${i}`] ?? (i === 1 ? row["Email"] ?? row["email"] ?? row["Email Address"] : undefined);
|
|
2781
|
+
const rawType = row[`Email ${i} - Type`] ?? (i === 1 ? "work" : "other");
|
|
2782
|
+
if (val) {
|
|
2783
|
+
const type = rawType?.toLowerCase() === "personal" ? "personal" : rawType?.toLowerCase() === "other" ? "other" : "work";
|
|
2784
|
+
emails.push({ address: val, type, is_primary: i === 1 });
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2787
|
+
if (emails.length)
|
|
2788
|
+
contact.emails = emails;
|
|
2789
|
+
const phones = [];
|
|
2790
|
+
for (let i = 1;i <= 5; i++) {
|
|
2791
|
+
const val = row[`Phone ${i} - Value`] ?? row[`Phone ${i}`] ?? (i === 1 ? row["Phone"] ?? row["phone"] ?? row["Mobile"] : undefined);
|
|
2792
|
+
const rawType = row[`Phone ${i} - Type`] ?? (i === 1 ? "mobile" : "other");
|
|
2793
|
+
if (val) {
|
|
2794
|
+
const type = rawType?.toLowerCase().includes("mobile") || rawType?.toLowerCase().includes("cell") ? "mobile" : rawType?.toLowerCase().includes("work") ? "work" : rawType?.toLowerCase().includes("home") ? "home" : rawType?.toLowerCase().includes("fax") ? "fax" : "other";
|
|
2795
|
+
phones.push({ number: val, type, is_primary: i === 1 });
|
|
2796
|
+
}
|
|
2797
|
+
}
|
|
2798
|
+
if (phones.length)
|
|
2799
|
+
contact.phones = phones;
|
|
2800
|
+
return contact;
|
|
2801
|
+
}
|
|
2802
|
+
function importFromCsv(data) {
|
|
2803
|
+
const rows = parseCsv(data);
|
|
2804
|
+
return rows.map(csvRowToContact).filter(Boolean);
|
|
2805
|
+
}
|
|
2806
|
+
function parseVcf(data) {
|
|
2807
|
+
const contacts = [];
|
|
2808
|
+
const blocks = data.split(/BEGIN:VCARD/i).filter((b) => b.trim());
|
|
2809
|
+
for (const block of blocks) {
|
|
2810
|
+
try {
|
|
2811
|
+
const contact = parseVcfBlock(`BEGIN:VCARD
|
|
2812
|
+
` + block);
|
|
2813
|
+
if (contact)
|
|
2814
|
+
contacts.push(contact);
|
|
2815
|
+
} catch {}
|
|
2816
|
+
}
|
|
2817
|
+
return contacts;
|
|
2818
|
+
}
|
|
2819
|
+
function parseVcfBlock(block) {
|
|
2820
|
+
const unfolded = block.replace(/\r?\n[ \t]/g, "");
|
|
2821
|
+
const lines = unfolded.split(/\r?\n/).filter((l) => l.trim());
|
|
2822
|
+
const contact = { source: "import" };
|
|
2823
|
+
const emails = [];
|
|
2824
|
+
const phones = [];
|
|
2825
|
+
const addresses = [];
|
|
2826
|
+
const socials = [];
|
|
2827
|
+
for (const line of lines) {
|
|
2828
|
+
if (/^BEGIN:VCARD$/i.test(line) || /^END:VCARD$/i.test(line) || /^VERSION:/i.test(line))
|
|
2829
|
+
continue;
|
|
2830
|
+
const colonIdx = line.indexOf(":");
|
|
2831
|
+
if (colonIdx === -1)
|
|
2832
|
+
continue;
|
|
2833
|
+
const propPart = line.slice(0, colonIdx);
|
|
2834
|
+
const value = line.slice(colonIdx + 1).trim();
|
|
2835
|
+
const semicolonIdx = propPart.indexOf(";");
|
|
2836
|
+
const propName = (semicolonIdx === -1 ? propPart : propPart.slice(0, semicolonIdx)).toUpperCase();
|
|
2837
|
+
const params = semicolonIdx !== -1 ? propPart.slice(semicolonIdx + 1) : "";
|
|
2838
|
+
switch (propName) {
|
|
2839
|
+
case "FN":
|
|
2840
|
+
contact.display_name = decodeVcfValue(value);
|
|
2841
|
+
break;
|
|
2842
|
+
case "N": {
|
|
2843
|
+
const parts = value.split(";");
|
|
2844
|
+
contact.last_name = decodeVcfValue(parts[0] ?? "") || undefined;
|
|
2845
|
+
contact.first_name = decodeVcfValue(parts[1] ?? "") || undefined;
|
|
2846
|
+
break;
|
|
2847
|
+
}
|
|
2848
|
+
case "NICKNAME":
|
|
2849
|
+
contact.nickname = decodeVcfValue(value) || undefined;
|
|
2850
|
+
break;
|
|
2851
|
+
case "TITLE":
|
|
2852
|
+
contact.job_title = decodeVcfValue(value) || undefined;
|
|
2853
|
+
break;
|
|
2854
|
+
case "NOTE":
|
|
2855
|
+
contact.notes = decodeVcfValue(value) || undefined;
|
|
2856
|
+
break;
|
|
2857
|
+
case "BDAY":
|
|
2858
|
+
contact.birthday = value.replace(/^(\d{4})(\d{2})(\d{2})$/, "$1-$2-$3");
|
|
2859
|
+
break;
|
|
2860
|
+
case "EMAIL": {
|
|
2861
|
+
const typeMatch = params.match(/TYPE=([^;]+)/i);
|
|
2862
|
+
const rawLabel = typeMatch ? typeMatch[1].toLowerCase() : "work";
|
|
2863
|
+
const type = rawLabel.includes("personal") ? "personal" : rawLabel.includes("other") ? "other" : "work";
|
|
2864
|
+
const isPrimary = params.includes("PREF") || emails.length === 0;
|
|
2865
|
+
emails.push({ address: decodeVcfValue(value), type, is_primary: isPrimary });
|
|
2866
|
+
break;
|
|
2867
|
+
}
|
|
2868
|
+
case "TEL": {
|
|
2869
|
+
const typeMatch = params.match(/TYPE=([^;]+)/i);
|
|
2870
|
+
const rawLabel = typeMatch ? typeMatch[1].toLowerCase() : "mobile";
|
|
2871
|
+
const type = rawLabel.includes("cell") || rawLabel.includes("mobile") ? "mobile" : rawLabel.includes("work") ? "work" : rawLabel.includes("home") ? "home" : rawLabel.includes("fax") ? "fax" : "other";
|
|
2872
|
+
const isPrimary = params.includes("PREF") || phones.length === 0;
|
|
2873
|
+
phones.push({ number: decodeVcfValue(value), type, is_primary: isPrimary });
|
|
2874
|
+
break;
|
|
2875
|
+
}
|
|
2876
|
+
case "ADR": {
|
|
2877
|
+
const parts = value.split(";");
|
|
2878
|
+
const typeMatch = params.match(/TYPE=([^;]+)/i);
|
|
2879
|
+
const rawLabel = typeMatch ? typeMatch[1].toLowerCase().split(",")[0] : "physical";
|
|
2880
|
+
const type = rawLabel.includes("home") || rawLabel.includes("physical") ? "physical" : rawLabel.includes("mail") ? "mailing" : rawLabel.includes("bill") ? "billing" : "other";
|
|
2881
|
+
addresses.push({
|
|
2882
|
+
type,
|
|
2883
|
+
street: decodeVcfValue(parts[2] ?? "") || undefined,
|
|
2884
|
+
city: decodeVcfValue(parts[3] ?? "") || undefined,
|
|
2885
|
+
state: decodeVcfValue(parts[4] ?? "") || undefined,
|
|
2886
|
+
zip: decodeVcfValue(parts[5] ?? "") || undefined,
|
|
2887
|
+
country: decodeVcfValue(parts[6] ?? "") || undefined,
|
|
2888
|
+
is_primary: addresses.length === 0
|
|
2889
|
+
});
|
|
2890
|
+
break;
|
|
2891
|
+
}
|
|
2892
|
+
case "URL": {
|
|
2893
|
+
const url = decodeVcfValue(value);
|
|
2894
|
+
const platform = detectPlatform(url);
|
|
2895
|
+
socials.push({ platform, url, handle: url });
|
|
2896
|
+
break;
|
|
2897
|
+
}
|
|
2898
|
+
case "X-SOCIALPROFILE": {
|
|
2899
|
+
const typeMatch = params.match(/TYPE=([^;]+)/i);
|
|
2900
|
+
const platform = normalizePlatform(typeMatch?.[1] ?? "other");
|
|
2901
|
+
socials.push({ platform, handle: decodeVcfValue(value), url: value });
|
|
2902
|
+
break;
|
|
2903
|
+
}
|
|
2904
|
+
}
|
|
2905
|
+
}
|
|
2906
|
+
if (!contact.display_name) {
|
|
2907
|
+
if (contact.first_name || contact.last_name) {
|
|
2908
|
+
contact.display_name = [contact.first_name, contact.last_name].filter(Boolean).join(" ");
|
|
2909
|
+
} else {
|
|
2910
|
+
return null;
|
|
2911
|
+
}
|
|
2912
|
+
}
|
|
2913
|
+
if (emails.length)
|
|
2914
|
+
contact.emails = emails;
|
|
2915
|
+
if (phones.length)
|
|
2916
|
+
contact.phones = phones;
|
|
2917
|
+
if (addresses.length)
|
|
2918
|
+
contact.addresses = addresses;
|
|
2919
|
+
if (socials.length)
|
|
2920
|
+
contact.social_profiles = socials;
|
|
2921
|
+
return contact;
|
|
2922
|
+
}
|
|
2923
|
+
function decodeVcfValue(val) {
|
|
2924
|
+
return val.replace(/\\n/g, `
|
|
2925
|
+
`).replace(/\\,/g, ",").replace(/\\;/g, ";").replace(/\\\\/g, "\\");
|
|
2926
|
+
}
|
|
2927
|
+
function detectPlatform(url) {
|
|
2928
|
+
const lower = url.toLowerCase();
|
|
2929
|
+
if (lower.includes("twitter.com") || lower.includes("x.com"))
|
|
2930
|
+
return "twitter";
|
|
2931
|
+
if (lower.includes("linkedin.com"))
|
|
2932
|
+
return "linkedin";
|
|
2933
|
+
if (lower.includes("github.com"))
|
|
2934
|
+
return "github";
|
|
2935
|
+
if (lower.includes("instagram.com"))
|
|
2936
|
+
return "instagram";
|
|
2937
|
+
if (lower.includes("facebook.com"))
|
|
2938
|
+
return "facebook";
|
|
2939
|
+
if (lower.includes("youtube.com"))
|
|
2940
|
+
return "youtube";
|
|
2941
|
+
if (lower.includes("telegram"))
|
|
2942
|
+
return "telegram";
|
|
2943
|
+
if (lower.includes("discord"))
|
|
2944
|
+
return "discord";
|
|
2945
|
+
if (lower.includes("tiktok"))
|
|
2946
|
+
return "tiktok";
|
|
2947
|
+
if (lower.includes("bluesky") || lower.includes("bsky"))
|
|
2948
|
+
return "bluesky";
|
|
2949
|
+
return "other";
|
|
2950
|
+
}
|
|
2951
|
+
function normalizePlatform(raw) {
|
|
2952
|
+
const lower = raw.toLowerCase();
|
|
2953
|
+
const platforms = [
|
|
2954
|
+
"twitter",
|
|
2955
|
+
"linkedin",
|
|
2956
|
+
"github",
|
|
2957
|
+
"instagram",
|
|
2958
|
+
"telegram",
|
|
2959
|
+
"discord",
|
|
2960
|
+
"youtube",
|
|
2961
|
+
"tiktok",
|
|
2962
|
+
"bluesky",
|
|
2963
|
+
"facebook",
|
|
2964
|
+
"whatsapp",
|
|
2965
|
+
"snapchat",
|
|
2966
|
+
"reddit"
|
|
2967
|
+
];
|
|
2968
|
+
for (const p of platforms) {
|
|
2969
|
+
if (lower.includes(p))
|
|
2970
|
+
return p;
|
|
2971
|
+
}
|
|
2972
|
+
return "other";
|
|
2973
|
+
}
|
|
2974
|
+
function importFromJson(data) {
|
|
2975
|
+
let parsed;
|
|
2976
|
+
try {
|
|
2977
|
+
parsed = JSON.parse(data);
|
|
2978
|
+
} catch {
|
|
2979
|
+
throw new Error("Invalid JSON");
|
|
2980
|
+
}
|
|
2981
|
+
if (!Array.isArray(parsed)) {
|
|
2982
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
2983
|
+
parsed = [parsed];
|
|
2984
|
+
} else {
|
|
2985
|
+
throw new Error("JSON must be an array of contacts");
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2988
|
+
return parsed.map((obj) => {
|
|
2989
|
+
const displayName = obj.display_name ?? obj.name ?? [obj.first_name ?? "", obj.last_name ?? ""].filter(Boolean).join(" ") ?? "Unnamed";
|
|
2990
|
+
return {
|
|
2991
|
+
...obj,
|
|
2992
|
+
display_name: displayName,
|
|
2993
|
+
source: "import"
|
|
2994
|
+
};
|
|
2995
|
+
});
|
|
2996
|
+
}
|
|
2997
|
+
function parseLinkedInCsvLine(line) {
|
|
2998
|
+
return parseCsvLine(line);
|
|
2999
|
+
}
|
|
3000
|
+
function parseLinkedIn(csv) {
|
|
3001
|
+
const lines = csv.split(`
|
|
3002
|
+
`).filter((l) => l.trim());
|
|
3003
|
+
if (!lines.length)
|
|
3004
|
+
return [];
|
|
3005
|
+
const headers = parseLinkedInCsvLine(lines[0]).map((h) => h.replace(/"/g, "").trim());
|
|
3006
|
+
const firstNameIdx = headers.findIndex((h) => h === "First Name");
|
|
3007
|
+
const lastNameIdx = headers.findIndex((h) => h === "Last Name");
|
|
3008
|
+
const emailIdx = headers.findIndex((h) => h === "Email Address");
|
|
3009
|
+
const companyIdx = headers.findIndex((h) => h === "Company");
|
|
3010
|
+
const positionIdx = headers.findIndex((h) => h === "Position");
|
|
3011
|
+
const urlIdx = headers.findIndex((h) => h === "URL");
|
|
3012
|
+
const connectedIdx = headers.findIndex((h) => h === "Connected On");
|
|
3013
|
+
const results = [];
|
|
3014
|
+
for (let i = 1;i < lines.length; i++) {
|
|
3015
|
+
const cols = parseLinkedInCsvLine(lines[i]);
|
|
3016
|
+
const firstName = firstNameIdx >= 0 ? (cols[firstNameIdx] ?? "").trim() : "";
|
|
3017
|
+
const lastName = lastNameIdx >= 0 ? (cols[lastNameIdx] ?? "").trim() : "";
|
|
3018
|
+
if (!firstName && !lastName)
|
|
3019
|
+
continue;
|
|
3020
|
+
const contact = {
|
|
3021
|
+
first_name: firstName,
|
|
3022
|
+
last_name: lastName,
|
|
3023
|
+
display_name: `${firstName} ${lastName}`.trim(),
|
|
3024
|
+
source: "import"
|
|
3025
|
+
};
|
|
3026
|
+
if (emailIdx >= 0 && cols[emailIdx]?.trim()) {
|
|
3027
|
+
contact.emails = [{ address: cols[emailIdx].trim(), type: "work", is_primary: true }];
|
|
3028
|
+
}
|
|
3029
|
+
if (positionIdx >= 0 && cols[positionIdx]?.trim()) {
|
|
3030
|
+
contact.job_title = cols[positionIdx].trim();
|
|
3031
|
+
}
|
|
3032
|
+
if (urlIdx >= 0 && cols[urlIdx]?.trim()) {
|
|
3033
|
+
contact.social_profiles = [{ platform: "linkedin", url: cols[urlIdx].trim(), is_primary: true }];
|
|
3034
|
+
}
|
|
3035
|
+
if (companyIdx >= 0 && cols[companyIdx]?.trim()) {
|
|
3036
|
+
const connectedNote = connectedIdx >= 0 && cols[connectedIdx]?.trim() ? ` Connected on LinkedIn: ${cols[connectedIdx].trim()}` : "";
|
|
3037
|
+
contact.notes = `Company: ${cols[companyIdx].trim()}${connectedNote}`;
|
|
3038
|
+
} else if (connectedIdx >= 0 && cols[connectedIdx]?.trim()) {
|
|
3039
|
+
contact.notes = `Connected on LinkedIn: ${cols[connectedIdx].trim()}`;
|
|
3040
|
+
}
|
|
3041
|
+
results.push(contact);
|
|
3042
|
+
}
|
|
3043
|
+
return results;
|
|
3044
|
+
}
|
|
3045
|
+
function isLinkedInFormat(data) {
|
|
3046
|
+
const firstLine = data.split(`
|
|
3047
|
+
`)[0] ?? "";
|
|
3048
|
+
const lower = firstLine.toLowerCase();
|
|
3049
|
+
return lower.includes("first name") && lower.includes("url") && lower.includes("connected on");
|
|
3050
|
+
}
|
|
3051
|
+
async function importContacts(format, data) {
|
|
3052
|
+
switch (format) {
|
|
3053
|
+
case "csv":
|
|
3054
|
+
if (isLinkedInFormat(data))
|
|
3055
|
+
return parseLinkedIn(data);
|
|
3056
|
+
return importFromCsv(data);
|
|
3057
|
+
case "vcf":
|
|
3058
|
+
return parseVcf(data);
|
|
3059
|
+
case "json":
|
|
3060
|
+
return importFromJson(data);
|
|
3061
|
+
default:
|
|
3062
|
+
throw new Error(`Unsupported import format: ${format}`);
|
|
3063
|
+
}
|
|
3064
|
+
}
|
|
3065
|
+
// src/lib/export.ts
|
|
3066
|
+
function toJson(contacts) {
|
|
3067
|
+
return JSON.stringify(contacts, null, 2);
|
|
3068
|
+
}
|
|
3069
|
+
function escapeCsvField(val) {
|
|
3070
|
+
if (val == null)
|
|
3071
|
+
return "";
|
|
3072
|
+
const str = String(val);
|
|
3073
|
+
if (str.includes(",") || str.includes('"') || str.includes(`
|
|
3074
|
+
`)) {
|
|
3075
|
+
return `"${str.replace(/"/g, '""')}"`;
|
|
3076
|
+
}
|
|
3077
|
+
return str;
|
|
3078
|
+
}
|
|
3079
|
+
function toCsv(contacts) {
|
|
3080
|
+
const headers = [
|
|
3081
|
+
"First Name",
|
|
3082
|
+
"Last Name",
|
|
3083
|
+
"Name",
|
|
3084
|
+
"Nickname",
|
|
3085
|
+
"Job Title",
|
|
3086
|
+
"Company",
|
|
3087
|
+
"Email 1 - Value",
|
|
3088
|
+
"Email 1 - Type",
|
|
3089
|
+
"Email 2 - Value",
|
|
3090
|
+
"Email 2 - Type",
|
|
3091
|
+
"Phone 1 - Value",
|
|
3092
|
+
"Phone 1 - Type",
|
|
3093
|
+
"Phone 2 - Value",
|
|
3094
|
+
"Phone 2 - Type",
|
|
3095
|
+
"Address 1 - Street",
|
|
3096
|
+
"Address 1 - City",
|
|
3097
|
+
"Address 1 - State",
|
|
3098
|
+
"Address 1 - Postal Code",
|
|
3099
|
+
"Address 1 - Country",
|
|
3100
|
+
"Address 1 - Type",
|
|
3101
|
+
"Birthday",
|
|
3102
|
+
"Notes",
|
|
3103
|
+
"Tags"
|
|
3104
|
+
];
|
|
3105
|
+
const rows = [headers.map(escapeCsvField).join(",")];
|
|
3106
|
+
for (const c of contacts) {
|
|
3107
|
+
const emails = c.emails ?? [];
|
|
3108
|
+
const phones = c.phones ?? [];
|
|
3109
|
+
const addrs = c.addresses ?? [];
|
|
3110
|
+
const tags = (c.tags ?? []).map((t) => t.name).join(";");
|
|
3111
|
+
const row = [
|
|
3112
|
+
c.first_name,
|
|
3113
|
+
c.last_name,
|
|
3114
|
+
c.display_name,
|
|
3115
|
+
c.nickname,
|
|
3116
|
+
c.job_title,
|
|
3117
|
+
c.company?.name,
|
|
3118
|
+
emails[0]?.address,
|
|
3119
|
+
emails[0]?.type,
|
|
3120
|
+
emails[1]?.address,
|
|
3121
|
+
emails[1]?.type,
|
|
3122
|
+
phones[0]?.number,
|
|
3123
|
+
phones[0]?.type,
|
|
3124
|
+
phones[1]?.number,
|
|
3125
|
+
phones[1]?.type,
|
|
3126
|
+
addrs[0]?.street,
|
|
3127
|
+
addrs[0]?.city,
|
|
3128
|
+
addrs[0]?.state,
|
|
3129
|
+
addrs[0]?.zip,
|
|
3130
|
+
addrs[0]?.country,
|
|
3131
|
+
addrs[0]?.type,
|
|
3132
|
+
c.birthday,
|
|
3133
|
+
c.notes,
|
|
3134
|
+
tags
|
|
3135
|
+
];
|
|
3136
|
+
rows.push(row.map(escapeCsvField).join(","));
|
|
3137
|
+
}
|
|
3138
|
+
return rows.join(`
|
|
3139
|
+
`);
|
|
3140
|
+
}
|
|
3141
|
+
function escapeVcfValue(val) {
|
|
3142
|
+
if (!val)
|
|
3143
|
+
return "";
|
|
3144
|
+
return val.replace(/\\/g, "\\\\").replace(/,/g, "\\,").replace(/;/g, "\\;").replace(/\n/g, "\\n");
|
|
3145
|
+
}
|
|
3146
|
+
function foldVcfLine(line) {
|
|
3147
|
+
if (line.length <= 75)
|
|
3148
|
+
return line;
|
|
3149
|
+
const parts = [line.slice(0, 75)];
|
|
3150
|
+
let i = 75;
|
|
3151
|
+
while (i < line.length) {
|
|
3152
|
+
parts.push(" " + line.slice(i, i + 74));
|
|
3153
|
+
i += 74;
|
|
3154
|
+
}
|
|
3155
|
+
return parts.join(`\r
|
|
3156
|
+
`);
|
|
3157
|
+
}
|
|
3158
|
+
function toVcf(contacts) {
|
|
3159
|
+
const cards = [];
|
|
3160
|
+
for (const c of contacts) {
|
|
3161
|
+
const lines = ["BEGIN:VCARD", "VERSION:3.0"];
|
|
3162
|
+
lines.push(`FN:${escapeVcfValue(c.display_name)}`);
|
|
3163
|
+
lines.push(`N:${escapeVcfValue(c.last_name)};${escapeVcfValue(c.first_name)};;;`);
|
|
3164
|
+
if (c.nickname)
|
|
3165
|
+
lines.push(`NICKNAME:${escapeVcfValue(c.nickname)}`);
|
|
3166
|
+
if (c.job_title)
|
|
3167
|
+
lines.push(`TITLE:${escapeVcfValue(c.job_title)}`);
|
|
3168
|
+
if (c.company?.name)
|
|
3169
|
+
lines.push(`ORG:${escapeVcfValue(c.company.name)}`);
|
|
3170
|
+
if (c.birthday)
|
|
3171
|
+
lines.push(`BDAY:${c.birthday.replace(/-/g, "")}`);
|
|
3172
|
+
for (let i = 0;i < (c.emails ?? []).length; i++) {
|
|
3173
|
+
const e = c.emails[i];
|
|
3174
|
+
const pref = i === 0 || e.is_primary ? ";PREF" : "";
|
|
3175
|
+
lines.push(`EMAIL;TYPE=${e.type.toUpperCase()}${pref}:${escapeVcfValue(e.address)}`);
|
|
3176
|
+
}
|
|
3177
|
+
for (let i = 0;i < (c.phones ?? []).length; i++) {
|
|
3178
|
+
const p = c.phones[i];
|
|
3179
|
+
const pref = i === 0 || p.is_primary ? ";PREF" : "";
|
|
3180
|
+
const vcfType = p.type === "mobile" ? "CELL" : p.type.toUpperCase();
|
|
3181
|
+
lines.push(`TEL;TYPE=${vcfType}${pref}:${escapeVcfValue(p.number)}`);
|
|
3182
|
+
}
|
|
3183
|
+
for (let i = 0;i < (c.addresses ?? []).length; i++) {
|
|
3184
|
+
const a = c.addresses[i];
|
|
3185
|
+
const pref = i === 0 || a.is_primary ? ";PREF" : "";
|
|
3186
|
+
lines.push(`ADR;TYPE=${a.type.toUpperCase()}${pref}:;;${escapeVcfValue(a.street)};${escapeVcfValue(a.city)};${escapeVcfValue(a.state)};${escapeVcfValue(a.zip)};${escapeVcfValue(a.country)}`);
|
|
3187
|
+
}
|
|
3188
|
+
for (const sp of c.social_profiles ?? []) {
|
|
3189
|
+
if (sp.url)
|
|
3190
|
+
lines.push(`URL;TYPE=${sp.platform.toUpperCase()}:${escapeVcfValue(sp.url)}`);
|
|
3191
|
+
if (sp.handle)
|
|
3192
|
+
lines.push(`X-SOCIALPROFILE;TYPE=${sp.platform.toLowerCase()}:${escapeVcfValue(sp.handle)}`);
|
|
3193
|
+
}
|
|
3194
|
+
if (c.notes)
|
|
3195
|
+
lines.push(`NOTE:${escapeVcfValue(c.notes)}`);
|
|
3196
|
+
if (c.tags && c.tags.length > 0) {
|
|
3197
|
+
lines.push(`CATEGORIES:${c.tags.map((t) => escapeVcfValue(t.name)).join(",")}`);
|
|
3198
|
+
}
|
|
3199
|
+
lines.push(`UID:${c.id}`);
|
|
3200
|
+
lines.push("END:VCARD");
|
|
3201
|
+
cards.push(lines.map(foldVcfLine).join(`\r
|
|
3202
|
+
`));
|
|
3203
|
+
}
|
|
3204
|
+
return cards.join(`\r
|
|
3205
|
+
`);
|
|
3206
|
+
}
|
|
3207
|
+
async function exportContacts(format, contacts) {
|
|
3208
|
+
switch (format) {
|
|
3209
|
+
case "json":
|
|
3210
|
+
return toJson(contacts);
|
|
3211
|
+
case "csv":
|
|
3212
|
+
return toCsv(contacts);
|
|
3213
|
+
case "vcf":
|
|
3214
|
+
return toVcf(contacts);
|
|
3215
|
+
default:
|
|
3216
|
+
throw new Error(`Unsupported export format: ${format}`);
|
|
3217
|
+
}
|
|
3218
|
+
}
|
|
2136
3219
|
// src/lib/gmail-import.ts
|
|
2137
3220
|
function parseAddressHeader(header) {
|
|
2138
3221
|
const results = [];
|
|
@@ -2390,11 +3473,15 @@ async function pullGoogleContactsAsInputs(opts = {}) {
|
|
|
2390
3473
|
const people = opts.query ? await searchGoogleContacts(opts.query, opts) : await listGoogleContacts(opts);
|
|
2391
3474
|
return people.filter((p) => p.emailAddresses?.some((e) => e.value)).map(googlePersonToContactInput);
|
|
2392
3475
|
}
|
|
3476
|
+
|
|
3477
|
+
// src/index.ts
|
|
3478
|
+
init_types();
|
|
2393
3479
|
export {
|
|
2394
3480
|
updateVendorCommunication,
|
|
2395
3481
|
updateTag,
|
|
2396
3482
|
updateOrgMember,
|
|
2397
3483
|
updateGroup,
|
|
3484
|
+
updateDeal,
|
|
2398
3485
|
updateContactTask,
|
|
2399
3486
|
updateContact,
|
|
2400
3487
|
updateCompany,
|
|
@@ -2415,10 +3502,12 @@ export {
|
|
|
2415
3502
|
pushContactToGoogle,
|
|
2416
3503
|
pullGoogleContactsAsInputs,
|
|
2417
3504
|
parseName,
|
|
3505
|
+
parseLinkedIn,
|
|
2418
3506
|
parseAddressHeader,
|
|
2419
3507
|
mergeContacts,
|
|
2420
3508
|
markFollowUpDone,
|
|
2421
3509
|
logVendorCommunication,
|
|
3510
|
+
logEvent,
|
|
2422
3511
|
logActivity,
|
|
2423
3512
|
listVendorCommunications,
|
|
2424
3513
|
listTags,
|
|
@@ -2438,25 +3527,38 @@ export {
|
|
|
2438
3527
|
listGroups,
|
|
2439
3528
|
listGoogleContacts,
|
|
2440
3529
|
listFollowUpDue,
|
|
3530
|
+
listEvents,
|
|
3531
|
+
listDeals,
|
|
2441
3532
|
listContactsInGroup,
|
|
2442
3533
|
listContactsByTag,
|
|
2443
3534
|
listContacts,
|
|
2444
3535
|
listContactTasks,
|
|
3536
|
+
listContactAudit,
|
|
2445
3537
|
listCompanyRelationships,
|
|
2446
3538
|
listCompanyEmployees,
|
|
2447
3539
|
listCompaniesInGroup,
|
|
2448
3540
|
listCompanies,
|
|
3541
|
+
listColdContacts,
|
|
2449
3542
|
listApplications,
|
|
2450
3543
|
listActivity,
|
|
3544
|
+
importToApple,
|
|
3545
|
+
importFromCsv,
|
|
3546
|
+
importContacts,
|
|
2451
3547
|
googlePersonToContactInput,
|
|
3548
|
+
getUpcomingItems,
|
|
2452
3549
|
getTagByName,
|
|
2453
3550
|
getTag,
|
|
2454
3551
|
getRelationship,
|
|
2455
3552
|
getOrgMember,
|
|
2456
3553
|
getNote,
|
|
3554
|
+
getNetworkStats,
|
|
2457
3555
|
getGroup,
|
|
3556
|
+
getEvent,
|
|
2458
3557
|
getEntityTeam,
|
|
3558
|
+
getDealsByStage,
|
|
3559
|
+
getDeal,
|
|
2459
3560
|
getDatabase,
|
|
3561
|
+
getContactTimeline,
|
|
2460
3562
|
getContactTask,
|
|
2461
3563
|
getContactByEmail,
|
|
2462
3564
|
getContact,
|
|
@@ -2464,13 +3566,18 @@ export {
|
|
|
2464
3566
|
getCompany,
|
|
2465
3567
|
getApplication,
|
|
2466
3568
|
getActivity,
|
|
3569
|
+
generateBrief,
|
|
2467
3570
|
extractContactsFromGmail,
|
|
3571
|
+
exportFromApple,
|
|
3572
|
+
exportContacts,
|
|
2468
3573
|
domainToCompany,
|
|
2469
3574
|
deleteVendorCommunication,
|
|
2470
3575
|
deleteTag,
|
|
2471
3576
|
deleteRelationship,
|
|
2472
3577
|
deleteNote,
|
|
2473
3578
|
deleteGroup,
|
|
3579
|
+
deleteEvent,
|
|
3580
|
+
deleteDeal,
|
|
2474
3581
|
deleteContactTask,
|
|
2475
3582
|
deleteContact,
|
|
2476
3583
|
deleteCompanyRelationship,
|
|
@@ -2479,6 +3586,7 @@ export {
|
|
|
2479
3586
|
createTag,
|
|
2480
3587
|
createRelationship,
|
|
2481
3588
|
createGroup,
|
|
3589
|
+
createDeal,
|
|
2482
3590
|
createContactTask,
|
|
2483
3591
|
createContact,
|
|
2484
3592
|
createCompanyRelationship,
|
|
@@ -2487,6 +3595,7 @@ export {
|
|
|
2487
3595
|
contactToGoogleArgs,
|
|
2488
3596
|
checkEscalations,
|
|
2489
3597
|
autoLinkContactToCompany,
|
|
3598
|
+
auditContact,
|
|
2490
3599
|
archiveContact,
|
|
2491
3600
|
archiveCompany,
|
|
2492
3601
|
addTagToContact,
|