@hasna/contacts 0.5.2 → 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/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
@@ -4376,6 +4423,451 @@ async function ingestMeetingParticipants(event, db) {
4376
4423
 
4377
4424
  // src/index.ts
4378
4425
  init_contacts();
4426
+
4427
+ // src/lib/images.ts
4428
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, copyFileSync, unlinkSync, readdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
4429
+ import { join as join3, extname, basename } from "path";
4430
+ var IMAGES_DIR = join3(process.env["HOME"] || "~", ".contacts", "images");
4431
+ function ensureImagesDir() {
4432
+ if (!existsSync3(IMAGES_DIR))
4433
+ mkdirSync2(IMAGES_DIR, { recursive: true });
4434
+ }
4435
+ function getImagesDir() {
4436
+ ensureImagesDir();
4437
+ return IMAGES_DIR;
4438
+ }
4439
+ function saveImage(entityId, source, options) {
4440
+ ensureImagesDir();
4441
+ deleteImage(entityId);
4442
+ const base64Match = source.match(/^data:image\/(\w+);base64,(.+)$/s);
4443
+ if (base64Match) {
4444
+ const ext2 = base64Match[1] === "jpeg" ? "jpg" : base64Match[1];
4445
+ const data = Buffer.from(base64Match[2], "base64");
4446
+ const filename2 = `${entityId}.${ext2}`;
4447
+ writeFileSync(join3(IMAGES_DIR, filename2), data);
4448
+ return filename2;
4449
+ }
4450
+ if (!existsSync3(source) && /^[A-Za-z0-9+/=\n\r]+$/.test(source.trim()) && source.length > 100) {
4451
+ const ext2 = options?.format || "jpg";
4452
+ const data = Buffer.from(source.trim(), "base64");
4453
+ const filename2 = `${entityId}.${ext2}`;
4454
+ writeFileSync(join3(IMAGES_DIR, filename2), data);
4455
+ return filename2;
4456
+ }
4457
+ if (!existsSync3(source)) {
4458
+ throw new Error(`Image file not found: ${source}`);
4459
+ }
4460
+ const ext = extname(source).slice(1).toLowerCase() || "jpg";
4461
+ const validExts = ["jpg", "jpeg", "png", "gif", "webp", "svg", "avif", "ico"];
4462
+ if (!validExts.includes(ext)) {
4463
+ throw new Error(`Unsupported image format: ${ext}. Supported: ${validExts.join(", ")}`);
4464
+ }
4465
+ const filename = `${entityId}.${ext === "jpeg" ? "jpg" : ext}`;
4466
+ copyFileSync(source, join3(IMAGES_DIR, filename));
4467
+ return filename;
4468
+ }
4469
+ function getImagePath(entityId) {
4470
+ ensureImagesDir();
4471
+ const files = readdirSync(IMAGES_DIR);
4472
+ const match = files.find((f) => f.startsWith(`${entityId}.`));
4473
+ return match ? join3(IMAGES_DIR, match) : null;
4474
+ }
4475
+ function getImageAsBase64(entityId) {
4476
+ const path = getImagePath(entityId);
4477
+ if (!path)
4478
+ return null;
4479
+ const ext = extname(path).slice(1);
4480
+ const mime = ext === "jpg" ? "image/jpeg" : ext === "svg" ? "image/svg+xml" : `image/${ext}`;
4481
+ const data = readFileSync2(path);
4482
+ return `data:${mime};base64,${data.toString("base64")}`;
4483
+ }
4484
+ function deleteImage(entityId) {
4485
+ ensureImagesDir();
4486
+ const files = readdirSync(IMAGES_DIR);
4487
+ let deleted = false;
4488
+ for (const f of files) {
4489
+ if (f.startsWith(`${entityId}.`)) {
4490
+ unlinkSync(join3(IMAGES_DIR, f));
4491
+ deleted = true;
4492
+ }
4493
+ }
4494
+ return deleted;
4495
+ }
4496
+ function listImages() {
4497
+ ensureImagesDir();
4498
+ const files = readdirSync(IMAGES_DIR).filter((f) => !f.startsWith("."));
4499
+ return files.map((f) => ({
4500
+ entity_id: basename(f, extname(f)),
4501
+ filename: f,
4502
+ path: join3(IMAGES_DIR, f)
4503
+ }));
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
+ }
4379
4871
  export {
4380
4872
  updateVendorCommunication,
4381
4873
  updateTag,
@@ -4386,19 +4878,24 @@ export {
4386
4878
  updateContact,
4387
4879
  updateCompany,
4388
4880
  updateApplication,
4881
+ unlockVault,
4389
4882
  unarchiveContact,
4390
4883
  unarchiveCompany,
4884
+ setHealthData,
4391
4885
  setDealContactRole,
4392
4886
  semanticSearch,
4393
4887
  searchLearnings,
4394
4888
  searchGoogleContacts,
4395
4889
  searchContacts,
4396
4890
  searchCompanies,
4891
+ scanDocument,
4397
4892
  saveLearning,
4893
+ saveImage,
4398
4894
  runConnector,
4399
4895
  resolveIdentity,
4400
4896
  resolveByPartial,
4401
4897
  resetDatabase,
4898
+ requireVault,
4402
4899
  removeTagFromContact,
4403
4900
  removeTagFromCompany,
4404
4901
  removeOrgMember,
@@ -4421,6 +4918,7 @@ export {
4421
4918
  logEvent,
4422
4919
  logAgentActivity,
4423
4920
  logActivity,
4921
+ lockVault,
4424
4922
  listVendorCommunications,
4425
4923
  listTags,
4426
4924
  listRelationships,
@@ -4435,12 +4933,14 @@ export {
4435
4933
  listNotesForContactAtCompany,
4436
4934
  listNotes,
4437
4935
  listMissingInvoices,
4936
+ listImages,
4438
4937
  listGroupsForContact,
4439
4938
  listGroupsForCompany,
4440
4939
  listGroups,
4441
4940
  listGoogleContacts,
4442
4941
  listFollowUpDue,
4443
4942
  listEvents,
4943
+ listDocuments,
4444
4944
  listDeals,
4445
4945
  listContactsInGroup,
4446
4946
  listContactsByTag,
@@ -4454,6 +4954,9 @@ export {
4454
4954
  listColdContacts,
4455
4955
  listApplications,
4456
4956
  listActivity,
4957
+ isVaultUnlocked,
4958
+ isVaultInitialized,
4959
+ initVault,
4457
4960
  ingestMeetingParticipants,
4458
4961
  importToApple,
4459
4962
  importFromCsv,
@@ -4472,13 +4975,19 @@ export {
4472
4975
  getNetworkStats,
4473
4976
  getLearnings,
4474
4977
  getJobHistory,
4978
+ getImagesDir,
4979
+ getImagePath,
4980
+ getImageAsBase64,
4475
4981
  getIdentities,
4982
+ getHealthData,
4476
4983
  getGroup,
4477
4984
  getGhostContacts,
4478
4985
  getFreshnessScore,
4479
4986
  getFieldHistory,
4480
4987
  getEvent,
4481
4988
  getEntityTeam,
4989
+ getDocumentsDir,
4990
+ getDocument,
4482
4991
  getDealsByStage,
4483
4992
  getDealTeam,
4484
4993
  getDeal,
@@ -4505,6 +5014,8 @@ export {
4505
5014
  extractContactsFromEmailThread,
4506
5015
  exportFromApple,
4507
5016
  exportContacts,
5017
+ encryptFile,
5018
+ encrypt,
4508
5019
  embedContact,
4509
5020
  embedAllContacts,
4510
5021
  domainToCompany,
@@ -4514,14 +5025,19 @@ export {
4514
5025
  deleteRelationship,
4515
5026
  deleteNote,
4516
5027
  deleteLearning,
5028
+ deleteImage,
5029
+ deleteHealthData,
4517
5030
  deleteGroup,
4518
5031
  deleteEvent,
5032
+ deleteDocument,
4519
5033
  deleteDeal,
4520
5034
  deleteContactTask,
4521
5035
  deleteContact,
4522
5036
  deleteCompanyRelationship,
4523
5037
  deleteCompany,
4524
5038
  deleteApplication,
5039
+ decryptFile,
5040
+ decrypt,
4525
5041
  decayLearnings,
4526
5042
  createTag,
4527
5043
  createRelationship,
@@ -4553,11 +5069,13 @@ export {
4553
5069
  addJobEntry,
4554
5070
  addIdentity,
4555
5071
  addEmailToContact,
5072
+ addDocument,
4556
5073
  addContactToGroup,
4557
5074
  addCompanyToGroup,
4558
5075
  acquireLock,
4559
5076
  TagNotFoundError,
4560
5077
  DuplicateTagNameError,
5078
+ DOCUMENT_TYPES,
4561
5079
  ContactNotFoundError,
4562
5080
  ConnectorNotInstalledError,
4563
5081
  ConnectorAuthError,