@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/cli/index.js CHANGED
@@ -4,6 +4,7 @@ var __create = Object.create;
4
4
  var __getProtoOf = Object.getPrototypeOf;
5
5
  var __defProp = Object.defineProperty;
6
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
7
8
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
9
  var __toESM = (mod, isNodeMode, target) => {
9
10
  target = mod != null ? __create(__getProtoOf(mod)) : {};
@@ -16,6 +17,20 @@ var __toESM = (mod, isNodeMode, target) => {
16
17
  });
17
18
  return to;
18
19
  };
20
+ var __moduleCache = /* @__PURE__ */ new WeakMap;
21
+ var __toCommonJS = (from) => {
22
+ var entry = __moduleCache.get(from), desc;
23
+ if (entry)
24
+ return entry;
25
+ entry = __defProp({}, "__esModule", { value: true });
26
+ if (from && typeof from === "object" || typeof from === "function")
27
+ __getOwnPropNames(from).map((key) => !__hasOwnProp.call(entry, key) && __defProp(entry, key, {
28
+ get: () => from[key],
29
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
30
+ }));
31
+ __moduleCache.set(from, entry);
32
+ return entry;
33
+ };
19
34
  var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
20
35
  var __export = (target, all) => {
21
36
  for (var name in all)
@@ -2632,6 +2647,41 @@ var init_database = __esm(() => {
2632
2647
  created_at TEXT NOT NULL DEFAULT (datetime('now')),
2633
2648
  updated_at TEXT NOT NULL DEFAULT (datetime('now'))
2634
2649
  );
2650
+ `,
2651
+ `
2652
+ ALTER TABLE contacts ADD COLUMN sensitivity TEXT NOT NULL DEFAULT 'normal' CHECK(sensitivity IN ('normal','confidential','restricted'));
2653
+
2654
+ CREATE TABLE IF NOT EXISTS contact_documents (
2655
+ id TEXT PRIMARY KEY,
2656
+ contact_id TEXT NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
2657
+ doc_type TEXT NOT NULL,
2658
+ label TEXT,
2659
+ encrypted_value TEXT NOT NULL,
2660
+ iv TEXT NOT NULL,
2661
+ encrypted_file_path TEXT,
2662
+ metadata TEXT NOT NULL DEFAULT '{}',
2663
+ expires_at TEXT,
2664
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
2665
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
2666
+ );
2667
+
2668
+ CREATE TABLE IF NOT EXISTS contact_health (
2669
+ id TEXT PRIMARY KEY,
2670
+ contact_id TEXT NOT NULL UNIQUE REFERENCES contacts(id) ON DELETE CASCADE,
2671
+ blood_type TEXT,
2672
+ allergies TEXT NOT NULL DEFAULT '[]',
2673
+ medical_conditions TEXT NOT NULL DEFAULT '[]',
2674
+ medications TEXT NOT NULL DEFAULT '[]',
2675
+ emergency_contacts TEXT NOT NULL DEFAULT '[]',
2676
+ health_insurance_provider TEXT,
2677
+ health_insurance_id TEXT,
2678
+ primary_physician TEXT,
2679
+ primary_physician_phone TEXT,
2680
+ organ_donor INTEGER NOT NULL DEFAULT 0,
2681
+ notes TEXT,
2682
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
2683
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
2684
+ );
2635
2685
  `
2636
2686
  ];
2637
2687
  });
@@ -2681,6 +2731,7 @@ function rowToContact(row) {
2681
2731
  follow_up_at: row.follow_up_at ?? null,
2682
2732
  archived: !!row.archived,
2683
2733
  project_id: row.project_id ?? null,
2734
+ sensitivity: row.sensitivity ?? "normal",
2684
2735
  do_not_contact: !!row.do_not_contact,
2685
2736
  priority: row.priority ?? 3,
2686
2737
  timezone: row.timezone ?? null
@@ -2768,8 +2819,8 @@ function createContact(input, db) {
2768
2819
  const firstName = input.first_name ?? "";
2769
2820
  const lastName = input.last_name ?? "";
2770
2821
  const displayName = input.display_name ?? (firstName || lastName ? `${firstName} ${lastName}`.trim() : "Unnamed Contact");
2771
- 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)
2772
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
2822
+ 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)
2823
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
2773
2824
  id,
2774
2825
  firstName,
2775
2826
  lastName,
@@ -2788,6 +2839,7 @@ function createContact(input, db) {
2788
2839
  input.status ?? "active",
2789
2840
  input.follow_up_at ?? null,
2790
2841
  input.project_id ?? null,
2842
+ input.sensitivity ?? "normal",
2791
2843
  input.do_not_contact ? 1 : 0,
2792
2844
  input.priority ?? 3,
2793
2845
  input.timezone ?? null,
@@ -2836,6 +2888,7 @@ function listContacts(opts = {}, db) {
2836
2888
  order_by = "display_name",
2837
2889
  order_dir = "asc",
2838
2890
  include_dnc = false,
2891
+ include_restricted = false,
2839
2892
  priority_min,
2840
2893
  updated_since
2841
2894
  } = opts;
@@ -2846,6 +2899,9 @@ function listContacts(opts = {}, db) {
2846
2899
  if (!include_dnc) {
2847
2900
  conditions.push("c.do_not_contact = 0");
2848
2901
  }
2902
+ if (!include_restricted) {
2903
+ conditions.push("c.sensitivity != 'restricted'");
2904
+ }
2849
2905
  if (company_id) {
2850
2906
  conditions.push("c.company_id = ?");
2851
2907
  params.push(company_id);
@@ -2974,6 +3030,10 @@ function updateContact(id, input, db) {
2974
3030
  setClauses.push("project_id = ?");
2975
3031
  params.push(input.project_id);
2976
3032
  }
3033
+ if (input.sensitivity !== undefined) {
3034
+ setClauses.push("sensitivity = ?");
3035
+ params.push(input.sensitivity);
3036
+ }
2977
3037
  if (input.do_not_contact !== undefined) {
2978
3038
  setClauses.push("do_not_contact = ?");
2979
3039
  params.push(input.do_not_contact ? 1 : 0);
@@ -3021,26 +3081,26 @@ function searchContacts(query, db) {
3021
3081
  const ftsRows = d.query(`
3022
3082
  SELECT c.* FROM contacts c
3023
3083
  JOIN contacts_fts fts ON fts.id = c.id
3024
- WHERE contacts_fts MATCH ? AND c.archived = 0
3084
+ WHERE contacts_fts MATCH ? AND c.archived = 0 AND c.sensitivity != 'restricted'
3025
3085
  ORDER BY rank
3026
3086
  LIMIT 50
3027
3087
  `).all(`"${query.replace(/"/g, '""')}"*`);
3028
3088
  const emailRows = d.query(`
3029
3089
  SELECT DISTINCT c.* FROM contacts c
3030
3090
  JOIN emails e ON e.contact_id = c.id
3031
- WHERE e.address LIKE ? AND c.archived = 0
3091
+ WHERE e.address LIKE ? AND c.archived = 0 AND c.sensitivity != 'restricted'
3032
3092
  LIMIT 20
3033
3093
  `).all(`%${query}%`);
3034
3094
  const phoneRows = d.query(`
3035
3095
  SELECT DISTINCT c.* FROM contacts c
3036
3096
  JOIN phones p ON p.contact_id = c.id
3037
- WHERE p.number LIKE ? AND c.archived = 0
3097
+ WHERE p.number LIKE ? AND c.archived = 0 AND c.sensitivity != 'restricted'
3038
3098
  LIMIT 20
3039
3099
  `).all(`%${query}%`);
3040
3100
  const companyRows = d.query(`
3041
3101
  SELECT DISTINCT c.* FROM contacts c
3042
3102
  JOIN companies co ON co.id = c.company_id
3043
- WHERE co.name LIKE ? AND c.archived = 0
3103
+ WHERE co.name LIKE ? AND c.archived = 0 AND c.sensitivity != 'restricted'
3044
3104
  LIMIT 20
3045
3105
  `).all(`%${query}%`);
3046
3106
  const seen = new Set;
@@ -3263,6 +3323,19 @@ var init_contacts = __esm(() => {
3263
3323
  });
3264
3324
 
3265
3325
  // src/db/companies.ts
3326
+ var exports_companies = {};
3327
+ __export(exports_companies, {
3328
+ updateCompany: () => updateCompany,
3329
+ unarchiveCompany: () => unarchiveCompany,
3330
+ searchCompanies: () => searchCompanies,
3331
+ listOwnedEntities: () => listOwnedEntities,
3332
+ listCompanyEmployees: () => listCompanyEmployees,
3333
+ listCompanies: () => listCompanies,
3334
+ getCompany: () => getCompany,
3335
+ deleteCompany: () => deleteCompany,
3336
+ createCompany: () => createCompany,
3337
+ archiveCompany: () => archiveCompany
3338
+ });
3266
3339
  function rowToCompany2(row) {
3267
3340
  return {
3268
3341
  ...row,
@@ -3485,6 +3558,59 @@ function deleteCompany(id, db) {
3485
3558
  logActivity(d, { company_id: id, action: "company.deleted", details: `Deleted company: ${row.name}` });
3486
3559
  d.run(`DELETE FROM companies WHERE id = ?`, [id]);
3487
3560
  }
3561
+ function searchCompanies(query, db) {
3562
+ const d = db || getDatabase();
3563
+ const rows = d.query(`
3564
+ SELECT * FROM companies
3565
+ WHERE name LIKE ? OR domain LIKE ? OR description LIKE ? OR industry LIKE ?
3566
+ LIMIT 50
3567
+ `).all(`%${query}%`, `%${query}%`, `%${query}%`, `%${query}%`);
3568
+ return rows.map((row) => loadCompanyDetails(d, rowToCompany2(row)));
3569
+ }
3570
+ function listCompanyEmployees(companyId, db) {
3571
+ const d = db || getDatabase();
3572
+ const row = d.query(`SELECT id FROM companies WHERE id = ?`).get(companyId);
3573
+ if (!row)
3574
+ throw new CompanyNotFoundError(companyId);
3575
+ const rows = d.query(`SELECT * FROM contacts WHERE company_id = ? ORDER BY display_name ASC`).all(companyId);
3576
+ return rows.map((r) => ({
3577
+ ...r,
3578
+ source: r.source,
3579
+ custom_fields: JSON.parse(r.custom_fields || "{}"),
3580
+ preferred_contact_method: r.preferred_contact_method ?? null,
3581
+ status: r.status ?? "active",
3582
+ follow_up_at: r.follow_up_at ?? null,
3583
+ archived: !!r.archived,
3584
+ project_id: r.project_id ?? null,
3585
+ sensitivity: r.sensitivity ?? "normal",
3586
+ do_not_contact: !!r.do_not_contact,
3587
+ priority: r.priority ?? 3,
3588
+ timezone: r.timezone ?? null
3589
+ }));
3590
+ }
3591
+ function archiveCompany(id, db) {
3592
+ const d = db || getDatabase();
3593
+ const row = d.query(`SELECT * FROM companies WHERE id = ?`).get(id);
3594
+ if (!row)
3595
+ throw new CompanyNotFoundError(id);
3596
+ d.run(`UPDATE companies SET archived = 1, updated_at = ? WHERE id = ?`, [now(), id]);
3597
+ logActivity(d, { company_id: id, action: "company.archived", details: `Archived company: ${row.name}` });
3598
+ const updated = d.query(`SELECT * FROM companies WHERE id = ?`).get(id);
3599
+ return loadCompanyDetails(d, rowToCompany2(updated));
3600
+ }
3601
+ function unarchiveCompany(id, db) {
3602
+ const d = db || getDatabase();
3603
+ const row = d.query(`SELECT * FROM companies WHERE id = ?`).get(id);
3604
+ if (!row)
3605
+ throw new CompanyNotFoundError(id);
3606
+ d.run(`UPDATE companies SET archived = 0, updated_at = ? WHERE id = ?`, [now(), id]);
3607
+ logActivity(d, { company_id: id, action: "company.unarchived", details: `Unarchived company: ${row.name}` });
3608
+ const updated = d.query(`SELECT * FROM companies WHERE id = ?`).get(id);
3609
+ return loadCompanyDetails(d, rowToCompany2(updated));
3610
+ }
3611
+ function listOwnedEntities(db) {
3612
+ return listCompanies({ is_owned_entity: true }, db);
3613
+ }
3488
3614
  var init_companies = __esm(() => {
3489
3615
  init_types();
3490
3616
  init_database();
@@ -3859,6 +3985,7 @@ function listContactsByTag(tagId, db) {
3859
3985
  follow_up_at: r.follow_up_at ?? null,
3860
3986
  archived: !!r.archived,
3861
3987
  project_id: r.project_id ?? null,
3988
+ sensitivity: r.sensitivity ?? "normal",
3862
3989
  do_not_contact: !!r.do_not_contact,
3863
3990
  priority: r.priority ?? 3,
3864
3991
  timezone: r.timezone ?? null
@@ -4384,13 +4511,104 @@ async function exportContacts(format, contacts) {
4384
4511
  }
4385
4512
  }
4386
4513
 
4514
+ // src/lib/images.ts
4515
+ var exports_images = {};
4516
+ __export(exports_images, {
4517
+ saveImage: () => saveImage,
4518
+ listImages: () => listImages,
4519
+ getImagesDir: () => getImagesDir,
4520
+ getImagePath: () => getImagePath,
4521
+ getImageAsBase64: () => getImageAsBase64,
4522
+ deleteImage: () => deleteImage
4523
+ });
4524
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, copyFileSync, unlinkSync, readdirSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
4525
+ import { join as join3, extname, basename } from "path";
4526
+ function ensureImagesDir() {
4527
+ if (!existsSync3(IMAGES_DIR))
4528
+ mkdirSync3(IMAGES_DIR, { recursive: true });
4529
+ }
4530
+ function getImagesDir() {
4531
+ ensureImagesDir();
4532
+ return IMAGES_DIR;
4533
+ }
4534
+ function saveImage(entityId, source, options) {
4535
+ ensureImagesDir();
4536
+ deleteImage(entityId);
4537
+ const base64Match = source.match(/^data:image\/(\w+);base64,(.+)$/s);
4538
+ if (base64Match) {
4539
+ const ext2 = base64Match[1] === "jpeg" ? "jpg" : base64Match[1];
4540
+ const data = Buffer.from(base64Match[2], "base64");
4541
+ const filename2 = `${entityId}.${ext2}`;
4542
+ writeFileSync2(join3(IMAGES_DIR, filename2), data);
4543
+ return filename2;
4544
+ }
4545
+ if (!existsSync3(source) && /^[A-Za-z0-9+/=\n\r]+$/.test(source.trim()) && source.length > 100) {
4546
+ const ext2 = options?.format || "jpg";
4547
+ const data = Buffer.from(source.trim(), "base64");
4548
+ const filename2 = `${entityId}.${ext2}`;
4549
+ writeFileSync2(join3(IMAGES_DIR, filename2), data);
4550
+ return filename2;
4551
+ }
4552
+ if (!existsSync3(source)) {
4553
+ throw new Error(`Image file not found: ${source}`);
4554
+ }
4555
+ const ext = extname(source).slice(1).toLowerCase() || "jpg";
4556
+ const validExts = ["jpg", "jpeg", "png", "gif", "webp", "svg", "avif", "ico"];
4557
+ if (!validExts.includes(ext)) {
4558
+ throw new Error(`Unsupported image format: ${ext}. Supported: ${validExts.join(", ")}`);
4559
+ }
4560
+ const filename = `${entityId}.${ext === "jpeg" ? "jpg" : ext}`;
4561
+ copyFileSync(source, join3(IMAGES_DIR, filename));
4562
+ return filename;
4563
+ }
4564
+ function getImagePath(entityId) {
4565
+ ensureImagesDir();
4566
+ const files = readdirSync(IMAGES_DIR);
4567
+ const match = files.find((f) => f.startsWith(`${entityId}.`));
4568
+ return match ? join3(IMAGES_DIR, match) : null;
4569
+ }
4570
+ function getImageAsBase64(entityId) {
4571
+ const path = getImagePath(entityId);
4572
+ if (!path)
4573
+ return null;
4574
+ const ext = extname(path).slice(1);
4575
+ const mime = ext === "jpg" ? "image/jpeg" : ext === "svg" ? "image/svg+xml" : `image/${ext}`;
4576
+ const data = readFileSync2(path);
4577
+ return `data:${mime};base64,${data.toString("base64")}`;
4578
+ }
4579
+ function deleteImage(entityId) {
4580
+ ensureImagesDir();
4581
+ const files = readdirSync(IMAGES_DIR);
4582
+ let deleted = false;
4583
+ for (const f of files) {
4584
+ if (f.startsWith(`${entityId}.`)) {
4585
+ unlinkSync(join3(IMAGES_DIR, f));
4586
+ deleted = true;
4587
+ }
4588
+ }
4589
+ return deleted;
4590
+ }
4591
+ function listImages() {
4592
+ ensureImagesDir();
4593
+ const files = readdirSync(IMAGES_DIR).filter((f) => !f.startsWith("."));
4594
+ return files.map((f) => ({
4595
+ entity_id: basename(f, extname(f)),
4596
+ filename: f,
4597
+ path: join3(IMAGES_DIR, f)
4598
+ }));
4599
+ }
4600
+ var IMAGES_DIR;
4601
+ var init_images = __esm(() => {
4602
+ IMAGES_DIR = join3(process.env["HOME"] || "~", ".contacts", "images");
4603
+ });
4604
+
4387
4605
  // src/server/serve.ts
4388
4606
  var exports_serve = {};
4389
4607
  __export(exports_serve, {
4390
4608
  startServer: () => startServer
4391
4609
  });
4392
- import { existsSync as existsSync3 } from "fs";
4393
- import { join as join3 } from "path";
4610
+ import { existsSync as existsSync4 } from "fs";
4611
+ import { join as join4 } from "path";
4394
4612
  function json(data, status = 200) {
4395
4613
  return new Response(JSON.stringify(data), {
4396
4614
  status,
@@ -4595,8 +4813,60 @@ async function handleExport(req) {
4595
4813
  }
4596
4814
  });
4597
4815
  }
4816
+ async function handleImages(req, _url, segments) {
4817
+ const entityId = segments[2];
4818
+ if (!entityId)
4819
+ return apiError("Entity ID required");
4820
+ if (req.method === "GET") {
4821
+ const imagePath = getImagePath(entityId);
4822
+ if (!imagePath || !existsSync4(imagePath)) {
4823
+ return new Response(null, { status: 404, headers: { "Content-Type": "text/plain" } });
4824
+ }
4825
+ return new Response(Bun.file(imagePath), {
4826
+ headers: { "Cache-Control": "public, max-age=3600" }
4827
+ });
4828
+ }
4829
+ if (req.method === "POST") {
4830
+ const contentType = req.headers.get("content-type") || "";
4831
+ if (contentType.includes("multipart/form-data")) {
4832
+ const formData = await req.formData();
4833
+ const file = formData.get("image");
4834
+ if (!file)
4835
+ return apiError("No image file in form data");
4836
+ const ext = file.name?.split(".").pop() || "jpg";
4837
+ const buffer = Buffer.from(await file.arrayBuffer());
4838
+ const tmpPath = join4(getImagesDir(), `_upload_${entityId}.${ext}`);
4839
+ const { writeFileSync: wfs } = await import("fs");
4840
+ wfs(tmpPath, buffer);
4841
+ try {
4842
+ const filename = saveImage(entityId, tmpPath);
4843
+ const { unlinkSync: unlinkSync2 } = await import("fs");
4844
+ try {
4845
+ unlinkSync2(tmpPath);
4846
+ } catch {}
4847
+ return json({ ok: true, entity_id: entityId, filename });
4848
+ } catch (e) {
4849
+ return apiError(e instanceof Error ? e.message : "Upload failed");
4850
+ }
4851
+ }
4852
+ const body = await parseJson(req);
4853
+ if (!body?.image)
4854
+ return apiError("Provide image as base64 string or file upload");
4855
+ try {
4856
+ const filename = saveImage(entityId, body.image, { format: body.format });
4857
+ return json({ ok: true, entity_id: entityId, filename });
4858
+ } catch (e) {
4859
+ return apiError(e instanceof Error ? e.message : "Upload failed");
4860
+ }
4861
+ }
4862
+ if (req.method === "DELETE") {
4863
+ const deleted = deleteImage(entityId);
4864
+ return json({ ok: true, deleted });
4865
+ }
4866
+ return apiError("Method not allowed", 405);
4867
+ }
4598
4868
  function serveStaticFile(filePath) {
4599
- if (!existsSync3(filePath))
4869
+ if (!existsSync4(filePath))
4600
4870
  return null;
4601
4871
  return new Response(Bun.file(filePath));
4602
4872
  }
@@ -4636,12 +4906,15 @@ function startServer(port) {
4636
4906
  case "export":
4637
4907
  response = req.method === "GET" ? await handleExport(req) : apiError("Method not allowed", 405);
4638
4908
  break;
4909
+ case "images":
4910
+ response = await handleImages(req, url, segments);
4911
+ break;
4639
4912
  default:
4640
4913
  response = apiError("Not found", 404);
4641
4914
  }
4642
4915
  } else {
4643
- const filePath = join3(DASHBOARD_DIST, url.pathname === "/" ? "index.html" : url.pathname);
4644
- response = serveStaticFile(filePath) ?? serveStaticFile(join3(DASHBOARD_DIST, "index.html")) ?? new Response("Not Found", { status: 404 });
4916
+ const filePath = join4(DASHBOARD_DIST, url.pathname === "/" ? "index.html" : url.pathname);
4917
+ response = serveStaticFile(filePath) ?? serveStaticFile(join4(DASHBOARD_DIST, "index.html")) ?? new Response("Not Found", { status: 404 });
4645
4918
  }
4646
4919
  } catch (err) {
4647
4920
  console.error("Request error:", err);
@@ -4662,7 +4935,8 @@ var init_serve = __esm(() => {
4662
4935
  init_contacts();
4663
4936
  init_companies();
4664
4937
  init_tags();
4665
- DASHBOARD_DIST = join3(import.meta.dir, "../../dashboard/dist");
4938
+ init_images();
4939
+ DASHBOARD_DIST = join4(import.meta.dir, "../../dashboard/dist");
4666
4940
  });
4667
4941
 
4668
4942
  // src/db/notes.ts
@@ -5774,6 +6048,418 @@ var init_org_chart = __esm(() => {
5774
6048
  init_database();
5775
6049
  });
5776
6050
 
6051
+ // src/lib/vault.ts
6052
+ var exports_vault = {};
6053
+ __export(exports_vault, {
6054
+ unlockVault: () => unlockVault,
6055
+ requireVault: () => requireVault,
6056
+ lockVault: () => lockVault,
6057
+ isVaultUnlocked: () => isVaultUnlocked,
6058
+ isVaultInitialized: () => isVaultInitialized,
6059
+ initVault: () => initVault,
6060
+ getDocumentsDir: () => getDocumentsDir,
6061
+ encryptFile: () => encryptFile,
6062
+ encrypt: () => encrypt,
6063
+ decryptFile: () => decryptFile,
6064
+ decrypt: () => decrypt
6065
+ });
6066
+ import { existsSync as existsSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4 } from "fs";
6067
+ import { join as join5 } from "path";
6068
+ import { createCipheriv, createDecipheriv, randomBytes, pbkdf2Sync, createHash } from "crypto";
6069
+ function deriveKey(passphrase, salt) {
6070
+ return pbkdf2Sync(passphrase, salt, 1e5, 32, "sha512");
6071
+ }
6072
+ function initVault(passphrase) {
6073
+ if (!existsSync5(VAULT_DIR))
6074
+ mkdirSync4(VAULT_DIR, { recursive: true });
6075
+ if (!existsSync5(DOCUMENTS_DIR))
6076
+ mkdirSync4(DOCUMENTS_DIR, { recursive: true });
6077
+ const salt = randomBytes(32);
6078
+ const key = deriveKey(passphrase, salt);
6079
+ const keyHash = createHash("sha256").update(key).digest("hex");
6080
+ const config = { salt: salt.toString("hex"), key_hash: keyHash, created_at: new Date().toISOString() };
6081
+ writeFileSync3(VAULT_CONFIG, JSON.stringify(config, null, 2));
6082
+ _derivedKey = key;
6083
+ }
6084
+ function isVaultInitialized() {
6085
+ return existsSync5(VAULT_CONFIG);
6086
+ }
6087
+ function unlockVault(passphrase) {
6088
+ if (!existsSync5(VAULT_CONFIG))
6089
+ throw new Error("Vault not initialized. Run 'contacts vault init' first.");
6090
+ const config = JSON.parse(readFileSync3(VAULT_CONFIG, "utf-8"));
6091
+ const salt = Buffer.from(config.salt, "hex");
6092
+ const key = deriveKey(passphrase, salt);
6093
+ const keyHash = createHash("sha256").update(key).digest("hex");
6094
+ if (keyHash !== config.key_hash)
6095
+ return false;
6096
+ _derivedKey = key;
6097
+ return true;
6098
+ }
6099
+ function lockVault() {
6100
+ _derivedKey = null;
6101
+ }
6102
+ function isVaultUnlocked() {
6103
+ return _derivedKey !== null;
6104
+ }
6105
+ function requireVault() {
6106
+ if (!_derivedKey)
6107
+ throw new Error("Vault is locked. Unlock with 'contacts vault unlock' or vault_unlock MCP tool first.");
6108
+ return _derivedKey;
6109
+ }
6110
+ function encrypt(plaintext) {
6111
+ const key = requireVault();
6112
+ const iv = randomBytes(16);
6113
+ const cipher = createCipheriv("aes-256-gcm", key, iv);
6114
+ let encrypted = cipher.update(plaintext, "utf8", "hex");
6115
+ encrypted += cipher.final("hex");
6116
+ const authTag = cipher.getAuthTag().toString("hex");
6117
+ return { ciphertext: encrypted + ":" + authTag, iv: iv.toString("hex") };
6118
+ }
6119
+ function decrypt(ciphertext, iv) {
6120
+ const key = requireVault();
6121
+ const [encData, authTag] = ciphertext.split(":");
6122
+ if (!encData || !authTag)
6123
+ throw new Error("Invalid ciphertext format");
6124
+ const decipher = createDecipheriv("aes-256-gcm", key, Buffer.from(iv, "hex"));
6125
+ decipher.setAuthTag(Buffer.from(authTag, "hex"));
6126
+ let decrypted = decipher.update(encData, "hex", "utf8");
6127
+ decrypted += decipher.final("utf8");
6128
+ return decrypted;
6129
+ }
6130
+ function encryptFile(sourcePath, entityId) {
6131
+ const key = requireVault();
6132
+ if (!existsSync5(DOCUMENTS_DIR))
6133
+ mkdirSync4(DOCUMENTS_DIR, { recursive: true });
6134
+ const data = readFileSync3(sourcePath);
6135
+ const iv = randomBytes(16);
6136
+ const cipher = createCipheriv("aes-256-gcm", key, iv);
6137
+ const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
6138
+ const authTag = cipher.getAuthTag();
6139
+ const destPath = join5(DOCUMENTS_DIR, `${entityId}.enc`);
6140
+ const output = Buffer.concat([iv, authTag, encrypted]);
6141
+ writeFileSync3(destPath, output);
6142
+ return destPath;
6143
+ }
6144
+ function decryptFile(encPath) {
6145
+ const key = requireVault();
6146
+ const data = readFileSync3(encPath);
6147
+ const iv = data.subarray(0, 16);
6148
+ const authTag = data.subarray(16, 32);
6149
+ const encrypted = data.subarray(32);
6150
+ const decipher = createDecipheriv("aes-256-gcm", key, iv);
6151
+ decipher.setAuthTag(authTag);
6152
+ return Buffer.concat([decipher.update(encrypted), decipher.final()]);
6153
+ }
6154
+ function getDocumentsDir() {
6155
+ if (!existsSync5(DOCUMENTS_DIR))
6156
+ mkdirSync4(DOCUMENTS_DIR, { recursive: true });
6157
+ return DOCUMENTS_DIR;
6158
+ }
6159
+ var VAULT_DIR, VAULT_CONFIG, DOCUMENTS_DIR, _derivedKey = null;
6160
+ var init_vault = __esm(() => {
6161
+ VAULT_DIR = join5(process.env["HOME"] || "~", ".contacts");
6162
+ VAULT_CONFIG = join5(VAULT_DIR, "vault.json");
6163
+ DOCUMENTS_DIR = join5(VAULT_DIR, "documents");
6164
+ });
6165
+
6166
+ // src/db/documents.ts
6167
+ var exports_documents = {};
6168
+ __export(exports_documents, {
6169
+ listDocuments: () => listDocuments,
6170
+ getDocument: () => getDocument,
6171
+ deleteDocument: () => deleteDocument,
6172
+ addDocument: () => addDocument,
6173
+ DOCUMENT_TYPES: () => DOCUMENT_TYPES
6174
+ });
6175
+ import { existsSync as existsSync6, unlinkSync as unlinkSync2 } from "fs";
6176
+ function addDocument(input, db) {
6177
+ requireVault();
6178
+ const _db2 = db || getDatabase();
6179
+ const id = uuid();
6180
+ const { ciphertext, iv } = encrypt(input.value);
6181
+ let encFilePath = null;
6182
+ if (input.file_path) {
6183
+ encFilePath = encryptFile(input.file_path, id);
6184
+ }
6185
+ _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());
6186
+ return getDocument(id, _db2);
6187
+ }
6188
+ function getDocument(id, db) {
6189
+ requireVault();
6190
+ const _db2 = db || getDatabase();
6191
+ const row = _db2.query(`SELECT * FROM contact_documents WHERE id = ?`).get(id);
6192
+ if (!row)
6193
+ throw new Error(`Document not found: ${id}`);
6194
+ return rowToDoc(row);
6195
+ }
6196
+ function listDocuments(contactId, db) {
6197
+ const _db2 = db || getDatabase();
6198
+ 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);
6199
+ return rows.map((r) => ({
6200
+ id: r.id,
6201
+ doc_type: r.doc_type,
6202
+ label: r.label,
6203
+ has_file: !!r.encrypted_file_path,
6204
+ expires_at: r.expires_at,
6205
+ created_at: r.created_at
6206
+ }));
6207
+ }
6208
+ function deleteDocument(id, db) {
6209
+ const _db2 = db || getDatabase();
6210
+ const row = _db2.query(`SELECT encrypted_file_path FROM contact_documents WHERE id = ?`).get(id);
6211
+ if (row?.encrypted_file_path && existsSync6(row.encrypted_file_path)) {
6212
+ try {
6213
+ unlinkSync2(row.encrypted_file_path);
6214
+ } catch {}
6215
+ }
6216
+ _db2.query(`DELETE FROM contact_documents WHERE id = ?`).run(id);
6217
+ }
6218
+ function rowToDoc(row) {
6219
+ return {
6220
+ id: row.id,
6221
+ contact_id: row.contact_id,
6222
+ doc_type: row.doc_type,
6223
+ label: row.label,
6224
+ value: decrypt(row.encrypted_value, row.iv),
6225
+ has_file: !!row.encrypted_file_path,
6226
+ metadata: JSON.parse(row.metadata || "{}"),
6227
+ expires_at: row.expires_at,
6228
+ created_at: row.created_at,
6229
+ updated_at: row.updated_at
6230
+ };
6231
+ }
6232
+ var DOCUMENT_TYPES;
6233
+ var init_documents = __esm(() => {
6234
+ init_database();
6235
+ init_vault();
6236
+ DOCUMENT_TYPES = [
6237
+ "passport",
6238
+ "national_id",
6239
+ "tax_id",
6240
+ "ssn",
6241
+ "drivers_license",
6242
+ "bank_account",
6243
+ "visa",
6244
+ "insurance",
6245
+ "contract",
6246
+ "certificate",
6247
+ "medical_record",
6248
+ "prescription",
6249
+ "allergy_list",
6250
+ "vaccination",
6251
+ "blood_type",
6252
+ "health_insurance",
6253
+ "medical_condition",
6254
+ "emergency_contact_medical",
6255
+ "other"
6256
+ ];
6257
+ });
6258
+
6259
+ // src/lib/document-scanner.ts
6260
+ var exports_document_scanner = {};
6261
+ __export(exports_document_scanner, {
6262
+ scanDocument: () => scanDocument
6263
+ });
6264
+ import { readFileSync as readFileSync4, existsSync as existsSync7 } from "fs";
6265
+ import { extname as extname2 } from "path";
6266
+ async function scanDocument(imageSource, docType) {
6267
+ const apiKey = process.env["OPENAI_API_KEY"];
6268
+ if (!apiKey) {
6269
+ throw new Error("OPENAI_API_KEY not set. Set it in ~/.secrets or environment to use document scanning.");
6270
+ }
6271
+ let imageData;
6272
+ if (imageSource.startsWith("data:image/")) {
6273
+ imageData = imageSource;
6274
+ } else if (existsSync7(imageSource)) {
6275
+ const buffer = readFileSync4(imageSource);
6276
+ const ext = extname2(imageSource).slice(1).toLowerCase();
6277
+ const mime = ext === "jpg" || ext === "jpeg" ? "image/jpeg" : ext === "png" ? "image/png" : ext === "webp" ? "image/webp" : ext === "gif" ? "image/gif" : "image/jpeg";
6278
+ imageData = `data:${mime};base64,${buffer.toString("base64")}`;
6279
+ } else if (/^[A-Za-z0-9+/=\n\r]+$/.test(imageSource.trim()) && imageSource.length > 100) {
6280
+ imageData = `data:image/jpeg;base64,${imageSource.trim()}`;
6281
+ } else {
6282
+ throw new Error(`Image source not found or invalid: ${imageSource.slice(0, 50)}...`);
6283
+ }
6284
+ const typeHint = docType ? ` This is a ${docType} document.` : "";
6285
+ const prompt = `Extract all text and structured data from this document image.${typeHint} Return a JSON object with these fields:
6286
+ - document_type: detected type (passport, national_id, drivers_license, tax_document, medical_record, prescription, insurance_card, bank_statement, visa, certificate, contract, other)
6287
+ - full_name: full name as shown
6288
+ - date_of_birth: in YYYY-MM-DD format if visible
6289
+ - document_number: main ID/document number
6290
+ - issuing_country: country code or name
6291
+ - issue_date: in YYYY-MM-DD format if visible
6292
+ - expiry_date: in YYYY-MM-DD format if visible
6293
+ - address: full address if visible
6294
+ - nationality: if visible
6295
+ - gender: if visible
6296
+ - mrz_code: Machine Readable Zone text if this is a passport/ID with MRZ
6297
+ - phone: any phone numbers visible
6298
+ - email: any email addresses visible
6299
+ - additional_fields: object with any other visible structured data
6300
+ - raw_text: all visible text transcribed
6301
+
6302
+ Only include fields that are actually visible in the document. Return valid JSON only.`;
6303
+ const response = await fetch("https://api.openai.com/v1/chat/completions", {
6304
+ method: "POST",
6305
+ headers: {
6306
+ Authorization: `Bearer ${apiKey}`,
6307
+ "Content-Type": "application/json"
6308
+ },
6309
+ body: JSON.stringify({
6310
+ model: "gpt-4o",
6311
+ messages: [
6312
+ {
6313
+ role: "user",
6314
+ content: [
6315
+ { type: "text", text: prompt },
6316
+ { type: "image_url", image_url: { url: imageData, detail: "high" } }
6317
+ ]
6318
+ }
6319
+ ],
6320
+ max_tokens: 2000,
6321
+ temperature: 0
6322
+ })
6323
+ });
6324
+ if (!response.ok) {
6325
+ const err = await response.text();
6326
+ throw new Error(`OpenAI API error: ${response.status} \u2014 ${err}`);
6327
+ }
6328
+ const data = await response.json();
6329
+ const content = data.choices?.[0]?.message?.content || "";
6330
+ const jsonMatch = content.match(/```json\s*([\s\S]*?)```/) || content.match(/\{[\s\S]*\}/);
6331
+ if (!jsonMatch) {
6332
+ return { fields: {}, raw_text: content, document_type: docType || "unknown", confidence: 0.3 };
6333
+ }
6334
+ try {
6335
+ const parsed = JSON.parse(jsonMatch[1] || jsonMatch[0]);
6336
+ const { document_type, raw_text, additional_fields, ...mainFields } = parsed;
6337
+ const fields = {};
6338
+ for (const [k, v] of Object.entries(mainFields)) {
6339
+ if (v && typeof v === "string")
6340
+ fields[k] = v;
6341
+ }
6342
+ if (additional_fields && typeof additional_fields === "object") {
6343
+ for (const [k, v] of Object.entries(additional_fields)) {
6344
+ if (v && typeof v === "string")
6345
+ fields[k] = v;
6346
+ }
6347
+ }
6348
+ return {
6349
+ fields,
6350
+ raw_text: raw_text || content,
6351
+ document_type: document_type || docType || "unknown",
6352
+ confidence: Object.keys(fields).length > 3 ? 0.9 : Object.keys(fields).length > 0 ? 0.7 : 0.3
6353
+ };
6354
+ } catch {
6355
+ return { fields: {}, raw_text: content, document_type: docType || "unknown", confidence: 0.3 };
6356
+ }
6357
+ }
6358
+ var init_document_scanner = () => {};
6359
+
6360
+ // src/db/health.ts
6361
+ var exports_health = {};
6362
+ __export(exports_health, {
6363
+ setHealthData: () => setHealthData,
6364
+ getHealthData: () => getHealthData,
6365
+ deleteHealthData: () => deleteHealthData
6366
+ });
6367
+ function setHealthData(contactId, input, db) {
6368
+ requireVault();
6369
+ const _db2 = db || getDatabase();
6370
+ const existing = _db2.query(`SELECT id FROM contact_health WHERE contact_id = ?`).get(contactId);
6371
+ if (existing) {
6372
+ const sets = [];
6373
+ const params = [];
6374
+ if (input.blood_type !== undefined) {
6375
+ sets.push("blood_type = ?");
6376
+ params.push(input.blood_type);
6377
+ }
6378
+ if (input.allergies !== undefined) {
6379
+ sets.push("allergies = ?");
6380
+ params.push(JSON.stringify(input.allergies));
6381
+ }
6382
+ if (input.medical_conditions !== undefined) {
6383
+ sets.push("medical_conditions = ?");
6384
+ params.push(JSON.stringify(input.medical_conditions));
6385
+ }
6386
+ if (input.medications !== undefined) {
6387
+ sets.push("medications = ?");
6388
+ params.push(JSON.stringify(input.medications));
6389
+ }
6390
+ if (input.emergency_contacts !== undefined) {
6391
+ sets.push("emergency_contacts = ?");
6392
+ params.push(JSON.stringify(input.emergency_contacts));
6393
+ }
6394
+ if (input.health_insurance_provider !== undefined) {
6395
+ sets.push("health_insurance_provider = ?");
6396
+ params.push(input.health_insurance_provider);
6397
+ }
6398
+ if (input.health_insurance_id !== undefined) {
6399
+ sets.push("health_insurance_id = ?");
6400
+ params.push(input.health_insurance_id);
6401
+ }
6402
+ if (input.primary_physician !== undefined) {
6403
+ sets.push("primary_physician = ?");
6404
+ params.push(input.primary_physician);
6405
+ }
6406
+ if (input.primary_physician_phone !== undefined) {
6407
+ sets.push("primary_physician_phone = ?");
6408
+ params.push(input.primary_physician_phone);
6409
+ }
6410
+ if (input.organ_donor !== undefined) {
6411
+ sets.push("organ_donor = ?");
6412
+ params.push(input.organ_donor ? 1 : 0);
6413
+ }
6414
+ if (input.notes !== undefined) {
6415
+ sets.push("notes = ?");
6416
+ params.push(input.notes);
6417
+ }
6418
+ if (sets.length) {
6419
+ sets.push("updated_at = ?");
6420
+ params.push(now());
6421
+ params.push(contactId);
6422
+ _db2.query(`UPDATE contact_health SET ${sets.join(", ")} WHERE contact_id = ?`).run(...params);
6423
+ }
6424
+ } else {
6425
+ const id = uuid();
6426
+ _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());
6427
+ }
6428
+ return getHealthData(contactId, _db2);
6429
+ }
6430
+ function getHealthData(contactId, db) {
6431
+ requireVault();
6432
+ const _db2 = db || getDatabase();
6433
+ const row = _db2.query(`SELECT * FROM contact_health WHERE contact_id = ?`).get(contactId);
6434
+ if (!row)
6435
+ return null;
6436
+ return {
6437
+ id: row.id,
6438
+ contact_id: row.contact_id,
6439
+ blood_type: row.blood_type,
6440
+ allergies: JSON.parse(row.allergies || "[]"),
6441
+ medical_conditions: JSON.parse(row.medical_conditions || "[]"),
6442
+ medications: JSON.parse(row.medications || "[]"),
6443
+ emergency_contacts: JSON.parse(row.emergency_contacts || "[]"),
6444
+ health_insurance_provider: row.health_insurance_provider,
6445
+ health_insurance_id: row.health_insurance_id,
6446
+ primary_physician: row.primary_physician,
6447
+ primary_physician_phone: row.primary_physician_phone,
6448
+ organ_donor: !!row.organ_donor,
6449
+ notes: row.notes,
6450
+ created_at: row.created_at,
6451
+ updated_at: row.updated_at
6452
+ };
6453
+ }
6454
+ function deleteHealthData(contactId, db) {
6455
+ const _db2 = db || getDatabase();
6456
+ _db2.query(`DELETE FROM contact_health WHERE contact_id = ?`).run(contactId);
6457
+ }
6458
+ var init_health = __esm(() => {
6459
+ init_database();
6460
+ init_vault();
6461
+ });
6462
+
5777
6463
  // node_modules/commander/esm.mjs
5778
6464
  var import__ = __toESM(require_commander(), 1);
5779
6465
  var {
@@ -6074,8 +6760,8 @@ function readConfig() {
6074
6760
  }
6075
6761
 
6076
6762
  // src/cli/index.tsx
6077
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync4, copyFileSync, statSync, mkdirSync as mkdirSync3, readdirSync } from "fs";
6078
- import { extname, join as join4 } from "path";
6763
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, existsSync as existsSync8, copyFileSync as copyFileSync2, statSync, mkdirSync as mkdirSync5, readdirSync as readdirSync2 } from "fs";
6764
+ import { extname as extname3, join as join6 } from "path";
6079
6765
  function renderTable(headers, rows) {
6080
6766
  const colWidths = headers.map((h) => h.length);
6081
6767
  for (const row of rows) {
@@ -6180,7 +6866,7 @@ async function confirm(question) {
6180
6866
  const answer = await prompt(question + " [y/N]");
6181
6867
  return answer.toLowerCase() === "y" || answer.toLowerCase() === "yes";
6182
6868
  }
6183
- program.name("contacts").description("Open Contacts \u2014 contact management for AI coding agents").version("0.5.0");
6869
+ program.name("contacts").description("Open Contacts \u2014 contact management for AI coding agents").version("0.6.0");
6184
6870
  function collect(val, prev) {
6185
6871
  return [...prev, val];
6186
6872
  }
@@ -6246,10 +6932,11 @@ Add New Contact
6246
6932
  \u2713 Contact created: ${contact.display_name} (${contact.id})
6247
6933
  `));
6248
6934
  });
6249
- program.command("list").description("List contacts").option("--tag <tag_id>", "Filter by tag ID").option("--company <id>", "Filter by company ID").option("--limit <n>", "Max results", "50").action(async (opts) => {
6935
+ program.command("list").description("List contacts").option("--tag <tag_id>", "Filter by tag ID").option("--company <id>", "Filter by company ID").option("--include-restricted", "Include restricted-sensitivity contacts").option("--limit <n>", "Max results", "50").action(async (opts) => {
6250
6936
  const result = listContacts({
6251
6937
  tag_id: opts.tag,
6252
6938
  company_id: opts.company,
6939
+ include_restricted: opts.includeRestricted,
6253
6940
  limit: parseInt(opts.limit, 10)
6254
6941
  });
6255
6942
  if (result.contacts.length === 0) {
@@ -6486,13 +7173,13 @@ Add New Tag
6486
7173
  `));
6487
7174
  });
6488
7175
  program.command("import <file>").description("Import contacts from CSV, vCard (.vcf), or JSON file").action(async (file) => {
6489
- if (!existsSync4(file)) {
7176
+ if (!existsSync8(file)) {
6490
7177
  console.error(chalk.red(`
6491
7178
  File not found: ${file}
6492
7179
  `));
6493
7180
  process.exit(1);
6494
7181
  }
6495
- const ext = extname(file).toLowerCase();
7182
+ const ext = extname3(file).toLowerCase();
6496
7183
  const formatMap = {
6497
7184
  ".csv": "csv",
6498
7185
  ".vcf": "vcf",
@@ -6506,7 +7193,7 @@ Unsupported file type: ${ext}. Use .csv, .vcf, or .json
6506
7193
  `));
6507
7194
  process.exit(1);
6508
7195
  }
6509
- const data = readFileSync2(file, "utf8");
7196
+ const data = readFileSync5(file, "utf8");
6510
7197
  console.log(chalk.blue(`
6511
7198
  Importing ${format.toUpperCase()} from ${file}...
6512
7199
  `));
@@ -6537,7 +7224,7 @@ Invalid format: ${format}. Use csv, vcf, or json
6537
7224
  const { contacts } = listContacts({ limit: 1e5 });
6538
7225
  const output = await exportContacts(format, contacts);
6539
7226
  if (opts.output) {
6540
- writeFileSync2(opts.output, output, "utf8");
7227
+ writeFileSync4(opts.output, output, "utf8");
6541
7228
  console.log(chalk.green(`
6542
7229
  \u2713 Exported ${contacts.length} contact(s) to ${opts.output}
6543
7230
  `));
@@ -6809,15 +7496,15 @@ program.command("init").description("Show setup info, stats, and configuration")
6809
7496
  console.log();
6810
7497
  });
6811
7498
  program.command("backup").description("Backup the contacts database").option("--output <path>", "Output path").option("--list", "List existing backups").action((opts) => {
6812
- const backupDir = join4(process.env["HOME"] || "~", ".contacts", "backups");
7499
+ const backupDir = join6(process.env["HOME"] || "~", ".contacts", "backups");
6813
7500
  if (opts.list) {
6814
- if (!existsSync4(backupDir)) {
7501
+ if (!existsSync8(backupDir)) {
6815
7502
  console.log(chalk.gray(`
6816
7503
  No backups found.
6817
7504
  `));
6818
7505
  return;
6819
7506
  }
6820
- const files = readdirSync(backupDir).filter((f) => f.endsWith(".db")).sort().reverse();
7507
+ const files = readdirSync2(backupDir).filter((f) => f.endsWith(".db")).sort().reverse();
6821
7508
  if (files.length === 0) {
6822
7509
  console.log(chalk.gray(`
6823
7510
  No backups found.
@@ -6828,7 +7515,7 @@ No backups found.
6828
7515
  Existing Backups:
6829
7516
  `));
6830
7517
  for (const f of files) {
6831
- const filePath = join4(backupDir, f);
7518
+ const filePath = join6(backupDir, f);
6832
7519
  const size2 = statSync(filePath).size;
6833
7520
  const mtime = statSync(filePath).mtime.toISOString().slice(0, 19).replace("T", " ");
6834
7521
  console.log(` ${chalk.cyan(f)} ${chalk.gray(`${(size2 / 1024).toFixed(1)} KB ${mtime}`)}`);
@@ -6837,16 +7524,16 @@ Existing Backups:
6837
7524
  return;
6838
7525
  }
6839
7526
  const src = getDbPath();
6840
- if (!existsSync4(src)) {
7527
+ if (!existsSync8(src)) {
6841
7528
  console.error(chalk.red(`
6842
7529
  Database not found: ${src}
6843
7530
  `));
6844
7531
  process.exit(1);
6845
7532
  }
6846
- mkdirSync3(backupDir, { recursive: true });
7533
+ mkdirSync5(backupDir, { recursive: true });
6847
7534
  const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
6848
- const dest = opts.output || join4(backupDir, `contacts-${ts}.db`);
6849
- copyFileSync(src, dest);
7535
+ const dest = opts.output || join6(backupDir, `contacts-${ts}.db`);
7536
+ copyFileSync2(src, dest);
6850
7537
  const size = statSync(dest).size;
6851
7538
  console.log(chalk.green(`
6852
7539
  \u2713 Backed up to ${dest} (${(size / 1024).toFixed(1)} KB)
@@ -8100,4 +8787,331 @@ dealsTeamCmd.command("assign <deal-id> <contact-id>").description("Assign a cont
8100
8787
  \u2713 ${contact.display_name} assigned as ${opts.role || "other"} in deal ${dealId}
8101
8788
  `));
8102
8789
  });
8790
+ var photoCmd = program.command("photo").description("Manage contact profile photos");
8791
+ photoCmd.command("set <contact-id> <image-path>").description("Set a contact's profile photo from a local file").action((contactId, imagePath) => {
8792
+ const { saveImage: saveImage2 } = (init_images(), __toCommonJS(exports_images));
8793
+ try {
8794
+ const contact = getContact(contactId);
8795
+ const filename = saveImage2(contactId, imagePath);
8796
+ updateContact(contactId, { avatar_url: `~/.contacts/images/${filename}` });
8797
+ console.log(chalk.green(`Photo set for ${contact.display_name}: ~/.contacts/images/${filename}`));
8798
+ } catch (e) {
8799
+ console.error(chalk.red(e instanceof Error ? e.message : String(e)));
8800
+ }
8801
+ });
8802
+ photoCmd.command("show <contact-id>").description("Show the path to a contact's profile photo").action((contactId) => {
8803
+ const { getImagePath: getImagePath2 } = (init_images(), __toCommonJS(exports_images));
8804
+ const contact = getContact(contactId);
8805
+ const path = getImagePath2(contactId);
8806
+ if (path) {
8807
+ console.log(`${chalk.bold(contact.display_name)}: ${chalk.cyan(path)}`);
8808
+ } else {
8809
+ console.log(chalk.yellow(`No photo set for ${contact.display_name}`));
8810
+ }
8811
+ });
8812
+ photoCmd.command("remove <contact-id>").description("Remove a contact's profile photo").action((contactId) => {
8813
+ const { deleteImage: deleteImage2 } = (init_images(), __toCommonJS(exports_images));
8814
+ const contact = getContact(contactId);
8815
+ const deleted = deleteImage2(contactId);
8816
+ if (deleted) {
8817
+ updateContact(contactId, { avatar_url: null });
8818
+ console.log(chalk.green(`Photo removed for ${contact.display_name}`));
8819
+ } else {
8820
+ console.log(chalk.yellow(`No photo found for ${contact.display_name}`));
8821
+ }
8822
+ });
8823
+ photoCmd.command("list").description("List all stored photos").action(() => {
8824
+ const { listImages: listImages2 } = (init_images(), __toCommonJS(exports_images));
8825
+ const images = listImages2();
8826
+ if (!images.length) {
8827
+ console.log(chalk.yellow("No photos stored"));
8828
+ return;
8829
+ }
8830
+ for (const img of images) {
8831
+ try {
8832
+ const contact = getContact(img.entity_id);
8833
+ console.log(`${chalk.bold(contact.display_name)}: ${chalk.cyan(img.path)}`);
8834
+ } catch {
8835
+ console.log(`${chalk.gray(img.entity_id)}: ${chalk.cyan(img.path)}`);
8836
+ }
8837
+ }
8838
+ });
8839
+ var logoCmd = program.command("logo").description("Manage company logos");
8840
+ logoCmd.command("set <company-id> <image-path>").description("Set a company's logo from a local file").action((companyId, imagePath) => {
8841
+ const { saveImage: saveImage2 } = (init_images(), __toCommonJS(exports_images));
8842
+ try {
8843
+ const company = getCompany(companyId);
8844
+ if (!company) {
8845
+ console.error(chalk.red("Company not found"));
8846
+ return;
8847
+ }
8848
+ const filename = saveImage2(companyId, imagePath);
8849
+ const { updateCompany: updateCompany2 } = (init_companies(), __toCommonJS(exports_companies));
8850
+ updateCompany2(companyId, { logo_url: `~/.contacts/images/${filename}` });
8851
+ console.log(chalk.green(`Logo set for ${company.name}: ~/.contacts/images/${filename}`));
8852
+ } catch (e) {
8853
+ console.error(chalk.red(e instanceof Error ? e.message : String(e)));
8854
+ }
8855
+ });
8856
+ logoCmd.command("show <company-id>").description("Show the path to a company's logo").action((companyId) => {
8857
+ const { getImagePath: getImagePath2 } = (init_images(), __toCommonJS(exports_images));
8858
+ const company = getCompany(companyId);
8859
+ if (!company) {
8860
+ console.error(chalk.red("Company not found"));
8861
+ return;
8862
+ }
8863
+ const path = getImagePath2(companyId);
8864
+ if (path) {
8865
+ console.log(`${chalk.bold(company.name)}: ${chalk.cyan(path)}`);
8866
+ } else {
8867
+ console.log(chalk.yellow(`No logo set for ${company.name}`));
8868
+ }
8869
+ });
8870
+ program.command("set-sensitivity <id> <level>").description("Set contact sensitivity level (normal, confidential, restricted)").action((id, level) => {
8871
+ if (!["normal", "confidential", "restricted"].includes(level)) {
8872
+ console.error(chalk.red(`
8873
+ Invalid sensitivity level: ${level}. Use: normal, confidential, restricted
8874
+ `));
8875
+ process.exit(1);
8876
+ }
8877
+ const contact = getContact(id);
8878
+ updateContact(id, { sensitivity: level });
8879
+ console.log(chalk.green(`
8880
+ Sensitivity set to ${level} for ${contact.display_name}
8881
+ `));
8882
+ });
8883
+ var vaultCmd = program.command("vault").description("Manage the encrypted document vault");
8884
+ function promptPassphrase(promptText) {
8885
+ const { createInterface } = __require("readline");
8886
+ return new Promise((resolve2) => {
8887
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
8888
+ process.stdout.write(promptText);
8889
+ rl.question("", (answer) => {
8890
+ rl.close();
8891
+ resolve2(answer);
8892
+ });
8893
+ });
8894
+ }
8895
+ vaultCmd.command("init").description("Initialize the encrypted vault").action(async () => {
8896
+ const { initVault: initVault2, isVaultInitialized: isVaultInitialized2 } = await Promise.resolve().then(() => (init_vault(), exports_vault));
8897
+ if (isVaultInitialized2()) {
8898
+ console.log(chalk.yellow(`
8899
+ Vault already initialized. Use "contacts vault unlock" to access it.
8900
+ `));
8901
+ return;
8902
+ }
8903
+ const passphrase = await promptPassphrase("Enter vault passphrase: ");
8904
+ if (!passphrase) {
8905
+ console.error(chalk.red("Passphrase is required."));
8906
+ process.exit(1);
8907
+ }
8908
+ const confirm2 = await promptPassphrase("Confirm passphrase: ");
8909
+ if (passphrase !== confirm2) {
8910
+ console.error(chalk.red("Passphrases do not match."));
8911
+ process.exit(1);
8912
+ }
8913
+ initVault2(passphrase);
8914
+ console.log(chalk.green(`
8915
+ Vault initialized and unlocked.
8916
+ `));
8917
+ });
8918
+ vaultCmd.command("unlock").description("Unlock the vault").action(async () => {
8919
+ const { unlockVault: unlockVault2 } = await Promise.resolve().then(() => (init_vault(), exports_vault));
8920
+ const passphrase = await promptPassphrase("Enter vault passphrase: ");
8921
+ const ok = unlockVault2(passphrase);
8922
+ if (!ok) {
8923
+ console.error(chalk.red(`
8924
+ Invalid passphrase.
8925
+ `));
8926
+ process.exit(1);
8927
+ }
8928
+ console.log(chalk.green(`
8929
+ Vault unlocked.
8930
+ `));
8931
+ });
8932
+ vaultCmd.command("lock").description("Lock the vault").action(async () => {
8933
+ const { lockVault: lockVault2 } = await Promise.resolve().then(() => (init_vault(), exports_vault));
8934
+ lockVault2();
8935
+ console.log(chalk.green(`
8936
+ Vault locked.
8937
+ `));
8938
+ });
8939
+ vaultCmd.command("status").description("Show vault status").action(async () => {
8940
+ const { isVaultInitialized: isVaultInitialized2, isVaultUnlocked: isVaultUnlocked2 } = await Promise.resolve().then(() => (init_vault(), exports_vault));
8941
+ const initialized = isVaultInitialized2();
8942
+ const unlocked = isVaultUnlocked2();
8943
+ console.log(chalk.bold.blue(`
8944
+ Vault Status:`));
8945
+ console.log(` Initialized: ${initialized ? chalk.green("yes") : chalk.red("no")}`);
8946
+ console.log(` Unlocked: ${unlocked ? chalk.green("yes") : chalk.red("no")}`);
8947
+ if (initialized) {
8948
+ const db = getDatabase();
8949
+ try {
8950
+ const docCount = db.query("SELECT COUNT(*) as n FROM contact_documents").get().n;
8951
+ console.log(` Documents: ${chalk.cyan(String(docCount))}`);
8952
+ } catch {}
8953
+ }
8954
+ console.log();
8955
+ });
8956
+ var docsCmd = program.command("docs").description("Manage encrypted contact documents");
8957
+ docsCmd.command("add <contact-id>").description("Add an encrypted document").option("--type <type>", "Document type (passport, national_id, tax_id, ssn, drivers_license, bank_account, visa, insurance, contract, certificate, medical_record, prescription, allergy_list, vaccination, blood_type, health_insurance, medical_condition, emergency_contact_medical, other)", "other").option("--label <label>", "Document label").option("--value <value>", "Document value (required)").option("--file <path>", "File to encrypt and attach").option("--expires <date>", "Expiry date (YYYY-MM-DD)").action(async (contactId, opts) => {
8958
+ const { addDocument: addDocument2 } = await Promise.resolve().then(() => (init_documents(), exports_documents));
8959
+ if (!opts.value) {
8960
+ console.error(chalk.red("--value is required"));
8961
+ process.exit(1);
8962
+ }
8963
+ const doc = addDocument2({
8964
+ contact_id: contactId,
8965
+ doc_type: opts.type || "other",
8966
+ label: opts.label,
8967
+ value: opts.value,
8968
+ file_path: opts.file,
8969
+ expires_at: opts.expires
8970
+ });
8971
+ console.log(chalk.green(`
8972
+ Document added: ${doc.doc_type} (${doc.id})
8973
+ `));
8974
+ });
8975
+ docsCmd.command("list <contact-id>").description("List documents for a contact (metadata only)").action(async (contactId) => {
8976
+ const { listDocuments: listDocuments2 } = await Promise.resolve().then(() => (init_documents(), exports_documents));
8977
+ const docs = listDocuments2(contactId);
8978
+ if (!docs.length) {
8979
+ console.log(chalk.gray(`
8980
+ No documents found.
8981
+ `));
8982
+ return;
8983
+ }
8984
+ console.log();
8985
+ renderTable(["Type", "Label", "Has File", "Expires", "Created"], docs.map((d) => ({
8986
+ Type: d.doc_type,
8987
+ Label: d.label || "",
8988
+ "Has File": d.has_file ? "yes" : "no",
8989
+ Expires: d.expires_at ? d.expires_at.slice(0, 10) : "",
8990
+ Created: d.created_at.slice(0, 10)
8991
+ })));
8992
+ console.log(chalk.gray(`
8993
+ ${docs.length} document(s)
8994
+ `));
8995
+ });
8996
+ docsCmd.command("show <doc-id>").description("Show a document with decrypted value (vault must be unlocked)").action(async (docId) => {
8997
+ const { getDocument: getDocument2 } = await Promise.resolve().then(() => (init_documents(), exports_documents));
8998
+ const doc = getDocument2(docId);
8999
+ console.log(chalk.bold.blue(`
9000
+ Document: ${doc.doc_type}`));
9001
+ if (doc.label)
9002
+ console.log(chalk.gray(" Label: ") + doc.label);
9003
+ console.log(chalk.gray(" Value: ") + doc.value);
9004
+ console.log(chalk.gray(" Has File:") + (doc.has_file ? " yes" : " no"));
9005
+ if (doc.expires_at)
9006
+ console.log(chalk.gray(" Expires: ") + doc.expires_at.slice(0, 10));
9007
+ console.log(chalk.gray(` ID: ${doc.id}
9008
+ `));
9009
+ });
9010
+ docsCmd.command("remove <doc-id>").description("Delete a document").action(async (docId) => {
9011
+ const { deleteDocument: deleteDocument2 } = await Promise.resolve().then(() => (init_documents(), exports_documents));
9012
+ deleteDocument2(docId);
9013
+ console.log(chalk.green(`
9014
+ Document deleted: ${docId}
9015
+ `));
9016
+ });
9017
+ docsCmd.command("scan <image-path>").description("Scan a document image using AI vision").option("--contact <id>", "Contact ID to associate with").option("--type <type>", "Document type hint").action(async (imagePath, opts) => {
9018
+ const { scanDocument: scanDocument2 } = await Promise.resolve().then(() => (init_document_scanner(), exports_document_scanner));
9019
+ console.log(chalk.blue(`
9020
+ Scanning document...
9021
+ `));
9022
+ const result = await scanDocument2(imagePath, opts.type);
9023
+ console.log(chalk.bold(` Type: ${result.document_type} Confidence: ${(result.confidence * 100).toFixed(0)}%
9024
+ `));
9025
+ console.log(chalk.yellow(" Extracted fields:"));
9026
+ for (const [k, v] of Object.entries(result.fields)) {
9027
+ console.log(` ${chalk.gray(k.padEnd(20))} ${v}`);
9028
+ }
9029
+ console.log();
9030
+ });
9031
+ docsCmd.command("types").description("List all valid document types").action(async () => {
9032
+ const { DOCUMENT_TYPES: DOCUMENT_TYPES2 } = await Promise.resolve().then(() => (init_documents(), exports_documents));
9033
+ console.log(chalk.bold.blue(`
9034
+ Document Types:
9035
+ `));
9036
+ for (const t of DOCUMENT_TYPES2) {
9037
+ console.log(` ${chalk.cyan(t)}`);
9038
+ }
9039
+ console.log();
9040
+ });
9041
+ var healthCmd = program.command("health").description("Manage contact health data (vault required)");
9042
+ healthCmd.command("show <id>").description("Show health data for a contact").action(async (id) => {
9043
+ const { getHealthData: getHealthData2 } = await Promise.resolve().then(() => (init_health(), exports_health));
9044
+ const contact = getContact(id);
9045
+ const health = getHealthData2(id);
9046
+ if (!health) {
9047
+ console.log(chalk.gray(`
9048
+ No health data for ${contact.display_name}.
9049
+ `));
9050
+ return;
9051
+ }
9052
+ console.log(chalk.bold.blue(`
9053
+ Health: ${contact.display_name}
9054
+ `));
9055
+ if (health.blood_type)
9056
+ console.log(chalk.gray(" Blood Type: ") + health.blood_type);
9057
+ if (health.allergies.length)
9058
+ console.log(chalk.gray(" Allergies: ") + health.allergies.join(", "));
9059
+ if (health.medical_conditions.length)
9060
+ console.log(chalk.gray(" Conditions: ") + health.medical_conditions.join(", "));
9061
+ if (health.medications.length)
9062
+ console.log(chalk.gray(" Medications: ") + health.medications.join(", "));
9063
+ if (health.emergency_contacts.length) {
9064
+ console.log(chalk.yellow(`
9065
+ Emergency Contacts:`));
9066
+ for (const ec of health.emergency_contacts) {
9067
+ console.log(` ${chalk.bold(ec.name)} ${ec.phone} ${chalk.gray(ec.relationship)}`);
9068
+ }
9069
+ }
9070
+ if (health.health_insurance_provider)
9071
+ console.log(chalk.gray(`
9072
+ Insurance: `) + `${health.health_insurance_provider} (${health.health_insurance_id || "no ID"})`);
9073
+ if (health.primary_physician)
9074
+ console.log(chalk.gray(" Physician: ") + `${health.primary_physician} ${health.primary_physician_phone || ""}`);
9075
+ console.log(chalk.gray(" Organ Donor: ") + (health.organ_donor ? "yes" : "no"));
9076
+ if (health.notes)
9077
+ console.log(chalk.gray(" Notes: ") + health.notes);
9078
+ console.log();
9079
+ });
9080
+ healthCmd.command("set <id>").description("Set health data for a contact").option("--blood-type <type>", "Blood type (e.g. A+, O-)").option("--allergies <list>", "Comma-separated allergies").option("--conditions <list>", "Comma-separated medical conditions").option("--medications <list>", "Comma-separated medications").option("--insurance-provider <name>", "Health insurance provider").option("--insurance-id <id>", "Health insurance ID").option("--physician <name>", "Primary physician").option("--physician-phone <phone>", "Physician phone").option("--organ-donor", "Mark as organ donor").option("--notes <text>", "Health notes").action(async (id, opts) => {
9081
+ const { setHealthData: setHealthData2 } = await Promise.resolve().then(() => (init_health(), exports_health));
9082
+ const contact = getContact(id);
9083
+ const input = {};
9084
+ if (opts.bloodType)
9085
+ input.blood_type = opts.bloodType;
9086
+ if (opts.allergies)
9087
+ input.allergies = opts.allergies.split(",").map((s) => s.trim());
9088
+ if (opts.conditions)
9089
+ input.medical_conditions = opts.conditions.split(",").map((s) => s.trim());
9090
+ if (opts.medications)
9091
+ input.medications = opts.medications.split(",").map((s) => s.trim());
9092
+ if (opts.insuranceProvider)
9093
+ input.health_insurance_provider = opts.insuranceProvider;
9094
+ if (opts.insuranceId)
9095
+ input.health_insurance_id = opts.insuranceId;
9096
+ if (opts.physician)
9097
+ input.primary_physician = opts.physician;
9098
+ if (opts.physicianPhone)
9099
+ input.primary_physician_phone = opts.physicianPhone;
9100
+ if (opts.organDonor !== undefined)
9101
+ input.organ_donor = opts.organDonor;
9102
+ if (opts.notes)
9103
+ input.notes = opts.notes;
9104
+ setHealthData2(id, input);
9105
+ console.log(chalk.green(`
9106
+ Health data updated for ${contact.display_name}
9107
+ `));
9108
+ });
9109
+ healthCmd.command("clear <id>").description("Delete all health data for a contact").action(async (id) => {
9110
+ const { deleteHealthData: deleteHealthData2 } = await Promise.resolve().then(() => (init_health(), exports_health));
9111
+ const contact = getContact(id);
9112
+ deleteHealthData2(id);
9113
+ console.log(chalk.green(`
9114
+ Health data cleared for ${contact.display_name}
9115
+ `));
9116
+ });
8103
9117
  program.parse(process.argv);