@hasna/contacts 0.6.5 → 0.6.6
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 +3903 -1611
- package/dist/db/companies.d.ts.map +1 -1
- package/dist/db/contacts.d.ts +4 -0
- package/dist/db/contacts.d.ts.map +1 -1
- package/dist/db/coordination.d.ts +30 -0
- package/dist/db/coordination.d.ts.map +1 -0
- package/dist/db/database.d.ts +1 -0
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/documents.d.ts +38 -0
- package/dist/db/documents.d.ts.map +1 -0
- package/dist/db/field-history.d.ts +17 -0
- package/dist/db/field-history.d.ts.map +1 -0
- package/dist/db/freshness.d.ts +24 -0
- package/dist/db/freshness.d.ts.map +1 -0
- package/dist/db/graph.d.ts +21 -0
- package/dist/db/graph.d.ts.map +1 -0
- package/dist/db/health.d.ts +40 -0
- package/dist/db/health.d.ts.map +1 -0
- package/dist/db/identity.d.ts +32 -0
- package/dist/db/identity.d.ts.map +1 -0
- package/dist/db/job-history.d.ts +29 -0
- package/dist/db/job-history.d.ts.map +1 -0
- package/dist/db/learnings.d.ts +43 -0
- package/dist/db/learnings.d.ts.map +1 -0
- package/dist/db/org-chart.d.ts +37 -0
- package/dist/db/org-chart.d.ts.map +1 -0
- package/dist/db/signals.d.ts +18 -0
- package/dist/db/signals.d.ts.map +1 -0
- package/dist/db/tags.d.ts.map +1 -1
- package/dist/index.d.ts +33 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1577 -89
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/context.d.ts +5 -0
- package/dist/lib/context.d.ts.map +1 -0
- package/dist/lib/document-scanner.d.ts +8 -0
- package/dist/lib/document-scanner.d.ts.map +1 -0
- package/dist/lib/embeddings.d.ts +9 -0
- package/dist/lib/embeddings.d.ts.map +1 -0
- package/dist/lib/freshness.d.ts +19 -0
- package/dist/lib/freshness.d.ts.map +1 -0
- package/dist/lib/images.d.ts +30 -0
- package/dist/lib/images.d.ts.map +1 -0
- package/dist/lib/learning-maintenance.d.ts +8 -0
- package/dist/lib/learning-maintenance.d.ts.map +1 -0
- package/dist/lib/meeting-capture.d.ts +15 -0
- package/dist/lib/meeting-capture.d.ts.map +1 -0
- package/dist/lib/signals.d.ts +9 -0
- package/dist/lib/signals.d.ts.map +1 -0
- package/dist/lib/signature-parser.d.ts +36 -0
- package/dist/lib/signature-parser.d.ts.map +1 -0
- package/dist/lib/vault.d.ts +24 -0
- package/dist/lib/vault.d.ts.map +1 -0
- package/dist/mcp/index.js +1951 -81
- package/dist/server/index.js +352 -15
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/types/index.d.ts +6 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -14,16 +14,34 @@ var __export = (target, all) => {
|
|
|
14
14
|
});
|
|
15
15
|
};
|
|
16
16
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
17
|
+
var __require = import.meta.require;
|
|
17
18
|
|
|
18
19
|
// src/db/database.ts
|
|
19
20
|
import { Database } from "bun:sqlite";
|
|
20
|
-
import { existsSync, mkdirSync } from "fs";
|
|
21
|
+
import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
|
|
21
22
|
import { dirname, join, resolve } from "path";
|
|
23
|
+
function getDataDir() {
|
|
24
|
+
const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
25
|
+
const newDir = join(home, ".hasna", "contacts");
|
|
26
|
+
const oldDir = join(home, ".contacts");
|
|
27
|
+
if (existsSync(oldDir) && !existsSync(newDir)) {
|
|
28
|
+
mkdirSync(newDir, { recursive: true });
|
|
29
|
+
for (const file of readdirSync(oldDir)) {
|
|
30
|
+
const oldPath = join(oldDir, file);
|
|
31
|
+
if (statSync(oldPath).isFile()) {
|
|
32
|
+
copyFileSync(oldPath, join(newDir, file));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
mkdirSync(newDir, { recursive: true });
|
|
37
|
+
return newDir;
|
|
38
|
+
}
|
|
22
39
|
function getDbPath() {
|
|
40
|
+
if (process.env["HASNA_CONTACTS_DB_PATH"])
|
|
41
|
+
return process.env["HASNA_CONTACTS_DB_PATH"];
|
|
23
42
|
if (process.env["CONTACTS_DB_PATH"])
|
|
24
43
|
return process.env["CONTACTS_DB_PATH"];
|
|
25
|
-
|
|
26
|
-
return join(home, ".contacts", "contacts.db");
|
|
44
|
+
return join(getDataDir(), "contacts.db");
|
|
27
45
|
}
|
|
28
46
|
function ensureDir(filePath) {
|
|
29
47
|
if (filePath === ":memory:")
|
|
@@ -411,6 +429,177 @@ var init_database = __esm(() => {
|
|
|
411
429
|
deal_id TEXT REFERENCES deals(id) ON DELETE SET NULL,
|
|
412
430
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
413
431
|
);
|
|
432
|
+
`,
|
|
433
|
+
`
|
|
434
|
+
-- CON-00069: temporal field history
|
|
435
|
+
CREATE TABLE IF NOT EXISTS contact_field_history (
|
|
436
|
+
id TEXT PRIMARY KEY,
|
|
437
|
+
contact_id TEXT NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
|
|
438
|
+
field_name TEXT NOT NULL,
|
|
439
|
+
old_value TEXT,
|
|
440
|
+
new_value TEXT,
|
|
441
|
+
valid_from TEXT NOT NULL DEFAULT (datetime('now')),
|
|
442
|
+
source TEXT,
|
|
443
|
+
confidence TEXT NOT NULL DEFAULT 'imported' CHECK(confidence IN ('verified','inferred','imported','stale')),
|
|
444
|
+
created_by TEXT,
|
|
445
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
446
|
+
);
|
|
447
|
+
|
|
448
|
+
-- CON-00070: job history
|
|
449
|
+
CREATE TABLE IF NOT EXISTS job_history (
|
|
450
|
+
id TEXT PRIMARY KEY,
|
|
451
|
+
contact_id TEXT NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
|
|
452
|
+
company_id TEXT REFERENCES companies(id) ON DELETE SET NULL,
|
|
453
|
+
company_name TEXT NOT NULL,
|
|
454
|
+
title TEXT,
|
|
455
|
+
start_date TEXT,
|
|
456
|
+
end_date TEXT,
|
|
457
|
+
is_current INTEGER NOT NULL DEFAULT 0,
|
|
458
|
+
inferred INTEGER NOT NULL DEFAULT 0,
|
|
459
|
+
source TEXT,
|
|
460
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
461
|
+
);
|
|
462
|
+
|
|
463
|
+
-- CON-00071: learnings
|
|
464
|
+
CREATE TABLE IF NOT EXISTS contact_learnings (
|
|
465
|
+
id TEXT PRIMARY KEY,
|
|
466
|
+
contact_id TEXT NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
|
|
467
|
+
content TEXT NOT NULL,
|
|
468
|
+
type TEXT NOT NULL DEFAULT 'fact' CHECK(type IN ('preference','fact','inference','warning','signal')),
|
|
469
|
+
confidence INTEGER NOT NULL DEFAULT 70 CHECK(confidence BETWEEN 0 AND 100),
|
|
470
|
+
importance INTEGER NOT NULL DEFAULT 5 CHECK(importance BETWEEN 1 AND 10),
|
|
471
|
+
learned_by TEXT,
|
|
472
|
+
session_id TEXT,
|
|
473
|
+
visibility TEXT NOT NULL DEFAULT 'shared' CHECK(visibility IN ('private','shared','human')),
|
|
474
|
+
tags TEXT NOT NULL DEFAULT '[]',
|
|
475
|
+
confirmed_count INTEGER NOT NULL DEFAULT 0,
|
|
476
|
+
contradicts_id TEXT REFERENCES contact_learnings(id) ON DELETE SET NULL,
|
|
477
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
478
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
479
|
+
);
|
|
480
|
+
|
|
481
|
+
-- CON-00072: coordination
|
|
482
|
+
CREATE TABLE IF NOT EXISTS contact_locks (
|
|
483
|
+
id TEXT PRIMARY KEY,
|
|
484
|
+
contact_id TEXT NOT NULL UNIQUE REFERENCES contacts(id) ON DELETE CASCADE,
|
|
485
|
+
agent_name TEXT NOT NULL,
|
|
486
|
+
reason TEXT,
|
|
487
|
+
acquired_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
488
|
+
expires_at TEXT NOT NULL,
|
|
489
|
+
session_id TEXT
|
|
490
|
+
);
|
|
491
|
+
CREATE TABLE IF NOT EXISTS contact_agent_activity (
|
|
492
|
+
id TEXT PRIMARY KEY,
|
|
493
|
+
contact_id TEXT NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
|
|
494
|
+
agent_name TEXT NOT NULL,
|
|
495
|
+
action TEXT NOT NULL,
|
|
496
|
+
details TEXT,
|
|
497
|
+
session_id TEXT,
|
|
498
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
499
|
+
);
|
|
500
|
+
|
|
501
|
+
-- CON-00073: relationship graph extra columns
|
|
502
|
+
ALTER TABLE contact_relationships ADD COLUMN strength_score INTEGER NOT NULL DEFAULT 50;
|
|
503
|
+
ALTER TABLE contact_relationships ADD COLUMN interaction_count INTEGER NOT NULL DEFAULT 0;
|
|
504
|
+
ALTER TABLE contact_relationships ADD COLUMN last_interaction TEXT;
|
|
505
|
+
ALTER TABLE contact_relationships ADD COLUMN relationship_status TEXT NOT NULL DEFAULT 'stable' CHECK(relationship_status IN ('warming','stable','cooling','ghost'));
|
|
506
|
+
|
|
507
|
+
-- CON-00074: identity resolution
|
|
508
|
+
CREATE TABLE IF NOT EXISTS contact_identities (
|
|
509
|
+
id TEXT PRIMARY KEY,
|
|
510
|
+
contact_id TEXT NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
|
|
511
|
+
system TEXT NOT NULL,
|
|
512
|
+
external_id TEXT NOT NULL,
|
|
513
|
+
external_url TEXT,
|
|
514
|
+
confidence TEXT NOT NULL DEFAULT 'inferred' CHECK(confidence IN ('verified','inferred')),
|
|
515
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
516
|
+
UNIQUE(system, external_id)
|
|
517
|
+
);
|
|
518
|
+
ALTER TABLE contacts ADD COLUMN canonical_id TEXT;
|
|
519
|
+
|
|
520
|
+
-- CON-00076: relationship signals
|
|
521
|
+
ALTER TABLE contacts ADD COLUMN relationship_health INTEGER NOT NULL DEFAULT 50;
|
|
522
|
+
ALTER TABLE contacts ADD COLUMN avg_response_hours REAL;
|
|
523
|
+
ALTER TABLE contacts ADD COLUMN preferred_channel TEXT;
|
|
524
|
+
ALTER TABLE contacts ADD COLUMN engagement_status TEXT NOT NULL DEFAULT 'new' CHECK(engagement_status IN ('warming','stable','cooling','ghost','new'));
|
|
525
|
+
ALTER TABLE contacts ADD COLUMN interaction_count_30d INTEGER NOT NULL DEFAULT 0;
|
|
526
|
+
ALTER TABLE contacts ADD COLUMN interaction_count_90d INTEGER NOT NULL DEFAULT 0;
|
|
527
|
+
|
|
528
|
+
-- CON-00079: freshness scoring
|
|
529
|
+
CREATE TABLE IF NOT EXISTS contact_field_confidence (
|
|
530
|
+
id TEXT PRIMARY KEY,
|
|
531
|
+
contact_id TEXT NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
|
|
532
|
+
field_name TEXT NOT NULL,
|
|
533
|
+
confidence TEXT NOT NULL DEFAULT 'imported' CHECK(confidence IN ('verified','inferred','imported','stale')),
|
|
534
|
+
source TEXT,
|
|
535
|
+
last_verified_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
536
|
+
UNIQUE(contact_id, field_name)
|
|
537
|
+
);
|
|
538
|
+
|
|
539
|
+
-- CON-00080: org chart
|
|
540
|
+
CREATE TABLE IF NOT EXISTS org_chart_edges (
|
|
541
|
+
id TEXT PRIMARY KEY,
|
|
542
|
+
company_id TEXT NOT NULL REFERENCES companies(id) ON DELETE CASCADE,
|
|
543
|
+
contact_a_id TEXT NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
|
|
544
|
+
contact_b_id TEXT NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
|
|
545
|
+
edge_type TEXT NOT NULL CHECK(edge_type IN ('reports_to','manages','collaborates_with','peer')),
|
|
546
|
+
inferred INTEGER NOT NULL DEFAULT 0,
|
|
547
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
548
|
+
UNIQUE(company_id, contact_a_id, contact_b_id, edge_type)
|
|
549
|
+
);
|
|
550
|
+
CREATE TABLE IF NOT EXISTS deal_contact_roles (
|
|
551
|
+
id TEXT PRIMARY KEY,
|
|
552
|
+
deal_id TEXT NOT NULL REFERENCES deals(id) ON DELETE CASCADE,
|
|
553
|
+
contact_id TEXT NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
|
|
554
|
+
account_role TEXT NOT NULL CHECK(account_role IN ('economic_buyer','technical_evaluator','champion','blocker','influencer','user','sponsor','other')),
|
|
555
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
556
|
+
UNIQUE(deal_id, contact_id)
|
|
557
|
+
);
|
|
558
|
+
|
|
559
|
+
-- CON-00075: embeddings
|
|
560
|
+
CREATE TABLE IF NOT EXISTS contact_embeddings (
|
|
561
|
+
contact_id TEXT PRIMARY KEY REFERENCES contacts(id) ON DELETE CASCADE,
|
|
562
|
+
embedding TEXT NOT NULL,
|
|
563
|
+
model TEXT NOT NULL DEFAULT 'tfidf',
|
|
564
|
+
embedded_text TEXT,
|
|
565
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
566
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
567
|
+
);
|
|
568
|
+
`,
|
|
569
|
+
`
|
|
570
|
+
ALTER TABLE contacts ADD COLUMN sensitivity TEXT NOT NULL DEFAULT 'normal' CHECK(sensitivity IN ('normal','confidential','restricted'));
|
|
571
|
+
|
|
572
|
+
CREATE TABLE IF NOT EXISTS contact_documents (
|
|
573
|
+
id TEXT PRIMARY KEY,
|
|
574
|
+
contact_id TEXT NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
|
|
575
|
+
doc_type TEXT NOT NULL,
|
|
576
|
+
label TEXT,
|
|
577
|
+
encrypted_value TEXT NOT NULL,
|
|
578
|
+
iv TEXT NOT NULL,
|
|
579
|
+
encrypted_file_path TEXT,
|
|
580
|
+
metadata TEXT NOT NULL DEFAULT '{}',
|
|
581
|
+
expires_at TEXT,
|
|
582
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
583
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
584
|
+
);
|
|
585
|
+
|
|
586
|
+
CREATE TABLE IF NOT EXISTS contact_health (
|
|
587
|
+
id TEXT PRIMARY KEY,
|
|
588
|
+
contact_id TEXT NOT NULL UNIQUE REFERENCES contacts(id) ON DELETE CASCADE,
|
|
589
|
+
blood_type TEXT,
|
|
590
|
+
allergies TEXT NOT NULL DEFAULT '[]',
|
|
591
|
+
medical_conditions TEXT NOT NULL DEFAULT '[]',
|
|
592
|
+
medications TEXT NOT NULL DEFAULT '[]',
|
|
593
|
+
emergency_contacts TEXT NOT NULL DEFAULT '[]',
|
|
594
|
+
health_insurance_provider TEXT,
|
|
595
|
+
health_insurance_id TEXT,
|
|
596
|
+
primary_physician TEXT,
|
|
597
|
+
primary_physician_phone TEXT,
|
|
598
|
+
organ_donor INTEGER NOT NULL DEFAULT 0,
|
|
599
|
+
notes TEXT,
|
|
600
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
601
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
602
|
+
);
|
|
414
603
|
`
|
|
415
604
|
];
|
|
416
605
|
});
|
|
@@ -497,6 +686,7 @@ __export(exports_contacts, {
|
|
|
497
686
|
getContactProjectIds: () => getContactProjectIds,
|
|
498
687
|
getContactByEmail: () => getContactByEmail,
|
|
499
688
|
getContact: () => getContact,
|
|
689
|
+
findOrCreateContact: () => findOrCreateContact,
|
|
500
690
|
deleteContact: () => deleteContact,
|
|
501
691
|
createContact: () => createContact,
|
|
502
692
|
autoLinkContactToCompany: () => autoLinkContactToCompany,
|
|
@@ -514,6 +704,7 @@ function rowToContact(row) {
|
|
|
514
704
|
follow_up_at: row.follow_up_at ?? null,
|
|
515
705
|
archived: !!row.archived,
|
|
516
706
|
project_id: row.project_id ?? null,
|
|
707
|
+
sensitivity: row.sensitivity ?? "normal",
|
|
517
708
|
do_not_contact: !!row.do_not_contact,
|
|
518
709
|
priority: row.priority ?? 3,
|
|
519
710
|
timezone: row.timezone ?? null
|
|
@@ -601,8 +792,8 @@ function createContact(input, db) {
|
|
|
601
792
|
const firstName = input.first_name ?? "";
|
|
602
793
|
const lastName = input.last_name ?? "";
|
|
603
794
|
const displayName = input.display_name ?? (firstName || lastName ? `${firstName} ${lastName}`.trim() : "Unnamed Contact");
|
|
604
|
-
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)
|
|
605
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
795
|
+
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, sensitivity, do_not_contact, priority, timezone, created_at, updated_at)
|
|
796
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
606
797
|
id,
|
|
607
798
|
firstName,
|
|
608
799
|
lastName,
|
|
@@ -621,6 +812,7 @@ function createContact(input, db) {
|
|
|
621
812
|
input.status ?? "active",
|
|
622
813
|
input.follow_up_at ?? null,
|
|
623
814
|
input.project_id ?? null,
|
|
815
|
+
input.sensitivity ?? "normal",
|
|
624
816
|
input.do_not_contact ? 1 : 0,
|
|
625
817
|
input.priority ?? 3,
|
|
626
818
|
input.timezone ?? null,
|
|
@@ -669,6 +861,7 @@ function listContacts(opts = {}, db) {
|
|
|
669
861
|
order_by = "display_name",
|
|
670
862
|
order_dir = "asc",
|
|
671
863
|
include_dnc = false,
|
|
864
|
+
include_restricted = false,
|
|
672
865
|
priority_min,
|
|
673
866
|
updated_since
|
|
674
867
|
} = opts;
|
|
@@ -679,6 +872,9 @@ function listContacts(opts = {}, db) {
|
|
|
679
872
|
if (!include_dnc) {
|
|
680
873
|
conditions.push("c.do_not_contact = 0");
|
|
681
874
|
}
|
|
875
|
+
if (!include_restricted) {
|
|
876
|
+
conditions.push("c.sensitivity != 'restricted'");
|
|
877
|
+
}
|
|
682
878
|
if (company_id) {
|
|
683
879
|
conditions.push("c.company_id = ?");
|
|
684
880
|
params.push(company_id);
|
|
@@ -807,6 +1003,10 @@ function updateContact(id, input, db) {
|
|
|
807
1003
|
setClauses.push("project_id = ?");
|
|
808
1004
|
params.push(input.project_id);
|
|
809
1005
|
}
|
|
1006
|
+
if (input.sensitivity !== undefined) {
|
|
1007
|
+
setClauses.push("sensitivity = ?");
|
|
1008
|
+
params.push(input.sensitivity);
|
|
1009
|
+
}
|
|
810
1010
|
if (input.do_not_contact !== undefined) {
|
|
811
1011
|
setClauses.push("do_not_contact = ?");
|
|
812
1012
|
params.push(input.do_not_contact ? 1 : 0);
|
|
@@ -854,26 +1054,26 @@ function searchContacts(query, db) {
|
|
|
854
1054
|
const ftsRows = d.query(`
|
|
855
1055
|
SELECT c.* FROM contacts c
|
|
856
1056
|
JOIN contacts_fts fts ON fts.id = c.id
|
|
857
|
-
WHERE contacts_fts MATCH ? AND c.archived = 0
|
|
1057
|
+
WHERE contacts_fts MATCH ? AND c.archived = 0 AND c.sensitivity != 'restricted'
|
|
858
1058
|
ORDER BY rank
|
|
859
1059
|
LIMIT 50
|
|
860
1060
|
`).all(`"${query.replace(/"/g, '""')}"*`);
|
|
861
1061
|
const emailRows = d.query(`
|
|
862
1062
|
SELECT DISTINCT c.* FROM contacts c
|
|
863
1063
|
JOIN emails e ON e.contact_id = c.id
|
|
864
|
-
WHERE e.address LIKE ? AND c.archived = 0
|
|
1064
|
+
WHERE e.address LIKE ? AND c.archived = 0 AND c.sensitivity != 'restricted'
|
|
865
1065
|
LIMIT 20
|
|
866
1066
|
`).all(`%${query}%`);
|
|
867
1067
|
const phoneRows = d.query(`
|
|
868
1068
|
SELECT DISTINCT c.* FROM contacts c
|
|
869
1069
|
JOIN phones p ON p.contact_id = c.id
|
|
870
|
-
WHERE p.number LIKE ? AND c.archived = 0
|
|
1070
|
+
WHERE p.number LIKE ? AND c.archived = 0 AND c.sensitivity != 'restricted'
|
|
871
1071
|
LIMIT 20
|
|
872
1072
|
`).all(`%${query}%`);
|
|
873
1073
|
const companyRows = d.query(`
|
|
874
1074
|
SELECT DISTINCT c.* FROM contacts c
|
|
875
1075
|
JOIN companies co ON co.id = c.company_id
|
|
876
|
-
WHERE co.name LIKE ? AND c.archived = 0
|
|
1076
|
+
WHERE co.name LIKE ? AND c.archived = 0 AND c.sensitivity != 'restricted'
|
|
877
1077
|
LIMIT 20
|
|
878
1078
|
`).all(`%${query}%`);
|
|
879
1079
|
const seen = new Set;
|
|
@@ -1026,6 +1226,25 @@ function listColdContacts(days, db) {
|
|
|
1026
1226
|
LIMIT 100`).all(`-${days}`);
|
|
1027
1227
|
return rows.map((row) => loadContactDetails(d, rowToContact(row)));
|
|
1028
1228
|
}
|
|
1229
|
+
async function findOrCreateContact(input, db) {
|
|
1230
|
+
const d = db || getDatabase();
|
|
1231
|
+
const emailAddresses = (input.emails ?? []).map((e) => e.address);
|
|
1232
|
+
for (const addr of emailAddresses) {
|
|
1233
|
+
const emailRow = d.query(`SELECT contact_id FROM emails WHERE LOWER(address) = LOWER(?) AND contact_id IS NOT NULL LIMIT 1`).get(addr);
|
|
1234
|
+
if (emailRow) {
|
|
1235
|
+
return { contact: getContact(emailRow.contact_id, d), created: false };
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
const nameQuery = input.display_name ?? (input.first_name || input.last_name ? `${input.first_name ?? ""} ${input.last_name ?? ""}`.trim() : null);
|
|
1239
|
+
if (nameQuery) {
|
|
1240
|
+
const results = searchContacts(nameQuery, d);
|
|
1241
|
+
if (results.length > 0 && results[0]) {
|
|
1242
|
+
return { contact: results[0], created: false };
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
const contact = createContact(input, d);
|
|
1246
|
+
return { contact, created: true };
|
|
1247
|
+
}
|
|
1029
1248
|
function autoLinkContactToCompany(contactId, db) {
|
|
1030
1249
|
const d = db || getDatabase();
|
|
1031
1250
|
const row = d.query(`SELECT * FROM contacts WHERE id = ?`).get(contactId);
|
|
@@ -1076,6 +1295,96 @@ var init_contacts = __esm(() => {
|
|
|
1076
1295
|
init_activity();
|
|
1077
1296
|
});
|
|
1078
1297
|
|
|
1298
|
+
// src/db/events.ts
|
|
1299
|
+
var exports_events = {};
|
|
1300
|
+
__export(exports_events, {
|
|
1301
|
+
logEvent: () => logEvent,
|
|
1302
|
+
listEvents: () => listEvents,
|
|
1303
|
+
getEvent: () => getEvent,
|
|
1304
|
+
deleteEvent: () => deleteEvent
|
|
1305
|
+
});
|
|
1306
|
+
function rowToEvent(row) {
|
|
1307
|
+
let contact_ids = [];
|
|
1308
|
+
try {
|
|
1309
|
+
contact_ids = JSON.parse(row.contact_ids);
|
|
1310
|
+
} catch {
|
|
1311
|
+
contact_ids = [];
|
|
1312
|
+
}
|
|
1313
|
+
return {
|
|
1314
|
+
id: row.id,
|
|
1315
|
+
title: row.title,
|
|
1316
|
+
type: row.type,
|
|
1317
|
+
event_date: row.event_date,
|
|
1318
|
+
duration_min: row.duration_min,
|
|
1319
|
+
contact_ids,
|
|
1320
|
+
company_id: row.company_id,
|
|
1321
|
+
notes: row.notes,
|
|
1322
|
+
outcome: row.outcome,
|
|
1323
|
+
deal_id: row.deal_id,
|
|
1324
|
+
created_at: row.created_at
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
function logEvent(input, db) {
|
|
1328
|
+
const d = db || getDatabase();
|
|
1329
|
+
const id = uuid();
|
|
1330
|
+
const timestamp = now();
|
|
1331
|
+
d.run(`INSERT INTO events (id, title, type, event_date, duration_min, contact_ids, company_id, notes, outcome, deal_id, created_at)
|
|
1332
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
1333
|
+
id,
|
|
1334
|
+
input.title,
|
|
1335
|
+
input.type ?? "meeting",
|
|
1336
|
+
input.event_date,
|
|
1337
|
+
input.duration_min ?? null,
|
|
1338
|
+
JSON.stringify(input.contact_ids ?? []),
|
|
1339
|
+
input.company_id ?? null,
|
|
1340
|
+
input.notes ?? null,
|
|
1341
|
+
input.outcome ?? null,
|
|
1342
|
+
input.deal_id ?? null,
|
|
1343
|
+
timestamp
|
|
1344
|
+
]);
|
|
1345
|
+
return rowToEvent(d.query(`SELECT * FROM events WHERE id = ?`).get(id));
|
|
1346
|
+
}
|
|
1347
|
+
function getEvent(id, db) {
|
|
1348
|
+
const d = db || getDatabase();
|
|
1349
|
+
const row = d.query(`SELECT * FROM events WHERE id = ?`).get(id);
|
|
1350
|
+
return row ? rowToEvent(row) : null;
|
|
1351
|
+
}
|
|
1352
|
+
function listEvents(opts = {}, db) {
|
|
1353
|
+
const d = db || getDatabase();
|
|
1354
|
+
const conditions = [];
|
|
1355
|
+
const params = [];
|
|
1356
|
+
if (opts.contact_id) {
|
|
1357
|
+
conditions.push("contact_ids LIKE ?");
|
|
1358
|
+
params.push(`%${opts.contact_id}%`);
|
|
1359
|
+
}
|
|
1360
|
+
if (opts.company_id) {
|
|
1361
|
+
conditions.push("company_id = ?");
|
|
1362
|
+
params.push(opts.company_id);
|
|
1363
|
+
}
|
|
1364
|
+
if (opts.type) {
|
|
1365
|
+
conditions.push("type = ?");
|
|
1366
|
+
params.push(opts.type);
|
|
1367
|
+
}
|
|
1368
|
+
if (opts.date_from) {
|
|
1369
|
+
conditions.push("event_date >= ?");
|
|
1370
|
+
params.push(opts.date_from);
|
|
1371
|
+
}
|
|
1372
|
+
if (opts.date_to) {
|
|
1373
|
+
conditions.push("event_date <= ?");
|
|
1374
|
+
params.push(opts.date_to);
|
|
1375
|
+
}
|
|
1376
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1377
|
+
const rows = d.query(`SELECT * FROM events ${where} ORDER BY event_date DESC`).all(...params);
|
|
1378
|
+
return rows.map(rowToEvent);
|
|
1379
|
+
}
|
|
1380
|
+
function deleteEvent(id, db) {
|
|
1381
|
+
const d = db || getDatabase();
|
|
1382
|
+
d.run(`DELETE FROM events WHERE id = ?`, [id]);
|
|
1383
|
+
}
|
|
1384
|
+
var init_events = __esm(() => {
|
|
1385
|
+
init_database();
|
|
1386
|
+
});
|
|
1387
|
+
|
|
1079
1388
|
// src/index.ts
|
|
1080
1389
|
init_database();
|
|
1081
1390
|
init_contacts();
|
|
@@ -1330,6 +1639,7 @@ function listCompanyEmployees(companyId, db) {
|
|
|
1330
1639
|
follow_up_at: r.follow_up_at ?? null,
|
|
1331
1640
|
archived: !!r.archived,
|
|
1332
1641
|
project_id: r.project_id ?? null,
|
|
1642
|
+
sensitivity: r.sensitivity ?? "normal",
|
|
1333
1643
|
do_not_contact: !!r.do_not_contact,
|
|
1334
1644
|
priority: r.priority ?? 3,
|
|
1335
1645
|
timezone: r.timezone ?? null
|
|
@@ -1460,6 +1770,7 @@ function listContactsByTag(tagId, db) {
|
|
|
1460
1770
|
follow_up_at: r.follow_up_at ?? null,
|
|
1461
1771
|
archived: !!r.archived,
|
|
1462
1772
|
project_id: r.project_id ?? null,
|
|
1773
|
+
sensitivity: r.sensitivity ?? "normal",
|
|
1463
1774
|
do_not_contact: !!r.do_not_contact,
|
|
1464
1775
|
priority: r.priority ?? 3,
|
|
1465
1776
|
timezone: r.timezone ?? null
|
|
@@ -2253,86 +2564,10 @@ function getDealsByStage(db) {
|
|
|
2253
2564
|
}
|
|
2254
2565
|
return result;
|
|
2255
2566
|
}
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
try {
|
|
2261
|
-
contact_ids = JSON.parse(row.contact_ids);
|
|
2262
|
-
} catch {
|
|
2263
|
-
contact_ids = [];
|
|
2264
|
-
}
|
|
2265
|
-
return {
|
|
2266
|
-
id: row.id,
|
|
2267
|
-
title: row.title,
|
|
2268
|
-
type: row.type,
|
|
2269
|
-
event_date: row.event_date,
|
|
2270
|
-
duration_min: row.duration_min,
|
|
2271
|
-
contact_ids,
|
|
2272
|
-
company_id: row.company_id,
|
|
2273
|
-
notes: row.notes,
|
|
2274
|
-
outcome: row.outcome,
|
|
2275
|
-
deal_id: row.deal_id,
|
|
2276
|
-
created_at: row.created_at
|
|
2277
|
-
};
|
|
2278
|
-
}
|
|
2279
|
-
function logEvent(input, db) {
|
|
2280
|
-
const d = db || getDatabase();
|
|
2281
|
-
const id = uuid();
|
|
2282
|
-
const timestamp = now();
|
|
2283
|
-
d.run(`INSERT INTO events (id, title, type, event_date, duration_min, contact_ids, company_id, notes, outcome, deal_id, created_at)
|
|
2284
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
2285
|
-
id,
|
|
2286
|
-
input.title,
|
|
2287
|
-
input.type ?? "meeting",
|
|
2288
|
-
input.event_date,
|
|
2289
|
-
input.duration_min ?? null,
|
|
2290
|
-
JSON.stringify(input.contact_ids ?? []),
|
|
2291
|
-
input.company_id ?? null,
|
|
2292
|
-
input.notes ?? null,
|
|
2293
|
-
input.outcome ?? null,
|
|
2294
|
-
input.deal_id ?? null,
|
|
2295
|
-
timestamp
|
|
2296
|
-
]);
|
|
2297
|
-
return rowToEvent(d.query(`SELECT * FROM events WHERE id = ?`).get(id));
|
|
2298
|
-
}
|
|
2299
|
-
function getEvent(id, db) {
|
|
2300
|
-
const d = db || getDatabase();
|
|
2301
|
-
const row = d.query(`SELECT * FROM events WHERE id = ?`).get(id);
|
|
2302
|
-
return row ? rowToEvent(row) : null;
|
|
2303
|
-
}
|
|
2304
|
-
function listEvents(opts = {}, db) {
|
|
2305
|
-
const d = db || getDatabase();
|
|
2306
|
-
const conditions = [];
|
|
2307
|
-
const params = [];
|
|
2308
|
-
if (opts.contact_id) {
|
|
2309
|
-
conditions.push("contact_ids LIKE ?");
|
|
2310
|
-
params.push(`%${opts.contact_id}%`);
|
|
2311
|
-
}
|
|
2312
|
-
if (opts.company_id) {
|
|
2313
|
-
conditions.push("company_id = ?");
|
|
2314
|
-
params.push(opts.company_id);
|
|
2315
|
-
}
|
|
2316
|
-
if (opts.type) {
|
|
2317
|
-
conditions.push("type = ?");
|
|
2318
|
-
params.push(opts.type);
|
|
2319
|
-
}
|
|
2320
|
-
if (opts.date_from) {
|
|
2321
|
-
conditions.push("event_date >= ?");
|
|
2322
|
-
params.push(opts.date_from);
|
|
2323
|
-
}
|
|
2324
|
-
if (opts.date_to) {
|
|
2325
|
-
conditions.push("event_date <= ?");
|
|
2326
|
-
params.push(opts.date_to);
|
|
2327
|
-
}
|
|
2328
|
-
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
2329
|
-
const rows = d.query(`SELECT * FROM events ${where} ORDER BY event_date DESC`).all(...params);
|
|
2330
|
-
return rows.map(rowToEvent);
|
|
2331
|
-
}
|
|
2332
|
-
function deleteEvent(id, db) {
|
|
2333
|
-
const d = db || getDatabase();
|
|
2334
|
-
d.run(`DELETE FROM events WHERE id = ?`, [id]);
|
|
2335
|
-
}
|
|
2567
|
+
|
|
2568
|
+
// src/index.ts
|
|
2569
|
+
init_events();
|
|
2570
|
+
|
|
2336
2571
|
// src/db/notes.ts
|
|
2337
2572
|
init_types();
|
|
2338
2573
|
init_database();
|
|
@@ -3531,6 +3766,1182 @@ async function pullGoogleContactsAsInputs(opts = {}) {
|
|
|
3531
3766
|
|
|
3532
3767
|
// src/index.ts
|
|
3533
3768
|
init_types();
|
|
3769
|
+
|
|
3770
|
+
// src/db/field-history.ts
|
|
3771
|
+
init_database();
|
|
3772
|
+
function recordFieldChange(contactId, fieldName, oldValue, newValue, source, createdBy, db) {
|
|
3773
|
+
const _db2 = db || getDatabase();
|
|
3774
|
+
_db2.query(`INSERT INTO contact_field_history(id,contact_id,field_name,old_value,new_value,valid_from,source,created_by,created_at) VALUES(?,?,?,?,?,?,?,?,?)`).run(uuid(), contactId, fieldName, oldValue != null ? String(oldValue) : null, newValue != null ? String(newValue) : null, now(), source || null, createdBy || null, now());
|
|
3775
|
+
}
|
|
3776
|
+
function getFieldHistory(contactId, fieldName, db) {
|
|
3777
|
+
const _db2 = db || getDatabase();
|
|
3778
|
+
if (fieldName) {
|
|
3779
|
+
return _db2.query(`SELECT * FROM contact_field_history WHERE contact_id=? AND field_name=? ORDER BY valid_from DESC`).all(contactId, fieldName);
|
|
3780
|
+
}
|
|
3781
|
+
return _db2.query(`SELECT * FROM contact_field_history WHERE contact_id=? ORDER BY valid_from DESC`).all(contactId);
|
|
3782
|
+
}
|
|
3783
|
+
function getContactAt(contactId, timestamp, db) {
|
|
3784
|
+
const _db2 = db || getDatabase();
|
|
3785
|
+
const rows = _db2.query(`SELECT field_name, new_value FROM contact_field_history WHERE contact_id=? AND valid_from<=? ORDER BY valid_from ASC`).all(contactId, timestamp);
|
|
3786
|
+
const result = {};
|
|
3787
|
+
for (const r of rows) {
|
|
3788
|
+
if (r.new_value != null)
|
|
3789
|
+
result[r.field_name] = r.new_value;
|
|
3790
|
+
}
|
|
3791
|
+
return result;
|
|
3792
|
+
}
|
|
3793
|
+
// src/db/job-history.ts
|
|
3794
|
+
init_database();
|
|
3795
|
+
function rowToJob(r) {
|
|
3796
|
+
return { ...r, is_current: !!r["is_current"], inferred: !!r["inferred"] };
|
|
3797
|
+
}
|
|
3798
|
+
function addJobEntry(contactId, input, db) {
|
|
3799
|
+
const _db2 = db || getDatabase();
|
|
3800
|
+
if (input.is_current) {
|
|
3801
|
+
_db2.query(`UPDATE job_history SET is_current=0, end_date=COALESCE(end_date,?) WHERE contact_id=? AND is_current=1`).run(new Date().toISOString().slice(0, 10), contactId);
|
|
3802
|
+
}
|
|
3803
|
+
const id = uuid();
|
|
3804
|
+
_db2.query(`INSERT INTO job_history(id,contact_id,company_id,company_name,title,start_date,end_date,is_current,inferred,source,created_at) VALUES(?,?,?,?,?,?,?,?,?,?,?)`).run(id, contactId, input.company_id || null, input.company_name, input.title || null, input.start_date || null, input.end_date || null, input.is_current ? 1 : 0, input.inferred ? 1 : 0, input.source || null, now());
|
|
3805
|
+
return rowToJob(_db2.query(`SELECT * FROM job_history WHERE id=?`).get(id));
|
|
3806
|
+
}
|
|
3807
|
+
function getJobHistory(contactId, db) {
|
|
3808
|
+
const _db2 = db || getDatabase();
|
|
3809
|
+
return _db2.query(`SELECT * FROM job_history WHERE contact_id=? ORDER BY is_current DESC, start_date DESC`).all(contactId).map(rowToJob);
|
|
3810
|
+
}
|
|
3811
|
+
function getCurrentRole(contactId, db) {
|
|
3812
|
+
const _db2 = db || getDatabase();
|
|
3813
|
+
const r = _db2.query(`SELECT * FROM job_history WHERE contact_id=? AND is_current=1`).get(contactId);
|
|
3814
|
+
return r ? rowToJob(r) : null;
|
|
3815
|
+
}
|
|
3816
|
+
function getPreviousEmployers(contactId, db) {
|
|
3817
|
+
const _db2 = db || getDatabase();
|
|
3818
|
+
return _db2.query(`SELECT * FROM job_history WHERE contact_id=? AND is_current=0 ORDER BY start_date DESC`).all(contactId).map(rowToJob);
|
|
3819
|
+
}
|
|
3820
|
+
// src/db/learnings.ts
|
|
3821
|
+
init_database();
|
|
3822
|
+
function rowToLearning(r) {
|
|
3823
|
+
return { ...r, tags: JSON.parse(r["tags"] || "[]") };
|
|
3824
|
+
}
|
|
3825
|
+
function saveLearning(contactId, input, db) {
|
|
3826
|
+
const _db2 = db || getDatabase();
|
|
3827
|
+
const id = uuid();
|
|
3828
|
+
_db2.query(`INSERT INTO contact_learnings(id,contact_id,content,type,confidence,importance,learned_by,session_id,visibility,tags,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)`).run(id, contactId, input.content, input.type || "fact", input.confidence ?? 70, input.importance ?? 5, input.learned_by || null, input.session_id || null, input.visibility || "shared", JSON.stringify(input.tags || []), now(), now());
|
|
3829
|
+
return rowToLearning(_db2.query(`SELECT * FROM contact_learnings WHERE id=?`).get(id));
|
|
3830
|
+
}
|
|
3831
|
+
function getLearnings(contactId, opts = {}, db) {
|
|
3832
|
+
const _db2 = db || getDatabase();
|
|
3833
|
+
let sql = `SELECT * FROM contact_learnings WHERE contact_id=?`;
|
|
3834
|
+
const params = [contactId];
|
|
3835
|
+
if (opts.type) {
|
|
3836
|
+
sql += ` AND type=?`;
|
|
3837
|
+
params.push(opts.type);
|
|
3838
|
+
}
|
|
3839
|
+
if (opts.min_importance) {
|
|
3840
|
+
sql += ` AND importance>=?`;
|
|
3841
|
+
params.push(opts.min_importance);
|
|
3842
|
+
}
|
|
3843
|
+
if (opts.visibility) {
|
|
3844
|
+
sql += ` AND visibility=?`;
|
|
3845
|
+
params.push(opts.visibility);
|
|
3846
|
+
}
|
|
3847
|
+
sql += ` ORDER BY importance DESC, confidence DESC`;
|
|
3848
|
+
return _db2.query(sql).all(...params).map(rowToLearning);
|
|
3849
|
+
}
|
|
3850
|
+
function searchLearnings(query, opts = {}, db) {
|
|
3851
|
+
const _db2 = db || getDatabase();
|
|
3852
|
+
let sql = `SELECT * FROM contact_learnings WHERE content LIKE ?`;
|
|
3853
|
+
const params = [`%${query}%`];
|
|
3854
|
+
if (opts.type) {
|
|
3855
|
+
sql += ` AND type=?`;
|
|
3856
|
+
params.push(opts.type);
|
|
3857
|
+
}
|
|
3858
|
+
if (opts.contact_id) {
|
|
3859
|
+
sql += ` AND contact_id=?`;
|
|
3860
|
+
params.push(opts.contact_id);
|
|
3861
|
+
}
|
|
3862
|
+
sql += ` ORDER BY importance DESC, confidence DESC LIMIT 50`;
|
|
3863
|
+
return _db2.query(sql).all(...params).map(rowToLearning);
|
|
3864
|
+
}
|
|
3865
|
+
function confirmLearning(learningId, _agentName, db) {
|
|
3866
|
+
const _db2 = db || getDatabase();
|
|
3867
|
+
_db2.query(`UPDATE contact_learnings SET confirmed_count=confirmed_count+1, confidence=MIN(100,confidence+10), updated_at=? WHERE id=?`).run(now(), learningId);
|
|
3868
|
+
}
|
|
3869
|
+
function decayLearnings(db) {
|
|
3870
|
+
const _db2 = db || getDatabase();
|
|
3871
|
+
const cutoff = new Date(Date.now() - 30 * 86400000).toISOString();
|
|
3872
|
+
const result = _db2.query(`UPDATE contact_learnings SET confidence=MAX(10,confidence-5), updated_at=? WHERE confirmed_count=0 AND created_at<? AND confidence>10`).run(now(), cutoff);
|
|
3873
|
+
return result.changes || 0;
|
|
3874
|
+
}
|
|
3875
|
+
function deleteLearning(learningId, db) {
|
|
3876
|
+
const _db2 = db || getDatabase();
|
|
3877
|
+
_db2.query(`DELETE FROM contact_learnings WHERE id=?`).run(learningId);
|
|
3878
|
+
}
|
|
3879
|
+
// src/db/coordination.ts
|
|
3880
|
+
init_database();
|
|
3881
|
+
function acquireLock(contactId, agentName, ttlSeconds = 300, reason, sessionId, db) {
|
|
3882
|
+
const _db2 = db || getDatabase();
|
|
3883
|
+
cleanExpiredLocks(_db2);
|
|
3884
|
+
const existing = _db2.query(`SELECT * FROM contact_locks WHERE contact_id=?`).get(contactId);
|
|
3885
|
+
if (existing)
|
|
3886
|
+
return { acquired: false, held_by: existing.agent_name, lock: existing };
|
|
3887
|
+
const id = uuid();
|
|
3888
|
+
const expiresAt = new Date(Date.now() + ttlSeconds * 1000).toISOString();
|
|
3889
|
+
_db2.query(`INSERT INTO contact_locks(id,contact_id,agent_name,reason,acquired_at,expires_at,session_id) VALUES(?,?,?,?,?,?,?)`).run(id, contactId, agentName, reason || null, now(), expiresAt, sessionId || null);
|
|
3890
|
+
return {
|
|
3891
|
+
acquired: true,
|
|
3892
|
+
lock: _db2.query(`SELECT * FROM contact_locks WHERE id=?`).get(id)
|
|
3893
|
+
};
|
|
3894
|
+
}
|
|
3895
|
+
function releaseLock(contactId, agentName, db) {
|
|
3896
|
+
const _db2 = db || getDatabase();
|
|
3897
|
+
const result = _db2.query(`DELETE FROM contact_locks WHERE contact_id=? AND agent_name=?`).run(contactId, agentName);
|
|
3898
|
+
return (result.changes || 0) > 0;
|
|
3899
|
+
}
|
|
3900
|
+
function checkLock(contactId, db) {
|
|
3901
|
+
const _db2 = db || getDatabase();
|
|
3902
|
+
cleanExpiredLocks(_db2);
|
|
3903
|
+
return _db2.query(`SELECT * FROM contact_locks WHERE contact_id=?`).get(contactId);
|
|
3904
|
+
}
|
|
3905
|
+
function cleanExpiredLocks(db) {
|
|
3906
|
+
const _db2 = db || getDatabase();
|
|
3907
|
+
_db2.query(`DELETE FROM contact_locks WHERE expires_at<?`).run(now());
|
|
3908
|
+
}
|
|
3909
|
+
function logAgentActivity(contactId, agentName, action, details, sessionId, db) {
|
|
3910
|
+
const _db2 = db || getDatabase();
|
|
3911
|
+
_db2.query(`INSERT INTO contact_agent_activity(id,contact_id,agent_name,action,details,session_id,created_at) VALUES(?,?,?,?,?,?,?)`).run(uuid(), contactId, agentName, action, details || null, sessionId || null, now());
|
|
3912
|
+
}
|
|
3913
|
+
function getAgentActivity(contactId, limit = 20, db) {
|
|
3914
|
+
const _db2 = db || getDatabase();
|
|
3915
|
+
return _db2.query(`SELECT * FROM contact_agent_activity WHERE contact_id=? ORDER BY created_at DESC LIMIT ?`).all(contactId, limit);
|
|
3916
|
+
}
|
|
3917
|
+
// src/db/graph.ts
|
|
3918
|
+
init_database();
|
|
3919
|
+
function computeRelationshipStrength(contactId, db) {
|
|
3920
|
+
const _db2 = db || getDatabase();
|
|
3921
|
+
const contact = _db2.query(`SELECT last_contacted_at, interaction_count_30d, interaction_count_90d FROM contacts WHERE id=?`).get(contactId);
|
|
3922
|
+
if (!contact)
|
|
3923
|
+
return 0;
|
|
3924
|
+
let score = 50;
|
|
3925
|
+
if (contact.last_contacted_at) {
|
|
3926
|
+
const days = Math.floor((Date.now() - new Date(contact.last_contacted_at).getTime()) / 86400000);
|
|
3927
|
+
score += days < 7 ? 30 : days < 30 ? 20 : days < 90 ? 5 : -20;
|
|
3928
|
+
} else {
|
|
3929
|
+
score -= 20;
|
|
3930
|
+
}
|
|
3931
|
+
score += Math.min(20, (contact.interaction_count_30d || 0) * 4);
|
|
3932
|
+
return Math.max(0, Math.min(100, score));
|
|
3933
|
+
}
|
|
3934
|
+
function findWarmPath(fromContactId, toContactId, db) {
|
|
3935
|
+
const _db2 = db || getDatabase();
|
|
3936
|
+
const visited = new Set([fromContactId]);
|
|
3937
|
+
const queue = [{ id: fromContactId, path: [] }];
|
|
3938
|
+
while (queue.length) {
|
|
3939
|
+
const item = queue.shift();
|
|
3940
|
+
const { id, path } = item;
|
|
3941
|
+
if (id === toContactId)
|
|
3942
|
+
return path;
|
|
3943
|
+
if (path.length >= 4)
|
|
3944
|
+
continue;
|
|
3945
|
+
const neighbors = _db2.query(`SELECT cr.*, c.display_name FROM contact_relationships cr JOIN contacts c ON (CASE WHEN cr.contact_a_id=? THEN cr.contact_b_id ELSE cr.contact_a_id END)=c.id WHERE (cr.contact_a_id=? OR cr.contact_b_id=?) LIMIT 20`).all(id, id, id);
|
|
3946
|
+
for (const n of neighbors) {
|
|
3947
|
+
const nextId = n.contact_a_id === id ? n.contact_b_id : n.contact_a_id;
|
|
3948
|
+
if (visited.has(nextId))
|
|
3949
|
+
continue;
|
|
3950
|
+
visited.add(nextId);
|
|
3951
|
+
queue.push({
|
|
3952
|
+
id: nextId,
|
|
3953
|
+
path: [
|
|
3954
|
+
...path,
|
|
3955
|
+
{ contact_id: nextId, display_name: n.display_name, strength: n.strength_score || 50 }
|
|
3956
|
+
]
|
|
3957
|
+
});
|
|
3958
|
+
}
|
|
3959
|
+
}
|
|
3960
|
+
return [];
|
|
3961
|
+
}
|
|
3962
|
+
function findConnectionsAtCompany(companyId, db) {
|
|
3963
|
+
const _db2 = db || getDatabase();
|
|
3964
|
+
return _db2.query(`SELECT c.id as contact_id, c.display_name, c.job_title, c.relationship_health as strength FROM contacts c WHERE c.company_id=? AND c.archived=0 ORDER BY c.relationship_health DESC`).all(companyId);
|
|
3965
|
+
}
|
|
3966
|
+
function detectCoolingRelationships(db) {
|
|
3967
|
+
const _db2 = db || getDatabase();
|
|
3968
|
+
const cutoff = new Date(Date.now() - 45 * 86400000).toISOString();
|
|
3969
|
+
return _db2.query(`SELECT id as contact_id, display_name, CAST((julianday('now') - julianday(last_contacted_at)) AS INTEGER) as days_since FROM contacts WHERE last_contacted_at IS NOT NULL AND last_contacted_at < ? AND engagement_status != 'ghost' AND archived=0 ORDER BY last_contacted_at ASC LIMIT 50`).all(cutoff);
|
|
3970
|
+
}
|
|
3971
|
+
// src/db/identity.ts
|
|
3972
|
+
init_database();
|
|
3973
|
+
function addIdentity(contactId, system, externalId, externalUrl, confidence = "inferred", db) {
|
|
3974
|
+
const _db2 = db || getDatabase();
|
|
3975
|
+
const id = uuid();
|
|
3976
|
+
_db2.query(`INSERT OR REPLACE INTO contact_identities(id,contact_id,system,external_id,external_url,confidence,created_at) VALUES(?,?,?,?,?,?,?)`).run(id, contactId, system, externalId, externalUrl || null, confidence, now());
|
|
3977
|
+
return _db2.query(`SELECT * FROM contact_identities WHERE id=?`).get(id);
|
|
3978
|
+
}
|
|
3979
|
+
function resolveIdentity(system, externalId, db) {
|
|
3980
|
+
const _db2 = db || getDatabase();
|
|
3981
|
+
const row = _db2.query(`SELECT c.id, c.display_name FROM contacts c JOIN contact_identities ci ON c.id=ci.contact_id WHERE ci.system=? AND ci.external_id=?`).get(system, externalId);
|
|
3982
|
+
return row || null;
|
|
3983
|
+
}
|
|
3984
|
+
function resolveByPartial(partial, db) {
|
|
3985
|
+
const _db2 = db || getDatabase();
|
|
3986
|
+
const matches = new Map;
|
|
3987
|
+
const addMatch = (id, name, title, score, reason) => {
|
|
3988
|
+
const existing = matches.get(id);
|
|
3989
|
+
if (existing) {
|
|
3990
|
+
existing.confidence_score = Math.min(100, existing.confidence_score + score);
|
|
3991
|
+
existing.match_reasons.push(reason);
|
|
3992
|
+
} else {
|
|
3993
|
+
matches.set(id, {
|
|
3994
|
+
contact: { id, display_name: name, job_title: title },
|
|
3995
|
+
confidence_score: score,
|
|
3996
|
+
match_reasons: [reason]
|
|
3997
|
+
});
|
|
3998
|
+
}
|
|
3999
|
+
};
|
|
4000
|
+
if (partial.email) {
|
|
4001
|
+
const rows = _db2.query(`SELECT c.id, c.display_name, c.job_title FROM contacts c JOIN emails e ON c.id=e.contact_id WHERE LOWER(e.address)=LOWER(?)`).all(partial.email);
|
|
4002
|
+
rows.forEach((r) => addMatch(r.id, r.display_name, r.job_title, 90, `email match: ${partial.email}`));
|
|
4003
|
+
}
|
|
4004
|
+
if (partial.linkedin_url) {
|
|
4005
|
+
const rows = _db2.query(`SELECT c.id, c.display_name, c.job_title FROM contacts c JOIN social_profiles sp ON c.id=sp.contact_id WHERE sp.platform='linkedin' AND sp.url LIKE ?`).all(`%${partial.linkedin_url.split("/").pop()}%`);
|
|
4006
|
+
rows.forEach((r) => addMatch(r.id, r.display_name, r.job_title, 85, `linkedin match`));
|
|
4007
|
+
}
|
|
4008
|
+
if (partial.name) {
|
|
4009
|
+
const rows = _db2.query(`SELECT id, display_name, job_title FROM contacts WHERE display_name LIKE ? AND archived=0 LIMIT 10`).all(`%${partial.name}%`);
|
|
4010
|
+
rows.forEach((r) => addMatch(r.id, r.display_name, r.job_title, 40, `name match: ${partial.name}`));
|
|
4011
|
+
}
|
|
4012
|
+
return Array.from(matches.values()).sort((a, b) => b.confidence_score - a.confidence_score);
|
|
4013
|
+
}
|
|
4014
|
+
function getIdentities(contactId, db) {
|
|
4015
|
+
const _db2 = db || getDatabase();
|
|
4016
|
+
return _db2.query(`SELECT * FROM contact_identities WHERE contact_id=? ORDER BY created_at DESC`).all(contactId);
|
|
4017
|
+
}
|
|
4018
|
+
// src/db/org-chart.ts
|
|
4019
|
+
init_database();
|
|
4020
|
+
function addOrgChartEdge(companyId, contactAId, contactBId, edgeType, inferred = false, db) {
|
|
4021
|
+
const _db2 = db || getDatabase();
|
|
4022
|
+
const id = uuid();
|
|
4023
|
+
_db2.query(`INSERT OR IGNORE INTO org_chart_edges(id,company_id,contact_a_id,contact_b_id,edge_type,inferred,created_at) VALUES(?,?,?,?,?,?,?)`).run(id, companyId, contactAId, contactBId, edgeType, inferred ? 1 : 0, now());
|
|
4024
|
+
return _db2.query(`SELECT * FROM org_chart_edges WHERE company_id=? AND contact_a_id=? AND contact_b_id=? AND edge_type=?`).get(companyId, contactAId, contactBId, edgeType);
|
|
4025
|
+
}
|
|
4026
|
+
function listOrgChart(companyId, db) {
|
|
4027
|
+
const _db2 = db || getDatabase();
|
|
4028
|
+
return _db2.query(`SELECT oe.*, ca.display_name as contact_a_name, cb.display_name as contact_b_name FROM org_chart_edges oe JOIN contacts ca ON oe.contact_a_id=ca.id JOIN contacts cb ON oe.contact_b_id=cb.id WHERE oe.company_id=?`).all(companyId);
|
|
4029
|
+
}
|
|
4030
|
+
function setDealContactRole(dealId, contactId, accountRole, db) {
|
|
4031
|
+
const _db2 = db || getDatabase();
|
|
4032
|
+
const id = uuid();
|
|
4033
|
+
_db2.query(`INSERT OR REPLACE INTO deal_contact_roles(id,deal_id,contact_id,account_role,created_at) VALUES(?,?,?,?,?)`).run(id, dealId, contactId, accountRole, now());
|
|
4034
|
+
return _db2.query(`SELECT * FROM deal_contact_roles WHERE deal_id=? AND contact_id=?`).get(dealId, contactId);
|
|
4035
|
+
}
|
|
4036
|
+
function getDealTeam(dealId, db) {
|
|
4037
|
+
const _db2 = db || getDatabase();
|
|
4038
|
+
return _db2.query(`SELECT dr.*, c.display_name, c.job_title FROM deal_contact_roles dr JOIN contacts c ON dr.contact_id=c.id WHERE dr.deal_id=?`).all(dealId);
|
|
4039
|
+
}
|
|
4040
|
+
function getCoverageGaps(companyId, db) {
|
|
4041
|
+
const _db2 = db || getDatabase();
|
|
4042
|
+
const total = _db2.query(`SELECT COUNT(*) c FROM contacts WHERE company_id=? AND archived=0`).get(companyId).c;
|
|
4043
|
+
const hasManager = _db2.query(`SELECT COUNT(*) c FROM org_chart_edges WHERE company_id=? AND edge_type='manages'`).get(companyId).c > 0;
|
|
4044
|
+
const hasEco = _db2.query(`SELECT COUNT(*) c FROM deal_contact_roles dr JOIN deals d ON dr.deal_id=d.id WHERE d.company_id=? AND dr.account_role='economic_buyer'`).get(companyId).c > 0;
|
|
4045
|
+
const hasTech = _db2.query(`SELECT COUNT(*) c FROM deal_contact_roles dr JOIN deals d ON dr.deal_id=d.id WHERE d.company_id=? AND dr.account_role='technical_evaluator'`).get(companyId).c > 0;
|
|
4046
|
+
const missing = [
|
|
4047
|
+
!hasManager && "org chart relationships",
|
|
4048
|
+
!hasEco && "economic buyer",
|
|
4049
|
+
!hasTech && "technical evaluator"
|
|
4050
|
+
].filter(Boolean);
|
|
4051
|
+
return {
|
|
4052
|
+
total_contacts: total,
|
|
4053
|
+
has_manager: hasManager,
|
|
4054
|
+
has_technical: hasTech,
|
|
4055
|
+
has_economic_buyer: hasEco,
|
|
4056
|
+
suggestion: missing.length ? `Missing: ${missing.join(", ")}` : "Good coverage"
|
|
4057
|
+
};
|
|
4058
|
+
}
|
|
4059
|
+
// src/db/signals.ts
|
|
4060
|
+
init_database();
|
|
4061
|
+
function getRelationshipSignals(contactId, db) {
|
|
4062
|
+
const _db2 = db || getDatabase();
|
|
4063
|
+
const row = _db2.query(`SELECT id as contact_id, display_name, last_contacted_at, interaction_count_30d, engagement_status, relationship_health FROM contacts WHERE id=?`).get(contactId);
|
|
4064
|
+
if (!row)
|
|
4065
|
+
return [];
|
|
4066
|
+
const daysSince = row.last_contacted_at ? Math.floor((Date.now() - new Date(row.last_contacted_at).getTime()) / 86400000) : null;
|
|
4067
|
+
const signals = [];
|
|
4068
|
+
const cnt = row.interaction_count_30d || 0;
|
|
4069
|
+
const health = row.relationship_health ?? 50;
|
|
4070
|
+
if (daysSince === null || daysSince > 180) {
|
|
4071
|
+
signals.push({ ...row, signal_type: "ghost", days_since_contact: daysSince, reason: "No contact in 180+ days or never contacted" });
|
|
4072
|
+
} else if (daysSince > 60 && cnt === 0) {
|
|
4073
|
+
signals.push({ ...row, signal_type: "cooling", days_since_contact: daysSince, reason: `No contact in ${daysSince} days, no recent interactions` });
|
|
4074
|
+
} else if (cnt > 3 && health > 70) {
|
|
4075
|
+
signals.push({ ...row, signal_type: "warming", days_since_contact: daysSince, reason: `${cnt} interactions in last 30 days, health score ${health}` });
|
|
4076
|
+
} else {
|
|
4077
|
+
signals.push({ ...row, signal_type: "healthy", days_since_contact: daysSince, reason: `Last contact ${daysSince}d ago, ${cnt} interactions in 30d` });
|
|
4078
|
+
}
|
|
4079
|
+
return signals;
|
|
4080
|
+
}
|
|
4081
|
+
function getGhostContacts(db) {
|
|
4082
|
+
const _db2 = db || getDatabase();
|
|
4083
|
+
const rows = _db2.query(`SELECT id as contact_id, display_name, last_contacted_at, interaction_count_30d, engagement_status, relationship_health FROM contacts WHERE (last_contacted_at IS NULL OR julianday('now') - julianday(last_contacted_at) > 180) AND archived=0 ORDER BY last_contacted_at ASC LIMIT 50`).all();
|
|
4084
|
+
return rows.map((r) => ({
|
|
4085
|
+
...r,
|
|
4086
|
+
signal_type: "ghost",
|
|
4087
|
+
days_since_contact: r.last_contacted_at ? Math.floor((Date.now() - new Date(r.last_contacted_at).getTime()) / 86400000) : null,
|
|
4088
|
+
reason: "No contact in 180+ days or never contacted"
|
|
4089
|
+
}));
|
|
4090
|
+
}
|
|
4091
|
+
function getWarmingContacts(db) {
|
|
4092
|
+
const _db2 = db || getDatabase();
|
|
4093
|
+
const rows = _db2.query(`SELECT id as contact_id, display_name, last_contacted_at, interaction_count_30d, engagement_status, relationship_health FROM contacts WHERE interaction_count_30d > 2 AND relationship_health > 60 AND archived=0 ORDER BY relationship_health DESC LIMIT 50`).all();
|
|
4094
|
+
return rows.map((r) => ({
|
|
4095
|
+
...r,
|
|
4096
|
+
signal_type: "warming",
|
|
4097
|
+
days_since_contact: r.last_contacted_at ? Math.floor((Date.now() - new Date(r.last_contacted_at).getTime()) / 86400000) : null,
|
|
4098
|
+
reason: `${r.interaction_count_30d} interactions in last 30 days`
|
|
4099
|
+
}));
|
|
4100
|
+
}
|
|
4101
|
+
function recomputeAllSignals(db) {
|
|
4102
|
+
const _db2 = db || getDatabase();
|
|
4103
|
+
_db2.query(`
|
|
4104
|
+
UPDATE contacts SET
|
|
4105
|
+
engagement_status = CASE
|
|
4106
|
+
WHEN interaction_count_30d > 3 THEN 'warm'
|
|
4107
|
+
WHEN last_contacted_at IS NULL OR julianday('now') - julianday(last_contacted_at) > 180 THEN 'ghost'
|
|
4108
|
+
WHEN julianday('now') - julianday(last_contacted_at) > 60 THEN 'cooling'
|
|
4109
|
+
ELSE 'active'
|
|
4110
|
+
END,
|
|
4111
|
+
updated_at = datetime('now')
|
|
4112
|
+
WHERE archived = 0
|
|
4113
|
+
`).run();
|
|
4114
|
+
const result = _db2.query(`SELECT changes() as n`).get();
|
|
4115
|
+
return { updated: result?.n ?? 0 };
|
|
4116
|
+
}
|
|
4117
|
+
// src/db/freshness.ts
|
|
4118
|
+
init_database();
|
|
4119
|
+
var SCORED_FIELDS = ["display_name", "job_title", "company_id", "emails", "phones", "last_contacted_at"];
|
|
4120
|
+
function getFreshnessScore(contactId, db) {
|
|
4121
|
+
const _db2 = db || getDatabase();
|
|
4122
|
+
const contact = _db2.query(`SELECT * FROM contacts WHERE id=?`).get(contactId);
|
|
4123
|
+
if (!contact)
|
|
4124
|
+
throw new Error(`Contact not found: ${contactId}`);
|
|
4125
|
+
let historyRows = [];
|
|
4126
|
+
try {
|
|
4127
|
+
historyRows = _db2.query(`SELECT field_name, new_value, source, created_at FROM contact_field_history WHERE contact_id=? ORDER BY created_at DESC`).all(contactId);
|
|
4128
|
+
} catch {}
|
|
4129
|
+
let verifiedRows = [];
|
|
4130
|
+
try {
|
|
4131
|
+
verifiedRows = _db2.query(`SELECT field_name, verified_at, source FROM field_verifications WHERE contact_id=?`).all(contactId);
|
|
4132
|
+
} catch {}
|
|
4133
|
+
const verifiedMap = new Map(verifiedRows.map((r) => [r.field_name, r]));
|
|
4134
|
+
const historyMap = new Map;
|
|
4135
|
+
for (const r of historyRows) {
|
|
4136
|
+
if (!historyMap.has(r.field_name))
|
|
4137
|
+
historyMap.set(r.field_name, r);
|
|
4138
|
+
}
|
|
4139
|
+
const fields = SCORED_FIELDS.map((field) => {
|
|
4140
|
+
let value = null;
|
|
4141
|
+
if (field === "emails") {
|
|
4142
|
+
const emailRow = _db2.query(`SELECT address FROM emails WHERE contact_id=? LIMIT 1`).get(contactId);
|
|
4143
|
+
value = emailRow?.address ?? null;
|
|
4144
|
+
} else if (field === "phones") {
|
|
4145
|
+
const phoneRow = _db2.query(`SELECT number FROM phones WHERE contact_id=? LIMIT 1`).get(contactId);
|
|
4146
|
+
value = phoneRow?.number ?? null;
|
|
4147
|
+
} else {
|
|
4148
|
+
value = contact[field] != null ? String(contact[field]) : null;
|
|
4149
|
+
}
|
|
4150
|
+
const verified = verifiedMap.get(field);
|
|
4151
|
+
const history = historyMap.get(field);
|
|
4152
|
+
let confidence = "unknown";
|
|
4153
|
+
let days_old = null;
|
|
4154
|
+
let last_verified_at = null;
|
|
4155
|
+
let source = null;
|
|
4156
|
+
if (verified) {
|
|
4157
|
+
confidence = "verified";
|
|
4158
|
+
last_verified_at = verified.verified_at;
|
|
4159
|
+
source = verified.source;
|
|
4160
|
+
days_old = Math.floor((Date.now() - new Date(verified.verified_at).getTime()) / 86400000);
|
|
4161
|
+
} else if (history) {
|
|
4162
|
+
confidence = history.source === "import" ? "imported" : "inferred";
|
|
4163
|
+
last_verified_at = history.created_at;
|
|
4164
|
+
source = history.source;
|
|
4165
|
+
days_old = Math.floor((Date.now() - new Date(history.created_at).getTime()) / 86400000);
|
|
4166
|
+
if (days_old > 365)
|
|
4167
|
+
confidence = "stale";
|
|
4168
|
+
} else if (value) {
|
|
4169
|
+
confidence = "inferred";
|
|
4170
|
+
}
|
|
4171
|
+
return { field_name: field, value, last_verified_at, source, confidence, days_old };
|
|
4172
|
+
});
|
|
4173
|
+
const fieldScore = fields.reduce((acc, f) => {
|
|
4174
|
+
if (!f.value)
|
|
4175
|
+
return acc;
|
|
4176
|
+
if (f.confidence === "verified")
|
|
4177
|
+
return acc + 20;
|
|
4178
|
+
if (f.confidence === "imported" || f.confidence === "inferred")
|
|
4179
|
+
return acc + 10;
|
|
4180
|
+
return acc + 5;
|
|
4181
|
+
}, 0);
|
|
4182
|
+
const overall_score = Math.min(100, fieldScore);
|
|
4183
|
+
return {
|
|
4184
|
+
contact_id: contactId,
|
|
4185
|
+
overall_score,
|
|
4186
|
+
fields,
|
|
4187
|
+
stale_fields: fields.filter((f) => f.confidence === "stale" || !f.value && f.field_name !== "phones").map((f) => f.field_name),
|
|
4188
|
+
verified_fields: fields.filter((f) => f.confidence === "verified").map((f) => f.field_name)
|
|
4189
|
+
};
|
|
4190
|
+
}
|
|
4191
|
+
function getStaleContacts(threshold = 40, db) {
|
|
4192
|
+
const _db2 = db || getDatabase();
|
|
4193
|
+
const rows = _db2.query(`SELECT * FROM (
|
|
4194
|
+
SELECT c.id as contact_id, c.display_name,
|
|
4195
|
+
(CASE WHEN c.job_title IS NOT NULL THEN 15 ELSE 0 END +
|
|
4196
|
+
CASE WHEN c.company_id IS NOT NULL THEN 15 ELSE 0 END +
|
|
4197
|
+
CASE WHEN c.last_contacted_at IS NOT NULL THEN 20 ELSE 0 END +
|
|
4198
|
+
CASE WHEN EXISTS(SELECT 1 FROM emails WHERE contact_id=c.id) THEN 20 ELSE 0 END +
|
|
4199
|
+
CASE WHEN EXISTS(SELECT 1 FROM phones WHERE contact_id=c.id) THEN 15 ELSE 0 END +
|
|
4200
|
+
CASE WHEN c.notes IS NOT NULL THEN 10 ELSE 0 END +
|
|
4201
|
+
CASE WHEN EXISTS(SELECT 1 FROM contact_tags WHERE contact_id=c.id) THEN 5 ELSE 0 END
|
|
4202
|
+
) as score
|
|
4203
|
+
FROM contacts c WHERE c.archived=0
|
|
4204
|
+
) WHERE score < ? ORDER BY score ASC LIMIT 100`).all(threshold);
|
|
4205
|
+
return rows;
|
|
4206
|
+
}
|
|
4207
|
+
function markFieldVerified(contactId, fieldName, source, db) {
|
|
4208
|
+
const _db2 = db || getDatabase();
|
|
4209
|
+
try {
|
|
4210
|
+
_db2.query(`INSERT OR REPLACE INTO field_verifications(contact_id,field_name,verified_at,source) VALUES(?,?,?,?)`).run(contactId, fieldName, now(), source || null);
|
|
4211
|
+
} catch {
|
|
4212
|
+
_db2.query(`INSERT INTO activity_log(id,contact_id,action,details,created_at) VALUES(?,?,?,?,?)`).run(crypto.randomUUID(), contactId, "field.verified", JSON.stringify({ field_name: fieldName, source }), now());
|
|
4213
|
+
}
|
|
4214
|
+
}
|
|
4215
|
+
// src/lib/context.ts
|
|
4216
|
+
init_database();
|
|
4217
|
+
init_contacts();
|
|
4218
|
+
function getContactCard(contactId, db) {
|
|
4219
|
+
const _db2 = db || getDatabase();
|
|
4220
|
+
const c = getContact(contactId, _db2);
|
|
4221
|
+
const emails = c.emails;
|
|
4222
|
+
const phones = c.phones;
|
|
4223
|
+
const company = c.company;
|
|
4224
|
+
return {
|
|
4225
|
+
id: c.id,
|
|
4226
|
+
display_name: c.display_name,
|
|
4227
|
+
job_title: c.job_title,
|
|
4228
|
+
company: company?.name,
|
|
4229
|
+
primary_email: emails?.find((e) => e.is_primary)?.address || emails?.[0]?.address,
|
|
4230
|
+
primary_phone: phones?.find((p) => p.is_primary)?.number || phones?.[0]?.number
|
|
4231
|
+
};
|
|
4232
|
+
}
|
|
4233
|
+
function getContactBrief(contactId, taskContext, db) {
|
|
4234
|
+
const _db2 = db || getDatabase();
|
|
4235
|
+
const c = getContact(contactId, _db2);
|
|
4236
|
+
const notes = listNotes(contactId, _db2).slice(0, 3);
|
|
4237
|
+
const learnings = getLearnings(contactId, { min_importance: 7 }, _db2).slice(0, 5);
|
|
4238
|
+
const ctx = (taskContext ?? "").toLowerCase();
|
|
4239
|
+
const lastContactedAt = c.last_contacted_at;
|
|
4240
|
+
const daysSince = lastContactedAt ? Math.floor((Date.now() - new Date(lastContactedAt).getTime()) / 86400000) : null;
|
|
4241
|
+
const company = c.company;
|
|
4242
|
+
const brief = {
|
|
4243
|
+
id: c.id,
|
|
4244
|
+
display_name: c.display_name,
|
|
4245
|
+
job_title: c.job_title,
|
|
4246
|
+
company: company?.name,
|
|
4247
|
+
status: c.status,
|
|
4248
|
+
last_contacted: daysSince !== null ? `${daysSince}d ago` : "never",
|
|
4249
|
+
relationship_health: c.relationship_health,
|
|
4250
|
+
engagement_status: c.engagement_status,
|
|
4251
|
+
preferred_contact: c.preferred_contact_method || c.preferred_channel
|
|
4252
|
+
};
|
|
4253
|
+
if (ctx.includes("meeting") || ctx.includes("call") || ctx.includes("prep")) {
|
|
4254
|
+
brief.recent_notes = notes.map((n) => ({ date: n.created_at?.slice(0, 10), content: n.body }));
|
|
4255
|
+
brief.key_learnings = learnings.map((l) => l.content);
|
|
4256
|
+
}
|
|
4257
|
+
if (ctx.includes("outreach") || ctx.includes("email")) {
|
|
4258
|
+
brief.preferred_channel = c.preferred_channel;
|
|
4259
|
+
brief.follow_up_at = c.follow_up_at;
|
|
4260
|
+
}
|
|
4261
|
+
if (ctx.includes("deal")) {
|
|
4262
|
+
const dealCompany = c.company;
|
|
4263
|
+
brief.company_details = dealCompany ? { name: dealCompany.name, domain: dealCompany.domain } : null;
|
|
4264
|
+
}
|
|
4265
|
+
if (learnings.length)
|
|
4266
|
+
brief.top_learnings = learnings.map((l) => l.content);
|
|
4267
|
+
return brief;
|
|
4268
|
+
}
|
|
4269
|
+
async function assembleContext(contactIds, format = "meeting_prep", db) {
|
|
4270
|
+
const _db2 = db || getDatabase();
|
|
4271
|
+
const briefs = contactIds.map((id) => {
|
|
4272
|
+
try {
|
|
4273
|
+
return getContactBrief(id, format, _db2);
|
|
4274
|
+
} catch {
|
|
4275
|
+
return { id, error: "not found" };
|
|
4276
|
+
}
|
|
4277
|
+
});
|
|
4278
|
+
return { format, contact_count: contactIds.length, assembled_at: new Date().toISOString(), contacts: briefs };
|
|
4279
|
+
}
|
|
4280
|
+
// src/lib/embeddings.ts
|
|
4281
|
+
init_database();
|
|
4282
|
+
function tokenize(text) {
|
|
4283
|
+
return text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t) => t.length > 2);
|
|
4284
|
+
}
|
|
4285
|
+
function buildTfIdf(tokens) {
|
|
4286
|
+
const freq = new Map;
|
|
4287
|
+
for (const t of tokens)
|
|
4288
|
+
freq.set(t, (freq.get(t) || 0) + 1);
|
|
4289
|
+
const max = Math.max(...freq.values(), 1);
|
|
4290
|
+
const result = new Map;
|
|
4291
|
+
freq.forEach((v, k) => result.set(k, v / max));
|
|
4292
|
+
return result;
|
|
4293
|
+
}
|
|
4294
|
+
function cosineSimilarity(a, b) {
|
|
4295
|
+
let dot = 0, normA = 0, normB = 0;
|
|
4296
|
+
a.forEach((v, k) => {
|
|
4297
|
+
if (b.has(k))
|
|
4298
|
+
dot += v * b.get(k);
|
|
4299
|
+
normA += v * v;
|
|
4300
|
+
});
|
|
4301
|
+
b.forEach((v) => normB += v * v);
|
|
4302
|
+
return normA && normB ? dot / (Math.sqrt(normA) * Math.sqrt(normB)) : 0;
|
|
4303
|
+
}
|
|
4304
|
+
function buildContactEmbeddingText(contact) {
|
|
4305
|
+
const tags = contact.tags ?? [];
|
|
4306
|
+
const socialProfiles = contact.social_profiles ?? [];
|
|
4307
|
+
const company = contact.company;
|
|
4308
|
+
const parts = [
|
|
4309
|
+
contact.display_name,
|
|
4310
|
+
contact.job_title,
|
|
4311
|
+
contact.notes,
|
|
4312
|
+
company?.name,
|
|
4313
|
+
company?.industry,
|
|
4314
|
+
...tags.map((t) => t.name),
|
|
4315
|
+
...socialProfiles.map((s) => s.platform)
|
|
4316
|
+
].filter(Boolean);
|
|
4317
|
+
return parts.join(" ");
|
|
4318
|
+
}
|
|
4319
|
+
async function embedContact(contactId, db) {
|
|
4320
|
+
const { getContact: getContact2 } = await Promise.resolve().then(() => (init_contacts(), exports_contacts));
|
|
4321
|
+
const _db2 = db || getDatabase();
|
|
4322
|
+
const contact = getContact2(contactId, _db2);
|
|
4323
|
+
const text = buildContactEmbeddingText(contact);
|
|
4324
|
+
const tokens = tokenize(text);
|
|
4325
|
+
const tfidf = buildTfIdf(tokens);
|
|
4326
|
+
const embedding = JSON.stringify(Array.from(tfidf.entries()).sort((a, b) => b[1] - a[1]).slice(0, 100));
|
|
4327
|
+
_db2.query(`INSERT OR REPLACE INTO contact_embeddings(contact_id, embedding, model, embedded_text, created_at, updated_at) VALUES(?,?,'tfidf',?,?,?)`).run(contactId, embedding, text.slice(0, 500), now(), now());
|
|
4328
|
+
}
|
|
4329
|
+
async function embedAllContacts(db) {
|
|
4330
|
+
const _db2 = db || getDatabase();
|
|
4331
|
+
const contacts = _db2.query(`SELECT id FROM contacts WHERE archived=0`).all();
|
|
4332
|
+
for (const c of contacts) {
|
|
4333
|
+
try {
|
|
4334
|
+
await embedContact(c.id, _db2);
|
|
4335
|
+
} catch {}
|
|
4336
|
+
}
|
|
4337
|
+
return contacts.length;
|
|
4338
|
+
}
|
|
4339
|
+
function semanticSearch(query, limit = 10, db) {
|
|
4340
|
+
const _db2 = db || getDatabase();
|
|
4341
|
+
const queryTokens = buildTfIdf(tokenize(query));
|
|
4342
|
+
let embeddings = [];
|
|
4343
|
+
try {
|
|
4344
|
+
embeddings = _db2.query(`SELECT contact_id, embedding FROM contact_embeddings`).all();
|
|
4345
|
+
} catch {
|
|
4346
|
+
return [];
|
|
4347
|
+
}
|
|
4348
|
+
const results = embeddings.map((e) => {
|
|
4349
|
+
try {
|
|
4350
|
+
const emb = new Map(JSON.parse(e.embedding));
|
|
4351
|
+
return { contact_id: e.contact_id, score: cosineSimilarity(queryTokens, emb) };
|
|
4352
|
+
} catch {
|
|
4353
|
+
return { contact_id: e.contact_id, score: 0 };
|
|
4354
|
+
}
|
|
4355
|
+
}).filter((r) => r.score > 0).sort((a, b) => b.score - a.score).slice(0, limit);
|
|
4356
|
+
return results;
|
|
4357
|
+
}
|
|
4358
|
+
// src/lib/signature-parser.ts
|
|
4359
|
+
function parseEmailSignature(text) {
|
|
4360
|
+
const result = {};
|
|
4361
|
+
const phoneMatch = text.match(/(\+?[\d\s\-\(\)]{7,20})/);
|
|
4362
|
+
if (phoneMatch)
|
|
4363
|
+
result.phone = phoneMatch[1]?.trim();
|
|
4364
|
+
const emailMatch = text.match(/[\w.+-]+@[\w-]+\.[a-z]{2,}/i);
|
|
4365
|
+
if (emailMatch)
|
|
4366
|
+
result.email = emailMatch[0];
|
|
4367
|
+
const linkedinMatch = text.match(/(?:linkedin\.com\/in\/)([\w-]+)/i);
|
|
4368
|
+
if (linkedinMatch)
|
|
4369
|
+
result.linkedin = `https://linkedin.com/in/${linkedinMatch[1]}`;
|
|
4370
|
+
const websiteMatch = text.match(/https?:\/\/(?!linkedin)(?!twitter)[\w.-]+\.[a-z]{2,}/i);
|
|
4371
|
+
if (websiteMatch)
|
|
4372
|
+
result.website = websiteMatch[0];
|
|
4373
|
+
const lines = text.split(`
|
|
4374
|
+
`).map((l) => l.trim()).filter((l) => l.length > 2 && l.length < 80);
|
|
4375
|
+
if (lines[0])
|
|
4376
|
+
result.name = lines[0];
|
|
4377
|
+
for (const line of lines.slice(1)) {
|
|
4378
|
+
if (line.match(/\b(CEO|CTO|VP|Director|Manager|Engineer|Partner|Associate|Consultant|Analyst|President|Founder)\b/i)) {
|
|
4379
|
+
result.title = line;
|
|
4380
|
+
} else if (!result.company && line.match(/^[A-Z][A-Za-z\s,\.]+$/) && !line.includes("@")) {
|
|
4381
|
+
result.company = line;
|
|
4382
|
+
}
|
|
4383
|
+
}
|
|
4384
|
+
return result;
|
|
4385
|
+
}
|
|
4386
|
+
function extractContactsFromEmailThread(participants) {
|
|
4387
|
+
return participants.map((p) => {
|
|
4388
|
+
const sig = p.signature ? parseEmailSignature(p.signature) : {};
|
|
4389
|
+
const name = p.name || sig.name || p.email.split("@")[0] || "Unknown";
|
|
4390
|
+
const contact = {
|
|
4391
|
+
display_name: name,
|
|
4392
|
+
emails: [{ address: p.email, type: "work", is_primary: true }],
|
|
4393
|
+
source: "import"
|
|
4394
|
+
};
|
|
4395
|
+
if (sig.title)
|
|
4396
|
+
contact.job_title = sig.title;
|
|
4397
|
+
if (sig.phone)
|
|
4398
|
+
contact.phones = [{ number: sig.phone, type: "work", is_primary: true }];
|
|
4399
|
+
if (sig.linkedin)
|
|
4400
|
+
contact.social_profiles = [{ platform: "linkedin", url: sig.linkedin, is_primary: true }];
|
|
4401
|
+
if (sig.website)
|
|
4402
|
+
contact.website = sig.website;
|
|
4403
|
+
return contact;
|
|
4404
|
+
});
|
|
4405
|
+
}
|
|
4406
|
+
// src/lib/meeting-capture.ts
|
|
4407
|
+
init_database();
|
|
4408
|
+
async function ingestMeetingParticipants(event, db) {
|
|
4409
|
+
const { findOrCreateContact: findOrCreateContact2 } = await Promise.resolve().then(() => (init_contacts(), exports_contacts));
|
|
4410
|
+
const { logEvent: logEvent2 } = await Promise.resolve().then(() => (init_events(), exports_events));
|
|
4411
|
+
const _db2 = db || getDatabase();
|
|
4412
|
+
let created = 0;
|
|
4413
|
+
let updated = 0;
|
|
4414
|
+
const ids = [];
|
|
4415
|
+
for (const a of event.attendees) {
|
|
4416
|
+
try {
|
|
4417
|
+
const nameParts = a.name.split(" ");
|
|
4418
|
+
const result = await findOrCreateContact2({
|
|
4419
|
+
display_name: a.name,
|
|
4420
|
+
first_name: nameParts[0],
|
|
4421
|
+
last_name: nameParts.slice(1).join(" ") || undefined,
|
|
4422
|
+
emails: [{ address: a.email, type: "work", is_primary: true }],
|
|
4423
|
+
source: "import"
|
|
4424
|
+
}, _db2);
|
|
4425
|
+
ids.push(result.contact.id);
|
|
4426
|
+
if (result.created)
|
|
4427
|
+
created++;
|
|
4428
|
+
else
|
|
4429
|
+
updated++;
|
|
4430
|
+
} catch {}
|
|
4431
|
+
}
|
|
4432
|
+
if (ids.length) {
|
|
4433
|
+
try {
|
|
4434
|
+
logEvent2({
|
|
4435
|
+
title: event.title,
|
|
4436
|
+
type: "meeting",
|
|
4437
|
+
event_date: event.event_date,
|
|
4438
|
+
contact_ids: ids,
|
|
4439
|
+
notes: event.context
|
|
4440
|
+
}, _db2);
|
|
4441
|
+
} catch {}
|
|
4442
|
+
}
|
|
4443
|
+
return { created, updated, contact_ids: ids };
|
|
4444
|
+
}
|
|
4445
|
+
|
|
4446
|
+
// src/index.ts
|
|
4447
|
+
init_contacts();
|
|
4448
|
+
|
|
4449
|
+
// src/lib/images.ts
|
|
4450
|
+
init_database();
|
|
4451
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, copyFileSync as copyFileSync2, unlinkSync, readdirSync as readdirSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
4452
|
+
import { join as join3, extname, basename } from "path";
|
|
4453
|
+
var IMAGES_DIR = join3(getDataDir(), "images");
|
|
4454
|
+
function ensureImagesDir() {
|
|
4455
|
+
if (!existsSync3(IMAGES_DIR))
|
|
4456
|
+
mkdirSync2(IMAGES_DIR, { recursive: true });
|
|
4457
|
+
}
|
|
4458
|
+
function getImagesDir() {
|
|
4459
|
+
ensureImagesDir();
|
|
4460
|
+
return IMAGES_DIR;
|
|
4461
|
+
}
|
|
4462
|
+
function saveImage(entityId, source, options) {
|
|
4463
|
+
ensureImagesDir();
|
|
4464
|
+
deleteImage(entityId);
|
|
4465
|
+
const base64Match = source.match(/^data:image\/(\w+);base64,(.+)$/s);
|
|
4466
|
+
if (base64Match) {
|
|
4467
|
+
const ext2 = base64Match[1] === "jpeg" ? "jpg" : base64Match[1];
|
|
4468
|
+
const data = Buffer.from(base64Match[2], "base64");
|
|
4469
|
+
const filename2 = `${entityId}.${ext2}`;
|
|
4470
|
+
writeFileSync(join3(IMAGES_DIR, filename2), data);
|
|
4471
|
+
return filename2;
|
|
4472
|
+
}
|
|
4473
|
+
if (!existsSync3(source) && /^[A-Za-z0-9+/=\n\r]+$/.test(source.trim()) && source.length > 100) {
|
|
4474
|
+
const ext2 = options?.format || "jpg";
|
|
4475
|
+
const data = Buffer.from(source.trim(), "base64");
|
|
4476
|
+
const filename2 = `${entityId}.${ext2}`;
|
|
4477
|
+
writeFileSync(join3(IMAGES_DIR, filename2), data);
|
|
4478
|
+
return filename2;
|
|
4479
|
+
}
|
|
4480
|
+
if (!existsSync3(source)) {
|
|
4481
|
+
throw new Error(`Image file not found: ${source}`);
|
|
4482
|
+
}
|
|
4483
|
+
const ext = extname(source).slice(1).toLowerCase() || "jpg";
|
|
4484
|
+
const validExts = ["jpg", "jpeg", "png", "gif", "webp", "svg", "avif", "ico"];
|
|
4485
|
+
if (!validExts.includes(ext)) {
|
|
4486
|
+
throw new Error(`Unsupported image format: ${ext}. Supported: ${validExts.join(", ")}`);
|
|
4487
|
+
}
|
|
4488
|
+
const filename = `${entityId}.${ext === "jpeg" ? "jpg" : ext}`;
|
|
4489
|
+
copyFileSync2(source, join3(IMAGES_DIR, filename));
|
|
4490
|
+
return filename;
|
|
4491
|
+
}
|
|
4492
|
+
function getImagePath(entityId) {
|
|
4493
|
+
ensureImagesDir();
|
|
4494
|
+
const files = readdirSync2(IMAGES_DIR);
|
|
4495
|
+
const match = files.find((f) => f.startsWith(`${entityId}.`));
|
|
4496
|
+
return match ? join3(IMAGES_DIR, match) : null;
|
|
4497
|
+
}
|
|
4498
|
+
function getImageAsBase64(entityId) {
|
|
4499
|
+
const path = getImagePath(entityId);
|
|
4500
|
+
if (!path)
|
|
4501
|
+
return null;
|
|
4502
|
+
const ext = extname(path).slice(1);
|
|
4503
|
+
const mime = ext === "jpg" ? "image/jpeg" : ext === "svg" ? "image/svg+xml" : `image/${ext}`;
|
|
4504
|
+
const data = readFileSync2(path);
|
|
4505
|
+
return `data:${mime};base64,${data.toString("base64")}`;
|
|
4506
|
+
}
|
|
4507
|
+
function deleteImage(entityId) {
|
|
4508
|
+
ensureImagesDir();
|
|
4509
|
+
const files = readdirSync2(IMAGES_DIR);
|
|
4510
|
+
let deleted = false;
|
|
4511
|
+
for (const f of files) {
|
|
4512
|
+
if (f.startsWith(`${entityId}.`)) {
|
|
4513
|
+
unlinkSync(join3(IMAGES_DIR, f));
|
|
4514
|
+
deleted = true;
|
|
4515
|
+
}
|
|
4516
|
+
}
|
|
4517
|
+
return deleted;
|
|
4518
|
+
}
|
|
4519
|
+
function listImages() {
|
|
4520
|
+
ensureImagesDir();
|
|
4521
|
+
const files = readdirSync2(IMAGES_DIR).filter((f) => !f.startsWith("."));
|
|
4522
|
+
return files.map((f) => ({
|
|
4523
|
+
entity_id: basename(f, extname(f)),
|
|
4524
|
+
filename: f,
|
|
4525
|
+
path: join3(IMAGES_DIR, f)
|
|
4526
|
+
}));
|
|
4527
|
+
}
|
|
4528
|
+
// src/lib/vault.ts
|
|
4529
|
+
init_database();
|
|
4530
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3, unlinkSync as unlinkSync2 } from "fs";
|
|
4531
|
+
import { join as join4 } from "path";
|
|
4532
|
+
import { createCipheriv, createDecipheriv, randomBytes, pbkdf2Sync, createHash } from "crypto";
|
|
4533
|
+
var VAULT_DIR = getDataDir();
|
|
4534
|
+
var VAULT_CONFIG = join4(VAULT_DIR, "vault.json");
|
|
4535
|
+
var VAULT_SESSION = join4(VAULT_DIR, ".vault-session");
|
|
4536
|
+
var DOCUMENTS_DIR = join4(VAULT_DIR, "documents");
|
|
4537
|
+
var SESSION_TTL_MS = 30 * 60 * 1000;
|
|
4538
|
+
var _derivedKey = null;
|
|
4539
|
+
function deriveKey(passphrase, salt) {
|
|
4540
|
+
return pbkdf2Sync(passphrase, salt, 1e5, 32, "sha512");
|
|
4541
|
+
}
|
|
4542
|
+
function saveSession(key) {
|
|
4543
|
+
const session = {
|
|
4544
|
+
key: key.toString("hex"),
|
|
4545
|
+
expires_at: new Date(Date.now() + SESSION_TTL_MS).toISOString()
|
|
4546
|
+
};
|
|
4547
|
+
writeFileSync2(VAULT_SESSION, JSON.stringify(session), { mode: 384 });
|
|
4548
|
+
}
|
|
4549
|
+
function loadSession() {
|
|
4550
|
+
if (!existsSync4(VAULT_SESSION))
|
|
4551
|
+
return null;
|
|
4552
|
+
try {
|
|
4553
|
+
const session = JSON.parse(readFileSync3(VAULT_SESSION, "utf-8"));
|
|
4554
|
+
if (new Date(session.expires_at).getTime() < Date.now()) {
|
|
4555
|
+
try {
|
|
4556
|
+
unlinkSync2(VAULT_SESSION);
|
|
4557
|
+
} catch {}
|
|
4558
|
+
return null;
|
|
4559
|
+
}
|
|
4560
|
+
return Buffer.from(session.key, "hex");
|
|
4561
|
+
} catch {
|
|
4562
|
+
return null;
|
|
4563
|
+
}
|
|
4564
|
+
}
|
|
4565
|
+
function clearSession() {
|
|
4566
|
+
try {
|
|
4567
|
+
if (existsSync4(VAULT_SESSION))
|
|
4568
|
+
unlinkSync2(VAULT_SESSION);
|
|
4569
|
+
} catch {}
|
|
4570
|
+
}
|
|
4571
|
+
function initVault(passphrase) {
|
|
4572
|
+
if (!existsSync4(VAULT_DIR))
|
|
4573
|
+
mkdirSync3(VAULT_DIR, { recursive: true });
|
|
4574
|
+
if (!existsSync4(DOCUMENTS_DIR))
|
|
4575
|
+
mkdirSync3(DOCUMENTS_DIR, { recursive: true });
|
|
4576
|
+
const salt = randomBytes(32);
|
|
4577
|
+
const key = deriveKey(passphrase, salt);
|
|
4578
|
+
const keyHash = createHash("sha256").update(key).digest("hex");
|
|
4579
|
+
const config = { salt: salt.toString("hex"), key_hash: keyHash, created_at: new Date().toISOString() };
|
|
4580
|
+
writeFileSync2(VAULT_CONFIG, JSON.stringify(config, null, 2));
|
|
4581
|
+
_derivedKey = key;
|
|
4582
|
+
saveSession(key);
|
|
4583
|
+
}
|
|
4584
|
+
function isVaultInitialized() {
|
|
4585
|
+
return existsSync4(VAULT_CONFIG);
|
|
4586
|
+
}
|
|
4587
|
+
function unlockVault(passphrase) {
|
|
4588
|
+
if (!existsSync4(VAULT_CONFIG))
|
|
4589
|
+
throw new Error("Vault not initialized. Run 'contacts vault init' first.");
|
|
4590
|
+
const config = JSON.parse(readFileSync3(VAULT_CONFIG, "utf-8"));
|
|
4591
|
+
const salt = Buffer.from(config.salt, "hex");
|
|
4592
|
+
const key = deriveKey(passphrase, salt);
|
|
4593
|
+
const keyHash = createHash("sha256").update(key).digest("hex");
|
|
4594
|
+
if (keyHash !== config.key_hash)
|
|
4595
|
+
return false;
|
|
4596
|
+
_derivedKey = key;
|
|
4597
|
+
saveSession(key);
|
|
4598
|
+
return true;
|
|
4599
|
+
}
|
|
4600
|
+
function lockVault() {
|
|
4601
|
+
_derivedKey = null;
|
|
4602
|
+
clearSession();
|
|
4603
|
+
}
|
|
4604
|
+
function isVaultUnlocked() {
|
|
4605
|
+
if (_derivedKey)
|
|
4606
|
+
return true;
|
|
4607
|
+
const sessionKey = loadSession();
|
|
4608
|
+
if (sessionKey) {
|
|
4609
|
+
_derivedKey = sessionKey;
|
|
4610
|
+
return true;
|
|
4611
|
+
}
|
|
4612
|
+
return false;
|
|
4613
|
+
}
|
|
4614
|
+
function requireVault() {
|
|
4615
|
+
if (_derivedKey)
|
|
4616
|
+
return _derivedKey;
|
|
4617
|
+
const sessionKey = loadSession();
|
|
4618
|
+
if (sessionKey) {
|
|
4619
|
+
_derivedKey = sessionKey;
|
|
4620
|
+
return _derivedKey;
|
|
4621
|
+
}
|
|
4622
|
+
throw new Error("Vault is locked. Unlock with 'contacts vault unlock --passphrase <pass>' or vault_unlock MCP tool first.");
|
|
4623
|
+
}
|
|
4624
|
+
function encrypt(plaintext) {
|
|
4625
|
+
const key = requireVault();
|
|
4626
|
+
const iv = randomBytes(16);
|
|
4627
|
+
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
4628
|
+
let encrypted = cipher.update(plaintext, "utf8", "hex");
|
|
4629
|
+
encrypted += cipher.final("hex");
|
|
4630
|
+
const authTag = cipher.getAuthTag().toString("hex");
|
|
4631
|
+
return { ciphertext: encrypted + ":" + authTag, iv: iv.toString("hex") };
|
|
4632
|
+
}
|
|
4633
|
+
function decrypt(ciphertext, iv) {
|
|
4634
|
+
const key = requireVault();
|
|
4635
|
+
const [encData, authTag] = ciphertext.split(":");
|
|
4636
|
+
if (!encData || !authTag)
|
|
4637
|
+
throw new Error("Invalid ciphertext format");
|
|
4638
|
+
const decipher = createDecipheriv("aes-256-gcm", key, Buffer.from(iv, "hex"));
|
|
4639
|
+
decipher.setAuthTag(Buffer.from(authTag, "hex"));
|
|
4640
|
+
let decrypted = decipher.update(encData, "hex", "utf8");
|
|
4641
|
+
decrypted += decipher.final("utf8");
|
|
4642
|
+
return decrypted;
|
|
4643
|
+
}
|
|
4644
|
+
function storeFile(sourcePath, entityId) {
|
|
4645
|
+
if (!existsSync4(DOCUMENTS_DIR))
|
|
4646
|
+
mkdirSync3(DOCUMENTS_DIR, { recursive: true });
|
|
4647
|
+
const ext = sourcePath.split(".").pop() || "bin";
|
|
4648
|
+
const destPath = join4(DOCUMENTS_DIR, `${entityId}.${ext}`);
|
|
4649
|
+
const data = readFileSync3(sourcePath);
|
|
4650
|
+
writeFileSync2(destPath, data);
|
|
4651
|
+
return destPath;
|
|
4652
|
+
}
|
|
4653
|
+
function getDocumentFilePath(entityId) {
|
|
4654
|
+
if (!existsSync4(DOCUMENTS_DIR))
|
|
4655
|
+
return null;
|
|
4656
|
+
const { readdirSync: readdirSync3 } = __require("fs");
|
|
4657
|
+
const files = readdirSync3(DOCUMENTS_DIR);
|
|
4658
|
+
const match = files.find((f) => f.startsWith(`${entityId}.`) && !f.endsWith(".enc"));
|
|
4659
|
+
return match ? join4(DOCUMENTS_DIR, match) : null;
|
|
4660
|
+
}
|
|
4661
|
+
function decryptFile(encPath) {
|
|
4662
|
+
const key = requireVault();
|
|
4663
|
+
const data = readFileSync3(encPath);
|
|
4664
|
+
const iv = data.subarray(0, 16);
|
|
4665
|
+
const authTag = data.subarray(16, 32);
|
|
4666
|
+
const encrypted = data.subarray(32);
|
|
4667
|
+
const decipher = createDecipheriv("aes-256-gcm", key, iv);
|
|
4668
|
+
decipher.setAuthTag(authTag);
|
|
4669
|
+
return Buffer.concat([decipher.update(encrypted), decipher.final()]);
|
|
4670
|
+
}
|
|
4671
|
+
function getDocumentsDir() {
|
|
4672
|
+
if (!existsSync4(DOCUMENTS_DIR))
|
|
4673
|
+
mkdirSync3(DOCUMENTS_DIR, { recursive: true });
|
|
4674
|
+
return DOCUMENTS_DIR;
|
|
4675
|
+
}
|
|
4676
|
+
// src/db/documents.ts
|
|
4677
|
+
init_database();
|
|
4678
|
+
import { existsSync as existsSync5, unlinkSync as unlinkSync3 } from "fs";
|
|
4679
|
+
var DOCUMENT_TYPES = [
|
|
4680
|
+
"passport",
|
|
4681
|
+
"national_id",
|
|
4682
|
+
"tax_id",
|
|
4683
|
+
"ssn",
|
|
4684
|
+
"drivers_license",
|
|
4685
|
+
"bank_account",
|
|
4686
|
+
"visa",
|
|
4687
|
+
"insurance",
|
|
4688
|
+
"contract",
|
|
4689
|
+
"certificate",
|
|
4690
|
+
"medical_record",
|
|
4691
|
+
"prescription",
|
|
4692
|
+
"allergy_list",
|
|
4693
|
+
"vaccination",
|
|
4694
|
+
"blood_type",
|
|
4695
|
+
"health_insurance",
|
|
4696
|
+
"medical_condition",
|
|
4697
|
+
"emergency_contact_medical",
|
|
4698
|
+
"other"
|
|
4699
|
+
];
|
|
4700
|
+
function addDocument(input, db) {
|
|
4701
|
+
requireVault();
|
|
4702
|
+
const _db2 = db || getDatabase();
|
|
4703
|
+
const id = uuid();
|
|
4704
|
+
const { ciphertext, iv } = encrypt(input.value);
|
|
4705
|
+
let filePath = null;
|
|
4706
|
+
if (input.file_path) {
|
|
4707
|
+
filePath = storeFile(input.file_path, id);
|
|
4708
|
+
}
|
|
4709
|
+
_db2.query(`INSERT INTO contact_documents (id, contact_id, doc_type, label, encrypted_value, iv, encrypted_file_path, metadata, expires_at, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?)`).run(id, input.contact_id, input.doc_type, input.label ?? null, ciphertext, iv, filePath, JSON.stringify(input.metadata || {}), input.expires_at ?? null, now(), now());
|
|
4710
|
+
return getDocument(id, _db2);
|
|
4711
|
+
}
|
|
4712
|
+
function getDocument(id, db) {
|
|
4713
|
+
requireVault();
|
|
4714
|
+
const _db2 = db || getDatabase();
|
|
4715
|
+
const row = _db2.query(`SELECT * FROM contact_documents WHERE id = ?`).get(id);
|
|
4716
|
+
if (!row)
|
|
4717
|
+
throw new Error(`Document not found: ${id}`);
|
|
4718
|
+
return rowToDoc(row);
|
|
4719
|
+
}
|
|
4720
|
+
function listDocuments(contactId, db) {
|
|
4721
|
+
const _db2 = db || getDatabase();
|
|
4722
|
+
const rows = _db2.query(`SELECT id, doc_type, label, encrypted_file_path, expires_at, created_at FROM contact_documents WHERE contact_id = ? ORDER BY created_at DESC`).all(contactId);
|
|
4723
|
+
return rows.map((r) => ({
|
|
4724
|
+
id: r.id,
|
|
4725
|
+
doc_type: r.doc_type,
|
|
4726
|
+
label: r.label,
|
|
4727
|
+
has_file: !!r.encrypted_file_path,
|
|
4728
|
+
expires_at: r.expires_at,
|
|
4729
|
+
created_at: r.created_at
|
|
4730
|
+
}));
|
|
4731
|
+
}
|
|
4732
|
+
function deleteDocument(id, db) {
|
|
4733
|
+
const _db2 = db || getDatabase();
|
|
4734
|
+
const row = _db2.query(`SELECT encrypted_file_path FROM contact_documents WHERE id = ?`).get(id);
|
|
4735
|
+
if (row?.encrypted_file_path && existsSync5(row.encrypted_file_path)) {
|
|
4736
|
+
try {
|
|
4737
|
+
unlinkSync3(row.encrypted_file_path);
|
|
4738
|
+
} catch {}
|
|
4739
|
+
}
|
|
4740
|
+
_db2.query(`DELETE FROM contact_documents WHERE id = ?`).run(id);
|
|
4741
|
+
}
|
|
4742
|
+
function rowToDoc(row) {
|
|
4743
|
+
return {
|
|
4744
|
+
id: row.id,
|
|
4745
|
+
contact_id: row.contact_id,
|
|
4746
|
+
doc_type: row.doc_type,
|
|
4747
|
+
label: row.label,
|
|
4748
|
+
value: decrypt(row.encrypted_value, row.iv),
|
|
4749
|
+
has_file: !!row.encrypted_file_path,
|
|
4750
|
+
file_path: row.encrypted_file_path,
|
|
4751
|
+
metadata: JSON.parse(row.metadata || "{}"),
|
|
4752
|
+
expires_at: row.expires_at,
|
|
4753
|
+
created_at: row.created_at,
|
|
4754
|
+
updated_at: row.updated_at
|
|
4755
|
+
};
|
|
4756
|
+
}
|
|
4757
|
+
// src/db/health.ts
|
|
4758
|
+
init_database();
|
|
4759
|
+
function setHealthData(contactId, input, db) {
|
|
4760
|
+
requireVault();
|
|
4761
|
+
const _db2 = db || getDatabase();
|
|
4762
|
+
const existing = _db2.query(`SELECT id FROM contact_health WHERE contact_id = ?`).get(contactId);
|
|
4763
|
+
if (existing) {
|
|
4764
|
+
const sets = [];
|
|
4765
|
+
const params = [];
|
|
4766
|
+
if (input.blood_type !== undefined) {
|
|
4767
|
+
sets.push("blood_type = ?");
|
|
4768
|
+
params.push(input.blood_type);
|
|
4769
|
+
}
|
|
4770
|
+
if (input.allergies !== undefined) {
|
|
4771
|
+
sets.push("allergies = ?");
|
|
4772
|
+
params.push(JSON.stringify(input.allergies));
|
|
4773
|
+
}
|
|
4774
|
+
if (input.medical_conditions !== undefined) {
|
|
4775
|
+
sets.push("medical_conditions = ?");
|
|
4776
|
+
params.push(JSON.stringify(input.medical_conditions));
|
|
4777
|
+
}
|
|
4778
|
+
if (input.medications !== undefined) {
|
|
4779
|
+
sets.push("medications = ?");
|
|
4780
|
+
params.push(JSON.stringify(input.medications));
|
|
4781
|
+
}
|
|
4782
|
+
if (input.emergency_contacts !== undefined) {
|
|
4783
|
+
sets.push("emergency_contacts = ?");
|
|
4784
|
+
params.push(JSON.stringify(input.emergency_contacts));
|
|
4785
|
+
}
|
|
4786
|
+
if (input.health_insurance_provider !== undefined) {
|
|
4787
|
+
sets.push("health_insurance_provider = ?");
|
|
4788
|
+
params.push(input.health_insurance_provider);
|
|
4789
|
+
}
|
|
4790
|
+
if (input.health_insurance_id !== undefined) {
|
|
4791
|
+
sets.push("health_insurance_id = ?");
|
|
4792
|
+
params.push(input.health_insurance_id);
|
|
4793
|
+
}
|
|
4794
|
+
if (input.primary_physician !== undefined) {
|
|
4795
|
+
sets.push("primary_physician = ?");
|
|
4796
|
+
params.push(input.primary_physician);
|
|
4797
|
+
}
|
|
4798
|
+
if (input.primary_physician_phone !== undefined) {
|
|
4799
|
+
sets.push("primary_physician_phone = ?");
|
|
4800
|
+
params.push(input.primary_physician_phone);
|
|
4801
|
+
}
|
|
4802
|
+
if (input.organ_donor !== undefined) {
|
|
4803
|
+
sets.push("organ_donor = ?");
|
|
4804
|
+
params.push(input.organ_donor ? 1 : 0);
|
|
4805
|
+
}
|
|
4806
|
+
if (input.notes !== undefined) {
|
|
4807
|
+
sets.push("notes = ?");
|
|
4808
|
+
params.push(input.notes);
|
|
4809
|
+
}
|
|
4810
|
+
if (sets.length) {
|
|
4811
|
+
sets.push("updated_at = ?");
|
|
4812
|
+
params.push(now());
|
|
4813
|
+
params.push(contactId);
|
|
4814
|
+
_db2.query(`UPDATE contact_health SET ${sets.join(", ")} WHERE contact_id = ?`).run(...params);
|
|
4815
|
+
}
|
|
4816
|
+
} else {
|
|
4817
|
+
const id = uuid();
|
|
4818
|
+
_db2.query(`INSERT INTO contact_health (id, contact_id, blood_type, allergies, medical_conditions, medications, emergency_contacts, health_insurance_provider, health_insurance_id, primary_physician, primary_physician_phone, organ_donor, notes, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`).run(id, contactId, input.blood_type ?? null, JSON.stringify(input.allergies || []), JSON.stringify(input.medical_conditions || []), JSON.stringify(input.medications || []), JSON.stringify(input.emergency_contacts || []), input.health_insurance_provider ?? null, input.health_insurance_id ?? null, input.primary_physician ?? null, input.primary_physician_phone ?? null, input.organ_donor ? 1 : 0, input.notes ?? null, now(), now());
|
|
4819
|
+
}
|
|
4820
|
+
return getHealthData(contactId, _db2);
|
|
4821
|
+
}
|
|
4822
|
+
function getHealthData(contactId, db) {
|
|
4823
|
+
requireVault();
|
|
4824
|
+
const _db2 = db || getDatabase();
|
|
4825
|
+
const row = _db2.query(`SELECT * FROM contact_health WHERE contact_id = ?`).get(contactId);
|
|
4826
|
+
if (!row)
|
|
4827
|
+
return null;
|
|
4828
|
+
return {
|
|
4829
|
+
id: row.id,
|
|
4830
|
+
contact_id: row.contact_id,
|
|
4831
|
+
blood_type: row.blood_type,
|
|
4832
|
+
allergies: JSON.parse(row.allergies || "[]"),
|
|
4833
|
+
medical_conditions: JSON.parse(row.medical_conditions || "[]"),
|
|
4834
|
+
medications: JSON.parse(row.medications || "[]"),
|
|
4835
|
+
emergency_contacts: JSON.parse(row.emergency_contacts || "[]"),
|
|
4836
|
+
health_insurance_provider: row.health_insurance_provider,
|
|
4837
|
+
health_insurance_id: row.health_insurance_id,
|
|
4838
|
+
primary_physician: row.primary_physician,
|
|
4839
|
+
primary_physician_phone: row.primary_physician_phone,
|
|
4840
|
+
organ_donor: !!row.organ_donor,
|
|
4841
|
+
notes: row.notes,
|
|
4842
|
+
created_at: row.created_at,
|
|
4843
|
+
updated_at: row.updated_at
|
|
4844
|
+
};
|
|
4845
|
+
}
|
|
4846
|
+
function deleteHealthData(contactId, db) {
|
|
4847
|
+
const _db2 = db || getDatabase();
|
|
4848
|
+
_db2.query(`DELETE FROM contact_health WHERE contact_id = ?`).run(contactId);
|
|
4849
|
+
}
|
|
4850
|
+
// src/lib/document-scanner.ts
|
|
4851
|
+
import { readFileSync as readFileSync4, existsSync as existsSync6 } from "fs";
|
|
4852
|
+
import { extname as extname2 } from "path";
|
|
4853
|
+
async function scanDocument(imageSource, docType) {
|
|
4854
|
+
const apiKey = process.env["OPENAI_API_KEY"];
|
|
4855
|
+
if (!apiKey) {
|
|
4856
|
+
throw new Error("OPENAI_API_KEY not set. Set it in ~/.secrets or environment to use document scanning.");
|
|
4857
|
+
}
|
|
4858
|
+
let imageData;
|
|
4859
|
+
if (imageSource.startsWith("data:image/")) {
|
|
4860
|
+
imageData = imageSource;
|
|
4861
|
+
} else if (existsSync6(imageSource)) {
|
|
4862
|
+
const buffer = readFileSync4(imageSource);
|
|
4863
|
+
const ext = extname2(imageSource).slice(1).toLowerCase();
|
|
4864
|
+
const mime = ext === "jpg" || ext === "jpeg" ? "image/jpeg" : ext === "png" ? "image/png" : ext === "webp" ? "image/webp" : ext === "gif" ? "image/gif" : "image/jpeg";
|
|
4865
|
+
imageData = `data:${mime};base64,${buffer.toString("base64")}`;
|
|
4866
|
+
} else if (/^[A-Za-z0-9+/=\n\r]+$/.test(imageSource.trim()) && imageSource.length > 100) {
|
|
4867
|
+
imageData = `data:image/jpeg;base64,${imageSource.trim()}`;
|
|
4868
|
+
} else {
|
|
4869
|
+
throw new Error(`Image source not found or invalid: ${imageSource.slice(0, 50)}...`);
|
|
4870
|
+
}
|
|
4871
|
+
const typeHint = docType ? ` This is a ${docType} document.` : "";
|
|
4872
|
+
const prompt = `Extract all text and structured data from this document image.${typeHint} Return a JSON object with these fields:
|
|
4873
|
+
- document_type: detected type (passport, national_id, drivers_license, tax_document, medical_record, prescription, insurance_card, bank_statement, visa, certificate, contract, other)
|
|
4874
|
+
- full_name: full name as shown
|
|
4875
|
+
- date_of_birth: in YYYY-MM-DD format if visible
|
|
4876
|
+
- document_number: main ID/document number
|
|
4877
|
+
- issuing_country: country code or name
|
|
4878
|
+
- issue_date: in YYYY-MM-DD format if visible
|
|
4879
|
+
- expiry_date: in YYYY-MM-DD format if visible
|
|
4880
|
+
- address: full address if visible
|
|
4881
|
+
- nationality: if visible
|
|
4882
|
+
- gender: if visible
|
|
4883
|
+
- mrz_code: Machine Readable Zone text if this is a passport/ID with MRZ
|
|
4884
|
+
- phone: any phone numbers visible
|
|
4885
|
+
- email: any email addresses visible
|
|
4886
|
+
- additional_fields: object with any other visible structured data
|
|
4887
|
+
- raw_text: all visible text transcribed
|
|
4888
|
+
|
|
4889
|
+
Only include fields that are actually visible in the document. Return valid JSON only.`;
|
|
4890
|
+
const response = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
4891
|
+
method: "POST",
|
|
4892
|
+
headers: {
|
|
4893
|
+
Authorization: `Bearer ${apiKey}`,
|
|
4894
|
+
"Content-Type": "application/json"
|
|
4895
|
+
},
|
|
4896
|
+
body: JSON.stringify({
|
|
4897
|
+
model: "gpt-4o",
|
|
4898
|
+
messages: [
|
|
4899
|
+
{
|
|
4900
|
+
role: "user",
|
|
4901
|
+
content: [
|
|
4902
|
+
{ type: "text", text: prompt },
|
|
4903
|
+
{ type: "image_url", image_url: { url: imageData, detail: "high" } }
|
|
4904
|
+
]
|
|
4905
|
+
}
|
|
4906
|
+
],
|
|
4907
|
+
max_tokens: 2000,
|
|
4908
|
+
temperature: 0
|
|
4909
|
+
})
|
|
4910
|
+
});
|
|
4911
|
+
if (!response.ok) {
|
|
4912
|
+
const err = await response.text();
|
|
4913
|
+
throw new Error(`OpenAI API error: ${response.status} \u2014 ${err}`);
|
|
4914
|
+
}
|
|
4915
|
+
const data = await response.json();
|
|
4916
|
+
const content = data.choices?.[0]?.message?.content || "";
|
|
4917
|
+
const jsonMatch = content.match(/```json\s*([\s\S]*?)```/) || content.match(/\{[\s\S]*\}/);
|
|
4918
|
+
if (!jsonMatch) {
|
|
4919
|
+
return { fields: {}, raw_text: content, document_type: docType || "unknown", confidence: 0.3 };
|
|
4920
|
+
}
|
|
4921
|
+
try {
|
|
4922
|
+
const parsed = JSON.parse(jsonMatch[1] || jsonMatch[0]);
|
|
4923
|
+
const { document_type, raw_text, additional_fields, ...mainFields } = parsed;
|
|
4924
|
+
const fields = {};
|
|
4925
|
+
for (const [k, v] of Object.entries(mainFields)) {
|
|
4926
|
+
if (v && typeof v === "string")
|
|
4927
|
+
fields[k] = v;
|
|
4928
|
+
}
|
|
4929
|
+
if (additional_fields && typeof additional_fields === "object") {
|
|
4930
|
+
for (const [k, v] of Object.entries(additional_fields)) {
|
|
4931
|
+
if (v && typeof v === "string")
|
|
4932
|
+
fields[k] = v;
|
|
4933
|
+
}
|
|
4934
|
+
}
|
|
4935
|
+
return {
|
|
4936
|
+
fields,
|
|
4937
|
+
raw_text: raw_text || content,
|
|
4938
|
+
document_type: document_type || docType || "unknown",
|
|
4939
|
+
confidence: Object.keys(fields).length > 3 ? 0.9 : Object.keys(fields).length > 0 ? 0.7 : 0.3
|
|
4940
|
+
};
|
|
4941
|
+
} catch {
|
|
4942
|
+
return { fields: {}, raw_text: content, document_type: docType || "unknown", confidence: 0.3 };
|
|
4943
|
+
}
|
|
4944
|
+
}
|
|
3534
4945
|
export {
|
|
3535
4946
|
updateVendorCommunication,
|
|
3536
4947
|
updateTag,
|
|
@@ -3541,29 +4952,48 @@ export {
|
|
|
3541
4952
|
updateContact,
|
|
3542
4953
|
updateCompany,
|
|
3543
4954
|
updateApplication,
|
|
4955
|
+
unlockVault,
|
|
3544
4956
|
unarchiveContact,
|
|
3545
4957
|
unarchiveCompany,
|
|
4958
|
+
storeFile,
|
|
4959
|
+
setHealthData,
|
|
4960
|
+
setDealContactRole,
|
|
4961
|
+
semanticSearch,
|
|
4962
|
+
searchLearnings,
|
|
3546
4963
|
searchGoogleContacts,
|
|
3547
4964
|
searchContacts,
|
|
3548
4965
|
searchCompanies,
|
|
4966
|
+
scanDocument,
|
|
4967
|
+
saveLearning,
|
|
4968
|
+
saveImage,
|
|
3549
4969
|
runConnector,
|
|
4970
|
+
resolveIdentity,
|
|
4971
|
+
resolveByPartial,
|
|
3550
4972
|
resetDatabase,
|
|
4973
|
+
requireVault,
|
|
3551
4974
|
removeTagFromContact,
|
|
3552
4975
|
removeTagFromCompany,
|
|
3553
4976
|
removeOrgMember,
|
|
3554
4977
|
removeContactFromGroup,
|
|
3555
4978
|
removeCompanyFromGroup,
|
|
4979
|
+
releaseLock,
|
|
4980
|
+
recordFieldChange,
|
|
4981
|
+
recomputeAllSignals,
|
|
3556
4982
|
readConnectorTokens,
|
|
3557
4983
|
pushContactToGoogle,
|
|
3558
4984
|
pullGoogleContactsAsInputs,
|
|
3559
4985
|
parseName,
|
|
3560
4986
|
parseLinkedIn,
|
|
4987
|
+
parseEmailSignature,
|
|
3561
4988
|
parseAddressHeader,
|
|
3562
4989
|
mergeContacts,
|
|
3563
4990
|
markFollowUpDone,
|
|
4991
|
+
markFieldVerified,
|
|
3564
4992
|
logVendorCommunication,
|
|
3565
4993
|
logEvent,
|
|
4994
|
+
logAgentActivity,
|
|
3566
4995
|
logActivity,
|
|
4996
|
+
lockVault,
|
|
3567
4997
|
listVendorCommunications,
|
|
3568
4998
|
listTags,
|
|
3569
4999
|
listRelationships,
|
|
@@ -3574,15 +5004,18 @@ export {
|
|
|
3574
5004
|
listOverdueTasks,
|
|
3575
5005
|
listOrgMembersForContact,
|
|
3576
5006
|
listOrgMembers,
|
|
5007
|
+
listOrgChart,
|
|
3577
5008
|
listNotesForContactAtCompany,
|
|
3578
5009
|
listNotes,
|
|
3579
5010
|
listMissingInvoices,
|
|
5011
|
+
listImages,
|
|
3580
5012
|
listGroupsForContact,
|
|
3581
5013
|
listGroupsForCompany,
|
|
3582
5014
|
listGroups,
|
|
3583
5015
|
listGoogleContacts,
|
|
3584
5016
|
listFollowUpDue,
|
|
3585
5017
|
listEvents,
|
|
5018
|
+
listDocuments,
|
|
3586
5019
|
listDeals,
|
|
3587
5020
|
listContactsInGroup,
|
|
3588
5021
|
listContactsByTag,
|
|
@@ -3596,48 +5029,91 @@ export {
|
|
|
3596
5029
|
listColdContacts,
|
|
3597
5030
|
listApplications,
|
|
3598
5031
|
listActivity,
|
|
5032
|
+
isVaultUnlocked,
|
|
5033
|
+
isVaultInitialized,
|
|
5034
|
+
initVault,
|
|
5035
|
+
ingestMeetingParticipants,
|
|
3599
5036
|
importToApple,
|
|
3600
5037
|
importFromCsv,
|
|
3601
5038
|
importContacts,
|
|
3602
5039
|
googlePersonToContactInput,
|
|
5040
|
+
getWarmingContacts,
|
|
3603
5041
|
getUpcomingItems,
|
|
3604
5042
|
getTagByName,
|
|
3605
5043
|
getTag,
|
|
5044
|
+
getStaleContacts,
|
|
5045
|
+
getRelationshipSignals,
|
|
3606
5046
|
getRelationship,
|
|
5047
|
+
getPreviousEmployers,
|
|
3607
5048
|
getOrgMember,
|
|
3608
5049
|
getNote,
|
|
3609
5050
|
getNetworkStats,
|
|
5051
|
+
getLearnings,
|
|
5052
|
+
getJobHistory,
|
|
5053
|
+
getImagesDir,
|
|
5054
|
+
getImagePath,
|
|
5055
|
+
getImageAsBase64,
|
|
5056
|
+
getIdentities,
|
|
5057
|
+
getHealthData,
|
|
3610
5058
|
getGroup,
|
|
5059
|
+
getGhostContacts,
|
|
5060
|
+
getFreshnessScore,
|
|
5061
|
+
getFieldHistory,
|
|
3611
5062
|
getEvent,
|
|
3612
5063
|
getEntityTeam,
|
|
5064
|
+
getDocumentsDir,
|
|
5065
|
+
getDocumentFilePath,
|
|
5066
|
+
getDocument,
|
|
3613
5067
|
getDealsByStage,
|
|
5068
|
+
getDealTeam,
|
|
3614
5069
|
getDeal,
|
|
3615
5070
|
getDatabase,
|
|
5071
|
+
getCurrentRole,
|
|
5072
|
+
getCoverageGaps,
|
|
3616
5073
|
getContactTimeline,
|
|
3617
5074
|
getContactTask,
|
|
5075
|
+
getContactCard,
|
|
3618
5076
|
getContactByEmail,
|
|
5077
|
+
getContactBrief,
|
|
5078
|
+
getContactAt,
|
|
3619
5079
|
getContact,
|
|
3620
5080
|
getConnectorTokenPath,
|
|
3621
5081
|
getCompany,
|
|
3622
5082
|
getApplication,
|
|
5083
|
+
getAgentActivity,
|
|
3623
5084
|
getActivity,
|
|
3624
5085
|
generateBrief,
|
|
5086
|
+
findWarmPath,
|
|
5087
|
+
findOrCreateContact,
|
|
5088
|
+
findConnectionsAtCompany,
|
|
3625
5089
|
extractContactsFromGmail,
|
|
5090
|
+
extractContactsFromEmailThread,
|
|
3626
5091
|
exportFromApple,
|
|
3627
5092
|
exportContacts,
|
|
5093
|
+
encrypt,
|
|
5094
|
+
embedContact,
|
|
5095
|
+
embedAllContacts,
|
|
3628
5096
|
domainToCompany,
|
|
5097
|
+
detectCoolingRelationships,
|
|
3629
5098
|
deleteVendorCommunication,
|
|
3630
5099
|
deleteTag,
|
|
3631
5100
|
deleteRelationship,
|
|
3632
5101
|
deleteNote,
|
|
5102
|
+
deleteLearning,
|
|
5103
|
+
deleteImage,
|
|
5104
|
+
deleteHealthData,
|
|
3633
5105
|
deleteGroup,
|
|
3634
5106
|
deleteEvent,
|
|
5107
|
+
deleteDocument,
|
|
3635
5108
|
deleteDeal,
|
|
3636
5109
|
deleteContactTask,
|
|
3637
5110
|
deleteContact,
|
|
3638
5111
|
deleteCompanyRelationship,
|
|
3639
5112
|
deleteCompany,
|
|
3640
5113
|
deleteApplication,
|
|
5114
|
+
decryptFile,
|
|
5115
|
+
decrypt,
|
|
5116
|
+
decayLearnings,
|
|
3641
5117
|
createTag,
|
|
3642
5118
|
createRelationship,
|
|
3643
5119
|
createGroup,
|
|
@@ -3648,21 +5124,33 @@ export {
|
|
|
3648
5124
|
createCompany,
|
|
3649
5125
|
createApplication,
|
|
3650
5126
|
contactToGoogleArgs,
|
|
5127
|
+
confirmLearning,
|
|
5128
|
+
computeRelationshipStrength,
|
|
5129
|
+
cleanExpiredLocks,
|
|
5130
|
+
checkLock,
|
|
3651
5131
|
checkEscalations,
|
|
5132
|
+
buildContactEmbeddingText,
|
|
3652
5133
|
autoLinkContactToCompany,
|
|
3653
5134
|
auditContact,
|
|
5135
|
+
assembleContext,
|
|
3654
5136
|
archiveContact,
|
|
3655
5137
|
archiveCompany,
|
|
3656
5138
|
addTagToContact,
|
|
3657
5139
|
addTagToCompany,
|
|
3658
5140
|
addPhoneToContact,
|
|
3659
5141
|
addOrgMember,
|
|
5142
|
+
addOrgChartEdge,
|
|
3660
5143
|
addNote,
|
|
5144
|
+
addJobEntry,
|
|
5145
|
+
addIdentity,
|
|
3661
5146
|
addEmailToContact,
|
|
5147
|
+
addDocument,
|
|
3662
5148
|
addContactToGroup,
|
|
3663
5149
|
addCompanyToGroup,
|
|
5150
|
+
acquireLock,
|
|
3664
5151
|
TagNotFoundError,
|
|
3665
5152
|
DuplicateTagNameError,
|
|
5153
|
+
DOCUMENT_TYPES,
|
|
3666
5154
|
ContactNotFoundError,
|
|
3667
5155
|
ConnectorNotInstalledError,
|
|
3668
5156
|
ConnectorAuthError,
|