@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 +1042 -28
- package/dist/db/companies.d.ts.map +1 -1
- package/dist/db/contacts.d.ts.map +1 -1
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/documents.d.ts +37 -0
- package/dist/db/documents.d.ts.map +1 -0
- package/dist/db/health.d.ts +40 -0
- package/dist/db/health.d.ts.map +1 -0
- package/dist/db/tags.d.ts.map +1 -1
- package/dist/index.d.ts +9 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +524 -6
- package/dist/lib/document-scanner.d.ts +8 -0
- package/dist/lib/document-scanner.d.ts.map +1 -0
- package/dist/lib/images.d.ts +30 -0
- package/dist/lib/images.d.ts.map +1 -0
- package/dist/lib/vault.d.ts +15 -0
- package/dist/lib/vault.d.ts.map +1 -0
- package/dist/mcp/index.js +636 -9
- package/dist/server/index.js +190 -12
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/types/index.d.ts +6 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
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
|
|
4393
|
-
import { join as
|
|
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 (!
|
|
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 =
|
|
4644
|
-
response = serveStaticFile(filePath) ?? serveStaticFile(
|
|
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
|
-
|
|
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
|
|
6078
|
-
import { extname, join as
|
|
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.
|
|
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 (!
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
7499
|
+
const backupDir = join6(process.env["HOME"] || "~", ".contacts", "backups");
|
|
6813
7500
|
if (opts.list) {
|
|
6814
|
-
if (!
|
|
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 =
|
|
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 =
|
|
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 (!
|
|
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
|
-
|
|
7533
|
+
mkdirSync5(backupDir, { recursive: true });
|
|
6847
7534
|
const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
6848
|
-
const dest = opts.output ||
|
|
6849
|
-
|
|
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);
|