@hasna/contacts 0.2.2 → 0.2.4
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 +144 -14
- package/dist/db/companies.d.ts +2 -0
- package/dist/db/companies.d.ts.map +1 -1
- package/dist/db/contacts.d.ts +7 -1
- package/dist/db/contacts.d.ts.map +1 -1
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/groups.d.ts +11 -1
- package/dist/db/groups.d.ts.map +1 -1
- package/dist/db/tags.d.ts.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +274 -16
- package/dist/lib/gmail-import.d.ts +38 -0
- package/dist/lib/gmail-import.d.ts.map +1 -0
- package/dist/lib/gmail-import.test.d.ts +2 -0
- package/dist/lib/gmail-import.test.d.ts.map +1 -0
- package/dist/mcp/index.js +875 -31
- package/dist/server/index.js +115 -10
- package/dist/types/index.d.ts +41 -1
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +45 -31
package/dist/index.js
CHANGED
|
@@ -188,6 +188,20 @@ var MIGRATIONS = [
|
|
|
188
188
|
group_id TEXT NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
|
|
189
189
|
PRIMARY KEY (contact_id, group_id)
|
|
190
190
|
);
|
|
191
|
+
`,
|
|
192
|
+
`
|
|
193
|
+
ALTER TABLE contacts ADD COLUMN status TEXT DEFAULT 'active';
|
|
194
|
+
ALTER TABLE contacts ADD COLUMN follow_up_at TEXT;
|
|
195
|
+
ALTER TABLE contacts ADD COLUMN archived INTEGER NOT NULL DEFAULT 0;
|
|
196
|
+
ALTER TABLE contacts ADD COLUMN project_id TEXT;
|
|
197
|
+
ALTER TABLE companies ADD COLUMN archived INTEGER NOT NULL DEFAULT 0;
|
|
198
|
+
ALTER TABLE companies ADD COLUMN project_id TEXT;
|
|
199
|
+
|
|
200
|
+
CREATE TABLE IF NOT EXISTS company_groups (
|
|
201
|
+
company_id TEXT NOT NULL REFERENCES companies(id) ON DELETE CASCADE,
|
|
202
|
+
group_id TEXT NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
|
|
203
|
+
PRIMARY KEY (company_id, group_id)
|
|
204
|
+
);
|
|
191
205
|
`
|
|
192
206
|
];
|
|
193
207
|
var _db = null;
|
|
@@ -299,7 +313,11 @@ function rowToContact(row) {
|
|
|
299
313
|
...row,
|
|
300
314
|
source: row.source,
|
|
301
315
|
custom_fields: JSON.parse(row.custom_fields || "{}"),
|
|
302
|
-
preferred_contact_method: row.preferred_contact_method ?? null
|
|
316
|
+
preferred_contact_method: row.preferred_contact_method ?? null,
|
|
317
|
+
status: row.status ?? "active",
|
|
318
|
+
follow_up_at: row.follow_up_at ?? null,
|
|
319
|
+
archived: !!row.archived,
|
|
320
|
+
project_id: row.project_id ?? null
|
|
303
321
|
};
|
|
304
322
|
}
|
|
305
323
|
function rowToEmail(row) {
|
|
@@ -336,7 +354,9 @@ function rowToTag(row) {
|
|
|
336
354
|
function rowToCompany(row) {
|
|
337
355
|
return {
|
|
338
356
|
...row,
|
|
339
|
-
custom_fields: JSON.parse(row.custom_fields || "{}")
|
|
357
|
+
custom_fields: JSON.parse(row.custom_fields || "{}"),
|
|
358
|
+
archived: !!row.archived,
|
|
359
|
+
project_id: row.project_id ?? null
|
|
340
360
|
};
|
|
341
361
|
}
|
|
342
362
|
function insertEmails(db, contactId, companyId, emails) {
|
|
@@ -380,8 +400,8 @@ function createContact(input, db) {
|
|
|
380
400
|
const firstName = input.first_name ?? "";
|
|
381
401
|
const lastName = input.last_name ?? "";
|
|
382
402
|
const displayName = input.display_name ?? (firstName || lastName ? `${firstName} ${lastName}`.trim() : "Unnamed Contact");
|
|
383
|
-
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, created_at, updated_at)
|
|
384
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
403
|
+
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)
|
|
404
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
385
405
|
id,
|
|
386
406
|
firstName,
|
|
387
407
|
lastName,
|
|
@@ -397,6 +417,9 @@ function createContact(input, db) {
|
|
|
397
417
|
input.last_contacted_at ?? null,
|
|
398
418
|
input.website ?? null,
|
|
399
419
|
input.preferred_contact_method ?? null,
|
|
420
|
+
input.status ?? "active",
|
|
421
|
+
input.follow_up_at ?? null,
|
|
422
|
+
input.project_id ?? null,
|
|
400
423
|
timestamp,
|
|
401
424
|
timestamp
|
|
402
425
|
]);
|
|
@@ -431,12 +454,21 @@ function listContacts(opts = {}, db) {
|
|
|
431
454
|
offset = 0,
|
|
432
455
|
company_id,
|
|
433
456
|
tag_id,
|
|
457
|
+
tag_ids,
|
|
434
458
|
source,
|
|
459
|
+
status,
|
|
460
|
+
project_id,
|
|
461
|
+
archived = false,
|
|
462
|
+
follow_up_due,
|
|
463
|
+
last_contacted_after,
|
|
464
|
+
last_contacted_before,
|
|
435
465
|
order_by = "display_name",
|
|
436
466
|
order_dir = "asc"
|
|
437
467
|
} = opts;
|
|
438
468
|
const conditions = [];
|
|
439
469
|
const params = [];
|
|
470
|
+
conditions.push("c.archived = ?");
|
|
471
|
+
params.push(archived ? 1 : 0);
|
|
440
472
|
if (company_id) {
|
|
441
473
|
conditions.push("c.company_id = ?");
|
|
442
474
|
params.push(company_id);
|
|
@@ -445,12 +477,37 @@ function listContacts(opts = {}, db) {
|
|
|
445
477
|
conditions.push("c.source = ?");
|
|
446
478
|
params.push(source);
|
|
447
479
|
}
|
|
480
|
+
if (status) {
|
|
481
|
+
conditions.push("c.status = ?");
|
|
482
|
+
params.push(status);
|
|
483
|
+
}
|
|
484
|
+
if (project_id) {
|
|
485
|
+
conditions.push("c.project_id = ?");
|
|
486
|
+
params.push(project_id);
|
|
487
|
+
}
|
|
448
488
|
if (tag_id) {
|
|
449
489
|
conditions.push("EXISTS (SELECT 1 FROM contact_tags ct WHERE ct.contact_id = c.id AND ct.tag_id = ?)");
|
|
450
490
|
params.push(tag_id);
|
|
451
491
|
}
|
|
492
|
+
if (tag_ids?.length) {
|
|
493
|
+
for (const tid of tag_ids) {
|
|
494
|
+
conditions.push("EXISTS (SELECT 1 FROM contact_tags ct WHERE ct.contact_id = c.id AND ct.tag_id = ?)");
|
|
495
|
+
params.push(tid);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
if (follow_up_due) {
|
|
499
|
+
conditions.push("c.follow_up_at IS NOT NULL AND c.follow_up_at <= datetime('now')");
|
|
500
|
+
}
|
|
501
|
+
if (last_contacted_after) {
|
|
502
|
+
conditions.push("c.last_contacted_at >= ?");
|
|
503
|
+
params.push(last_contacted_after);
|
|
504
|
+
}
|
|
505
|
+
if (last_contacted_before) {
|
|
506
|
+
conditions.push("c.last_contacted_at <= ?");
|
|
507
|
+
params.push(last_contacted_before);
|
|
508
|
+
}
|
|
452
509
|
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
453
|
-
const validOrderBy = ["display_name", "created_at", "updated_at"].includes(order_by) ? order_by : "display_name";
|
|
510
|
+
const validOrderBy = ["display_name", "created_at", "updated_at", "last_contacted_at", "follow_up_at"].includes(order_by) ? order_by : "display_name";
|
|
454
511
|
const validOrderDir = order_dir === "desc" ? "DESC" : "ASC";
|
|
455
512
|
const totalRow = d.query(`SELECT COUNT(*) as total FROM contacts c ${where}`).get(...params);
|
|
456
513
|
const rows = d.query(`SELECT c.* FROM contacts c ${where} ORDER BY c.${validOrderBy} ${validOrderDir} LIMIT ? OFFSET ?`).all(...params, limit, offset);
|
|
@@ -520,8 +577,36 @@ function updateContact(id, input, db) {
|
|
|
520
577
|
setClauses.push("preferred_contact_method = ?");
|
|
521
578
|
params.push(input.preferred_contact_method);
|
|
522
579
|
}
|
|
580
|
+
if (input.status !== undefined) {
|
|
581
|
+
setClauses.push("status = ?");
|
|
582
|
+
params.push(input.status);
|
|
583
|
+
}
|
|
584
|
+
if (input.follow_up_at !== undefined) {
|
|
585
|
+
setClauses.push("follow_up_at = ?");
|
|
586
|
+
params.push(input.follow_up_at);
|
|
587
|
+
}
|
|
588
|
+
if (input.project_id !== undefined) {
|
|
589
|
+
setClauses.push("project_id = ?");
|
|
590
|
+
params.push(input.project_id);
|
|
591
|
+
}
|
|
523
592
|
params.push(id);
|
|
524
593
|
d.run(`UPDATE contacts SET ${setClauses.join(", ")} WHERE id = ?`, params);
|
|
594
|
+
if (input.emails_add?.length) {
|
|
595
|
+
for (const e of input.emails_add) {
|
|
596
|
+
const exists = d.query(`SELECT id FROM emails WHERE contact_id = ? AND LOWER(address) = LOWER(?)`).get(id, e.address);
|
|
597
|
+
if (!exists) {
|
|
598
|
+
d.run(`INSERT INTO emails (id, contact_id, company_id, address, type, is_primary) VALUES (?, ?, NULL, ?, ?, ?)`, [uuid(), id, e.address, e.type ?? "work", e.is_primary ? 1 : 0]);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
if (input.phones_add?.length) {
|
|
603
|
+
for (const p of input.phones_add) {
|
|
604
|
+
const exists = d.query(`SELECT id FROM phones WHERE contact_id = ? AND number = ?`).get(id, p.number);
|
|
605
|
+
if (!exists) {
|
|
606
|
+
d.run(`INSERT INTO phones (id, contact_id, company_id, number, country_code, type, is_primary) VALUES (?, ?, NULL, ?, ?, ?, ?)`, [uuid(), id, p.number, p.country_code ?? null, p.type ?? "mobile", p.is_primary ? 1 : 0]);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
525
610
|
logActivity(d, { contact_id: id, action: "contact.updated", details: `Updated contact: ${existing.display_name}` });
|
|
526
611
|
const row = d.query(`SELECT * FROM contacts WHERE id = ?`).get(id);
|
|
527
612
|
return loadContactDetails(d, rowToContact(row));
|
|
@@ -539,25 +624,31 @@ function searchContacts(query, db) {
|
|
|
539
624
|
const ftsRows = d.query(`
|
|
540
625
|
SELECT c.* FROM contacts c
|
|
541
626
|
JOIN contacts_fts fts ON fts.id = c.id
|
|
542
|
-
WHERE contacts_fts MATCH ?
|
|
627
|
+
WHERE contacts_fts MATCH ? AND c.archived = 0
|
|
543
628
|
ORDER BY rank
|
|
544
629
|
LIMIT 50
|
|
545
630
|
`).all(`"${query.replace(/"/g, '""')}"*`);
|
|
546
631
|
const emailRows = d.query(`
|
|
547
632
|
SELECT DISTINCT c.* FROM contacts c
|
|
548
633
|
JOIN emails e ON e.contact_id = c.id
|
|
549
|
-
WHERE e.address LIKE ?
|
|
634
|
+
WHERE e.address LIKE ? AND c.archived = 0
|
|
550
635
|
LIMIT 20
|
|
551
636
|
`).all(`%${query}%`);
|
|
552
637
|
const phoneRows = d.query(`
|
|
553
638
|
SELECT DISTINCT c.* FROM contacts c
|
|
554
639
|
JOIN phones p ON p.contact_id = c.id
|
|
555
|
-
WHERE p.number LIKE ?
|
|
640
|
+
WHERE p.number LIKE ? AND c.archived = 0
|
|
641
|
+
LIMIT 20
|
|
642
|
+
`).all(`%${query}%`);
|
|
643
|
+
const companyRows = d.query(`
|
|
644
|
+
SELECT DISTINCT c.* FROM contacts c
|
|
645
|
+
JOIN companies co ON co.id = c.company_id
|
|
646
|
+
WHERE co.name LIKE ? AND c.archived = 0
|
|
556
647
|
LIMIT 20
|
|
557
648
|
`).all(`%${query}%`);
|
|
558
649
|
const seen = new Set;
|
|
559
650
|
const allRows = [];
|
|
560
|
-
for (const row of [...ftsRows, ...emailRows, ...phoneRows]) {
|
|
651
|
+
for (const row of [...ftsRows, ...emailRows, ...phoneRows, ...companyRows]) {
|
|
561
652
|
if (!seen.has(row.id)) {
|
|
562
653
|
seen.add(row.id);
|
|
563
654
|
allRows.push(row);
|
|
@@ -578,8 +669,24 @@ function mergeContacts(keepId, mergeId, db) {
|
|
|
578
669
|
const mergeRow = d.query(`SELECT * FROM contacts WHERE id = ?`).get(mergeId);
|
|
579
670
|
if (!mergeRow)
|
|
580
671
|
throw new ContactNotFoundError(mergeId);
|
|
581
|
-
d.
|
|
582
|
-
|
|
672
|
+
const mergeEmailRows = d.query(`SELECT * FROM emails WHERE contact_id = ?`).all(mergeId);
|
|
673
|
+
for (const e of mergeEmailRows) {
|
|
674
|
+
const exists = d.query(`SELECT id FROM emails WHERE contact_id = ? AND LOWER(address) = LOWER(?)`).get(keepId, e.address);
|
|
675
|
+
if (exists) {
|
|
676
|
+
d.run(`DELETE FROM emails WHERE id = ?`, [e.id]);
|
|
677
|
+
} else {
|
|
678
|
+
d.run(`UPDATE emails SET contact_id = ? WHERE id = ?`, [keepId, e.id]);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
const mergePhoneRows = d.query(`SELECT * FROM phones WHERE contact_id = ?`).all(mergeId);
|
|
682
|
+
for (const p of mergePhoneRows) {
|
|
683
|
+
const exists = d.query(`SELECT id FROM phones WHERE contact_id = ? AND number = ?`).get(keepId, p.number);
|
|
684
|
+
if (exists) {
|
|
685
|
+
d.run(`DELETE FROM phones WHERE id = ?`, [p.id]);
|
|
686
|
+
} else {
|
|
687
|
+
d.run(`UPDATE phones SET contact_id = ? WHERE id = ?`, [keepId, p.id]);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
583
690
|
d.run(`UPDATE addresses SET contact_id = ? WHERE contact_id = ?`, [keepId, mergeId]);
|
|
584
691
|
d.run(`UPDATE social_profiles SET contact_id = ? WHERE contact_id = ?`, [keepId, mergeId]);
|
|
585
692
|
const mergeTags = d.query(`SELECT tag_id FROM contact_tags WHERE contact_id = ?`).all(mergeId);
|
|
@@ -626,11 +733,86 @@ function mergeContacts(keepId, mergeId, db) {
|
|
|
626
733
|
const finalRow = d.query(`SELECT * FROM contacts WHERE id = ?`).get(keepId);
|
|
627
734
|
return loadContactDetails(d, rowToContact(finalRow));
|
|
628
735
|
}
|
|
736
|
+
function getContactByEmail(email, db) {
|
|
737
|
+
const d = db || getDatabase();
|
|
738
|
+
const emailRow = d.query(`SELECT contact_id FROM emails WHERE LOWER(address) = LOWER(?) AND contact_id IS NOT NULL LIMIT 1`).get(email);
|
|
739
|
+
if (!emailRow)
|
|
740
|
+
return null;
|
|
741
|
+
const row = d.query(`SELECT * FROM contacts WHERE id = ?`).get(emailRow.contact_id);
|
|
742
|
+
if (!row)
|
|
743
|
+
return null;
|
|
744
|
+
return loadContactDetails(d, rowToContact(row));
|
|
745
|
+
}
|
|
746
|
+
function addEmailToContact(contactId, email, db) {
|
|
747
|
+
const d = db || getDatabase();
|
|
748
|
+
const row = d.query(`SELECT * FROM contacts WHERE id = ?`).get(contactId);
|
|
749
|
+
if (!row)
|
|
750
|
+
throw new ContactNotFoundError(contactId);
|
|
751
|
+
const exists = d.query(`SELECT id FROM emails WHERE contact_id = ? AND LOWER(address) = LOWER(?)`).get(contactId, email.address);
|
|
752
|
+
if (!exists) {
|
|
753
|
+
d.run(`INSERT INTO emails (id, contact_id, company_id, address, type, is_primary) VALUES (?, ?, NULL, ?, ?, ?)`, [uuid(), contactId, email.address, email.type ?? "work", email.is_primary ? 1 : 0]);
|
|
754
|
+
}
|
|
755
|
+
const updated = d.query(`SELECT * FROM contacts WHERE id = ?`).get(contactId);
|
|
756
|
+
return loadContactDetails(d, rowToContact(updated));
|
|
757
|
+
}
|
|
758
|
+
function addPhoneToContact(contactId, phone, db) {
|
|
759
|
+
const d = db || getDatabase();
|
|
760
|
+
const row = d.query(`SELECT * FROM contacts WHERE id = ?`).get(contactId);
|
|
761
|
+
if (!row)
|
|
762
|
+
throw new ContactNotFoundError(contactId);
|
|
763
|
+
const exists = d.query(`SELECT id FROM phones WHERE contact_id = ? AND number = ?`).get(contactId, phone.number);
|
|
764
|
+
if (!exists) {
|
|
765
|
+
d.run(`INSERT INTO phones (id, contact_id, company_id, number, country_code, type, is_primary) VALUES (?, ?, NULL, ?, ?, ?, ?)`, [uuid(), contactId, phone.number, phone.country_code ?? null, phone.type ?? "mobile", phone.is_primary ? 1 : 0]);
|
|
766
|
+
}
|
|
767
|
+
const updated = d.query(`SELECT * FROM contacts WHERE id = ?`).get(contactId);
|
|
768
|
+
return loadContactDetails(d, rowToContact(updated));
|
|
769
|
+
}
|
|
770
|
+
function archiveContact(id, db) {
|
|
771
|
+
const d = db || getDatabase();
|
|
772
|
+
const row = d.query(`SELECT * FROM contacts WHERE id = ?`).get(id);
|
|
773
|
+
if (!row)
|
|
774
|
+
throw new ContactNotFoundError(id);
|
|
775
|
+
d.run(`UPDATE contacts SET archived = 1, updated_at = ? WHERE id = ?`, [now(), id]);
|
|
776
|
+
logActivity(d, { contact_id: id, action: "contact.archived", details: `Archived contact: ${row.display_name}` });
|
|
777
|
+
const updated = d.query(`SELECT * FROM contacts WHERE id = ?`).get(id);
|
|
778
|
+
return loadContactDetails(d, rowToContact(updated));
|
|
779
|
+
}
|
|
780
|
+
function unarchiveContact(id, db) {
|
|
781
|
+
const d = db || getDatabase();
|
|
782
|
+
const row = d.query(`SELECT * FROM contacts WHERE id = ?`).get(id);
|
|
783
|
+
if (!row)
|
|
784
|
+
throw new ContactNotFoundError(id);
|
|
785
|
+
d.run(`UPDATE contacts SET archived = 0, updated_at = ? WHERE id = ?`, [now(), id]);
|
|
786
|
+
logActivity(d, { contact_id: id, action: "contact.unarchived", details: `Unarchived contact: ${row.display_name}` });
|
|
787
|
+
const updated = d.query(`SELECT * FROM contacts WHERE id = ?`).get(id);
|
|
788
|
+
return loadContactDetails(d, rowToContact(updated));
|
|
789
|
+
}
|
|
790
|
+
function autoLinkContactToCompany(contactId, db) {
|
|
791
|
+
const d = db || getDatabase();
|
|
792
|
+
const row = d.query(`SELECT * FROM contacts WHERE id = ?`).get(contactId);
|
|
793
|
+
if (!row || row.company_id)
|
|
794
|
+
return null;
|
|
795
|
+
const emailRow = d.query(`SELECT address FROM emails WHERE contact_id = ? AND contact_id IS NOT NULL LIMIT 1`).get(contactId);
|
|
796
|
+
if (!emailRow)
|
|
797
|
+
return null;
|
|
798
|
+
const domain = emailRow.address.split("@")[1];
|
|
799
|
+
if (!domain)
|
|
800
|
+
return null;
|
|
801
|
+
const companyRow = d.query(`SELECT id FROM companies WHERE domain = ? LIMIT 1`).get(domain);
|
|
802
|
+
if (!companyRow)
|
|
803
|
+
return null;
|
|
804
|
+
d.run(`UPDATE contacts SET company_id = ?, updated_at = ? WHERE id = ?`, [companyRow.id, now(), contactId]);
|
|
805
|
+
logActivity(d, { contact_id: contactId, action: "contact.auto_linked", details: `Auto-linked to company via email domain: ${domain}` });
|
|
806
|
+
const updated = d.query(`SELECT * FROM contacts WHERE id = ?`).get(contactId);
|
|
807
|
+
return loadContactDetails(d, rowToContact(updated));
|
|
808
|
+
}
|
|
629
809
|
// src/db/companies.ts
|
|
630
810
|
function rowToCompany2(row) {
|
|
631
811
|
return {
|
|
632
812
|
...row,
|
|
633
|
-
custom_fields: JSON.parse(row.custom_fields || "{}")
|
|
813
|
+
custom_fields: JSON.parse(row.custom_fields || "{}"),
|
|
814
|
+
archived: !!row.archived,
|
|
815
|
+
project_id: row.project_id ?? null
|
|
634
816
|
};
|
|
635
817
|
}
|
|
636
818
|
function insertEmails2(db, companyId, emails) {
|
|
@@ -740,15 +922,23 @@ function listCompanies(opts = {}, db) {
|
|
|
740
922
|
offset = 0,
|
|
741
923
|
industry,
|
|
742
924
|
tag_id,
|
|
925
|
+
project_id,
|
|
926
|
+
archived = false,
|
|
743
927
|
order_by = "name",
|
|
744
928
|
order_dir = "asc"
|
|
745
929
|
} = opts;
|
|
746
930
|
const conditions = [];
|
|
747
931
|
const params = [];
|
|
932
|
+
conditions.push("co.archived = ?");
|
|
933
|
+
params.push(archived ? 1 : 0);
|
|
748
934
|
if (industry) {
|
|
749
935
|
conditions.push("co.industry = ?");
|
|
750
936
|
params.push(industry);
|
|
751
937
|
}
|
|
938
|
+
if (project_id) {
|
|
939
|
+
conditions.push("co.project_id = ?");
|
|
940
|
+
params.push(project_id);
|
|
941
|
+
}
|
|
752
942
|
if (tag_id) {
|
|
753
943
|
conditions.push("EXISTS (SELECT 1 FROM company_tags ct WHERE ct.company_id = co.id AND ct.tag_id = ?)");
|
|
754
944
|
params.push(tag_id);
|
|
@@ -804,6 +994,10 @@ function updateCompany(id, input, db) {
|
|
|
804
994
|
setClauses.push("custom_fields = ?");
|
|
805
995
|
params.push(JSON.stringify(input.custom_fields));
|
|
806
996
|
}
|
|
997
|
+
if ("project_id" in input && input.project_id !== undefined) {
|
|
998
|
+
setClauses.push("project_id = ?");
|
|
999
|
+
params.push(input.project_id);
|
|
1000
|
+
}
|
|
807
1001
|
params.push(id);
|
|
808
1002
|
d.run(`UPDATE companies SET ${setClauses.join(", ")} WHERE id = ?`, params);
|
|
809
1003
|
logActivity(d, { company_id: id, action: "company.updated", details: `Updated company: ${existing.name}` });
|
|
@@ -837,9 +1031,33 @@ function listCompanyEmployees(companyId, db) {
|
|
|
837
1031
|
...r,
|
|
838
1032
|
source: r.source,
|
|
839
1033
|
custom_fields: JSON.parse(r.custom_fields || "{}"),
|
|
840
|
-
preferred_contact_method: r.preferred_contact_method ?? null
|
|
1034
|
+
preferred_contact_method: r.preferred_contact_method ?? null,
|
|
1035
|
+
status: r.status ?? "active",
|
|
1036
|
+
follow_up_at: r.follow_up_at ?? null,
|
|
1037
|
+
archived: !!r.archived,
|
|
1038
|
+
project_id: r.project_id ?? null
|
|
841
1039
|
}));
|
|
842
1040
|
}
|
|
1041
|
+
function archiveCompany(id, db) {
|
|
1042
|
+
const d = db || getDatabase();
|
|
1043
|
+
const row = d.query(`SELECT * FROM companies WHERE id = ?`).get(id);
|
|
1044
|
+
if (!row)
|
|
1045
|
+
throw new CompanyNotFoundError(id);
|
|
1046
|
+
d.run(`UPDATE companies SET archived = 1, updated_at = ? WHERE id = ?`, [now(), id]);
|
|
1047
|
+
logActivity(d, { company_id: id, action: "company.archived", details: `Archived company: ${row.name}` });
|
|
1048
|
+
const updated = d.query(`SELECT * FROM companies WHERE id = ?`).get(id);
|
|
1049
|
+
return loadCompanyDetails(d, rowToCompany2(updated));
|
|
1050
|
+
}
|
|
1051
|
+
function unarchiveCompany(id, db) {
|
|
1052
|
+
const d = db || getDatabase();
|
|
1053
|
+
const row = d.query(`SELECT * FROM companies WHERE id = ?`).get(id);
|
|
1054
|
+
if (!row)
|
|
1055
|
+
throw new CompanyNotFoundError(id);
|
|
1056
|
+
d.run(`UPDATE companies SET archived = 0, updated_at = ? WHERE id = ?`, [now(), id]);
|
|
1057
|
+
logActivity(d, { company_id: id, action: "company.unarchived", details: `Unarchived company: ${row.name}` });
|
|
1058
|
+
const updated = d.query(`SELECT * FROM companies WHERE id = ?`).get(id);
|
|
1059
|
+
return loadCompanyDetails(d, rowToCompany2(updated));
|
|
1060
|
+
}
|
|
843
1061
|
// src/db/tags.ts
|
|
844
1062
|
function rowToTag2(row) {
|
|
845
1063
|
return { ...row };
|
|
@@ -935,7 +1153,11 @@ function listContactsByTag(tagId, db) {
|
|
|
935
1153
|
...r,
|
|
936
1154
|
source: r.source,
|
|
937
1155
|
custom_fields: JSON.parse(r.custom_fields || "{}"),
|
|
938
|
-
preferred_contact_method: r.preferred_contact_method ?? null
|
|
1156
|
+
preferred_contact_method: r.preferred_contact_method ?? null,
|
|
1157
|
+
status: r.status ?? "active",
|
|
1158
|
+
follow_up_at: r.follow_up_at ?? null,
|
|
1159
|
+
archived: !!r.archived,
|
|
1160
|
+
project_id: r.project_id ?? null
|
|
939
1161
|
}));
|
|
940
1162
|
}
|
|
941
1163
|
function addTagToCompany(companyId, tagId, db) {
|
|
@@ -1007,7 +1229,10 @@ function getGroup(db, id) {
|
|
|
1007
1229
|
return db.query(`SELECT * FROM groups WHERE id = ?`).get(id);
|
|
1008
1230
|
}
|
|
1009
1231
|
function listGroups(db) {
|
|
1010
|
-
return db.query(`SELECT g.*,
|
|
1232
|
+
return db.query(`SELECT g.*,
|
|
1233
|
+
(SELECT COUNT(*) FROM contact_groups cg WHERE cg.group_id = g.id) as member_count,
|
|
1234
|
+
(SELECT COUNT(*) FROM company_groups cog WHERE cog.group_id = g.id) as company_count
|
|
1235
|
+
FROM groups g ORDER BY g.name`).all();
|
|
1011
1236
|
}
|
|
1012
1237
|
function updateGroup(db, id, input) {
|
|
1013
1238
|
const fields = [];
|
|
@@ -1030,7 +1255,11 @@ function deleteGroup(db, id) {
|
|
|
1030
1255
|
db.query(`DELETE FROM groups WHERE id = ?`).run(id);
|
|
1031
1256
|
}
|
|
1032
1257
|
function addContactToGroup(db, contactId, groupId) {
|
|
1033
|
-
db.query(`
|
|
1258
|
+
const existing = db.query(`SELECT 1 FROM contact_groups WHERE contact_id = ? AND group_id = ?`).get(contactId, groupId);
|
|
1259
|
+
if (existing)
|
|
1260
|
+
return { added: false, already_member: true };
|
|
1261
|
+
db.query(`INSERT INTO contact_groups(contact_id, group_id) VALUES(?,?)`).run(contactId, groupId);
|
|
1262
|
+
return { added: true, already_member: false };
|
|
1034
1263
|
}
|
|
1035
1264
|
function removeContactFromGroup(db, contactId, groupId) {
|
|
1036
1265
|
db.query(`DELETE FROM contact_groups WHERE contact_id = ? AND group_id = ?`).run(contactId, groupId);
|
|
@@ -1042,28 +1271,50 @@ function listContactsInGroup(db, groupId) {
|
|
|
1042
1271
|
function listGroupsForContact(db, contactId) {
|
|
1043
1272
|
return db.query(`SELECT g.* FROM groups g JOIN contact_groups cg ON g.id = cg.group_id WHERE cg.contact_id = ? ORDER BY g.name`).all(contactId);
|
|
1044
1273
|
}
|
|
1274
|
+
function addCompanyToGroup(db, companyId, groupId) {
|
|
1275
|
+
const existing = db.query(`SELECT 1 FROM company_groups WHERE company_id = ? AND group_id = ?`).get(companyId, groupId);
|
|
1276
|
+
if (existing)
|
|
1277
|
+
return { added: false, already_member: true };
|
|
1278
|
+
db.query(`INSERT INTO company_groups(company_id, group_id) VALUES(?,?)`).run(companyId, groupId);
|
|
1279
|
+
return { added: true, already_member: false };
|
|
1280
|
+
}
|
|
1281
|
+
function removeCompanyFromGroup(db, companyId, groupId) {
|
|
1282
|
+
db.query(`DELETE FROM company_groups WHERE company_id = ? AND group_id = ?`).run(companyId, groupId);
|
|
1283
|
+
}
|
|
1284
|
+
function listCompaniesInGroup(db, groupId) {
|
|
1285
|
+
const rows = db.query(`SELECT company_id FROM company_groups WHERE group_id = ?`).all(groupId);
|
|
1286
|
+
return rows.map((r) => r.company_id);
|
|
1287
|
+
}
|
|
1288
|
+
function listGroupsForCompany(db, companyId) {
|
|
1289
|
+
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);
|
|
1290
|
+
}
|
|
1045
1291
|
export {
|
|
1046
1292
|
updateTag,
|
|
1047
1293
|
updateGroup,
|
|
1048
1294
|
updateContact,
|
|
1049
1295
|
updateCompany,
|
|
1296
|
+
unarchiveContact,
|
|
1297
|
+
unarchiveCompany,
|
|
1050
1298
|
searchContacts,
|
|
1051
1299
|
searchCompanies,
|
|
1052
1300
|
resetDatabase,
|
|
1053
1301
|
removeTagFromContact,
|
|
1054
1302
|
removeTagFromCompany,
|
|
1055
1303
|
removeContactFromGroup,
|
|
1304
|
+
removeCompanyFromGroup,
|
|
1056
1305
|
mergeContacts,
|
|
1057
1306
|
logActivity,
|
|
1058
1307
|
listTags,
|
|
1059
1308
|
listRelationships,
|
|
1060
1309
|
listRecentContacts,
|
|
1061
1310
|
listGroupsForContact,
|
|
1311
|
+
listGroupsForCompany,
|
|
1062
1312
|
listGroups,
|
|
1063
1313
|
listContactsInGroup,
|
|
1064
1314
|
listContactsByTag,
|
|
1065
1315
|
listContacts,
|
|
1066
1316
|
listCompanyEmployees,
|
|
1317
|
+
listCompaniesInGroup,
|
|
1067
1318
|
listCompanies,
|
|
1068
1319
|
listActivity,
|
|
1069
1320
|
getTagByName,
|
|
@@ -1071,6 +1322,7 @@ export {
|
|
|
1071
1322
|
getRelationship,
|
|
1072
1323
|
getGroup,
|
|
1073
1324
|
getDatabase,
|
|
1325
|
+
getContactByEmail,
|
|
1074
1326
|
getContact,
|
|
1075
1327
|
getCompany,
|
|
1076
1328
|
getActivity,
|
|
@@ -1084,9 +1336,15 @@ export {
|
|
|
1084
1336
|
createGroup,
|
|
1085
1337
|
createContact,
|
|
1086
1338
|
createCompany,
|
|
1339
|
+
autoLinkContactToCompany,
|
|
1340
|
+
archiveContact,
|
|
1341
|
+
archiveCompany,
|
|
1087
1342
|
addTagToContact,
|
|
1088
1343
|
addTagToCompany,
|
|
1344
|
+
addPhoneToContact,
|
|
1345
|
+
addEmailToContact,
|
|
1089
1346
|
addContactToGroup,
|
|
1347
|
+
addCompanyToGroup,
|
|
1090
1348
|
TagNotFoundError,
|
|
1091
1349
|
DuplicateTagNameError,
|
|
1092
1350
|
ContactNotFoundError,
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gmail contact import — fetches messages matching a query, extracts unique
|
|
3
|
+
* senders/recipients, and returns them as CreateContactInput objects.
|
|
4
|
+
*
|
|
5
|
+
* Uses the Gmail REST API directly with tokens stored at
|
|
6
|
+
* ~/.connectors/connect-gmail/profiles/default/tokens.json
|
|
7
|
+
* (requires connect-gmail auth login to be completed first).
|
|
8
|
+
*/
|
|
9
|
+
import type { CreateContactInput } from "../types/index.js";
|
|
10
|
+
/**
|
|
11
|
+
* Parse a single RFC 5322 address header value into { name, email } pairs.
|
|
12
|
+
* Handles: "Name <email>", "<email>", "email", and comma-separated lists.
|
|
13
|
+
*/
|
|
14
|
+
export declare function parseAddressHeader(header: string): Array<{
|
|
15
|
+
name: string;
|
|
16
|
+
email: string;
|
|
17
|
+
}>;
|
|
18
|
+
export interface GmailImportOptions {
|
|
19
|
+
query: string;
|
|
20
|
+
/** Max messages to scan (default 200, max 500) */
|
|
21
|
+
max_messages?: number;
|
|
22
|
+
/** Gmail profile to use (default: "default") */
|
|
23
|
+
gmail_profile?: string;
|
|
24
|
+
tag_ids?: string[];
|
|
25
|
+
group_id?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface ExtractedContact {
|
|
28
|
+
email: string;
|
|
29
|
+
name: string;
|
|
30
|
+
company_hint: string | null;
|
|
31
|
+
contact_input: CreateContactInput;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Fetch messages matching `query` from Gmail and extract unique contacts.
|
|
35
|
+
* Does NOT write to the database — returns prepared CreateContactInput objects.
|
|
36
|
+
*/
|
|
37
|
+
export declare function extractContactsFromGmail(opts: GmailImportOptions): Promise<ExtractedContact[]>;
|
|
38
|
+
//# sourceMappingURL=gmail-import.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gmail-import.d.ts","sourceRoot":"","sources":["../../src/lib/gmail-import.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAoD5D;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAoBzF;AAqCD,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,kDAAkD;IAClD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gDAAgD;IAChD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,aAAa,EAAE,kBAAkB,CAAC;CACnC;AAED;;;GAGG;AACH,wBAAsB,wBAAwB,CAC5C,IAAI,EAAE,kBAAkB,GACvB,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAoF7B"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gmail-import.test.d.ts","sourceRoot":"","sources":["../../src/lib/gmail-import.test.ts"],"names":[],"mappings":""}
|