@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/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"}