@hasna/contacts 0.5.3 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +727 -20
- package/dist/db/companies.d.ts.map +1 -1
- package/dist/db/contacts.d.ts.map +1 -1
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/documents.d.ts +37 -0
- package/dist/db/documents.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/tags.d.ts.map +1 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +439 -6
- package/dist/lib/document-scanner.d.ts +8 -0
- package/dist/lib/document-scanner.d.ts.map +1 -0
- package/dist/lib/vault.d.ts +15 -0
- package/dist/lib/vault.d.ts.map +1 -0
- package/dist/mcp/index.js +522 -9
- package/dist/server/index.js +51 -6
- package/dist/types/index.d.ts +6 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -543,6 +543,41 @@ var init_database = __esm(() => {
|
|
|
543
543
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
544
544
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
545
545
|
);
|
|
546
|
+
`,
|
|
547
|
+
`
|
|
548
|
+
ALTER TABLE contacts ADD COLUMN sensitivity TEXT NOT NULL DEFAULT 'normal' CHECK(sensitivity IN ('normal','confidential','restricted'));
|
|
549
|
+
|
|
550
|
+
CREATE TABLE IF NOT EXISTS contact_documents (
|
|
551
|
+
id TEXT PRIMARY KEY,
|
|
552
|
+
contact_id TEXT NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
|
|
553
|
+
doc_type TEXT NOT NULL,
|
|
554
|
+
label TEXT,
|
|
555
|
+
encrypted_value TEXT NOT NULL,
|
|
556
|
+
iv TEXT NOT NULL,
|
|
557
|
+
encrypted_file_path TEXT,
|
|
558
|
+
metadata TEXT NOT NULL DEFAULT '{}',
|
|
559
|
+
expires_at TEXT,
|
|
560
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
561
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
562
|
+
);
|
|
563
|
+
|
|
564
|
+
CREATE TABLE IF NOT EXISTS contact_health (
|
|
565
|
+
id TEXT PRIMARY KEY,
|
|
566
|
+
contact_id TEXT NOT NULL UNIQUE REFERENCES contacts(id) ON DELETE CASCADE,
|
|
567
|
+
blood_type TEXT,
|
|
568
|
+
allergies TEXT NOT NULL DEFAULT '[]',
|
|
569
|
+
medical_conditions TEXT NOT NULL DEFAULT '[]',
|
|
570
|
+
medications TEXT NOT NULL DEFAULT '[]',
|
|
571
|
+
emergency_contacts TEXT NOT NULL DEFAULT '[]',
|
|
572
|
+
health_insurance_provider TEXT,
|
|
573
|
+
health_insurance_id TEXT,
|
|
574
|
+
primary_physician TEXT,
|
|
575
|
+
primary_physician_phone TEXT,
|
|
576
|
+
organ_donor INTEGER NOT NULL DEFAULT 0,
|
|
577
|
+
notes TEXT,
|
|
578
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
579
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
580
|
+
);
|
|
546
581
|
`
|
|
547
582
|
];
|
|
548
583
|
});
|
|
@@ -647,6 +682,7 @@ function rowToContact(row) {
|
|
|
647
682
|
follow_up_at: row.follow_up_at ?? null,
|
|
648
683
|
archived: !!row.archived,
|
|
649
684
|
project_id: row.project_id ?? null,
|
|
685
|
+
sensitivity: row.sensitivity ?? "normal",
|
|
650
686
|
do_not_contact: !!row.do_not_contact,
|
|
651
687
|
priority: row.priority ?? 3,
|
|
652
688
|
timezone: row.timezone ?? null
|
|
@@ -734,8 +770,8 @@ function createContact(input, db) {
|
|
|
734
770
|
const firstName = input.first_name ?? "";
|
|
735
771
|
const lastName = input.last_name ?? "";
|
|
736
772
|
const displayName = input.display_name ?? (firstName || lastName ? `${firstName} ${lastName}`.trim() : "Unnamed Contact");
|
|
737
|
-
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)
|
|
738
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
773
|
+
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)
|
|
774
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
739
775
|
id,
|
|
740
776
|
firstName,
|
|
741
777
|
lastName,
|
|
@@ -754,6 +790,7 @@ function createContact(input, db) {
|
|
|
754
790
|
input.status ?? "active",
|
|
755
791
|
input.follow_up_at ?? null,
|
|
756
792
|
input.project_id ?? null,
|
|
793
|
+
input.sensitivity ?? "normal",
|
|
757
794
|
input.do_not_contact ? 1 : 0,
|
|
758
795
|
input.priority ?? 3,
|
|
759
796
|
input.timezone ?? null,
|
|
@@ -802,6 +839,7 @@ function listContacts(opts = {}, db) {
|
|
|
802
839
|
order_by = "display_name",
|
|
803
840
|
order_dir = "asc",
|
|
804
841
|
include_dnc = false,
|
|
842
|
+
include_restricted = false,
|
|
805
843
|
priority_min,
|
|
806
844
|
updated_since
|
|
807
845
|
} = opts;
|
|
@@ -812,6 +850,9 @@ function listContacts(opts = {}, db) {
|
|
|
812
850
|
if (!include_dnc) {
|
|
813
851
|
conditions.push("c.do_not_contact = 0");
|
|
814
852
|
}
|
|
853
|
+
if (!include_restricted) {
|
|
854
|
+
conditions.push("c.sensitivity != 'restricted'");
|
|
855
|
+
}
|
|
815
856
|
if (company_id) {
|
|
816
857
|
conditions.push("c.company_id = ?");
|
|
817
858
|
params.push(company_id);
|
|
@@ -940,6 +981,10 @@ function updateContact(id, input, db) {
|
|
|
940
981
|
setClauses.push("project_id = ?");
|
|
941
982
|
params.push(input.project_id);
|
|
942
983
|
}
|
|
984
|
+
if (input.sensitivity !== undefined) {
|
|
985
|
+
setClauses.push("sensitivity = ?");
|
|
986
|
+
params.push(input.sensitivity);
|
|
987
|
+
}
|
|
943
988
|
if (input.do_not_contact !== undefined) {
|
|
944
989
|
setClauses.push("do_not_contact = ?");
|
|
945
990
|
params.push(input.do_not_contact ? 1 : 0);
|
|
@@ -987,26 +1032,26 @@ function searchContacts(query, db) {
|
|
|
987
1032
|
const ftsRows = d.query(`
|
|
988
1033
|
SELECT c.* FROM contacts c
|
|
989
1034
|
JOIN contacts_fts fts ON fts.id = c.id
|
|
990
|
-
WHERE contacts_fts MATCH ? AND c.archived = 0
|
|
1035
|
+
WHERE contacts_fts MATCH ? AND c.archived = 0 AND c.sensitivity != 'restricted'
|
|
991
1036
|
ORDER BY rank
|
|
992
1037
|
LIMIT 50
|
|
993
1038
|
`).all(`"${query.replace(/"/g, '""')}"*`);
|
|
994
1039
|
const emailRows = d.query(`
|
|
995
1040
|
SELECT DISTINCT c.* FROM contacts c
|
|
996
1041
|
JOIN emails e ON e.contact_id = c.id
|
|
997
|
-
WHERE e.address LIKE ? AND c.archived = 0
|
|
1042
|
+
WHERE e.address LIKE ? AND c.archived = 0 AND c.sensitivity != 'restricted'
|
|
998
1043
|
LIMIT 20
|
|
999
1044
|
`).all(`%${query}%`);
|
|
1000
1045
|
const phoneRows = d.query(`
|
|
1001
1046
|
SELECT DISTINCT c.* FROM contacts c
|
|
1002
1047
|
JOIN phones p ON p.contact_id = c.id
|
|
1003
|
-
WHERE p.number LIKE ? AND c.archived = 0
|
|
1048
|
+
WHERE p.number LIKE ? AND c.archived = 0 AND c.sensitivity != 'restricted'
|
|
1004
1049
|
LIMIT 20
|
|
1005
1050
|
`).all(`%${query}%`);
|
|
1006
1051
|
const companyRows = d.query(`
|
|
1007
1052
|
SELECT DISTINCT c.* FROM contacts c
|
|
1008
1053
|
JOIN companies co ON co.id = c.company_id
|
|
1009
|
-
WHERE co.name LIKE ? AND c.archived = 0
|
|
1054
|
+
WHERE co.name LIKE ? AND c.archived = 0 AND c.sensitivity != 'restricted'
|
|
1010
1055
|
LIMIT 20
|
|
1011
1056
|
`).all(`%${query}%`);
|
|
1012
1057
|
const seen = new Set;
|
|
@@ -1572,6 +1617,7 @@ function listCompanyEmployees(companyId, db) {
|
|
|
1572
1617
|
follow_up_at: r.follow_up_at ?? null,
|
|
1573
1618
|
archived: !!r.archived,
|
|
1574
1619
|
project_id: r.project_id ?? null,
|
|
1620
|
+
sensitivity: r.sensitivity ?? "normal",
|
|
1575
1621
|
do_not_contact: !!r.do_not_contact,
|
|
1576
1622
|
priority: r.priority ?? 3,
|
|
1577
1623
|
timezone: r.timezone ?? null
|
|
@@ -1702,6 +1748,7 @@ function listContactsByTag(tagId, db) {
|
|
|
1702
1748
|
follow_up_at: r.follow_up_at ?? null,
|
|
1703
1749
|
archived: !!r.archived,
|
|
1704
1750
|
project_id: r.project_id ?? null,
|
|
1751
|
+
sensitivity: r.sensitivity ?? "normal",
|
|
1705
1752
|
do_not_contact: !!r.do_not_contact,
|
|
1706
1753
|
priority: r.priority ?? 3,
|
|
1707
1754
|
timezone: r.timezone ?? null
|
|
@@ -4455,6 +4502,372 @@ function listImages() {
|
|
|
4455
4502
|
path: join3(IMAGES_DIR, f)
|
|
4456
4503
|
}));
|
|
4457
4504
|
}
|
|
4505
|
+
// src/lib/vault.ts
|
|
4506
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3 } from "fs";
|
|
4507
|
+
import { join as join4 } from "path";
|
|
4508
|
+
import { createCipheriv, createDecipheriv, randomBytes, pbkdf2Sync, createHash } from "crypto";
|
|
4509
|
+
var VAULT_DIR = join4(process.env["HOME"] || "~", ".contacts");
|
|
4510
|
+
var VAULT_CONFIG = join4(VAULT_DIR, "vault.json");
|
|
4511
|
+
var DOCUMENTS_DIR = join4(VAULT_DIR, "documents");
|
|
4512
|
+
var _derivedKey = null;
|
|
4513
|
+
function deriveKey(passphrase, salt) {
|
|
4514
|
+
return pbkdf2Sync(passphrase, salt, 1e5, 32, "sha512");
|
|
4515
|
+
}
|
|
4516
|
+
function initVault(passphrase) {
|
|
4517
|
+
if (!existsSync4(VAULT_DIR))
|
|
4518
|
+
mkdirSync3(VAULT_DIR, { recursive: true });
|
|
4519
|
+
if (!existsSync4(DOCUMENTS_DIR))
|
|
4520
|
+
mkdirSync3(DOCUMENTS_DIR, { recursive: true });
|
|
4521
|
+
const salt = randomBytes(32);
|
|
4522
|
+
const key = deriveKey(passphrase, salt);
|
|
4523
|
+
const keyHash = createHash("sha256").update(key).digest("hex");
|
|
4524
|
+
const config = { salt: salt.toString("hex"), key_hash: keyHash, created_at: new Date().toISOString() };
|
|
4525
|
+
writeFileSync2(VAULT_CONFIG, JSON.stringify(config, null, 2));
|
|
4526
|
+
_derivedKey = key;
|
|
4527
|
+
}
|
|
4528
|
+
function isVaultInitialized() {
|
|
4529
|
+
return existsSync4(VAULT_CONFIG);
|
|
4530
|
+
}
|
|
4531
|
+
function unlockVault(passphrase) {
|
|
4532
|
+
if (!existsSync4(VAULT_CONFIG))
|
|
4533
|
+
throw new Error("Vault not initialized. Run 'contacts vault init' first.");
|
|
4534
|
+
const config = JSON.parse(readFileSync3(VAULT_CONFIG, "utf-8"));
|
|
4535
|
+
const salt = Buffer.from(config.salt, "hex");
|
|
4536
|
+
const key = deriveKey(passphrase, salt);
|
|
4537
|
+
const keyHash = createHash("sha256").update(key).digest("hex");
|
|
4538
|
+
if (keyHash !== config.key_hash)
|
|
4539
|
+
return false;
|
|
4540
|
+
_derivedKey = key;
|
|
4541
|
+
return true;
|
|
4542
|
+
}
|
|
4543
|
+
function lockVault() {
|
|
4544
|
+
_derivedKey = null;
|
|
4545
|
+
}
|
|
4546
|
+
function isVaultUnlocked() {
|
|
4547
|
+
return _derivedKey !== null;
|
|
4548
|
+
}
|
|
4549
|
+
function requireVault() {
|
|
4550
|
+
if (!_derivedKey)
|
|
4551
|
+
throw new Error("Vault is locked. Unlock with 'contacts vault unlock' or vault_unlock MCP tool first.");
|
|
4552
|
+
return _derivedKey;
|
|
4553
|
+
}
|
|
4554
|
+
function encrypt(plaintext) {
|
|
4555
|
+
const key = requireVault();
|
|
4556
|
+
const iv = randomBytes(16);
|
|
4557
|
+
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
4558
|
+
let encrypted = cipher.update(plaintext, "utf8", "hex");
|
|
4559
|
+
encrypted += cipher.final("hex");
|
|
4560
|
+
const authTag = cipher.getAuthTag().toString("hex");
|
|
4561
|
+
return { ciphertext: encrypted + ":" + authTag, iv: iv.toString("hex") };
|
|
4562
|
+
}
|
|
4563
|
+
function decrypt(ciphertext, iv) {
|
|
4564
|
+
const key = requireVault();
|
|
4565
|
+
const [encData, authTag] = ciphertext.split(":");
|
|
4566
|
+
if (!encData || !authTag)
|
|
4567
|
+
throw new Error("Invalid ciphertext format");
|
|
4568
|
+
const decipher = createDecipheriv("aes-256-gcm", key, Buffer.from(iv, "hex"));
|
|
4569
|
+
decipher.setAuthTag(Buffer.from(authTag, "hex"));
|
|
4570
|
+
let decrypted = decipher.update(encData, "hex", "utf8");
|
|
4571
|
+
decrypted += decipher.final("utf8");
|
|
4572
|
+
return decrypted;
|
|
4573
|
+
}
|
|
4574
|
+
function encryptFile(sourcePath, entityId) {
|
|
4575
|
+
const key = requireVault();
|
|
4576
|
+
if (!existsSync4(DOCUMENTS_DIR))
|
|
4577
|
+
mkdirSync3(DOCUMENTS_DIR, { recursive: true });
|
|
4578
|
+
const data = readFileSync3(sourcePath);
|
|
4579
|
+
const iv = randomBytes(16);
|
|
4580
|
+
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
4581
|
+
const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
|
|
4582
|
+
const authTag = cipher.getAuthTag();
|
|
4583
|
+
const destPath = join4(DOCUMENTS_DIR, `${entityId}.enc`);
|
|
4584
|
+
const output = Buffer.concat([iv, authTag, encrypted]);
|
|
4585
|
+
writeFileSync2(destPath, output);
|
|
4586
|
+
return destPath;
|
|
4587
|
+
}
|
|
4588
|
+
function decryptFile(encPath) {
|
|
4589
|
+
const key = requireVault();
|
|
4590
|
+
const data = readFileSync3(encPath);
|
|
4591
|
+
const iv = data.subarray(0, 16);
|
|
4592
|
+
const authTag = data.subarray(16, 32);
|
|
4593
|
+
const encrypted = data.subarray(32);
|
|
4594
|
+
const decipher = createDecipheriv("aes-256-gcm", key, iv);
|
|
4595
|
+
decipher.setAuthTag(authTag);
|
|
4596
|
+
return Buffer.concat([decipher.update(encrypted), decipher.final()]);
|
|
4597
|
+
}
|
|
4598
|
+
function getDocumentsDir() {
|
|
4599
|
+
if (!existsSync4(DOCUMENTS_DIR))
|
|
4600
|
+
mkdirSync3(DOCUMENTS_DIR, { recursive: true });
|
|
4601
|
+
return DOCUMENTS_DIR;
|
|
4602
|
+
}
|
|
4603
|
+
// src/db/documents.ts
|
|
4604
|
+
init_database();
|
|
4605
|
+
import { existsSync as existsSync5, unlinkSync as unlinkSync2 } from "fs";
|
|
4606
|
+
var DOCUMENT_TYPES = [
|
|
4607
|
+
"passport",
|
|
4608
|
+
"national_id",
|
|
4609
|
+
"tax_id",
|
|
4610
|
+
"ssn",
|
|
4611
|
+
"drivers_license",
|
|
4612
|
+
"bank_account",
|
|
4613
|
+
"visa",
|
|
4614
|
+
"insurance",
|
|
4615
|
+
"contract",
|
|
4616
|
+
"certificate",
|
|
4617
|
+
"medical_record",
|
|
4618
|
+
"prescription",
|
|
4619
|
+
"allergy_list",
|
|
4620
|
+
"vaccination",
|
|
4621
|
+
"blood_type",
|
|
4622
|
+
"health_insurance",
|
|
4623
|
+
"medical_condition",
|
|
4624
|
+
"emergency_contact_medical",
|
|
4625
|
+
"other"
|
|
4626
|
+
];
|
|
4627
|
+
function addDocument(input, db) {
|
|
4628
|
+
requireVault();
|
|
4629
|
+
const _db2 = db || getDatabase();
|
|
4630
|
+
const id = uuid();
|
|
4631
|
+
const { ciphertext, iv } = encrypt(input.value);
|
|
4632
|
+
let encFilePath = null;
|
|
4633
|
+
if (input.file_path) {
|
|
4634
|
+
encFilePath = encryptFile(input.file_path, id);
|
|
4635
|
+
}
|
|
4636
|
+
_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, encFilePath, JSON.stringify(input.metadata || {}), input.expires_at ?? null, now(), now());
|
|
4637
|
+
return getDocument(id, _db2);
|
|
4638
|
+
}
|
|
4639
|
+
function getDocument(id, db) {
|
|
4640
|
+
requireVault();
|
|
4641
|
+
const _db2 = db || getDatabase();
|
|
4642
|
+
const row = _db2.query(`SELECT * FROM contact_documents WHERE id = ?`).get(id);
|
|
4643
|
+
if (!row)
|
|
4644
|
+
throw new Error(`Document not found: ${id}`);
|
|
4645
|
+
return rowToDoc(row);
|
|
4646
|
+
}
|
|
4647
|
+
function listDocuments(contactId, db) {
|
|
4648
|
+
const _db2 = db || getDatabase();
|
|
4649
|
+
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);
|
|
4650
|
+
return rows.map((r) => ({
|
|
4651
|
+
id: r.id,
|
|
4652
|
+
doc_type: r.doc_type,
|
|
4653
|
+
label: r.label,
|
|
4654
|
+
has_file: !!r.encrypted_file_path,
|
|
4655
|
+
expires_at: r.expires_at,
|
|
4656
|
+
created_at: r.created_at
|
|
4657
|
+
}));
|
|
4658
|
+
}
|
|
4659
|
+
function deleteDocument(id, db) {
|
|
4660
|
+
const _db2 = db || getDatabase();
|
|
4661
|
+
const row = _db2.query(`SELECT encrypted_file_path FROM contact_documents WHERE id = ?`).get(id);
|
|
4662
|
+
if (row?.encrypted_file_path && existsSync5(row.encrypted_file_path)) {
|
|
4663
|
+
try {
|
|
4664
|
+
unlinkSync2(row.encrypted_file_path);
|
|
4665
|
+
} catch {}
|
|
4666
|
+
}
|
|
4667
|
+
_db2.query(`DELETE FROM contact_documents WHERE id = ?`).run(id);
|
|
4668
|
+
}
|
|
4669
|
+
function rowToDoc(row) {
|
|
4670
|
+
return {
|
|
4671
|
+
id: row.id,
|
|
4672
|
+
contact_id: row.contact_id,
|
|
4673
|
+
doc_type: row.doc_type,
|
|
4674
|
+
label: row.label,
|
|
4675
|
+
value: decrypt(row.encrypted_value, row.iv),
|
|
4676
|
+
has_file: !!row.encrypted_file_path,
|
|
4677
|
+
metadata: JSON.parse(row.metadata || "{}"),
|
|
4678
|
+
expires_at: row.expires_at,
|
|
4679
|
+
created_at: row.created_at,
|
|
4680
|
+
updated_at: row.updated_at
|
|
4681
|
+
};
|
|
4682
|
+
}
|
|
4683
|
+
// src/db/health.ts
|
|
4684
|
+
init_database();
|
|
4685
|
+
function setHealthData(contactId, input, db) {
|
|
4686
|
+
requireVault();
|
|
4687
|
+
const _db2 = db || getDatabase();
|
|
4688
|
+
const existing = _db2.query(`SELECT id FROM contact_health WHERE contact_id = ?`).get(contactId);
|
|
4689
|
+
if (existing) {
|
|
4690
|
+
const sets = [];
|
|
4691
|
+
const params = [];
|
|
4692
|
+
if (input.blood_type !== undefined) {
|
|
4693
|
+
sets.push("blood_type = ?");
|
|
4694
|
+
params.push(input.blood_type);
|
|
4695
|
+
}
|
|
4696
|
+
if (input.allergies !== undefined) {
|
|
4697
|
+
sets.push("allergies = ?");
|
|
4698
|
+
params.push(JSON.stringify(input.allergies));
|
|
4699
|
+
}
|
|
4700
|
+
if (input.medical_conditions !== undefined) {
|
|
4701
|
+
sets.push("medical_conditions = ?");
|
|
4702
|
+
params.push(JSON.stringify(input.medical_conditions));
|
|
4703
|
+
}
|
|
4704
|
+
if (input.medications !== undefined) {
|
|
4705
|
+
sets.push("medications = ?");
|
|
4706
|
+
params.push(JSON.stringify(input.medications));
|
|
4707
|
+
}
|
|
4708
|
+
if (input.emergency_contacts !== undefined) {
|
|
4709
|
+
sets.push("emergency_contacts = ?");
|
|
4710
|
+
params.push(JSON.stringify(input.emergency_contacts));
|
|
4711
|
+
}
|
|
4712
|
+
if (input.health_insurance_provider !== undefined) {
|
|
4713
|
+
sets.push("health_insurance_provider = ?");
|
|
4714
|
+
params.push(input.health_insurance_provider);
|
|
4715
|
+
}
|
|
4716
|
+
if (input.health_insurance_id !== undefined) {
|
|
4717
|
+
sets.push("health_insurance_id = ?");
|
|
4718
|
+
params.push(input.health_insurance_id);
|
|
4719
|
+
}
|
|
4720
|
+
if (input.primary_physician !== undefined) {
|
|
4721
|
+
sets.push("primary_physician = ?");
|
|
4722
|
+
params.push(input.primary_physician);
|
|
4723
|
+
}
|
|
4724
|
+
if (input.primary_physician_phone !== undefined) {
|
|
4725
|
+
sets.push("primary_physician_phone = ?");
|
|
4726
|
+
params.push(input.primary_physician_phone);
|
|
4727
|
+
}
|
|
4728
|
+
if (input.organ_donor !== undefined) {
|
|
4729
|
+
sets.push("organ_donor = ?");
|
|
4730
|
+
params.push(input.organ_donor ? 1 : 0);
|
|
4731
|
+
}
|
|
4732
|
+
if (input.notes !== undefined) {
|
|
4733
|
+
sets.push("notes = ?");
|
|
4734
|
+
params.push(input.notes);
|
|
4735
|
+
}
|
|
4736
|
+
if (sets.length) {
|
|
4737
|
+
sets.push("updated_at = ?");
|
|
4738
|
+
params.push(now());
|
|
4739
|
+
params.push(contactId);
|
|
4740
|
+
_db2.query(`UPDATE contact_health SET ${sets.join(", ")} WHERE contact_id = ?`).run(...params);
|
|
4741
|
+
}
|
|
4742
|
+
} else {
|
|
4743
|
+
const id = uuid();
|
|
4744
|
+
_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());
|
|
4745
|
+
}
|
|
4746
|
+
return getHealthData(contactId, _db2);
|
|
4747
|
+
}
|
|
4748
|
+
function getHealthData(contactId, db) {
|
|
4749
|
+
requireVault();
|
|
4750
|
+
const _db2 = db || getDatabase();
|
|
4751
|
+
const row = _db2.query(`SELECT * FROM contact_health WHERE contact_id = ?`).get(contactId);
|
|
4752
|
+
if (!row)
|
|
4753
|
+
return null;
|
|
4754
|
+
return {
|
|
4755
|
+
id: row.id,
|
|
4756
|
+
contact_id: row.contact_id,
|
|
4757
|
+
blood_type: row.blood_type,
|
|
4758
|
+
allergies: JSON.parse(row.allergies || "[]"),
|
|
4759
|
+
medical_conditions: JSON.parse(row.medical_conditions || "[]"),
|
|
4760
|
+
medications: JSON.parse(row.medications || "[]"),
|
|
4761
|
+
emergency_contacts: JSON.parse(row.emergency_contacts || "[]"),
|
|
4762
|
+
health_insurance_provider: row.health_insurance_provider,
|
|
4763
|
+
health_insurance_id: row.health_insurance_id,
|
|
4764
|
+
primary_physician: row.primary_physician,
|
|
4765
|
+
primary_physician_phone: row.primary_physician_phone,
|
|
4766
|
+
organ_donor: !!row.organ_donor,
|
|
4767
|
+
notes: row.notes,
|
|
4768
|
+
created_at: row.created_at,
|
|
4769
|
+
updated_at: row.updated_at
|
|
4770
|
+
};
|
|
4771
|
+
}
|
|
4772
|
+
function deleteHealthData(contactId, db) {
|
|
4773
|
+
const _db2 = db || getDatabase();
|
|
4774
|
+
_db2.query(`DELETE FROM contact_health WHERE contact_id = ?`).run(contactId);
|
|
4775
|
+
}
|
|
4776
|
+
// src/lib/document-scanner.ts
|
|
4777
|
+
import { readFileSync as readFileSync4, existsSync as existsSync6 } from "fs";
|
|
4778
|
+
import { extname as extname2 } from "path";
|
|
4779
|
+
async function scanDocument(imageSource, docType) {
|
|
4780
|
+
const apiKey = process.env["OPENAI_API_KEY"];
|
|
4781
|
+
if (!apiKey) {
|
|
4782
|
+
throw new Error("OPENAI_API_KEY not set. Set it in ~/.secrets or environment to use document scanning.");
|
|
4783
|
+
}
|
|
4784
|
+
let imageData;
|
|
4785
|
+
if (imageSource.startsWith("data:image/")) {
|
|
4786
|
+
imageData = imageSource;
|
|
4787
|
+
} else if (existsSync6(imageSource)) {
|
|
4788
|
+
const buffer = readFileSync4(imageSource);
|
|
4789
|
+
const ext = extname2(imageSource).slice(1).toLowerCase();
|
|
4790
|
+
const mime = ext === "jpg" || ext === "jpeg" ? "image/jpeg" : ext === "png" ? "image/png" : ext === "webp" ? "image/webp" : ext === "gif" ? "image/gif" : "image/jpeg";
|
|
4791
|
+
imageData = `data:${mime};base64,${buffer.toString("base64")}`;
|
|
4792
|
+
} else if (/^[A-Za-z0-9+/=\n\r]+$/.test(imageSource.trim()) && imageSource.length > 100) {
|
|
4793
|
+
imageData = `data:image/jpeg;base64,${imageSource.trim()}`;
|
|
4794
|
+
} else {
|
|
4795
|
+
throw new Error(`Image source not found or invalid: ${imageSource.slice(0, 50)}...`);
|
|
4796
|
+
}
|
|
4797
|
+
const typeHint = docType ? ` This is a ${docType} document.` : "";
|
|
4798
|
+
const prompt = `Extract all text and structured data from this document image.${typeHint} Return a JSON object with these fields:
|
|
4799
|
+
- document_type: detected type (passport, national_id, drivers_license, tax_document, medical_record, prescription, insurance_card, bank_statement, visa, certificate, contract, other)
|
|
4800
|
+
- full_name: full name as shown
|
|
4801
|
+
- date_of_birth: in YYYY-MM-DD format if visible
|
|
4802
|
+
- document_number: main ID/document number
|
|
4803
|
+
- issuing_country: country code or name
|
|
4804
|
+
- issue_date: in YYYY-MM-DD format if visible
|
|
4805
|
+
- expiry_date: in YYYY-MM-DD format if visible
|
|
4806
|
+
- address: full address if visible
|
|
4807
|
+
- nationality: if visible
|
|
4808
|
+
- gender: if visible
|
|
4809
|
+
- mrz_code: Machine Readable Zone text if this is a passport/ID with MRZ
|
|
4810
|
+
- phone: any phone numbers visible
|
|
4811
|
+
- email: any email addresses visible
|
|
4812
|
+
- additional_fields: object with any other visible structured data
|
|
4813
|
+
- raw_text: all visible text transcribed
|
|
4814
|
+
|
|
4815
|
+
Only include fields that are actually visible in the document. Return valid JSON only.`;
|
|
4816
|
+
const response = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
4817
|
+
method: "POST",
|
|
4818
|
+
headers: {
|
|
4819
|
+
Authorization: `Bearer ${apiKey}`,
|
|
4820
|
+
"Content-Type": "application/json"
|
|
4821
|
+
},
|
|
4822
|
+
body: JSON.stringify({
|
|
4823
|
+
model: "gpt-4o",
|
|
4824
|
+
messages: [
|
|
4825
|
+
{
|
|
4826
|
+
role: "user",
|
|
4827
|
+
content: [
|
|
4828
|
+
{ type: "text", text: prompt },
|
|
4829
|
+
{ type: "image_url", image_url: { url: imageData, detail: "high" } }
|
|
4830
|
+
]
|
|
4831
|
+
}
|
|
4832
|
+
],
|
|
4833
|
+
max_tokens: 2000,
|
|
4834
|
+
temperature: 0
|
|
4835
|
+
})
|
|
4836
|
+
});
|
|
4837
|
+
if (!response.ok) {
|
|
4838
|
+
const err = await response.text();
|
|
4839
|
+
throw new Error(`OpenAI API error: ${response.status} \u2014 ${err}`);
|
|
4840
|
+
}
|
|
4841
|
+
const data = await response.json();
|
|
4842
|
+
const content = data.choices?.[0]?.message?.content || "";
|
|
4843
|
+
const jsonMatch = content.match(/```json\s*([\s\S]*?)```/) || content.match(/\{[\s\S]*\}/);
|
|
4844
|
+
if (!jsonMatch) {
|
|
4845
|
+
return { fields: {}, raw_text: content, document_type: docType || "unknown", confidence: 0.3 };
|
|
4846
|
+
}
|
|
4847
|
+
try {
|
|
4848
|
+
const parsed = JSON.parse(jsonMatch[1] || jsonMatch[0]);
|
|
4849
|
+
const { document_type, raw_text, additional_fields, ...mainFields } = parsed;
|
|
4850
|
+
const fields = {};
|
|
4851
|
+
for (const [k, v] of Object.entries(mainFields)) {
|
|
4852
|
+
if (v && typeof v === "string")
|
|
4853
|
+
fields[k] = v;
|
|
4854
|
+
}
|
|
4855
|
+
if (additional_fields && typeof additional_fields === "object") {
|
|
4856
|
+
for (const [k, v] of Object.entries(additional_fields)) {
|
|
4857
|
+
if (v && typeof v === "string")
|
|
4858
|
+
fields[k] = v;
|
|
4859
|
+
}
|
|
4860
|
+
}
|
|
4861
|
+
return {
|
|
4862
|
+
fields,
|
|
4863
|
+
raw_text: raw_text || content,
|
|
4864
|
+
document_type: document_type || docType || "unknown",
|
|
4865
|
+
confidence: Object.keys(fields).length > 3 ? 0.9 : Object.keys(fields).length > 0 ? 0.7 : 0.3
|
|
4866
|
+
};
|
|
4867
|
+
} catch {
|
|
4868
|
+
return { fields: {}, raw_text: content, document_type: docType || "unknown", confidence: 0.3 };
|
|
4869
|
+
}
|
|
4870
|
+
}
|
|
4458
4871
|
export {
|
|
4459
4872
|
updateVendorCommunication,
|
|
4460
4873
|
updateTag,
|
|
@@ -4465,20 +4878,24 @@ export {
|
|
|
4465
4878
|
updateContact,
|
|
4466
4879
|
updateCompany,
|
|
4467
4880
|
updateApplication,
|
|
4881
|
+
unlockVault,
|
|
4468
4882
|
unarchiveContact,
|
|
4469
4883
|
unarchiveCompany,
|
|
4884
|
+
setHealthData,
|
|
4470
4885
|
setDealContactRole,
|
|
4471
4886
|
semanticSearch,
|
|
4472
4887
|
searchLearnings,
|
|
4473
4888
|
searchGoogleContacts,
|
|
4474
4889
|
searchContacts,
|
|
4475
4890
|
searchCompanies,
|
|
4891
|
+
scanDocument,
|
|
4476
4892
|
saveLearning,
|
|
4477
4893
|
saveImage,
|
|
4478
4894
|
runConnector,
|
|
4479
4895
|
resolveIdentity,
|
|
4480
4896
|
resolveByPartial,
|
|
4481
4897
|
resetDatabase,
|
|
4898
|
+
requireVault,
|
|
4482
4899
|
removeTagFromContact,
|
|
4483
4900
|
removeTagFromCompany,
|
|
4484
4901
|
removeOrgMember,
|
|
@@ -4501,6 +4918,7 @@ export {
|
|
|
4501
4918
|
logEvent,
|
|
4502
4919
|
logAgentActivity,
|
|
4503
4920
|
logActivity,
|
|
4921
|
+
lockVault,
|
|
4504
4922
|
listVendorCommunications,
|
|
4505
4923
|
listTags,
|
|
4506
4924
|
listRelationships,
|
|
@@ -4522,6 +4940,7 @@ export {
|
|
|
4522
4940
|
listGoogleContacts,
|
|
4523
4941
|
listFollowUpDue,
|
|
4524
4942
|
listEvents,
|
|
4943
|
+
listDocuments,
|
|
4525
4944
|
listDeals,
|
|
4526
4945
|
listContactsInGroup,
|
|
4527
4946
|
listContactsByTag,
|
|
@@ -4535,6 +4954,9 @@ export {
|
|
|
4535
4954
|
listColdContacts,
|
|
4536
4955
|
listApplications,
|
|
4537
4956
|
listActivity,
|
|
4957
|
+
isVaultUnlocked,
|
|
4958
|
+
isVaultInitialized,
|
|
4959
|
+
initVault,
|
|
4538
4960
|
ingestMeetingParticipants,
|
|
4539
4961
|
importToApple,
|
|
4540
4962
|
importFromCsv,
|
|
@@ -4557,12 +4979,15 @@ export {
|
|
|
4557
4979
|
getImagePath,
|
|
4558
4980
|
getImageAsBase64,
|
|
4559
4981
|
getIdentities,
|
|
4982
|
+
getHealthData,
|
|
4560
4983
|
getGroup,
|
|
4561
4984
|
getGhostContacts,
|
|
4562
4985
|
getFreshnessScore,
|
|
4563
4986
|
getFieldHistory,
|
|
4564
4987
|
getEvent,
|
|
4565
4988
|
getEntityTeam,
|
|
4989
|
+
getDocumentsDir,
|
|
4990
|
+
getDocument,
|
|
4566
4991
|
getDealsByStage,
|
|
4567
4992
|
getDealTeam,
|
|
4568
4993
|
getDeal,
|
|
@@ -4589,6 +5014,8 @@ export {
|
|
|
4589
5014
|
extractContactsFromEmailThread,
|
|
4590
5015
|
exportFromApple,
|
|
4591
5016
|
exportContacts,
|
|
5017
|
+
encryptFile,
|
|
5018
|
+
encrypt,
|
|
4592
5019
|
embedContact,
|
|
4593
5020
|
embedAllContacts,
|
|
4594
5021
|
domainToCompany,
|
|
@@ -4599,14 +5026,18 @@ export {
|
|
|
4599
5026
|
deleteNote,
|
|
4600
5027
|
deleteLearning,
|
|
4601
5028
|
deleteImage,
|
|
5029
|
+
deleteHealthData,
|
|
4602
5030
|
deleteGroup,
|
|
4603
5031
|
deleteEvent,
|
|
5032
|
+
deleteDocument,
|
|
4604
5033
|
deleteDeal,
|
|
4605
5034
|
deleteContactTask,
|
|
4606
5035
|
deleteContact,
|
|
4607
5036
|
deleteCompanyRelationship,
|
|
4608
5037
|
deleteCompany,
|
|
4609
5038
|
deleteApplication,
|
|
5039
|
+
decryptFile,
|
|
5040
|
+
decrypt,
|
|
4610
5041
|
decayLearnings,
|
|
4611
5042
|
createTag,
|
|
4612
5043
|
createRelationship,
|
|
@@ -4638,11 +5069,13 @@ export {
|
|
|
4638
5069
|
addJobEntry,
|
|
4639
5070
|
addIdentity,
|
|
4640
5071
|
addEmailToContact,
|
|
5072
|
+
addDocument,
|
|
4641
5073
|
addContactToGroup,
|
|
4642
5074
|
addCompanyToGroup,
|
|
4643
5075
|
acquireLock,
|
|
4644
5076
|
TagNotFoundError,
|
|
4645
5077
|
DuplicateTagNameError,
|
|
5078
|
+
DOCUMENT_TYPES,
|
|
4646
5079
|
ContactNotFoundError,
|
|
4647
5080
|
ConnectorNotInstalledError,
|
|
4648
5081
|
ConnectorAuthError,
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface DocumentScanResult {
|
|
2
|
+
fields: Record<string, string>;
|
|
3
|
+
raw_text: string;
|
|
4
|
+
document_type: string;
|
|
5
|
+
confidence: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function scanDocument(imageSource: string, docType?: string): Promise<DocumentScanResult>;
|
|
8
|
+
//# sourceMappingURL=document-scanner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"document-scanner.d.ts","sourceRoot":"","sources":["../../src/lib/document-scanner.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,wBAAsB,YAAY,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAoGrG"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare function initVault(passphrase: string): void;
|
|
2
|
+
export declare function isVaultInitialized(): boolean;
|
|
3
|
+
export declare function unlockVault(passphrase: string): boolean;
|
|
4
|
+
export declare function lockVault(): void;
|
|
5
|
+
export declare function isVaultUnlocked(): boolean;
|
|
6
|
+
export declare function requireVault(): Buffer;
|
|
7
|
+
export declare function encrypt(plaintext: string): {
|
|
8
|
+
ciphertext: string;
|
|
9
|
+
iv: string;
|
|
10
|
+
};
|
|
11
|
+
export declare function decrypt(ciphertext: string, iv: string): string;
|
|
12
|
+
export declare function encryptFile(sourcePath: string, entityId: string): string;
|
|
13
|
+
export declare function decryptFile(encPath: string): Buffer;
|
|
14
|
+
export declare function getDocumentsDir(): string;
|
|
15
|
+
//# sourceMappingURL=vault.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vault.d.ts","sourceRoot":"","sources":["../../src/lib/vault.ts"],"names":[],"mappings":"AAoBA,wBAAgB,SAAS,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CASlD;AAED,wBAAgB,kBAAkB,IAAI,OAAO,CAE5C;AAED,wBAAgB,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CASvD;AAED,wBAAgB,SAAS,IAAI,IAAI,CAEhC;AAED,wBAAgB,eAAe,IAAI,OAAO,CAEzC;AAED,wBAAgB,YAAY,IAAI,MAAM,CAGrC;AAED,wBAAgB,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAQ7E;AAED,wBAAgB,OAAO,CAAC,UAAU,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAS9D;AAED,wBAAgB,WAAW,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAaxE;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CASnD;AAED,wBAAgB,eAAe,IAAI,MAAM,CAGxC"}
|