@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/mcp/index.js
CHANGED
|
@@ -557,6 +557,41 @@ var init_database = __esm(() => {
|
|
|
557
557
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
558
558
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
559
559
|
);
|
|
560
|
+
`,
|
|
561
|
+
`
|
|
562
|
+
ALTER TABLE contacts ADD COLUMN sensitivity TEXT NOT NULL DEFAULT 'normal' CHECK(sensitivity IN ('normal','confidential','restricted'));
|
|
563
|
+
|
|
564
|
+
CREATE TABLE IF NOT EXISTS contact_documents (
|
|
565
|
+
id TEXT PRIMARY KEY,
|
|
566
|
+
contact_id TEXT NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
|
|
567
|
+
doc_type TEXT NOT NULL,
|
|
568
|
+
label TEXT,
|
|
569
|
+
encrypted_value TEXT NOT NULL,
|
|
570
|
+
iv TEXT NOT NULL,
|
|
571
|
+
encrypted_file_path TEXT,
|
|
572
|
+
metadata TEXT NOT NULL DEFAULT '{}',
|
|
573
|
+
expires_at TEXT,
|
|
574
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
575
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
576
|
+
);
|
|
577
|
+
|
|
578
|
+
CREATE TABLE IF NOT EXISTS contact_health (
|
|
579
|
+
id TEXT PRIMARY KEY,
|
|
580
|
+
contact_id TEXT NOT NULL UNIQUE REFERENCES contacts(id) ON DELETE CASCADE,
|
|
581
|
+
blood_type TEXT,
|
|
582
|
+
allergies TEXT NOT NULL DEFAULT '[]',
|
|
583
|
+
medical_conditions TEXT NOT NULL DEFAULT '[]',
|
|
584
|
+
medications TEXT NOT NULL DEFAULT '[]',
|
|
585
|
+
emergency_contacts TEXT NOT NULL DEFAULT '[]',
|
|
586
|
+
health_insurance_provider TEXT,
|
|
587
|
+
health_insurance_id TEXT,
|
|
588
|
+
primary_physician TEXT,
|
|
589
|
+
primary_physician_phone TEXT,
|
|
590
|
+
organ_donor INTEGER NOT NULL DEFAULT 0,
|
|
591
|
+
notes TEXT,
|
|
592
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
593
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
594
|
+
);
|
|
560
595
|
`
|
|
561
596
|
];
|
|
562
597
|
});
|
|
@@ -656,6 +691,7 @@ function rowToContact(row) {
|
|
|
656
691
|
follow_up_at: row.follow_up_at ?? null,
|
|
657
692
|
archived: !!row.archived,
|
|
658
693
|
project_id: row.project_id ?? null,
|
|
694
|
+
sensitivity: row.sensitivity ?? "normal",
|
|
659
695
|
do_not_contact: !!row.do_not_contact,
|
|
660
696
|
priority: row.priority ?? 3,
|
|
661
697
|
timezone: row.timezone ?? null
|
|
@@ -743,8 +779,8 @@ function createContact(input, db) {
|
|
|
743
779
|
const firstName = input.first_name ?? "";
|
|
744
780
|
const lastName = input.last_name ?? "";
|
|
745
781
|
const displayName = input.display_name ?? (firstName || lastName ? `${firstName} ${lastName}`.trim() : "Unnamed Contact");
|
|
746
|
-
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)
|
|
747
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
782
|
+
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)
|
|
783
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
748
784
|
id,
|
|
749
785
|
firstName,
|
|
750
786
|
lastName,
|
|
@@ -763,6 +799,7 @@ function createContact(input, db) {
|
|
|
763
799
|
input.status ?? "active",
|
|
764
800
|
input.follow_up_at ?? null,
|
|
765
801
|
input.project_id ?? null,
|
|
802
|
+
input.sensitivity ?? "normal",
|
|
766
803
|
input.do_not_contact ? 1 : 0,
|
|
767
804
|
input.priority ?? 3,
|
|
768
805
|
input.timezone ?? null,
|
|
@@ -811,6 +848,7 @@ function listContacts(opts = {}, db) {
|
|
|
811
848
|
order_by = "display_name",
|
|
812
849
|
order_dir = "asc",
|
|
813
850
|
include_dnc = false,
|
|
851
|
+
include_restricted = false,
|
|
814
852
|
priority_min,
|
|
815
853
|
updated_since
|
|
816
854
|
} = opts;
|
|
@@ -821,6 +859,9 @@ function listContacts(opts = {}, db) {
|
|
|
821
859
|
if (!include_dnc) {
|
|
822
860
|
conditions.push("c.do_not_contact = 0");
|
|
823
861
|
}
|
|
862
|
+
if (!include_restricted) {
|
|
863
|
+
conditions.push("c.sensitivity != 'restricted'");
|
|
864
|
+
}
|
|
824
865
|
if (company_id) {
|
|
825
866
|
conditions.push("c.company_id = ?");
|
|
826
867
|
params.push(company_id);
|
|
@@ -949,6 +990,10 @@ function updateContact(id, input, db) {
|
|
|
949
990
|
setClauses.push("project_id = ?");
|
|
950
991
|
params.push(input.project_id);
|
|
951
992
|
}
|
|
993
|
+
if (input.sensitivity !== undefined) {
|
|
994
|
+
setClauses.push("sensitivity = ?");
|
|
995
|
+
params.push(input.sensitivity);
|
|
996
|
+
}
|
|
952
997
|
if (input.do_not_contact !== undefined) {
|
|
953
998
|
setClauses.push("do_not_contact = ?");
|
|
954
999
|
params.push(input.do_not_contact ? 1 : 0);
|
|
@@ -996,26 +1041,26 @@ function searchContacts(query, db) {
|
|
|
996
1041
|
const ftsRows = d.query(`
|
|
997
1042
|
SELECT c.* FROM contacts c
|
|
998
1043
|
JOIN contacts_fts fts ON fts.id = c.id
|
|
999
|
-
WHERE contacts_fts MATCH ? AND c.archived = 0
|
|
1044
|
+
WHERE contacts_fts MATCH ? AND c.archived = 0 AND c.sensitivity != 'restricted'
|
|
1000
1045
|
ORDER BY rank
|
|
1001
1046
|
LIMIT 50
|
|
1002
1047
|
`).all(`"${query.replace(/"/g, '""')}"*`);
|
|
1003
1048
|
const emailRows = d.query(`
|
|
1004
1049
|
SELECT DISTINCT c.* FROM contacts c
|
|
1005
1050
|
JOIN emails e ON e.contact_id = c.id
|
|
1006
|
-
WHERE e.address LIKE ? AND c.archived = 0
|
|
1051
|
+
WHERE e.address LIKE ? AND c.archived = 0 AND c.sensitivity != 'restricted'
|
|
1007
1052
|
LIMIT 20
|
|
1008
1053
|
`).all(`%${query}%`);
|
|
1009
1054
|
const phoneRows = d.query(`
|
|
1010
1055
|
SELECT DISTINCT c.* FROM contacts c
|
|
1011
1056
|
JOIN phones p ON p.contact_id = c.id
|
|
1012
|
-
WHERE p.number LIKE ? AND c.archived = 0
|
|
1057
|
+
WHERE p.number LIKE ? AND c.archived = 0 AND c.sensitivity != 'restricted'
|
|
1013
1058
|
LIMIT 20
|
|
1014
1059
|
`).all(`%${query}%`);
|
|
1015
1060
|
const companyRows = d.query(`
|
|
1016
1061
|
SELECT DISTINCT c.* FROM contacts c
|
|
1017
1062
|
JOIN companies co ON co.id = c.company_id
|
|
1018
|
-
WHERE co.name LIKE ? AND c.archived = 0
|
|
1063
|
+
WHERE co.name LIKE ? AND c.archived = 0 AND c.sensitivity != 'restricted'
|
|
1019
1064
|
LIMIT 20
|
|
1020
1065
|
`).all(`%${query}%`);
|
|
1021
1066
|
const seen = new Set;
|
|
@@ -4271,6 +4316,361 @@ function deleteImage(entityId) {
|
|
|
4271
4316
|
return deleted;
|
|
4272
4317
|
}
|
|
4273
4318
|
|
|
4319
|
+
// src/lib/vault.ts
|
|
4320
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3 } from "fs";
|
|
4321
|
+
import { join as join4 } from "path";
|
|
4322
|
+
import { createCipheriv, createDecipheriv, randomBytes, pbkdf2Sync, createHash } from "crypto";
|
|
4323
|
+
var VAULT_DIR = join4(process.env["HOME"] || "~", ".contacts");
|
|
4324
|
+
var VAULT_CONFIG = join4(VAULT_DIR, "vault.json");
|
|
4325
|
+
var DOCUMENTS_DIR = join4(VAULT_DIR, "documents");
|
|
4326
|
+
var _derivedKey = null;
|
|
4327
|
+
function deriveKey(passphrase, salt) {
|
|
4328
|
+
return pbkdf2Sync(passphrase, salt, 1e5, 32, "sha512");
|
|
4329
|
+
}
|
|
4330
|
+
function initVault(passphrase) {
|
|
4331
|
+
if (!existsSync4(VAULT_DIR))
|
|
4332
|
+
mkdirSync3(VAULT_DIR, { recursive: true });
|
|
4333
|
+
if (!existsSync4(DOCUMENTS_DIR))
|
|
4334
|
+
mkdirSync3(DOCUMENTS_DIR, { recursive: true });
|
|
4335
|
+
const salt = randomBytes(32);
|
|
4336
|
+
const key = deriveKey(passphrase, salt);
|
|
4337
|
+
const keyHash = createHash("sha256").update(key).digest("hex");
|
|
4338
|
+
const config = { salt: salt.toString("hex"), key_hash: keyHash, created_at: new Date().toISOString() };
|
|
4339
|
+
writeFileSync2(VAULT_CONFIG, JSON.stringify(config, null, 2));
|
|
4340
|
+
_derivedKey = key;
|
|
4341
|
+
}
|
|
4342
|
+
function isVaultInitialized() {
|
|
4343
|
+
return existsSync4(VAULT_CONFIG);
|
|
4344
|
+
}
|
|
4345
|
+
function unlockVault(passphrase) {
|
|
4346
|
+
if (!existsSync4(VAULT_CONFIG))
|
|
4347
|
+
throw new Error("Vault not initialized. Run 'contacts vault init' first.");
|
|
4348
|
+
const config = JSON.parse(readFileSync3(VAULT_CONFIG, "utf-8"));
|
|
4349
|
+
const salt = Buffer.from(config.salt, "hex");
|
|
4350
|
+
const key = deriveKey(passphrase, salt);
|
|
4351
|
+
const keyHash = createHash("sha256").update(key).digest("hex");
|
|
4352
|
+
if (keyHash !== config.key_hash)
|
|
4353
|
+
return false;
|
|
4354
|
+
_derivedKey = key;
|
|
4355
|
+
return true;
|
|
4356
|
+
}
|
|
4357
|
+
function lockVault() {
|
|
4358
|
+
_derivedKey = null;
|
|
4359
|
+
}
|
|
4360
|
+
function isVaultUnlocked() {
|
|
4361
|
+
return _derivedKey !== null;
|
|
4362
|
+
}
|
|
4363
|
+
function requireVault() {
|
|
4364
|
+
if (!_derivedKey)
|
|
4365
|
+
throw new Error("Vault is locked. Unlock with 'contacts vault unlock' or vault_unlock MCP tool first.");
|
|
4366
|
+
return _derivedKey;
|
|
4367
|
+
}
|
|
4368
|
+
function encrypt(plaintext) {
|
|
4369
|
+
const key = requireVault();
|
|
4370
|
+
const iv = randomBytes(16);
|
|
4371
|
+
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
4372
|
+
let encrypted = cipher.update(plaintext, "utf8", "hex");
|
|
4373
|
+
encrypted += cipher.final("hex");
|
|
4374
|
+
const authTag = cipher.getAuthTag().toString("hex");
|
|
4375
|
+
return { ciphertext: encrypted + ":" + authTag, iv: iv.toString("hex") };
|
|
4376
|
+
}
|
|
4377
|
+
function decrypt(ciphertext, iv) {
|
|
4378
|
+
const key = requireVault();
|
|
4379
|
+
const [encData, authTag] = ciphertext.split(":");
|
|
4380
|
+
if (!encData || !authTag)
|
|
4381
|
+
throw new Error("Invalid ciphertext format");
|
|
4382
|
+
const decipher = createDecipheriv("aes-256-gcm", key, Buffer.from(iv, "hex"));
|
|
4383
|
+
decipher.setAuthTag(Buffer.from(authTag, "hex"));
|
|
4384
|
+
let decrypted = decipher.update(encData, "hex", "utf8");
|
|
4385
|
+
decrypted += decipher.final("utf8");
|
|
4386
|
+
return decrypted;
|
|
4387
|
+
}
|
|
4388
|
+
function encryptFile(sourcePath, entityId) {
|
|
4389
|
+
const key = requireVault();
|
|
4390
|
+
if (!existsSync4(DOCUMENTS_DIR))
|
|
4391
|
+
mkdirSync3(DOCUMENTS_DIR, { recursive: true });
|
|
4392
|
+
const data = readFileSync3(sourcePath);
|
|
4393
|
+
const iv = randomBytes(16);
|
|
4394
|
+
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
4395
|
+
const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
|
|
4396
|
+
const authTag = cipher.getAuthTag();
|
|
4397
|
+
const destPath = join4(DOCUMENTS_DIR, `${entityId}.enc`);
|
|
4398
|
+
const output = Buffer.concat([iv, authTag, encrypted]);
|
|
4399
|
+
writeFileSync2(destPath, output);
|
|
4400
|
+
return destPath;
|
|
4401
|
+
}
|
|
4402
|
+
|
|
4403
|
+
// src/db/documents.ts
|
|
4404
|
+
init_database();
|
|
4405
|
+
import { existsSync as existsSync5, unlinkSync as unlinkSync2 } from "fs";
|
|
4406
|
+
var DOCUMENT_TYPES = [
|
|
4407
|
+
"passport",
|
|
4408
|
+
"national_id",
|
|
4409
|
+
"tax_id",
|
|
4410
|
+
"ssn",
|
|
4411
|
+
"drivers_license",
|
|
4412
|
+
"bank_account",
|
|
4413
|
+
"visa",
|
|
4414
|
+
"insurance",
|
|
4415
|
+
"contract",
|
|
4416
|
+
"certificate",
|
|
4417
|
+
"medical_record",
|
|
4418
|
+
"prescription",
|
|
4419
|
+
"allergy_list",
|
|
4420
|
+
"vaccination",
|
|
4421
|
+
"blood_type",
|
|
4422
|
+
"health_insurance",
|
|
4423
|
+
"medical_condition",
|
|
4424
|
+
"emergency_contact_medical",
|
|
4425
|
+
"other"
|
|
4426
|
+
];
|
|
4427
|
+
function addDocument(input, db) {
|
|
4428
|
+
requireVault();
|
|
4429
|
+
const _db2 = db || getDatabase();
|
|
4430
|
+
const id = uuid();
|
|
4431
|
+
const { ciphertext, iv } = encrypt(input.value);
|
|
4432
|
+
let encFilePath = null;
|
|
4433
|
+
if (input.file_path) {
|
|
4434
|
+
encFilePath = encryptFile(input.file_path, id);
|
|
4435
|
+
}
|
|
4436
|
+
_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());
|
|
4437
|
+
return getDocument(id, _db2);
|
|
4438
|
+
}
|
|
4439
|
+
function getDocument(id, db) {
|
|
4440
|
+
requireVault();
|
|
4441
|
+
const _db2 = db || getDatabase();
|
|
4442
|
+
const row = _db2.query(`SELECT * FROM contact_documents WHERE id = ?`).get(id);
|
|
4443
|
+
if (!row)
|
|
4444
|
+
throw new Error(`Document not found: ${id}`);
|
|
4445
|
+
return rowToDoc(row);
|
|
4446
|
+
}
|
|
4447
|
+
function listDocuments(contactId, db) {
|
|
4448
|
+
const _db2 = db || getDatabase();
|
|
4449
|
+
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);
|
|
4450
|
+
return rows.map((r) => ({
|
|
4451
|
+
id: r.id,
|
|
4452
|
+
doc_type: r.doc_type,
|
|
4453
|
+
label: r.label,
|
|
4454
|
+
has_file: !!r.encrypted_file_path,
|
|
4455
|
+
expires_at: r.expires_at,
|
|
4456
|
+
created_at: r.created_at
|
|
4457
|
+
}));
|
|
4458
|
+
}
|
|
4459
|
+
function deleteDocument(id, db) {
|
|
4460
|
+
const _db2 = db || getDatabase();
|
|
4461
|
+
const row = _db2.query(`SELECT encrypted_file_path FROM contact_documents WHERE id = ?`).get(id);
|
|
4462
|
+
if (row?.encrypted_file_path && existsSync5(row.encrypted_file_path)) {
|
|
4463
|
+
try {
|
|
4464
|
+
unlinkSync2(row.encrypted_file_path);
|
|
4465
|
+
} catch {}
|
|
4466
|
+
}
|
|
4467
|
+
_db2.query(`DELETE FROM contact_documents WHERE id = ?`).run(id);
|
|
4468
|
+
}
|
|
4469
|
+
function rowToDoc(row) {
|
|
4470
|
+
return {
|
|
4471
|
+
id: row.id,
|
|
4472
|
+
contact_id: row.contact_id,
|
|
4473
|
+
doc_type: row.doc_type,
|
|
4474
|
+
label: row.label,
|
|
4475
|
+
value: decrypt(row.encrypted_value, row.iv),
|
|
4476
|
+
has_file: !!row.encrypted_file_path,
|
|
4477
|
+
metadata: JSON.parse(row.metadata || "{}"),
|
|
4478
|
+
expires_at: row.expires_at,
|
|
4479
|
+
created_at: row.created_at,
|
|
4480
|
+
updated_at: row.updated_at
|
|
4481
|
+
};
|
|
4482
|
+
}
|
|
4483
|
+
|
|
4484
|
+
// src/db/health.ts
|
|
4485
|
+
init_database();
|
|
4486
|
+
function setHealthData(contactId, input, db) {
|
|
4487
|
+
requireVault();
|
|
4488
|
+
const _db2 = db || getDatabase();
|
|
4489
|
+
const existing = _db2.query(`SELECT id FROM contact_health WHERE contact_id = ?`).get(contactId);
|
|
4490
|
+
if (existing) {
|
|
4491
|
+
const sets = [];
|
|
4492
|
+
const params = [];
|
|
4493
|
+
if (input.blood_type !== undefined) {
|
|
4494
|
+
sets.push("blood_type = ?");
|
|
4495
|
+
params.push(input.blood_type);
|
|
4496
|
+
}
|
|
4497
|
+
if (input.allergies !== undefined) {
|
|
4498
|
+
sets.push("allergies = ?");
|
|
4499
|
+
params.push(JSON.stringify(input.allergies));
|
|
4500
|
+
}
|
|
4501
|
+
if (input.medical_conditions !== undefined) {
|
|
4502
|
+
sets.push("medical_conditions = ?");
|
|
4503
|
+
params.push(JSON.stringify(input.medical_conditions));
|
|
4504
|
+
}
|
|
4505
|
+
if (input.medications !== undefined) {
|
|
4506
|
+
sets.push("medications = ?");
|
|
4507
|
+
params.push(JSON.stringify(input.medications));
|
|
4508
|
+
}
|
|
4509
|
+
if (input.emergency_contacts !== undefined) {
|
|
4510
|
+
sets.push("emergency_contacts = ?");
|
|
4511
|
+
params.push(JSON.stringify(input.emergency_contacts));
|
|
4512
|
+
}
|
|
4513
|
+
if (input.health_insurance_provider !== undefined) {
|
|
4514
|
+
sets.push("health_insurance_provider = ?");
|
|
4515
|
+
params.push(input.health_insurance_provider);
|
|
4516
|
+
}
|
|
4517
|
+
if (input.health_insurance_id !== undefined) {
|
|
4518
|
+
sets.push("health_insurance_id = ?");
|
|
4519
|
+
params.push(input.health_insurance_id);
|
|
4520
|
+
}
|
|
4521
|
+
if (input.primary_physician !== undefined) {
|
|
4522
|
+
sets.push("primary_physician = ?");
|
|
4523
|
+
params.push(input.primary_physician);
|
|
4524
|
+
}
|
|
4525
|
+
if (input.primary_physician_phone !== undefined) {
|
|
4526
|
+
sets.push("primary_physician_phone = ?");
|
|
4527
|
+
params.push(input.primary_physician_phone);
|
|
4528
|
+
}
|
|
4529
|
+
if (input.organ_donor !== undefined) {
|
|
4530
|
+
sets.push("organ_donor = ?");
|
|
4531
|
+
params.push(input.organ_donor ? 1 : 0);
|
|
4532
|
+
}
|
|
4533
|
+
if (input.notes !== undefined) {
|
|
4534
|
+
sets.push("notes = ?");
|
|
4535
|
+
params.push(input.notes);
|
|
4536
|
+
}
|
|
4537
|
+
if (sets.length) {
|
|
4538
|
+
sets.push("updated_at = ?");
|
|
4539
|
+
params.push(now());
|
|
4540
|
+
params.push(contactId);
|
|
4541
|
+
_db2.query(`UPDATE contact_health SET ${sets.join(", ")} WHERE contact_id = ?`).run(...params);
|
|
4542
|
+
}
|
|
4543
|
+
} else {
|
|
4544
|
+
const id = uuid();
|
|
4545
|
+
_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());
|
|
4546
|
+
}
|
|
4547
|
+
return getHealthData(contactId, _db2);
|
|
4548
|
+
}
|
|
4549
|
+
function getHealthData(contactId, db) {
|
|
4550
|
+
requireVault();
|
|
4551
|
+
const _db2 = db || getDatabase();
|
|
4552
|
+
const row = _db2.query(`SELECT * FROM contact_health WHERE contact_id = ?`).get(contactId);
|
|
4553
|
+
if (!row)
|
|
4554
|
+
return null;
|
|
4555
|
+
return {
|
|
4556
|
+
id: row.id,
|
|
4557
|
+
contact_id: row.contact_id,
|
|
4558
|
+
blood_type: row.blood_type,
|
|
4559
|
+
allergies: JSON.parse(row.allergies || "[]"),
|
|
4560
|
+
medical_conditions: JSON.parse(row.medical_conditions || "[]"),
|
|
4561
|
+
medications: JSON.parse(row.medications || "[]"),
|
|
4562
|
+
emergency_contacts: JSON.parse(row.emergency_contacts || "[]"),
|
|
4563
|
+
health_insurance_provider: row.health_insurance_provider,
|
|
4564
|
+
health_insurance_id: row.health_insurance_id,
|
|
4565
|
+
primary_physician: row.primary_physician,
|
|
4566
|
+
primary_physician_phone: row.primary_physician_phone,
|
|
4567
|
+
organ_donor: !!row.organ_donor,
|
|
4568
|
+
notes: row.notes,
|
|
4569
|
+
created_at: row.created_at,
|
|
4570
|
+
updated_at: row.updated_at
|
|
4571
|
+
};
|
|
4572
|
+
}
|
|
4573
|
+
function deleteHealthData(contactId, db) {
|
|
4574
|
+
const _db2 = db || getDatabase();
|
|
4575
|
+
_db2.query(`DELETE FROM contact_health WHERE contact_id = ?`).run(contactId);
|
|
4576
|
+
}
|
|
4577
|
+
|
|
4578
|
+
// src/lib/document-scanner.ts
|
|
4579
|
+
import { readFileSync as readFileSync4, existsSync as existsSync6 } from "fs";
|
|
4580
|
+
import { extname as extname2 } from "path";
|
|
4581
|
+
async function scanDocument(imageSource, docType) {
|
|
4582
|
+
const apiKey = process.env["OPENAI_API_KEY"];
|
|
4583
|
+
if (!apiKey) {
|
|
4584
|
+
throw new Error("OPENAI_API_KEY not set. Set it in ~/.secrets or environment to use document scanning.");
|
|
4585
|
+
}
|
|
4586
|
+
let imageData;
|
|
4587
|
+
if (imageSource.startsWith("data:image/")) {
|
|
4588
|
+
imageData = imageSource;
|
|
4589
|
+
} else if (existsSync6(imageSource)) {
|
|
4590
|
+
const buffer = readFileSync4(imageSource);
|
|
4591
|
+
const ext = extname2(imageSource).slice(1).toLowerCase();
|
|
4592
|
+
const mime = ext === "jpg" || ext === "jpeg" ? "image/jpeg" : ext === "png" ? "image/png" : ext === "webp" ? "image/webp" : ext === "gif" ? "image/gif" : "image/jpeg";
|
|
4593
|
+
imageData = `data:${mime};base64,${buffer.toString("base64")}`;
|
|
4594
|
+
} else if (/^[A-Za-z0-9+/=\n\r]+$/.test(imageSource.trim()) && imageSource.length > 100) {
|
|
4595
|
+
imageData = `data:image/jpeg;base64,${imageSource.trim()}`;
|
|
4596
|
+
} else {
|
|
4597
|
+
throw new Error(`Image source not found or invalid: ${imageSource.slice(0, 50)}...`);
|
|
4598
|
+
}
|
|
4599
|
+
const typeHint = docType ? ` This is a ${docType} document.` : "";
|
|
4600
|
+
const prompt = `Extract all text and structured data from this document image.${typeHint} Return a JSON object with these fields:
|
|
4601
|
+
- document_type: detected type (passport, national_id, drivers_license, tax_document, medical_record, prescription, insurance_card, bank_statement, visa, certificate, contract, other)
|
|
4602
|
+
- full_name: full name as shown
|
|
4603
|
+
- date_of_birth: in YYYY-MM-DD format if visible
|
|
4604
|
+
- document_number: main ID/document number
|
|
4605
|
+
- issuing_country: country code or name
|
|
4606
|
+
- issue_date: in YYYY-MM-DD format if visible
|
|
4607
|
+
- expiry_date: in YYYY-MM-DD format if visible
|
|
4608
|
+
- address: full address if visible
|
|
4609
|
+
- nationality: if visible
|
|
4610
|
+
- gender: if visible
|
|
4611
|
+
- mrz_code: Machine Readable Zone text if this is a passport/ID with MRZ
|
|
4612
|
+
- phone: any phone numbers visible
|
|
4613
|
+
- email: any email addresses visible
|
|
4614
|
+
- additional_fields: object with any other visible structured data
|
|
4615
|
+
- raw_text: all visible text transcribed
|
|
4616
|
+
|
|
4617
|
+
Only include fields that are actually visible in the document. Return valid JSON only.`;
|
|
4618
|
+
const response = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
4619
|
+
method: "POST",
|
|
4620
|
+
headers: {
|
|
4621
|
+
Authorization: `Bearer ${apiKey}`,
|
|
4622
|
+
"Content-Type": "application/json"
|
|
4623
|
+
},
|
|
4624
|
+
body: JSON.stringify({
|
|
4625
|
+
model: "gpt-4o",
|
|
4626
|
+
messages: [
|
|
4627
|
+
{
|
|
4628
|
+
role: "user",
|
|
4629
|
+
content: [
|
|
4630
|
+
{ type: "text", text: prompt },
|
|
4631
|
+
{ type: "image_url", image_url: { url: imageData, detail: "high" } }
|
|
4632
|
+
]
|
|
4633
|
+
}
|
|
4634
|
+
],
|
|
4635
|
+
max_tokens: 2000,
|
|
4636
|
+
temperature: 0
|
|
4637
|
+
})
|
|
4638
|
+
});
|
|
4639
|
+
if (!response.ok) {
|
|
4640
|
+
const err = await response.text();
|
|
4641
|
+
throw new Error(`OpenAI API error: ${response.status} \u2014 ${err}`);
|
|
4642
|
+
}
|
|
4643
|
+
const data = await response.json();
|
|
4644
|
+
const content = data.choices?.[0]?.message?.content || "";
|
|
4645
|
+
const jsonMatch = content.match(/```json\s*([\s\S]*?)```/) || content.match(/\{[\s\S]*\}/);
|
|
4646
|
+
if (!jsonMatch) {
|
|
4647
|
+
return { fields: {}, raw_text: content, document_type: docType || "unknown", confidence: 0.3 };
|
|
4648
|
+
}
|
|
4649
|
+
try {
|
|
4650
|
+
const parsed = JSON.parse(jsonMatch[1] || jsonMatch[0]);
|
|
4651
|
+
const { document_type, raw_text, additional_fields, ...mainFields } = parsed;
|
|
4652
|
+
const fields = {};
|
|
4653
|
+
for (const [k, v] of Object.entries(mainFields)) {
|
|
4654
|
+
if (v && typeof v === "string")
|
|
4655
|
+
fields[k] = v;
|
|
4656
|
+
}
|
|
4657
|
+
if (additional_fields && typeof additional_fields === "object") {
|
|
4658
|
+
for (const [k, v] of Object.entries(additional_fields)) {
|
|
4659
|
+
if (v && typeof v === "string")
|
|
4660
|
+
fields[k] = v;
|
|
4661
|
+
}
|
|
4662
|
+
}
|
|
4663
|
+
return {
|
|
4664
|
+
fields,
|
|
4665
|
+
raw_text: raw_text || content,
|
|
4666
|
+
document_type: document_type || docType || "unknown",
|
|
4667
|
+
confidence: Object.keys(fields).length > 3 ? 0.9 : Object.keys(fields).length > 0 ? 0.7 : 0.3
|
|
4668
|
+
};
|
|
4669
|
+
} catch {
|
|
4670
|
+
return { fields: {}, raw_text: content, document_type: docType || "unknown", confidence: 0.3 };
|
|
4671
|
+
}
|
|
4672
|
+
}
|
|
4673
|
+
|
|
4274
4674
|
// src/mcp/index.ts
|
|
4275
4675
|
var server = new Server({ name: "contacts", version: "0.1.0" }, { capabilities: { tools: {} } });
|
|
4276
4676
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
@@ -4349,7 +4749,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
4349
4749
|
}
|
|
4350
4750
|
},
|
|
4351
4751
|
tag_ids: { type: "array", items: { type: "string" }, description: "Tag IDs to assign" },
|
|
4352
|
-
source: { type: "string", enum: ["manual", "import", "linkedin", "github", "twitter", "email", "calendar", "crm", "other"] }
|
|
4752
|
+
source: { type: "string", enum: ["manual", "import", "linkedin", "github", "twitter", "email", "calendar", "crm", "other"] },
|
|
4753
|
+
sensitivity: { type: "string", enum: ["normal", "confidential", "restricted"], description: "Contact sensitivity level (default: normal)" }
|
|
4353
4754
|
}
|
|
4354
4755
|
}
|
|
4355
4756
|
},
|
|
@@ -4385,6 +4786,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
4385
4786
|
project_id: { type: "string", description: "Primary project ID (single, null to clear)" },
|
|
4386
4787
|
project_ids: { type: "array", items: { type: "string" }, description: "Replace all project links with this array of todos project IDs" },
|
|
4387
4788
|
source: { type: "string", enum: ["manual", "import", "linkedin", "github", "twitter", "email", "calendar", "crm", "other"] },
|
|
4789
|
+
sensitivity: { type: "string", enum: ["normal", "confidential", "restricted"] },
|
|
4388
4790
|
emails_add: { type: "array", items: { type: "object", properties: { address: { type: "string" }, type: { type: "string" }, is_primary: { type: "boolean" } }, required: ["address"] }, description: "New email addresses to append (duplicates are skipped)" },
|
|
4389
4791
|
phones_add: { type: "array", items: { type: "object", properties: { number: { type: "string" }, type: { type: "string" }, country_code: { type: "string" }, is_primary: { type: "boolean" } }, required: ["number"] }, description: "New phone numbers to append (duplicates are skipped)" }
|
|
4390
4792
|
},
|
|
@@ -4413,6 +4815,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
4413
4815
|
status: { type: "string", enum: ["active", "pending_reply", "converted", "closed", "other"] },
|
|
4414
4816
|
project_id: { type: "string", description: "Filter by project ID" },
|
|
4415
4817
|
archived: { type: "boolean", description: "Include archived contacts (default false)" },
|
|
4818
|
+
include_restricted: { type: "boolean", description: "Include restricted-sensitivity contacts (default false)" },
|
|
4416
4819
|
follow_up_due: { type: "boolean", description: "Only return contacts whose follow_up_at is in the past" },
|
|
4417
4820
|
last_contacted_after: { type: "string", description: "ISO 8601 date \u2014 only contacts last contacted after this date" },
|
|
4418
4821
|
last_contacted_before: { type: "string", description: "ISO 8601 date \u2014 only contacts last contacted before this date" },
|
|
@@ -5737,7 +6140,20 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
5737
6140
|
{ name: "delete_contact_photo", description: "Remove a contact's profile photo.", inputSchema: { type: "object", properties: { contact_id: { type: "string" } }, required: ["contact_id"] } },
|
|
5738
6141
|
{ name: "set_company_logo", description: "Set a company's logo image. Provide either a local file path or base64-encoded image data. Stores image in ~/.contacts/images/ and updates logo_url.", inputSchema: { type: "object", properties: { company_id: { type: "string" }, image: { type: "string", description: "File path or base64 data" }, format: { type: "string", description: "Image format hint for raw base64" } }, required: ["company_id", "image"] } },
|
|
5739
6142
|
{ name: "get_company_logo", description: "Get a company's logo as base64 data URI.", inputSchema: { type: "object", properties: { company_id: { type: "string" } }, required: ["company_id"] } },
|
|
5740
|
-
{ name: "delete_company_logo", description: "Remove a company's logo image.", inputSchema: { type: "object", properties: { company_id: { type: "string" } }, required: ["company_id"] } }
|
|
6143
|
+
{ name: "delete_company_logo", description: "Remove a company's logo image.", inputSchema: { type: "object", properties: { company_id: { type: "string" } }, required: ["company_id"] } },
|
|
6144
|
+
{ name: "set_sensitivity", description: "Set a contact's sensitivity level (normal, confidential, restricted). Restricted contacts are hidden from list/search unless explicitly requested.", inputSchema: { type: "object", properties: { contact_id: { type: "string" }, sensitivity: { type: "string", enum: ["normal", "confidential", "restricted"] } }, required: ["contact_id", "sensitivity"] } },
|
|
6145
|
+
{ name: "vault_init", description: "Initialize the encrypted document vault with a passphrase. Must be called before storing documents or health data.", inputSchema: { type: "object", properties: { passphrase: { type: "string" } }, required: ["passphrase"] } },
|
|
6146
|
+
{ name: "vault_unlock", description: "Unlock the vault for this session with a passphrase.", inputSchema: { type: "object", properties: { passphrase: { type: "string" } }, required: ["passphrase"] } },
|
|
6147
|
+
{ name: "vault_lock", description: "Lock the vault, clearing the encryption key from memory.", inputSchema: { type: "object", properties: {} } },
|
|
6148
|
+
{ name: "vault_status", description: "Check vault initialization and lock status.", inputSchema: { type: "object", properties: {} } },
|
|
6149
|
+
{ name: "add_document", description: "Store an encrypted document for a contact (passport, tax_id, medical_record, etc.). Vault must be unlocked.", inputSchema: { type: "object", properties: { contact_id: { type: "string" }, doc_type: { type: "string", enum: [...DOCUMENT_TYPES] }, label: { type: "string" }, value: { type: "string", description: "Plaintext value (will be encrypted)" }, file_path: { type: "string", description: "Optional file to encrypt and attach" }, metadata: { type: "object" }, expires_at: { type: "string" } }, required: ["contact_id", "doc_type", "value"] } },
|
|
6150
|
+
{ name: "list_documents", description: "List documents for a contact (metadata only \u2014 no decryption needed).", inputSchema: { type: "object", properties: { contact_id: { type: "string" } }, required: ["contact_id"] } },
|
|
6151
|
+
{ name: "get_document", description: "Get a document with decrypted value. Vault must be unlocked.", inputSchema: { type: "object", properties: { document_id: { type: "string" } }, required: ["document_id"] } },
|
|
6152
|
+
{ name: "delete_document", description: "Delete a document and its encrypted file.", inputSchema: { type: "object", properties: { document_id: { type: "string" } }, required: ["document_id"] } },
|
|
6153
|
+
{ name: "scan_document", description: "Scan a document image using AI vision (OpenAI GPT-4o) to extract structured data. Optionally auto-save to vault.", inputSchema: { type: "object", properties: { image: { type: "string", description: "File path or base64 image data" }, doc_type: { type: "string", description: "Hint: passport, national_id, drivers_license, etc." }, contact_id: { type: "string", description: "Contact to associate scanned document with" }, auto_save: { type: "boolean", description: "Automatically save extracted data as a vault document" } }, required: ["image"] } },
|
|
6154
|
+
{ name: "set_health_data", description: "Set or update health data for a contact. Vault must be unlocked.", inputSchema: { type: "object", properties: { contact_id: { type: "string" }, blood_type: { type: "string" }, allergies: { type: "array", items: { type: "string" } }, medical_conditions: { type: "array", items: { type: "string" } }, medications: { type: "array", items: { type: "string" } }, emergency_contacts: { type: "array", items: { type: "object", properties: { name: { type: "string" }, phone: { type: "string" }, relationship: { type: "string" } }, required: ["name", "phone", "relationship"] } }, health_insurance_provider: { type: "string" }, health_insurance_id: { type: "string" }, primary_physician: { type: "string" }, primary_physician_phone: { type: "string" }, organ_donor: { type: "boolean" }, notes: { type: "string" } }, required: ["contact_id"] } },
|
|
6155
|
+
{ name: "get_health_data", description: "Get health data for a contact. Vault must be unlocked.", inputSchema: { type: "object", properties: { contact_id: { type: "string" } }, required: ["contact_id"] } },
|
|
6156
|
+
{ name: "delete_health_data", description: "Delete all health data for a contact.", inputSchema: { type: "object", properties: { contact_id: { type: "string" } }, required: ["contact_id"] } }
|
|
5741
6157
|
]
|
|
5742
6158
|
}));
|
|
5743
6159
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
@@ -5766,7 +6182,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
5766
6182
|
addresses: a.addresses,
|
|
5767
6183
|
social_profiles: a.social_profiles,
|
|
5768
6184
|
tag_ids: a.tag_ids,
|
|
5769
|
-
source: a.source
|
|
6185
|
+
source: a.source,
|
|
6186
|
+
sensitivity: a.sensitivity
|
|
5770
6187
|
};
|
|
5771
6188
|
const contact = createContact(input);
|
|
5772
6189
|
if (Array.isArray(a.project_ids) && a.project_ids.length > 0) {
|
|
@@ -5801,6 +6218,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
5801
6218
|
follow_up_at: rest.follow_up_at,
|
|
5802
6219
|
project_id: rest.project_id,
|
|
5803
6220
|
source: rest.source,
|
|
6221
|
+
sensitivity: rest.sensitivity,
|
|
5804
6222
|
emails_add: rest.emails_add,
|
|
5805
6223
|
phones_add: rest.phones_add
|
|
5806
6224
|
};
|
|
@@ -5824,6 +6242,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
5824
6242
|
status: a.status,
|
|
5825
6243
|
project_id: a.project_id,
|
|
5826
6244
|
archived: a.archived,
|
|
6245
|
+
include_restricted: a.include_restricted,
|
|
5827
6246
|
follow_up_due: a.follow_up_due,
|
|
5828
6247
|
last_contacted_after: a.last_contacted_after,
|
|
5829
6248
|
last_contacted_before: a.last_contacted_before,
|
|
@@ -7270,6 +7689,100 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
7270
7689
|
updateCompany(company_id, { logo_url: null });
|
|
7271
7690
|
return { content: [{ type: "text", text: JSON.stringify({ ok: true, deleted }) }] };
|
|
7272
7691
|
}
|
|
7692
|
+
case "set_sensitivity": {
|
|
7693
|
+
const contact = updateContact(a.contact_id, { sensitivity: a.sensitivity });
|
|
7694
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: true, contact_id: a.contact_id, sensitivity: a.sensitivity }) }] };
|
|
7695
|
+
}
|
|
7696
|
+
case "vault_init": {
|
|
7697
|
+
initVault(a.passphrase);
|
|
7698
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: true, message: "Vault initialized and unlocked" }) }] };
|
|
7699
|
+
}
|
|
7700
|
+
case "vault_unlock": {
|
|
7701
|
+
const ok = unlockVault(a.passphrase);
|
|
7702
|
+
if (!ok)
|
|
7703
|
+
return { content: [{ type: "text", text: "Invalid passphrase" }], isError: true };
|
|
7704
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: true, message: "Vault unlocked" }) }] };
|
|
7705
|
+
}
|
|
7706
|
+
case "vault_lock": {
|
|
7707
|
+
lockVault();
|
|
7708
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: true, message: "Vault locked" }) }] };
|
|
7709
|
+
}
|
|
7710
|
+
case "vault_status": {
|
|
7711
|
+
const initialized = isVaultInitialized();
|
|
7712
|
+
const unlocked = isVaultUnlocked();
|
|
7713
|
+
const db = getDatabase();
|
|
7714
|
+
let docCount = 0;
|
|
7715
|
+
try {
|
|
7716
|
+
docCount = db.query("SELECT COUNT(*) as n FROM contact_documents").get().n;
|
|
7717
|
+
} catch {}
|
|
7718
|
+
return { content: [{ type: "text", text: JSON.stringify({ initialized, unlocked, document_count: docCount }) }] };
|
|
7719
|
+
}
|
|
7720
|
+
case "add_document": {
|
|
7721
|
+
const doc = addDocument({
|
|
7722
|
+
contact_id: a.contact_id,
|
|
7723
|
+
doc_type: a.doc_type,
|
|
7724
|
+
label: a.label,
|
|
7725
|
+
value: a.value,
|
|
7726
|
+
file_path: a.file_path,
|
|
7727
|
+
metadata: a.metadata,
|
|
7728
|
+
expires_at: a.expires_at
|
|
7729
|
+
});
|
|
7730
|
+
return { content: [{ type: "text", text: JSON.stringify(doc, null, 2) }] };
|
|
7731
|
+
}
|
|
7732
|
+
case "list_documents": {
|
|
7733
|
+
const docs = listDocuments(a.contact_id);
|
|
7734
|
+
return { content: [{ type: "text", text: JSON.stringify(docs, null, 2) }] };
|
|
7735
|
+
}
|
|
7736
|
+
case "get_document": {
|
|
7737
|
+
const doc = getDocument(a.document_id);
|
|
7738
|
+
return { content: [{ type: "text", text: JSON.stringify(doc, null, 2) }] };
|
|
7739
|
+
}
|
|
7740
|
+
case "delete_document": {
|
|
7741
|
+
deleteDocument(a.document_id);
|
|
7742
|
+
return { content: [{ type: "text", text: JSON.stringify({ deleted: true }) }] };
|
|
7743
|
+
}
|
|
7744
|
+
case "scan_document": {
|
|
7745
|
+
const result = await scanDocument(a.image, a.doc_type);
|
|
7746
|
+
if (a.auto_save && a.contact_id && isVaultUnlocked()) {
|
|
7747
|
+
try {
|
|
7748
|
+
const doc = addDocument({
|
|
7749
|
+
contact_id: a.contact_id,
|
|
7750
|
+
doc_type: result.document_type || "other",
|
|
7751
|
+
label: `Scanned ${result.document_type}`,
|
|
7752
|
+
value: JSON.stringify(result.fields),
|
|
7753
|
+
metadata: { raw_text: result.raw_text, confidence: result.confidence }
|
|
7754
|
+
});
|
|
7755
|
+
return { content: [{ type: "text", text: JSON.stringify({ scan: result, saved_document: doc }, null, 2) }] };
|
|
7756
|
+
} catch (saveErr) {
|
|
7757
|
+
return { content: [{ type: "text", text: JSON.stringify({ scan: result, save_error: saveErr instanceof Error ? saveErr.message : String(saveErr) }, null, 2) }] };
|
|
7758
|
+
}
|
|
7759
|
+
}
|
|
7760
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
7761
|
+
}
|
|
7762
|
+
case "set_health_data": {
|
|
7763
|
+
const health = setHealthData(a.contact_id, {
|
|
7764
|
+
blood_type: a.blood_type,
|
|
7765
|
+
allergies: a.allergies,
|
|
7766
|
+
medical_conditions: a.medical_conditions,
|
|
7767
|
+
medications: a.medications,
|
|
7768
|
+
emergency_contacts: a.emergency_contacts,
|
|
7769
|
+
health_insurance_provider: a.health_insurance_provider,
|
|
7770
|
+
health_insurance_id: a.health_insurance_id,
|
|
7771
|
+
primary_physician: a.primary_physician,
|
|
7772
|
+
primary_physician_phone: a.primary_physician_phone,
|
|
7773
|
+
organ_donor: a.organ_donor,
|
|
7774
|
+
notes: a.notes
|
|
7775
|
+
});
|
|
7776
|
+
return { content: [{ type: "text", text: JSON.stringify(health, null, 2) }] };
|
|
7777
|
+
}
|
|
7778
|
+
case "get_health_data": {
|
|
7779
|
+
const health = getHealthData(a.contact_id);
|
|
7780
|
+
return { content: [{ type: "text", text: JSON.stringify(health, null, 2) }] };
|
|
7781
|
+
}
|
|
7782
|
+
case "delete_health_data": {
|
|
7783
|
+
deleteHealthData(a.contact_id);
|
|
7784
|
+
return { content: [{ type: "text", text: JSON.stringify({ deleted: true }) }] };
|
|
7785
|
+
}
|
|
7273
7786
|
default:
|
|
7274
7787
|
return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
|
|
7275
7788
|
}
|