@hasna/contacts 0.5.3 → 0.6.1
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 +732 -20
- package/dist/db/companies.d.ts.map +1 -1
- package/dist/db/contacts.d.ts.map +1 -1
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/documents.d.ts +37 -0
- package/dist/db/documents.d.ts.map +1 -0
- package/dist/db/health.d.ts +40 -0
- package/dist/db/health.d.ts.map +1 -0
- package/dist/db/tags.d.ts.map +1 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +439 -6
- package/dist/lib/document-scanner.d.ts +8 -0
- package/dist/lib/document-scanner.d.ts.map +1 -0
- package/dist/lib/vault.d.ts +15 -0
- package/dist/lib/vault.d.ts.map +1 -0
- package/dist/mcp/index.js +522 -9
- package/dist/server/index.js +51 -6
- package/dist/types/index.d.ts +6 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -2647,6 +2647,41 @@ var init_database = __esm(() => {
|
|
|
2647
2647
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
2648
2648
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
2649
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
|
+
);
|
|
2650
2685
|
`
|
|
2651
2686
|
];
|
|
2652
2687
|
});
|
|
@@ -2696,6 +2731,7 @@ function rowToContact(row) {
|
|
|
2696
2731
|
follow_up_at: row.follow_up_at ?? null,
|
|
2697
2732
|
archived: !!row.archived,
|
|
2698
2733
|
project_id: row.project_id ?? null,
|
|
2734
|
+
sensitivity: row.sensitivity ?? "normal",
|
|
2699
2735
|
do_not_contact: !!row.do_not_contact,
|
|
2700
2736
|
priority: row.priority ?? 3,
|
|
2701
2737
|
timezone: row.timezone ?? null
|
|
@@ -2783,8 +2819,8 @@ function createContact(input, db) {
|
|
|
2783
2819
|
const firstName = input.first_name ?? "";
|
|
2784
2820
|
const lastName = input.last_name ?? "";
|
|
2785
2821
|
const displayName = input.display_name ?? (firstName || lastName ? `${firstName} ${lastName}`.trim() : "Unnamed Contact");
|
|
2786
|
-
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)
|
|
2787
|
-
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
2788
2824
|
id,
|
|
2789
2825
|
firstName,
|
|
2790
2826
|
lastName,
|
|
@@ -2803,6 +2839,7 @@ function createContact(input, db) {
|
|
|
2803
2839
|
input.status ?? "active",
|
|
2804
2840
|
input.follow_up_at ?? null,
|
|
2805
2841
|
input.project_id ?? null,
|
|
2842
|
+
input.sensitivity ?? "normal",
|
|
2806
2843
|
input.do_not_contact ? 1 : 0,
|
|
2807
2844
|
input.priority ?? 3,
|
|
2808
2845
|
input.timezone ?? null,
|
|
@@ -2851,6 +2888,7 @@ function listContacts(opts = {}, db) {
|
|
|
2851
2888
|
order_by = "display_name",
|
|
2852
2889
|
order_dir = "asc",
|
|
2853
2890
|
include_dnc = false,
|
|
2891
|
+
include_restricted = false,
|
|
2854
2892
|
priority_min,
|
|
2855
2893
|
updated_since
|
|
2856
2894
|
} = opts;
|
|
@@ -2861,6 +2899,9 @@ function listContacts(opts = {}, db) {
|
|
|
2861
2899
|
if (!include_dnc) {
|
|
2862
2900
|
conditions.push("c.do_not_contact = 0");
|
|
2863
2901
|
}
|
|
2902
|
+
if (!include_restricted) {
|
|
2903
|
+
conditions.push("c.sensitivity != 'restricted'");
|
|
2904
|
+
}
|
|
2864
2905
|
if (company_id) {
|
|
2865
2906
|
conditions.push("c.company_id = ?");
|
|
2866
2907
|
params.push(company_id);
|
|
@@ -2989,6 +3030,10 @@ function updateContact(id, input, db) {
|
|
|
2989
3030
|
setClauses.push("project_id = ?");
|
|
2990
3031
|
params.push(input.project_id);
|
|
2991
3032
|
}
|
|
3033
|
+
if (input.sensitivity !== undefined) {
|
|
3034
|
+
setClauses.push("sensitivity = ?");
|
|
3035
|
+
params.push(input.sensitivity);
|
|
3036
|
+
}
|
|
2992
3037
|
if (input.do_not_contact !== undefined) {
|
|
2993
3038
|
setClauses.push("do_not_contact = ?");
|
|
2994
3039
|
params.push(input.do_not_contact ? 1 : 0);
|
|
@@ -3036,26 +3081,26 @@ function searchContacts(query, db) {
|
|
|
3036
3081
|
const ftsRows = d.query(`
|
|
3037
3082
|
SELECT c.* FROM contacts c
|
|
3038
3083
|
JOIN contacts_fts fts ON fts.id = c.id
|
|
3039
|
-
WHERE contacts_fts MATCH ? AND c.archived = 0
|
|
3084
|
+
WHERE contacts_fts MATCH ? AND c.archived = 0 AND c.sensitivity != 'restricted'
|
|
3040
3085
|
ORDER BY rank
|
|
3041
3086
|
LIMIT 50
|
|
3042
3087
|
`).all(`"${query.replace(/"/g, '""')}"*`);
|
|
3043
3088
|
const emailRows = d.query(`
|
|
3044
3089
|
SELECT DISTINCT c.* FROM contacts c
|
|
3045
3090
|
JOIN emails e ON e.contact_id = c.id
|
|
3046
|
-
WHERE e.address LIKE ? AND c.archived = 0
|
|
3091
|
+
WHERE e.address LIKE ? AND c.archived = 0 AND c.sensitivity != 'restricted'
|
|
3047
3092
|
LIMIT 20
|
|
3048
3093
|
`).all(`%${query}%`);
|
|
3049
3094
|
const phoneRows = d.query(`
|
|
3050
3095
|
SELECT DISTINCT c.* FROM contacts c
|
|
3051
3096
|
JOIN phones p ON p.contact_id = c.id
|
|
3052
|
-
WHERE p.number LIKE ? AND c.archived = 0
|
|
3097
|
+
WHERE p.number LIKE ? AND c.archived = 0 AND c.sensitivity != 'restricted'
|
|
3053
3098
|
LIMIT 20
|
|
3054
3099
|
`).all(`%${query}%`);
|
|
3055
3100
|
const companyRows = d.query(`
|
|
3056
3101
|
SELECT DISTINCT c.* FROM contacts c
|
|
3057
3102
|
JOIN companies co ON co.id = c.company_id
|
|
3058
|
-
WHERE co.name LIKE ? AND c.archived = 0
|
|
3103
|
+
WHERE co.name LIKE ? AND c.archived = 0 AND c.sensitivity != 'restricted'
|
|
3059
3104
|
LIMIT 20
|
|
3060
3105
|
`).all(`%${query}%`);
|
|
3061
3106
|
const seen = new Set;
|
|
@@ -3537,6 +3582,7 @@ function listCompanyEmployees(companyId, db) {
|
|
|
3537
3582
|
follow_up_at: r.follow_up_at ?? null,
|
|
3538
3583
|
archived: !!r.archived,
|
|
3539
3584
|
project_id: r.project_id ?? null,
|
|
3585
|
+
sensitivity: r.sensitivity ?? "normal",
|
|
3540
3586
|
do_not_contact: !!r.do_not_contact,
|
|
3541
3587
|
priority: r.priority ?? 3,
|
|
3542
3588
|
timezone: r.timezone ?? null
|
|
@@ -3939,6 +3985,7 @@ function listContactsByTag(tagId, db) {
|
|
|
3939
3985
|
follow_up_at: r.follow_up_at ?? null,
|
|
3940
3986
|
archived: !!r.archived,
|
|
3941
3987
|
project_id: r.project_id ?? null,
|
|
3988
|
+
sensitivity: r.sensitivity ?? "normal",
|
|
3942
3989
|
do_not_contact: !!r.do_not_contact,
|
|
3943
3990
|
priority: r.priority ?? 3,
|
|
3944
3991
|
timezone: r.timezone ?? null
|
|
@@ -6001,6 +6048,418 @@ var init_org_chart = __esm(() => {
|
|
|
6001
6048
|
init_database();
|
|
6002
6049
|
});
|
|
6003
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
|
+
|
|
6004
6463
|
// node_modules/commander/esm.mjs
|
|
6005
6464
|
var import__ = __toESM(require_commander(), 1);
|
|
6006
6465
|
var {
|
|
@@ -6301,8 +6760,8 @@ function readConfig() {
|
|
|
6301
6760
|
}
|
|
6302
6761
|
|
|
6303
6762
|
// src/cli/index.tsx
|
|
6304
|
-
import { readFileSync as
|
|
6305
|
-
import { extname 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";
|
|
6306
6765
|
function renderTable(headers, rows) {
|
|
6307
6766
|
const colWidths = headers.map((h) => h.length);
|
|
6308
6767
|
for (const row of rows) {
|
|
@@ -6407,7 +6866,7 @@ async function confirm(question) {
|
|
|
6407
6866
|
const answer = await prompt(question + " [y/N]");
|
|
6408
6867
|
return answer.toLowerCase() === "y" || answer.toLowerCase() === "yes";
|
|
6409
6868
|
}
|
|
6410
|
-
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");
|
|
6411
6870
|
function collect(val, prev) {
|
|
6412
6871
|
return [...prev, val];
|
|
6413
6872
|
}
|
|
@@ -6473,10 +6932,11 @@ Add New Contact
|
|
|
6473
6932
|
\u2713 Contact created: ${contact.display_name} (${contact.id})
|
|
6474
6933
|
`));
|
|
6475
6934
|
});
|
|
6476
|
-
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) => {
|
|
6477
6936
|
const result = listContacts({
|
|
6478
6937
|
tag_id: opts.tag,
|
|
6479
6938
|
company_id: opts.company,
|
|
6939
|
+
include_restricted: opts.includeRestricted,
|
|
6480
6940
|
limit: parseInt(opts.limit, 10)
|
|
6481
6941
|
});
|
|
6482
6942
|
if (result.contacts.length === 0) {
|
|
@@ -6713,13 +7173,13 @@ Add New Tag
|
|
|
6713
7173
|
`));
|
|
6714
7174
|
});
|
|
6715
7175
|
program.command("import <file>").description("Import contacts from CSV, vCard (.vcf), or JSON file").action(async (file) => {
|
|
6716
|
-
if (!
|
|
7176
|
+
if (!existsSync8(file)) {
|
|
6717
7177
|
console.error(chalk.red(`
|
|
6718
7178
|
File not found: ${file}
|
|
6719
7179
|
`));
|
|
6720
7180
|
process.exit(1);
|
|
6721
7181
|
}
|
|
6722
|
-
const ext =
|
|
7182
|
+
const ext = extname3(file).toLowerCase();
|
|
6723
7183
|
const formatMap = {
|
|
6724
7184
|
".csv": "csv",
|
|
6725
7185
|
".vcf": "vcf",
|
|
@@ -6733,7 +7193,7 @@ Unsupported file type: ${ext}. Use .csv, .vcf, or .json
|
|
|
6733
7193
|
`));
|
|
6734
7194
|
process.exit(1);
|
|
6735
7195
|
}
|
|
6736
|
-
const data =
|
|
7196
|
+
const data = readFileSync5(file, "utf8");
|
|
6737
7197
|
console.log(chalk.blue(`
|
|
6738
7198
|
Importing ${format.toUpperCase()} from ${file}...
|
|
6739
7199
|
`));
|
|
@@ -6764,7 +7224,7 @@ Invalid format: ${format}. Use csv, vcf, or json
|
|
|
6764
7224
|
const { contacts } = listContacts({ limit: 1e5 });
|
|
6765
7225
|
const output = await exportContacts(format, contacts);
|
|
6766
7226
|
if (opts.output) {
|
|
6767
|
-
|
|
7227
|
+
writeFileSync4(opts.output, output, "utf8");
|
|
6768
7228
|
console.log(chalk.green(`
|
|
6769
7229
|
\u2713 Exported ${contacts.length} contact(s) to ${opts.output}
|
|
6770
7230
|
`));
|
|
@@ -7036,9 +7496,9 @@ program.command("init").description("Show setup info, stats, and configuration")
|
|
|
7036
7496
|
console.log();
|
|
7037
7497
|
});
|
|
7038
7498
|
program.command("backup").description("Backup the contacts database").option("--output <path>", "Output path").option("--list", "List existing backups").action((opts) => {
|
|
7039
|
-
const backupDir =
|
|
7499
|
+
const backupDir = join6(process.env["HOME"] || "~", ".contacts", "backups");
|
|
7040
7500
|
if (opts.list) {
|
|
7041
|
-
if (!
|
|
7501
|
+
if (!existsSync8(backupDir)) {
|
|
7042
7502
|
console.log(chalk.gray(`
|
|
7043
7503
|
No backups found.
|
|
7044
7504
|
`));
|
|
@@ -7055,7 +7515,7 @@ No backups found.
|
|
|
7055
7515
|
Existing Backups:
|
|
7056
7516
|
`));
|
|
7057
7517
|
for (const f of files) {
|
|
7058
|
-
const filePath =
|
|
7518
|
+
const filePath = join6(backupDir, f);
|
|
7059
7519
|
const size2 = statSync(filePath).size;
|
|
7060
7520
|
const mtime = statSync(filePath).mtime.toISOString().slice(0, 19).replace("T", " ");
|
|
7061
7521
|
console.log(` ${chalk.cyan(f)} ${chalk.gray(`${(size2 / 1024).toFixed(1)} KB ${mtime}`)}`);
|
|
@@ -7064,15 +7524,15 @@ Existing Backups:
|
|
|
7064
7524
|
return;
|
|
7065
7525
|
}
|
|
7066
7526
|
const src = getDbPath();
|
|
7067
|
-
if (!
|
|
7527
|
+
if (!existsSync8(src)) {
|
|
7068
7528
|
console.error(chalk.red(`
|
|
7069
7529
|
Database not found: ${src}
|
|
7070
7530
|
`));
|
|
7071
7531
|
process.exit(1);
|
|
7072
7532
|
}
|
|
7073
|
-
|
|
7533
|
+
mkdirSync5(backupDir, { recursive: true });
|
|
7074
7534
|
const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
7075
|
-
const dest = opts.output ||
|
|
7535
|
+
const dest = opts.output || join6(backupDir, `contacts-${ts}.db`);
|
|
7076
7536
|
copyFileSync2(src, dest);
|
|
7077
7537
|
const size = statSync(dest).size;
|
|
7078
7538
|
console.log(chalk.green(`
|
|
@@ -8407,4 +8867,256 @@ logoCmd.command("show <company-id>").description("Show the path to a company's l
|
|
|
8407
8867
|
console.log(chalk.yellow(`No logo set for ${company.name}`));
|
|
8408
8868
|
}
|
|
8409
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").option("--passphrase <pass>", "Passphrase (non-interactive)").action(async (opts) => {
|
|
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
|
+
let passphrase;
|
|
8904
|
+
if (opts.passphrase) {
|
|
8905
|
+
passphrase = opts.passphrase;
|
|
8906
|
+
} else {
|
|
8907
|
+
passphrase = await promptPassphrase("Enter vault passphrase: ");
|
|
8908
|
+
if (!passphrase) {
|
|
8909
|
+
console.error(chalk.red("Passphrase is required."));
|
|
8910
|
+
process.exit(1);
|
|
8911
|
+
}
|
|
8912
|
+
const confirm2 = await promptPassphrase("Confirm passphrase: ");
|
|
8913
|
+
if (passphrase !== confirm2) {
|
|
8914
|
+
console.error(chalk.red("Passphrases do not match."));
|
|
8915
|
+
process.exit(1);
|
|
8916
|
+
}
|
|
8917
|
+
}
|
|
8918
|
+
initVault2(passphrase);
|
|
8919
|
+
console.log(chalk.green(`
|
|
8920
|
+
Vault initialized and unlocked.
|
|
8921
|
+
`));
|
|
8922
|
+
});
|
|
8923
|
+
vaultCmd.command("unlock").description("Unlock the vault").option("--passphrase <pass>", "Passphrase (non-interactive)").action(async (opts) => {
|
|
8924
|
+
const { unlockVault: unlockVault2 } = await Promise.resolve().then(() => (init_vault(), exports_vault));
|
|
8925
|
+
const passphrase = opts.passphrase || await promptPassphrase("Enter vault passphrase: ");
|
|
8926
|
+
const ok = unlockVault2(passphrase);
|
|
8927
|
+
if (!ok) {
|
|
8928
|
+
console.error(chalk.red(`
|
|
8929
|
+
Invalid passphrase.
|
|
8930
|
+
`));
|
|
8931
|
+
process.exit(1);
|
|
8932
|
+
}
|
|
8933
|
+
console.log(chalk.green(`
|
|
8934
|
+
Vault unlocked.
|
|
8935
|
+
`));
|
|
8936
|
+
});
|
|
8937
|
+
vaultCmd.command("lock").description("Lock the vault").action(async () => {
|
|
8938
|
+
const { lockVault: lockVault2 } = await Promise.resolve().then(() => (init_vault(), exports_vault));
|
|
8939
|
+
lockVault2();
|
|
8940
|
+
console.log(chalk.green(`
|
|
8941
|
+
Vault locked.
|
|
8942
|
+
`));
|
|
8943
|
+
});
|
|
8944
|
+
vaultCmd.command("status").description("Show vault status").action(async () => {
|
|
8945
|
+
const { isVaultInitialized: isVaultInitialized2, isVaultUnlocked: isVaultUnlocked2 } = await Promise.resolve().then(() => (init_vault(), exports_vault));
|
|
8946
|
+
const initialized = isVaultInitialized2();
|
|
8947
|
+
const unlocked = isVaultUnlocked2();
|
|
8948
|
+
console.log(chalk.bold.blue(`
|
|
8949
|
+
Vault Status:`));
|
|
8950
|
+
console.log(` Initialized: ${initialized ? chalk.green("yes") : chalk.red("no")}`);
|
|
8951
|
+
console.log(` Unlocked: ${unlocked ? chalk.green("yes") : chalk.red("no")}`);
|
|
8952
|
+
if (initialized) {
|
|
8953
|
+
const db = getDatabase();
|
|
8954
|
+
try {
|
|
8955
|
+
const docCount = db.query("SELECT COUNT(*) as n FROM contact_documents").get().n;
|
|
8956
|
+
console.log(` Documents: ${chalk.cyan(String(docCount))}`);
|
|
8957
|
+
} catch {}
|
|
8958
|
+
}
|
|
8959
|
+
console.log();
|
|
8960
|
+
});
|
|
8961
|
+
var docsCmd = program.command("docs").description("Manage encrypted contact documents");
|
|
8962
|
+
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) => {
|
|
8963
|
+
const { addDocument: addDocument2 } = await Promise.resolve().then(() => (init_documents(), exports_documents));
|
|
8964
|
+
if (!opts.value) {
|
|
8965
|
+
console.error(chalk.red("--value is required"));
|
|
8966
|
+
process.exit(1);
|
|
8967
|
+
}
|
|
8968
|
+
const doc = addDocument2({
|
|
8969
|
+
contact_id: contactId,
|
|
8970
|
+
doc_type: opts.type || "other",
|
|
8971
|
+
label: opts.label,
|
|
8972
|
+
value: opts.value,
|
|
8973
|
+
file_path: opts.file,
|
|
8974
|
+
expires_at: opts.expires
|
|
8975
|
+
});
|
|
8976
|
+
console.log(chalk.green(`
|
|
8977
|
+
Document added: ${doc.doc_type} (${doc.id})
|
|
8978
|
+
`));
|
|
8979
|
+
});
|
|
8980
|
+
docsCmd.command("list <contact-id>").description("List documents for a contact (metadata only)").action(async (contactId) => {
|
|
8981
|
+
const { listDocuments: listDocuments2 } = await Promise.resolve().then(() => (init_documents(), exports_documents));
|
|
8982
|
+
const docs = listDocuments2(contactId);
|
|
8983
|
+
if (!docs.length) {
|
|
8984
|
+
console.log(chalk.gray(`
|
|
8985
|
+
No documents found.
|
|
8986
|
+
`));
|
|
8987
|
+
return;
|
|
8988
|
+
}
|
|
8989
|
+
console.log();
|
|
8990
|
+
renderTable(["Type", "Label", "Has File", "Expires", "Created"], docs.map((d) => ({
|
|
8991
|
+
Type: d.doc_type,
|
|
8992
|
+
Label: d.label || "",
|
|
8993
|
+
"Has File": d.has_file ? "yes" : "no",
|
|
8994
|
+
Expires: d.expires_at ? d.expires_at.slice(0, 10) : "",
|
|
8995
|
+
Created: d.created_at.slice(0, 10)
|
|
8996
|
+
})));
|
|
8997
|
+
console.log(chalk.gray(`
|
|
8998
|
+
${docs.length} document(s)
|
|
8999
|
+
`));
|
|
9000
|
+
});
|
|
9001
|
+
docsCmd.command("show <doc-id>").description("Show a document with decrypted value (vault must be unlocked)").action(async (docId) => {
|
|
9002
|
+
const { getDocument: getDocument2 } = await Promise.resolve().then(() => (init_documents(), exports_documents));
|
|
9003
|
+
const doc = getDocument2(docId);
|
|
9004
|
+
console.log(chalk.bold.blue(`
|
|
9005
|
+
Document: ${doc.doc_type}`));
|
|
9006
|
+
if (doc.label)
|
|
9007
|
+
console.log(chalk.gray(" Label: ") + doc.label);
|
|
9008
|
+
console.log(chalk.gray(" Value: ") + doc.value);
|
|
9009
|
+
console.log(chalk.gray(" Has File:") + (doc.has_file ? " yes" : " no"));
|
|
9010
|
+
if (doc.expires_at)
|
|
9011
|
+
console.log(chalk.gray(" Expires: ") + doc.expires_at.slice(0, 10));
|
|
9012
|
+
console.log(chalk.gray(` ID: ${doc.id}
|
|
9013
|
+
`));
|
|
9014
|
+
});
|
|
9015
|
+
docsCmd.command("remove <doc-id>").description("Delete a document").action(async (docId) => {
|
|
9016
|
+
const { deleteDocument: deleteDocument2 } = await Promise.resolve().then(() => (init_documents(), exports_documents));
|
|
9017
|
+
deleteDocument2(docId);
|
|
9018
|
+
console.log(chalk.green(`
|
|
9019
|
+
Document deleted: ${docId}
|
|
9020
|
+
`));
|
|
9021
|
+
});
|
|
9022
|
+
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) => {
|
|
9023
|
+
const { scanDocument: scanDocument2 } = await Promise.resolve().then(() => (init_document_scanner(), exports_document_scanner));
|
|
9024
|
+
console.log(chalk.blue(`
|
|
9025
|
+
Scanning document...
|
|
9026
|
+
`));
|
|
9027
|
+
const result = await scanDocument2(imagePath, opts.type);
|
|
9028
|
+
console.log(chalk.bold(` Type: ${result.document_type} Confidence: ${(result.confidence * 100).toFixed(0)}%
|
|
9029
|
+
`));
|
|
9030
|
+
console.log(chalk.yellow(" Extracted fields:"));
|
|
9031
|
+
for (const [k, v] of Object.entries(result.fields)) {
|
|
9032
|
+
console.log(` ${chalk.gray(k.padEnd(20))} ${v}`);
|
|
9033
|
+
}
|
|
9034
|
+
console.log();
|
|
9035
|
+
});
|
|
9036
|
+
docsCmd.command("types").description("List all valid document types").action(async () => {
|
|
9037
|
+
const { DOCUMENT_TYPES: DOCUMENT_TYPES2 } = await Promise.resolve().then(() => (init_documents(), exports_documents));
|
|
9038
|
+
console.log(chalk.bold.blue(`
|
|
9039
|
+
Document Types:
|
|
9040
|
+
`));
|
|
9041
|
+
for (const t of DOCUMENT_TYPES2) {
|
|
9042
|
+
console.log(` ${chalk.cyan(t)}`);
|
|
9043
|
+
}
|
|
9044
|
+
console.log();
|
|
9045
|
+
});
|
|
9046
|
+
var healthCmd = program.command("health").description("Manage contact health data (vault required)");
|
|
9047
|
+
healthCmd.command("show <id>").description("Show health data for a contact").action(async (id) => {
|
|
9048
|
+
const { getHealthData: getHealthData2 } = await Promise.resolve().then(() => (init_health(), exports_health));
|
|
9049
|
+
const contact = getContact(id);
|
|
9050
|
+
const health = getHealthData2(id);
|
|
9051
|
+
if (!health) {
|
|
9052
|
+
console.log(chalk.gray(`
|
|
9053
|
+
No health data for ${contact.display_name}.
|
|
9054
|
+
`));
|
|
9055
|
+
return;
|
|
9056
|
+
}
|
|
9057
|
+
console.log(chalk.bold.blue(`
|
|
9058
|
+
Health: ${contact.display_name}
|
|
9059
|
+
`));
|
|
9060
|
+
if (health.blood_type)
|
|
9061
|
+
console.log(chalk.gray(" Blood Type: ") + health.blood_type);
|
|
9062
|
+
if (health.allergies.length)
|
|
9063
|
+
console.log(chalk.gray(" Allergies: ") + health.allergies.join(", "));
|
|
9064
|
+
if (health.medical_conditions.length)
|
|
9065
|
+
console.log(chalk.gray(" Conditions: ") + health.medical_conditions.join(", "));
|
|
9066
|
+
if (health.medications.length)
|
|
9067
|
+
console.log(chalk.gray(" Medications: ") + health.medications.join(", "));
|
|
9068
|
+
if (health.emergency_contacts.length) {
|
|
9069
|
+
console.log(chalk.yellow(`
|
|
9070
|
+
Emergency Contacts:`));
|
|
9071
|
+
for (const ec of health.emergency_contacts) {
|
|
9072
|
+
console.log(` ${chalk.bold(ec.name)} ${ec.phone} ${chalk.gray(ec.relationship)}`);
|
|
9073
|
+
}
|
|
9074
|
+
}
|
|
9075
|
+
if (health.health_insurance_provider)
|
|
9076
|
+
console.log(chalk.gray(`
|
|
9077
|
+
Insurance: `) + `${health.health_insurance_provider} (${health.health_insurance_id || "no ID"})`);
|
|
9078
|
+
if (health.primary_physician)
|
|
9079
|
+
console.log(chalk.gray(" Physician: ") + `${health.primary_physician} ${health.primary_physician_phone || ""}`);
|
|
9080
|
+
console.log(chalk.gray(" Organ Donor: ") + (health.organ_donor ? "yes" : "no"));
|
|
9081
|
+
if (health.notes)
|
|
9082
|
+
console.log(chalk.gray(" Notes: ") + health.notes);
|
|
9083
|
+
console.log();
|
|
9084
|
+
});
|
|
9085
|
+
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) => {
|
|
9086
|
+
const { setHealthData: setHealthData2 } = await Promise.resolve().then(() => (init_health(), exports_health));
|
|
9087
|
+
const contact = getContact(id);
|
|
9088
|
+
const input = {};
|
|
9089
|
+
if (opts.bloodType)
|
|
9090
|
+
input.blood_type = opts.bloodType;
|
|
9091
|
+
if (opts.allergies)
|
|
9092
|
+
input.allergies = opts.allergies.split(",").map((s) => s.trim());
|
|
9093
|
+
if (opts.conditions)
|
|
9094
|
+
input.medical_conditions = opts.conditions.split(",").map((s) => s.trim());
|
|
9095
|
+
if (opts.medications)
|
|
9096
|
+
input.medications = opts.medications.split(",").map((s) => s.trim());
|
|
9097
|
+
if (opts.insuranceProvider)
|
|
9098
|
+
input.health_insurance_provider = opts.insuranceProvider;
|
|
9099
|
+
if (opts.insuranceId)
|
|
9100
|
+
input.health_insurance_id = opts.insuranceId;
|
|
9101
|
+
if (opts.physician)
|
|
9102
|
+
input.primary_physician = opts.physician;
|
|
9103
|
+
if (opts.physicianPhone)
|
|
9104
|
+
input.primary_physician_phone = opts.physicianPhone;
|
|
9105
|
+
if (opts.organDonor !== undefined)
|
|
9106
|
+
input.organ_donor = opts.organDonor;
|
|
9107
|
+
if (opts.notes)
|
|
9108
|
+
input.notes = opts.notes;
|
|
9109
|
+
setHealthData2(id, input);
|
|
9110
|
+
console.log(chalk.green(`
|
|
9111
|
+
Health data updated for ${contact.display_name}
|
|
9112
|
+
`));
|
|
9113
|
+
});
|
|
9114
|
+
healthCmd.command("clear <id>").description("Delete all health data for a contact").action(async (id) => {
|
|
9115
|
+
const { deleteHealthData: deleteHealthData2 } = await Promise.resolve().then(() => (init_health(), exports_health));
|
|
9116
|
+
const contact = getContact(id);
|
|
9117
|
+
deleteHealthData2(id);
|
|
9118
|
+
console.log(chalk.green(`
|
|
9119
|
+
Health data cleared for ${contact.display_name}
|
|
9120
|
+
`));
|
|
9121
|
+
});
|
|
8410
9122
|
program.parse(process.argv);
|