@hasna/contacts 0.1.0 → 0.2.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/dashboard/dist/assets/index-0l6aQb1t.css +1 -0
- package/dashboard/dist/assets/index-opnZdkVD.js +229 -0
- package/dashboard/dist/index.html +2 -2
- package/dist/cli/index.js +531 -17
- package/dist/db/companies.d.ts.map +1 -1
- package/dist/db/companies.test.d.ts +2 -0
- package/dist/db/companies.test.d.ts.map +1 -0
- package/dist/db/contacts.d.ts +1 -0
- package/dist/db/contacts.d.ts.map +1 -1
- package/dist/db/contacts.test.d.ts +2 -0
- package/dist/db/contacts.test.d.ts.map +1 -0
- package/dist/db/database.d.ts +1 -0
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/groups.d.ts +12 -0
- package/dist/db/groups.d.ts.map +1 -0
- package/dist/db/relationships.test.d.ts +2 -0
- package/dist/db/relationships.test.d.ts.map +1 -0
- package/dist/db/tags.d.ts.map +1 -1
- package/dist/db/tags.test.d.ts +2 -0
- package/dist/db/tags.test.d.ts.map +1 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +102 -5
- package/dist/lib/config.d.ts +7 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/dedup.d.ts +10 -0
- package/dist/lib/dedup.d.ts.map +1 -0
- package/dist/lib/import.test.d.ts +2 -0
- package/dist/lib/import.test.d.ts.map +1 -0
- package/dist/mcp/index.js +567 -76
- package/dist/mcp/mcp.test.d.ts +2 -0
- package/dist/mcp/mcp.test.d.ts.map +1 -0
- package/dist/server/index.js +38 -3
- package/dist/types/index.d.ts +25 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/dashboard/dist/assets/index-B4ndI7Qt.js +0 -49
- package/dashboard/dist/assets/index-C5bn2HWO.css +0 -1
package/dist/mcp/index.js
CHANGED
|
@@ -181,6 +181,25 @@ var MIGRATIONS = [
|
|
|
181
181
|
END;
|
|
182
182
|
|
|
183
183
|
CREATE TABLE IF NOT EXISTS _migrations (version INTEGER PRIMARY KEY);
|
|
184
|
+
`,
|
|
185
|
+
`
|
|
186
|
+
ALTER TABLE contacts ADD COLUMN last_contacted_at TEXT;
|
|
187
|
+
ALTER TABLE contacts ADD COLUMN website TEXT;
|
|
188
|
+
ALTER TABLE contacts ADD COLUMN preferred_contact_method TEXT;
|
|
189
|
+
|
|
190
|
+
CREATE TABLE IF NOT EXISTS groups (
|
|
191
|
+
id TEXT PRIMARY KEY,
|
|
192
|
+
name TEXT NOT NULL UNIQUE,
|
|
193
|
+
description TEXT,
|
|
194
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
195
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
CREATE TABLE IF NOT EXISTS contact_groups (
|
|
199
|
+
contact_id TEXT NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
|
|
200
|
+
group_id TEXT NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
|
|
201
|
+
PRIMARY KEY (contact_id, group_id)
|
|
202
|
+
);
|
|
184
203
|
`
|
|
185
204
|
];
|
|
186
205
|
var _db = null;
|
|
@@ -263,7 +282,8 @@ function rowToContact(row) {
|
|
|
263
282
|
return {
|
|
264
283
|
...row,
|
|
265
284
|
source: row.source,
|
|
266
|
-
custom_fields: JSON.parse(row.custom_fields || "{}")
|
|
285
|
+
custom_fields: JSON.parse(row.custom_fields || "{}"),
|
|
286
|
+
preferred_contact_method: row.preferred_contact_method ?? null
|
|
267
287
|
};
|
|
268
288
|
}
|
|
269
289
|
function rowToEmail(row) {
|
|
@@ -344,8 +364,8 @@ function createContact(input, db) {
|
|
|
344
364
|
const firstName = input.first_name ?? "";
|
|
345
365
|
const lastName = input.last_name ?? "";
|
|
346
366
|
const displayName = input.display_name ?? (firstName || lastName ? `${firstName} ${lastName}`.trim() : "Unnamed Contact");
|
|
347
|
-
d.run(`INSERT INTO contacts (id, first_name, last_name, display_name, nickname, avatar_url, notes, birthday, company_id, job_title, source, custom_fields, created_at, updated_at)
|
|
348
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
367
|
+
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, created_at, updated_at)
|
|
368
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
349
369
|
id,
|
|
350
370
|
firstName,
|
|
351
371
|
lastName,
|
|
@@ -358,6 +378,9 @@ function createContact(input, db) {
|
|
|
358
378
|
input.job_title ?? null,
|
|
359
379
|
input.source ?? "manual",
|
|
360
380
|
JSON.stringify(input.custom_fields ?? {}),
|
|
381
|
+
input.last_contacted_at ?? null,
|
|
382
|
+
input.website ?? null,
|
|
383
|
+
input.preferred_contact_method ?? null,
|
|
361
384
|
timestamp,
|
|
362
385
|
timestamp
|
|
363
386
|
]);
|
|
@@ -469,6 +492,18 @@ function updateContact(id, input, db) {
|
|
|
469
492
|
setClauses.push("custom_fields = ?");
|
|
470
493
|
params.push(JSON.stringify(input.custom_fields));
|
|
471
494
|
}
|
|
495
|
+
if (input.last_contacted_at !== undefined) {
|
|
496
|
+
setClauses.push("last_contacted_at = ?");
|
|
497
|
+
params.push(input.last_contacted_at);
|
|
498
|
+
}
|
|
499
|
+
if (input.website !== undefined) {
|
|
500
|
+
setClauses.push("website = ?");
|
|
501
|
+
params.push(input.website);
|
|
502
|
+
}
|
|
503
|
+
if (input.preferred_contact_method !== undefined) {
|
|
504
|
+
setClauses.push("preferred_contact_method = ?");
|
|
505
|
+
params.push(input.preferred_contact_method);
|
|
506
|
+
}
|
|
472
507
|
params.push(id);
|
|
473
508
|
d.run(`UPDATE contacts SET ${setClauses.join(", ")} WHERE id = ?`, params);
|
|
474
509
|
logActivity(d, { contact_id: id, action: "contact.updated", details: `Updated contact: ${existing.display_name}` });
|
|
@@ -571,6 +606,96 @@ function mergeContacts(keepId, mergeId, db) {
|
|
|
571
606
|
return loadContactDetails(d, rowToContact(finalRow));
|
|
572
607
|
}
|
|
573
608
|
|
|
609
|
+
// src/db/groups.ts
|
|
610
|
+
function createGroup(db, input) {
|
|
611
|
+
const id = uuid();
|
|
612
|
+
db.query(`INSERT INTO groups(id, name, description, created_at, updated_at) VALUES(?,?,?,?,?)`).run(id, input.name, input.description ?? null, now(), now());
|
|
613
|
+
return getGroup(db, id);
|
|
614
|
+
}
|
|
615
|
+
function getGroup(db, id) {
|
|
616
|
+
return db.query(`SELECT * FROM groups WHERE id = ?`).get(id);
|
|
617
|
+
}
|
|
618
|
+
function listGroups(db) {
|
|
619
|
+
return db.query(`SELECT g.*, COUNT(cg.contact_id) as member_count FROM groups g LEFT JOIN contact_groups cg ON g.id = cg.group_id GROUP BY g.id ORDER BY g.name`).all();
|
|
620
|
+
}
|
|
621
|
+
function updateGroup(db, id, input) {
|
|
622
|
+
const fields = [];
|
|
623
|
+
const vals = [];
|
|
624
|
+
if (input.name !== undefined) {
|
|
625
|
+
fields.push("name = ?");
|
|
626
|
+
vals.push(input.name);
|
|
627
|
+
}
|
|
628
|
+
if (input.description !== undefined) {
|
|
629
|
+
fields.push("description = ?");
|
|
630
|
+
vals.push(input.description ?? null);
|
|
631
|
+
}
|
|
632
|
+
fields.push("updated_at = ?");
|
|
633
|
+
vals.push(now());
|
|
634
|
+
vals.push(id);
|
|
635
|
+
db.query(`UPDATE groups SET ${fields.join(", ")} WHERE id = ?`).run(...vals);
|
|
636
|
+
return getGroup(db, id);
|
|
637
|
+
}
|
|
638
|
+
function deleteGroup(db, id) {
|
|
639
|
+
db.query(`DELETE FROM groups WHERE id = ?`).run(id);
|
|
640
|
+
}
|
|
641
|
+
function addContactToGroup(db, contactId, groupId) {
|
|
642
|
+
db.query(`INSERT OR IGNORE INTO contact_groups(contact_id, group_id) VALUES(?,?)`).run(contactId, groupId);
|
|
643
|
+
}
|
|
644
|
+
function removeContactFromGroup(db, contactId, groupId) {
|
|
645
|
+
db.query(`DELETE FROM contact_groups WHERE contact_id = ? AND group_id = ?`).run(contactId, groupId);
|
|
646
|
+
}
|
|
647
|
+
function listContactsInGroup(db, groupId) {
|
|
648
|
+
const rows = db.query(`SELECT contact_id FROM contact_groups WHERE group_id = ?`).all(groupId);
|
|
649
|
+
return rows.map((r) => r.contact_id);
|
|
650
|
+
}
|
|
651
|
+
function listGroupsForContact(db, contactId) {
|
|
652
|
+
return db.query(`SELECT g.* FROM groups g JOIN contact_groups cg ON g.id = cg.group_id WHERE cg.contact_id = ? ORDER BY g.name`).all(contactId);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// src/db/tags.ts
|
|
656
|
+
function rowToTag2(row) {
|
|
657
|
+
return { ...row };
|
|
658
|
+
}
|
|
659
|
+
function createTag(input, db) {
|
|
660
|
+
const d = db || getDatabase();
|
|
661
|
+
const existing = d.query(`SELECT id FROM tags WHERE name = ?`).get(input.name);
|
|
662
|
+
if (existing)
|
|
663
|
+
throw new DuplicateTagNameError(input.name);
|
|
664
|
+
const id = uuid();
|
|
665
|
+
d.run(`INSERT INTO tags (id, name, color, description) VALUES (?, ?, ?, ?)`, [id, input.name, input.color ?? "#6366f1", input.description ?? null]);
|
|
666
|
+
return rowToTag2(d.query(`SELECT * FROM tags WHERE id = ?`).get(id));
|
|
667
|
+
}
|
|
668
|
+
function getTagByName(name, db) {
|
|
669
|
+
const d = db || getDatabase();
|
|
670
|
+
const row = d.query(`SELECT * FROM tags WHERE name = ?`).get(name);
|
|
671
|
+
return row ? rowToTag2(row) : null;
|
|
672
|
+
}
|
|
673
|
+
function listTags(db) {
|
|
674
|
+
const d = db || getDatabase();
|
|
675
|
+
return d.query(`SELECT * FROM tags ORDER BY name ASC`).all().map(rowToTag2);
|
|
676
|
+
}
|
|
677
|
+
function deleteTag(id, db) {
|
|
678
|
+
const d = db || getDatabase();
|
|
679
|
+
const row = d.query(`SELECT id FROM tags WHERE id = ?`).get(id);
|
|
680
|
+
if (!row)
|
|
681
|
+
throw new TagNotFoundError(id);
|
|
682
|
+
d.run(`DELETE FROM tags WHERE id = ?`, [id]);
|
|
683
|
+
}
|
|
684
|
+
function addTagToContact(contactId, tagId, db) {
|
|
685
|
+
const d = db || getDatabase();
|
|
686
|
+
const contact = d.query(`SELECT id FROM contacts WHERE id = ?`).get(contactId);
|
|
687
|
+
if (!contact)
|
|
688
|
+
throw new ContactNotFoundError(contactId);
|
|
689
|
+
const tag = d.query(`SELECT id FROM tags WHERE id = ?`).get(tagId);
|
|
690
|
+
if (!tag)
|
|
691
|
+
throw new TagNotFoundError(tagId);
|
|
692
|
+
d.run(`INSERT OR IGNORE INTO contact_tags (contact_id, tag_id) VALUES (?, ?)`, [contactId, tagId]);
|
|
693
|
+
}
|
|
694
|
+
function removeTagFromContact(contactId, tagId, db) {
|
|
695
|
+
const d = db || getDatabase();
|
|
696
|
+
d.run(`DELETE FROM contact_tags WHERE contact_id = ? AND tag_id = ?`, [contactId, tagId]);
|
|
697
|
+
}
|
|
698
|
+
|
|
574
699
|
// src/db/companies.ts
|
|
575
700
|
function rowToCompany2(row) {
|
|
576
701
|
return {
|
|
@@ -773,45 +898,6 @@ function searchCompanies(query, db) {
|
|
|
773
898
|
return rows.map((row) => loadCompanyDetails(d, rowToCompany2(row)));
|
|
774
899
|
}
|
|
775
900
|
|
|
776
|
-
// src/db/tags.ts
|
|
777
|
-
function rowToTag2(row) {
|
|
778
|
-
return { ...row };
|
|
779
|
-
}
|
|
780
|
-
function createTag(input, db) {
|
|
781
|
-
const d = db || getDatabase();
|
|
782
|
-
const existing = d.query(`SELECT id FROM tags WHERE name = ?`).get(input.name);
|
|
783
|
-
if (existing)
|
|
784
|
-
throw new DuplicateTagNameError(input.name);
|
|
785
|
-
const id = uuid();
|
|
786
|
-
d.run(`INSERT INTO tags (id, name, color, description) VALUES (?, ?, ?, ?)`, [id, input.name, input.color ?? "#6366f1", input.description ?? null]);
|
|
787
|
-
return rowToTag2(d.query(`SELECT * FROM tags WHERE id = ?`).get(id));
|
|
788
|
-
}
|
|
789
|
-
function listTags(db) {
|
|
790
|
-
const d = db || getDatabase();
|
|
791
|
-
return d.query(`SELECT * FROM tags ORDER BY name ASC`).all().map(rowToTag2);
|
|
792
|
-
}
|
|
793
|
-
function deleteTag(id, db) {
|
|
794
|
-
const d = db || getDatabase();
|
|
795
|
-
const row = d.query(`SELECT id FROM tags WHERE id = ?`).get(id);
|
|
796
|
-
if (!row)
|
|
797
|
-
throw new TagNotFoundError(id);
|
|
798
|
-
d.run(`DELETE FROM tags WHERE id = ?`, [id]);
|
|
799
|
-
}
|
|
800
|
-
function addTagToContact(contactId, tagId, db) {
|
|
801
|
-
const d = db || getDatabase();
|
|
802
|
-
const contact = d.query(`SELECT id FROM contacts WHERE id = ?`).get(contactId);
|
|
803
|
-
if (!contact)
|
|
804
|
-
throw new ContactNotFoundError(contactId);
|
|
805
|
-
const tag = d.query(`SELECT id FROM tags WHERE id = ?`).get(tagId);
|
|
806
|
-
if (!tag)
|
|
807
|
-
throw new TagNotFoundError(tagId);
|
|
808
|
-
d.run(`INSERT OR IGNORE INTO contact_tags (contact_id, tag_id) VALUES (?, ?)`, [contactId, tagId]);
|
|
809
|
-
}
|
|
810
|
-
function removeTagFromContact(contactId, tagId, db) {
|
|
811
|
-
const d = db || getDatabase();
|
|
812
|
-
d.run(`DELETE FROM contact_tags WHERE contact_id = ? AND tag_id = ?`, [contactId, tagId]);
|
|
813
|
-
}
|
|
814
|
-
|
|
815
901
|
// src/db/relationships.ts
|
|
816
902
|
function rowToRelationship(row) {
|
|
817
903
|
return {
|
|
@@ -1304,18 +1390,21 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
1304
1390
|
tools: [
|
|
1305
1391
|
{
|
|
1306
1392
|
name: "create_contact",
|
|
1307
|
-
description: "Create a new contact",
|
|
1393
|
+
description: "Create a new contact. Provide at minimum display_name or first_name+last_name. Emails, phones, addresses, and social_profiles are arrays of objects. relationship_type values: colleague|friend|family|reports_to|mentor|investor|partner|client|vendor|other. source values: manual|import|linkedin|github|twitter|email|calendar|crm|other.",
|
|
1308
1394
|
inputSchema: {
|
|
1309
1395
|
type: "object",
|
|
1310
1396
|
properties: {
|
|
1311
1397
|
first_name: { type: "string" },
|
|
1312
1398
|
last_name: { type: "string" },
|
|
1313
|
-
display_name: { type: "string", description: "Display name" },
|
|
1399
|
+
display_name: { type: "string", description: "Display name (auto-generated from first+last if omitted)" },
|
|
1314
1400
|
nickname: { type: "string" },
|
|
1315
1401
|
job_title: { type: "string" },
|
|
1316
1402
|
company_id: { type: "string" },
|
|
1317
1403
|
notes: { type: "string" },
|
|
1318
1404
|
birthday: { type: "string", description: "YYYY-MM-DD" },
|
|
1405
|
+
website: { type: "string", description: "Personal or professional website URL" },
|
|
1406
|
+
last_contacted_at: { type: "string", description: "ISO 8601 datetime of last contact" },
|
|
1407
|
+
preferred_contact_method: { type: "string", enum: ["email", "phone", "telegram", "whatsapp", "linkedin", "twitter", "other"] },
|
|
1319
1408
|
emails: {
|
|
1320
1409
|
type: "array",
|
|
1321
1410
|
items: {
|
|
@@ -1369,13 +1458,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
1369
1458
|
}
|
|
1370
1459
|
},
|
|
1371
1460
|
tag_ids: { type: "array", items: { type: "string" }, description: "Tag IDs to assign" },
|
|
1372
|
-
source: { type: "string" }
|
|
1461
|
+
source: { type: "string", enum: ["manual", "import", "linkedin", "github", "twitter", "email", "calendar", "crm", "other"] }
|
|
1373
1462
|
}
|
|
1374
1463
|
}
|
|
1375
1464
|
},
|
|
1376
1465
|
{
|
|
1377
1466
|
name: "get_contact",
|
|
1378
|
-
description: "Get a contact by ID",
|
|
1467
|
+
description: "Get a contact by ID, returning all details including emails, phones, addresses, social profiles, tags, and company.",
|
|
1379
1468
|
inputSchema: {
|
|
1380
1469
|
type: "object",
|
|
1381
1470
|
properties: { id: { type: "string" } },
|
|
@@ -1384,7 +1473,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
1384
1473
|
},
|
|
1385
1474
|
{
|
|
1386
1475
|
name: "update_contact",
|
|
1387
|
-
description: "Update an existing contact",
|
|
1476
|
+
description: "Update an existing contact's fields. Only provided fields are changed; omitted fields remain unchanged. Supports all contact fields including last_contacted_at, website, preferred_contact_method.",
|
|
1388
1477
|
inputSchema: {
|
|
1389
1478
|
type: "object",
|
|
1390
1479
|
properties: {
|
|
@@ -1396,14 +1485,18 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
1396
1485
|
job_title: { type: "string" },
|
|
1397
1486
|
company_id: { type: "string" },
|
|
1398
1487
|
notes: { type: "string" },
|
|
1399
|
-
birthday: { type: "string" }
|
|
1488
|
+
birthday: { type: "string", description: "YYYY-MM-DD" },
|
|
1489
|
+
website: { type: "string" },
|
|
1490
|
+
last_contacted_at: { type: "string", description: "ISO 8601 datetime of last contact" },
|
|
1491
|
+
preferred_contact_method: { type: "string", enum: ["email", "phone", "telegram", "whatsapp", "linkedin", "twitter", "other"] },
|
|
1492
|
+
source: { type: "string", enum: ["manual", "import", "linkedin", "github", "twitter", "email", "calendar", "crm", "other"] }
|
|
1400
1493
|
},
|
|
1401
1494
|
required: ["id"]
|
|
1402
1495
|
}
|
|
1403
1496
|
},
|
|
1404
1497
|
{
|
|
1405
1498
|
name: "delete_contact",
|
|
1406
|
-
description: "
|
|
1499
|
+
description: "Permanently delete a contact by ID. All associated emails, phones, addresses, tags, and relationships are also deleted.",
|
|
1407
1500
|
inputSchema: {
|
|
1408
1501
|
type: "object",
|
|
1409
1502
|
properties: { id: { type: "string" } },
|
|
@@ -1412,7 +1505,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
1412
1505
|
},
|
|
1413
1506
|
{
|
|
1414
1507
|
name: "list_contacts",
|
|
1415
|
-
description: "List contacts with optional filters",
|
|
1508
|
+
description: "List contacts with optional filters. Supports filtering by company, tag, or source. Returns paginated results with total count.",
|
|
1416
1509
|
inputSchema: {
|
|
1417
1510
|
type: "object",
|
|
1418
1511
|
properties: {
|
|
@@ -1427,7 +1520,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
1427
1520
|
},
|
|
1428
1521
|
{
|
|
1429
1522
|
name: "search_contacts",
|
|
1430
|
-
description: "Full-text search across contacts",
|
|
1523
|
+
description: "Full-text search across contacts by name, nickname, notes, job title, and more. Also searches email addresses and phone numbers. Returns up to 50 results.",
|
|
1431
1524
|
inputSchema: {
|
|
1432
1525
|
type: "object",
|
|
1433
1526
|
properties: { query: { type: "string" } },
|
|
@@ -1436,15 +1529,15 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
1436
1529
|
},
|
|
1437
1530
|
{
|
|
1438
1531
|
name: "create_company",
|
|
1439
|
-
description: "Create a new company",
|
|
1532
|
+
description: "Create a new company/organization. Attach emails, phones, addresses, and social_profiles as arrays. Tag with tag_ids.",
|
|
1440
1533
|
inputSchema: {
|
|
1441
1534
|
type: "object",
|
|
1442
1535
|
properties: {
|
|
1443
1536
|
name: { type: "string" },
|
|
1444
|
-
domain: { type: "string" },
|
|
1537
|
+
domain: { type: "string", description: "Primary domain, e.g. acme.com" },
|
|
1445
1538
|
description: { type: "string" },
|
|
1446
1539
|
industry: { type: "string" },
|
|
1447
|
-
size: { type: "string" },
|
|
1540
|
+
size: { type: "string", description: "e.g. '1-10', '11-50', '51-200', '201-500', '500+'" },
|
|
1448
1541
|
founded_year: { type: "number" },
|
|
1449
1542
|
notes: { type: "string" },
|
|
1450
1543
|
emails: { type: "array", items: { type: "object" } },
|
|
@@ -1458,7 +1551,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
1458
1551
|
},
|
|
1459
1552
|
{
|
|
1460
1553
|
name: "get_company",
|
|
1461
|
-
description: "Get a company by ID",
|
|
1554
|
+
description: "Get a company by ID, including all emails, phones, addresses, social profiles, tags, and employee count.",
|
|
1462
1555
|
inputSchema: {
|
|
1463
1556
|
type: "object",
|
|
1464
1557
|
properties: { id: { type: "string" } },
|
|
@@ -1467,7 +1560,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
1467
1560
|
},
|
|
1468
1561
|
{
|
|
1469
1562
|
name: "update_company",
|
|
1470
|
-
description: "Update an existing company",
|
|
1563
|
+
description: "Update an existing company's fields. Only provided fields are changed; omitted fields remain unchanged.",
|
|
1471
1564
|
inputSchema: {
|
|
1472
1565
|
type: "object",
|
|
1473
1566
|
properties: {
|
|
@@ -1485,7 +1578,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
1485
1578
|
},
|
|
1486
1579
|
{
|
|
1487
1580
|
name: "delete_company",
|
|
1488
|
-
description: "
|
|
1581
|
+
description: "Permanently delete a company by ID. Contacts that belong to this company will have their company_id cleared.",
|
|
1489
1582
|
inputSchema: {
|
|
1490
1583
|
type: "object",
|
|
1491
1584
|
properties: { id: { type: "string" } },
|
|
@@ -1494,7 +1587,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
1494
1587
|
},
|
|
1495
1588
|
{
|
|
1496
1589
|
name: "list_companies",
|
|
1497
|
-
description: "List companies with optional filters",
|
|
1590
|
+
description: "List companies with optional filters by industry or tag. Returns paginated results with total count.",
|
|
1498
1591
|
inputSchema: {
|
|
1499
1592
|
type: "object",
|
|
1500
1593
|
properties: {
|
|
@@ -1507,7 +1600,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
1507
1600
|
},
|
|
1508
1601
|
{
|
|
1509
1602
|
name: "search_companies",
|
|
1510
|
-
description: "Search companies by name or domain",
|
|
1603
|
+
description: "Search companies by name or domain using full-text search. Returns up to 50 matches.",
|
|
1511
1604
|
inputSchema: {
|
|
1512
1605
|
type: "object",
|
|
1513
1606
|
properties: { query: { type: "string" } },
|
|
@@ -1516,12 +1609,12 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
1516
1609
|
},
|
|
1517
1610
|
{
|
|
1518
1611
|
name: "create_tag",
|
|
1519
|
-
description: "Create a new tag",
|
|
1612
|
+
description: "Create a new tag for categorizing contacts and companies. Tags are shared across contacts and companies.",
|
|
1520
1613
|
inputSchema: {
|
|
1521
1614
|
type: "object",
|
|
1522
1615
|
properties: {
|
|
1523
1616
|
name: { type: "string" },
|
|
1524
|
-
color: { type: "string", description: "Hex color (e.g. #FF5733)" },
|
|
1617
|
+
color: { type: "string", description: "Hex color (e.g. #FF5733). Defaults to indigo." },
|
|
1525
1618
|
description: { type: "string" }
|
|
1526
1619
|
},
|
|
1527
1620
|
required: ["name"]
|
|
@@ -1529,12 +1622,12 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
1529
1622
|
},
|
|
1530
1623
|
{
|
|
1531
1624
|
name: "list_tags",
|
|
1532
|
-
description: "List all tags",
|
|
1625
|
+
description: "List all available tags with their colors and descriptions.",
|
|
1533
1626
|
inputSchema: { type: "object", properties: {} }
|
|
1534
1627
|
},
|
|
1535
1628
|
{
|
|
1536
1629
|
name: "delete_tag",
|
|
1537
|
-
description: "Delete a tag by ID",
|
|
1630
|
+
description: "Delete a tag by ID. The tag will be removed from all contacts and companies it was applied to.",
|
|
1538
1631
|
inputSchema: {
|
|
1539
1632
|
type: "object",
|
|
1540
1633
|
properties: { id: { type: "string" } },
|
|
@@ -1543,7 +1636,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
1543
1636
|
},
|
|
1544
1637
|
{
|
|
1545
1638
|
name: "add_tag_to_contact",
|
|
1546
|
-
description: "
|
|
1639
|
+
description: "Apply an existing tag to a contact. Use list_tags to find tag IDs.",
|
|
1547
1640
|
inputSchema: {
|
|
1548
1641
|
type: "object",
|
|
1549
1642
|
properties: {
|
|
@@ -1555,7 +1648,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
1555
1648
|
},
|
|
1556
1649
|
{
|
|
1557
1650
|
name: "remove_tag_from_contact",
|
|
1558
|
-
description: "Remove a tag from a contact",
|
|
1651
|
+
description: "Remove a tag from a contact. The tag itself is not deleted.",
|
|
1559
1652
|
inputSchema: {
|
|
1560
1653
|
type: "object",
|
|
1561
1654
|
properties: {
|
|
@@ -1567,7 +1660,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
1567
1660
|
},
|
|
1568
1661
|
{
|
|
1569
1662
|
name: "add_relationship",
|
|
1570
|
-
description: "
|
|
1663
|
+
description: "Link two contacts with a typed relationship. relationship_type: colleague (work peer), friend (personal), family (relative), reports_to (A reports to B), mentor (A mentors B), investor (A invests in B), partner (business partner), client, vendor, other.",
|
|
1571
1664
|
inputSchema: {
|
|
1572
1665
|
type: "object",
|
|
1573
1666
|
properties: {
|
|
@@ -1584,7 +1677,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
1584
1677
|
},
|
|
1585
1678
|
{
|
|
1586
1679
|
name: "list_relationships",
|
|
1587
|
-
description: "List all relationships for a contact",
|
|
1680
|
+
description: "List all relationships for a contact, showing both directions (where contact is A or B).",
|
|
1588
1681
|
inputSchema: {
|
|
1589
1682
|
type: "object",
|
|
1590
1683
|
properties: { contact_id: { type: "string" } },
|
|
@@ -1593,7 +1686,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
1593
1686
|
},
|
|
1594
1687
|
{
|
|
1595
1688
|
name: "delete_relationship",
|
|
1596
|
-
description: "Delete a relationship by ID",
|
|
1689
|
+
description: "Delete a relationship by its ID. Use list_relationships to find relationship IDs.",
|
|
1597
1690
|
inputSchema: {
|
|
1598
1691
|
type: "object",
|
|
1599
1692
|
properties: { id: { type: "string" } },
|
|
@@ -1602,7 +1695,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
1602
1695
|
},
|
|
1603
1696
|
{
|
|
1604
1697
|
name: "merge_contacts",
|
|
1605
|
-
description: "Merge two contacts \u2014
|
|
1698
|
+
description: "Merge two contacts into one \u2014 all emails, phones, addresses, tags, and relationships from merge_id are moved to keep_id, then merge_id is deleted. Fields missing in keep_id are filled from merge_id.",
|
|
1606
1699
|
inputSchema: {
|
|
1607
1700
|
type: "object",
|
|
1608
1701
|
properties: {
|
|
@@ -1614,32 +1707,226 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
1614
1707
|
},
|
|
1615
1708
|
{
|
|
1616
1709
|
name: "import_contacts",
|
|
1617
|
-
description: "Import contacts from CSV, vCard, or JSON format",
|
|
1710
|
+
description: "Import contacts from CSV (Google Contacts format), vCard (.vcf, v3/v4), or JSON array. Pass raw file contents as the data string. format: 'csv'|'vcf'|'json'.",
|
|
1618
1711
|
inputSchema: {
|
|
1619
1712
|
type: "object",
|
|
1620
1713
|
properties: {
|
|
1621
1714
|
format: { type: "string", enum: ["json", "csv", "vcf"] },
|
|
1622
|
-
data: { type: "string" }
|
|
1715
|
+
data: { type: "string", description: "Raw file contents (CSV text, vCard text, or JSON array string)" }
|
|
1623
1716
|
},
|
|
1624
1717
|
required: ["format", "data"]
|
|
1625
1718
|
}
|
|
1626
1719
|
},
|
|
1627
1720
|
{
|
|
1628
1721
|
name: "export_contacts",
|
|
1629
|
-
description: "Export contacts to CSV, vCard, or JSON format",
|
|
1722
|
+
description: "Export contacts to CSV, vCard (.vcf), or JSON format. Optionally specify contact_ids to export a subset; omit to export all contacts.",
|
|
1630
1723
|
inputSchema: {
|
|
1631
1724
|
type: "object",
|
|
1632
1725
|
properties: {
|
|
1633
1726
|
format: { type: "string", enum: ["json", "csv", "vcf"] },
|
|
1634
|
-
contact_ids: { type: "array", items: { type: "string" } }
|
|
1727
|
+
contact_ids: { type: "array", items: { type: "string" }, description: "Specific contact IDs to export (omit for all)" }
|
|
1635
1728
|
},
|
|
1636
1729
|
required: ["format"]
|
|
1637
1730
|
}
|
|
1638
1731
|
},
|
|
1639
1732
|
{
|
|
1640
1733
|
name: "get_stats",
|
|
1641
|
-
description: "Get database statistics
|
|
1734
|
+
description: "Get database statistics: total counts of contacts, companies, tags, and groups.",
|
|
1642
1735
|
inputSchema: { type: "object", properties: {} }
|
|
1736
|
+
},
|
|
1737
|
+
{
|
|
1738
|
+
name: "log_interaction",
|
|
1739
|
+
description: "Record a contact interaction \u2014 sets last_contacted_at to now (or a provided date) and optionally appends a timestamped note. Use this to track when you last spoke with someone.",
|
|
1740
|
+
inputSchema: {
|
|
1741
|
+
type: "object",
|
|
1742
|
+
properties: {
|
|
1743
|
+
contact_id: { type: "string" },
|
|
1744
|
+
note: { type: "string", description: "Optional note about the interaction to append to the contact's notes" },
|
|
1745
|
+
date: { type: "string", description: "ISO 8601 datetime (defaults to now)" }
|
|
1746
|
+
},
|
|
1747
|
+
required: ["contact_id"]
|
|
1748
|
+
}
|
|
1749
|
+
},
|
|
1750
|
+
{
|
|
1751
|
+
name: "find_or_create_contact",
|
|
1752
|
+
description: "Find an existing contact by email or name, or create a new one if not found. Returns { contact, found: boolean, created: boolean }. The #1 tool for agent workflows \u2014 avoids duplicate creation. Searches by email first (exact match), then by display_name.",
|
|
1753
|
+
inputSchema: {
|
|
1754
|
+
type: "object",
|
|
1755
|
+
properties: {
|
|
1756
|
+
first_name: { type: "string" },
|
|
1757
|
+
last_name: { type: "string" },
|
|
1758
|
+
display_name: { type: "string" },
|
|
1759
|
+
nickname: { type: "string" },
|
|
1760
|
+
job_title: { type: "string" },
|
|
1761
|
+
company_id: { type: "string" },
|
|
1762
|
+
notes: { type: "string" },
|
|
1763
|
+
birthday: { type: "string" },
|
|
1764
|
+
website: { type: "string" },
|
|
1765
|
+
last_contacted_at: { type: "string" },
|
|
1766
|
+
preferred_contact_method: { type: "string", enum: ["email", "phone", "telegram", "whatsapp", "linkedin", "twitter", "other"] },
|
|
1767
|
+
emails: { type: "array", items: { type: "object", properties: { address: { type: "string" }, type: { type: "string" }, is_primary: { type: "boolean" } }, required: ["address"] } },
|
|
1768
|
+
phones: { type: "array", items: { type: "object" } },
|
|
1769
|
+
addresses: { type: "array", items: { type: "object" } },
|
|
1770
|
+
social_profiles: { type: "array", items: { type: "object" } },
|
|
1771
|
+
tag_ids: { type: "array", items: { type: "string" } },
|
|
1772
|
+
source: { type: "string" }
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
},
|
|
1776
|
+
{
|
|
1777
|
+
name: "upsert_contact",
|
|
1778
|
+
description: "Update a contact if one with matching email exists, otherwise create a new one. Returns { contact, action: 'created'|'updated' }. Ideal for syncing data from external sources. Requires email in the emails array or as a top-level email field.",
|
|
1779
|
+
inputSchema: {
|
|
1780
|
+
type: "object",
|
|
1781
|
+
properties: {
|
|
1782
|
+
first_name: { type: "string" },
|
|
1783
|
+
last_name: { type: "string" },
|
|
1784
|
+
display_name: { type: "string" },
|
|
1785
|
+
nickname: { type: "string" },
|
|
1786
|
+
job_title: { type: "string" },
|
|
1787
|
+
company_id: { type: "string" },
|
|
1788
|
+
notes: { type: "string" },
|
|
1789
|
+
birthday: { type: "string" },
|
|
1790
|
+
website: { type: "string" },
|
|
1791
|
+
last_contacted_at: { type: "string" },
|
|
1792
|
+
preferred_contact_method: { type: "string", enum: ["email", "phone", "telegram", "whatsapp", "linkedin", "twitter", "other"] },
|
|
1793
|
+
email: { type: "string", description: "Primary email address (alternative to emails array)" },
|
|
1794
|
+
emails: { type: "array", items: { type: "object", properties: { address: { type: "string" }, type: { type: "string" }, is_primary: { type: "boolean" } }, required: ["address"] } },
|
|
1795
|
+
phones: { type: "array", items: { type: "object" } },
|
|
1796
|
+
addresses: { type: "array", items: { type: "object" } },
|
|
1797
|
+
social_profiles: { type: "array", items: { type: "object" } },
|
|
1798
|
+
tag_ids: { type: "array", items: { type: "string" } },
|
|
1799
|
+
source: { type: "string" }
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
},
|
|
1803
|
+
{
|
|
1804
|
+
name: "add_note",
|
|
1805
|
+
description: "Append a timestamped note to a contact's notes field. Fast and ergonomic \u2014 no need for full update_contact. Note is appended as '\\n\\n[YYYY-MM-DD] text' to existing notes.",
|
|
1806
|
+
inputSchema: {
|
|
1807
|
+
type: "object",
|
|
1808
|
+
properties: {
|
|
1809
|
+
contact_id: { type: "string" },
|
|
1810
|
+
note: { type: "string" }
|
|
1811
|
+
},
|
|
1812
|
+
required: ["contact_id", "note"]
|
|
1813
|
+
}
|
|
1814
|
+
},
|
|
1815
|
+
{
|
|
1816
|
+
name: "list_contacts_by_company",
|
|
1817
|
+
description: "List all contacts belonging to a specific company. Equivalent to list_contacts with company_id filter but more ergonomic.",
|
|
1818
|
+
inputSchema: {
|
|
1819
|
+
type: "object",
|
|
1820
|
+
properties: {
|
|
1821
|
+
company_id: { type: "string" },
|
|
1822
|
+
limit: { type: "number", description: "Max results (default 50)" },
|
|
1823
|
+
offset: { type: "number" }
|
|
1824
|
+
},
|
|
1825
|
+
required: ["company_id"]
|
|
1826
|
+
}
|
|
1827
|
+
},
|
|
1828
|
+
{
|
|
1829
|
+
name: "list_contacts_by_tag",
|
|
1830
|
+
description: "List all contacts with a specific tag. Accepts either a tag ID (UUID) or a tag name string.",
|
|
1831
|
+
inputSchema: {
|
|
1832
|
+
type: "object",
|
|
1833
|
+
properties: {
|
|
1834
|
+
tag: { type: "string", description: "Tag name or tag ID (UUID)" },
|
|
1835
|
+
limit: { type: "number", description: "Max results (default 50)" },
|
|
1836
|
+
offset: { type: "number" }
|
|
1837
|
+
},
|
|
1838
|
+
required: ["tag"]
|
|
1839
|
+
}
|
|
1840
|
+
},
|
|
1841
|
+
{
|
|
1842
|
+
name: "create_group",
|
|
1843
|
+
description: "Create a new group for organizing contacts. Groups are named collections that can hold multiple contacts.",
|
|
1844
|
+
inputSchema: {
|
|
1845
|
+
type: "object",
|
|
1846
|
+
properties: {
|
|
1847
|
+
name: { type: "string" },
|
|
1848
|
+
description: { type: "string" }
|
|
1849
|
+
},
|
|
1850
|
+
required: ["name"]
|
|
1851
|
+
}
|
|
1852
|
+
},
|
|
1853
|
+
{
|
|
1854
|
+
name: "list_groups",
|
|
1855
|
+
description: "List all groups with their member counts.",
|
|
1856
|
+
inputSchema: { type: "object", properties: {} }
|
|
1857
|
+
},
|
|
1858
|
+
{
|
|
1859
|
+
name: "get_group",
|
|
1860
|
+
description: "Get a group by ID.",
|
|
1861
|
+
inputSchema: {
|
|
1862
|
+
type: "object",
|
|
1863
|
+
properties: { id: { type: "string" } },
|
|
1864
|
+
required: ["id"]
|
|
1865
|
+
}
|
|
1866
|
+
},
|
|
1867
|
+
{
|
|
1868
|
+
name: "update_group",
|
|
1869
|
+
description: "Update a group's name or description.",
|
|
1870
|
+
inputSchema: {
|
|
1871
|
+
type: "object",
|
|
1872
|
+
properties: {
|
|
1873
|
+
id: { type: "string" },
|
|
1874
|
+
name: { type: "string" },
|
|
1875
|
+
description: { type: "string" }
|
|
1876
|
+
},
|
|
1877
|
+
required: ["id"]
|
|
1878
|
+
}
|
|
1879
|
+
},
|
|
1880
|
+
{
|
|
1881
|
+
name: "delete_group",
|
|
1882
|
+
description: "Delete a group by ID. Contacts in the group are not deleted \u2014 only the group itself.",
|
|
1883
|
+
inputSchema: {
|
|
1884
|
+
type: "object",
|
|
1885
|
+
properties: { id: { type: "string" } },
|
|
1886
|
+
required: ["id"]
|
|
1887
|
+
}
|
|
1888
|
+
},
|
|
1889
|
+
{
|
|
1890
|
+
name: "add_contact_to_group",
|
|
1891
|
+
description: "Add a contact to a group.",
|
|
1892
|
+
inputSchema: {
|
|
1893
|
+
type: "object",
|
|
1894
|
+
properties: {
|
|
1895
|
+
contact_id: { type: "string" },
|
|
1896
|
+
group_id: { type: "string" }
|
|
1897
|
+
},
|
|
1898
|
+
required: ["contact_id", "group_id"]
|
|
1899
|
+
}
|
|
1900
|
+
},
|
|
1901
|
+
{
|
|
1902
|
+
name: "remove_contact_from_group",
|
|
1903
|
+
description: "Remove a contact from a group.",
|
|
1904
|
+
inputSchema: {
|
|
1905
|
+
type: "object",
|
|
1906
|
+
properties: {
|
|
1907
|
+
contact_id: { type: "string" },
|
|
1908
|
+
group_id: { type: "string" }
|
|
1909
|
+
},
|
|
1910
|
+
required: ["contact_id", "group_id"]
|
|
1911
|
+
}
|
|
1912
|
+
},
|
|
1913
|
+
{
|
|
1914
|
+
name: "list_contacts_in_group",
|
|
1915
|
+
description: "List all contact IDs in a group.",
|
|
1916
|
+
inputSchema: {
|
|
1917
|
+
type: "object",
|
|
1918
|
+
properties: { group_id: { type: "string" } },
|
|
1919
|
+
required: ["group_id"]
|
|
1920
|
+
}
|
|
1921
|
+
},
|
|
1922
|
+
{
|
|
1923
|
+
name: "list_groups_for_contact",
|
|
1924
|
+
description: "List all groups that a contact belongs to.",
|
|
1925
|
+
inputSchema: {
|
|
1926
|
+
type: "object",
|
|
1927
|
+
properties: { contact_id: { type: "string" } },
|
|
1928
|
+
required: ["contact_id"]
|
|
1929
|
+
}
|
|
1643
1930
|
}
|
|
1644
1931
|
]
|
|
1645
1932
|
}));
|
|
@@ -1658,6 +1945,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1658
1945
|
company_id: a.company_id,
|
|
1659
1946
|
notes: a.notes,
|
|
1660
1947
|
birthday: a.birthday,
|
|
1948
|
+
website: a.website,
|
|
1949
|
+
last_contacted_at: a.last_contacted_at,
|
|
1950
|
+
preferred_contact_method: a.preferred_contact_method,
|
|
1661
1951
|
emails: a.emails,
|
|
1662
1952
|
phones: a.phones,
|
|
1663
1953
|
addresses: a.addresses,
|
|
@@ -1682,7 +1972,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1682
1972
|
job_title: rest.job_title,
|
|
1683
1973
|
company_id: rest.company_id,
|
|
1684
1974
|
notes: rest.notes,
|
|
1685
|
-
birthday: rest.birthday
|
|
1975
|
+
birthday: rest.birthday,
|
|
1976
|
+
website: rest.website,
|
|
1977
|
+
last_contacted_at: rest.last_contacted_at,
|
|
1978
|
+
preferred_contact_method: rest.preferred_contact_method,
|
|
1979
|
+
source: rest.source
|
|
1686
1980
|
};
|
|
1687
1981
|
const contact = updateContact(id, input);
|
|
1688
1982
|
return { content: [{ type: "text", text: JSON.stringify(contact, null, 2) }] };
|
|
@@ -1847,13 +2141,210 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1847
2141
|
const contactCount = db.prepare("SELECT COUNT(*) as count FROM contacts").get().count;
|
|
1848
2142
|
const companyCount = db.prepare("SELECT COUNT(*) as count FROM companies").get().count;
|
|
1849
2143
|
const tagCount = db.prepare("SELECT COUNT(*) as count FROM tags").get().count;
|
|
2144
|
+
const groupCount = db.prepare("SELECT COUNT(*) as count FROM groups").get().count;
|
|
1850
2145
|
return {
|
|
1851
2146
|
content: [{
|
|
1852
2147
|
type: "text",
|
|
1853
|
-
text: JSON.stringify({ contacts: contactCount, companies: companyCount, tags: tagCount }, null, 2)
|
|
2148
|
+
text: JSON.stringify({ contacts: contactCount, companies: companyCount, tags: tagCount, groups: groupCount }, null, 2)
|
|
1854
2149
|
}]
|
|
1855
2150
|
};
|
|
1856
2151
|
}
|
|
2152
|
+
case "log_interaction": {
|
|
2153
|
+
const contactId = a.contact_id;
|
|
2154
|
+
const interactionDate = a.date ?? new Date().toISOString();
|
|
2155
|
+
const note = a.note;
|
|
2156
|
+
const existing = getContact(contactId);
|
|
2157
|
+
const updateInput = { last_contacted_at: interactionDate };
|
|
2158
|
+
if (note) {
|
|
2159
|
+
const dateStr = interactionDate.slice(0, 10);
|
|
2160
|
+
const existingNotes = existing.notes ?? "";
|
|
2161
|
+
updateInput.notes = existingNotes ? `${existingNotes}
|
|
2162
|
+
|
|
2163
|
+
[${dateStr}] ${note}` : `[${dateStr}] ${note}`;
|
|
2164
|
+
}
|
|
2165
|
+
const contact = updateContact(contactId, updateInput);
|
|
2166
|
+
return { content: [{ type: "text", text: JSON.stringify(contact, null, 2) }] };
|
|
2167
|
+
}
|
|
2168
|
+
case "find_or_create_contact": {
|
|
2169
|
+
const db = getDatabase();
|
|
2170
|
+
const emailAddresses = a.emails?.map((e) => e.address) ?? [];
|
|
2171
|
+
let found = null;
|
|
2172
|
+
for (const addr of emailAddresses) {
|
|
2173
|
+
const emailRow = db.prepare(`SELECT contact_id FROM emails WHERE address = ? AND contact_id IS NOT NULL LIMIT 1`).get(addr);
|
|
2174
|
+
if (emailRow) {
|
|
2175
|
+
found = getContact(emailRow.contact_id);
|
|
2176
|
+
break;
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
if (!found && a.display_name) {
|
|
2180
|
+
const results = searchContacts(a.display_name);
|
|
2181
|
+
if (results.length > 0)
|
|
2182
|
+
found = results[0];
|
|
2183
|
+
}
|
|
2184
|
+
if (found) {
|
|
2185
|
+
return { content: [{ type: "text", text: JSON.stringify({ contact: found, found: true, created: false }, null, 2) }] };
|
|
2186
|
+
}
|
|
2187
|
+
const input = {
|
|
2188
|
+
first_name: a.first_name,
|
|
2189
|
+
last_name: a.last_name,
|
|
2190
|
+
display_name: a.display_name,
|
|
2191
|
+
nickname: a.nickname,
|
|
2192
|
+
job_title: a.job_title,
|
|
2193
|
+
company_id: a.company_id,
|
|
2194
|
+
notes: a.notes,
|
|
2195
|
+
birthday: a.birthday,
|
|
2196
|
+
website: a.website,
|
|
2197
|
+
last_contacted_at: a.last_contacted_at,
|
|
2198
|
+
preferred_contact_method: a.preferred_contact_method,
|
|
2199
|
+
emails: a.emails,
|
|
2200
|
+
phones: a.phones,
|
|
2201
|
+
addresses: a.addresses,
|
|
2202
|
+
social_profiles: a.social_profiles,
|
|
2203
|
+
tag_ids: a.tag_ids,
|
|
2204
|
+
source: a.source
|
|
2205
|
+
};
|
|
2206
|
+
const contact = createContact(input);
|
|
2207
|
+
return { content: [{ type: "text", text: JSON.stringify({ contact, found: false, created: true }, null, 2) }] };
|
|
2208
|
+
}
|
|
2209
|
+
case "upsert_contact": {
|
|
2210
|
+
const db = getDatabase();
|
|
2211
|
+
const upsertEmails = a.emails?.map((e) => e.address) ?? [];
|
|
2212
|
+
if (a.email)
|
|
2213
|
+
upsertEmails.unshift(a.email);
|
|
2214
|
+
let existingContact = null;
|
|
2215
|
+
for (const addr of upsertEmails) {
|
|
2216
|
+
const emailRow = db.prepare(`SELECT contact_id FROM emails WHERE address = ? AND contact_id IS NOT NULL LIMIT 1`).get(addr);
|
|
2217
|
+
if (emailRow) {
|
|
2218
|
+
existingContact = getContact(emailRow.contact_id);
|
|
2219
|
+
break;
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
if (existingContact) {
|
|
2223
|
+
const updateInput = {
|
|
2224
|
+
first_name: a.first_name,
|
|
2225
|
+
last_name: a.last_name,
|
|
2226
|
+
display_name: a.display_name,
|
|
2227
|
+
nickname: a.nickname,
|
|
2228
|
+
job_title: a.job_title,
|
|
2229
|
+
company_id: a.company_id,
|
|
2230
|
+
notes: a.notes,
|
|
2231
|
+
birthday: a.birthday,
|
|
2232
|
+
website: a.website,
|
|
2233
|
+
last_contacted_at: a.last_contacted_at,
|
|
2234
|
+
preferred_contact_method: a.preferred_contact_method,
|
|
2235
|
+
source: a.source
|
|
2236
|
+
};
|
|
2237
|
+
const updated = updateContact(existingContact.id, updateInput);
|
|
2238
|
+
return { content: [{ type: "text", text: JSON.stringify({ contact: updated, action: "updated" }, null, 2) }] };
|
|
2239
|
+
}
|
|
2240
|
+
const createInput = {
|
|
2241
|
+
first_name: a.first_name,
|
|
2242
|
+
last_name: a.last_name,
|
|
2243
|
+
display_name: a.display_name,
|
|
2244
|
+
nickname: a.nickname,
|
|
2245
|
+
job_title: a.job_title,
|
|
2246
|
+
company_id: a.company_id,
|
|
2247
|
+
notes: a.notes,
|
|
2248
|
+
birthday: a.birthday,
|
|
2249
|
+
website: a.website,
|
|
2250
|
+
last_contacted_at: a.last_contacted_at,
|
|
2251
|
+
preferred_contact_method: a.preferred_contact_method,
|
|
2252
|
+
emails: a.email ? [{ address: a.email, is_primary: true }, ...a.emails ?? []] : a.emails,
|
|
2253
|
+
phones: a.phones,
|
|
2254
|
+
addresses: a.addresses,
|
|
2255
|
+
social_profiles: a.social_profiles,
|
|
2256
|
+
tag_ids: a.tag_ids,
|
|
2257
|
+
source: a.source
|
|
2258
|
+
};
|
|
2259
|
+
const created = createContact(createInput);
|
|
2260
|
+
return { content: [{ type: "text", text: JSON.stringify({ contact: created, action: "created" }, null, 2) }] };
|
|
2261
|
+
}
|
|
2262
|
+
case "add_note": {
|
|
2263
|
+
const contactId = a.contact_id;
|
|
2264
|
+
const note = a.note;
|
|
2265
|
+
const existing = getContact(contactId);
|
|
2266
|
+
const dateStr = new Date().toISOString().slice(0, 10);
|
|
2267
|
+
const existingNotes = existing.notes ?? "";
|
|
2268
|
+
const updatedNotes = existingNotes ? `${existingNotes}
|
|
2269
|
+
|
|
2270
|
+
[${dateStr}] ${note}` : `[${dateStr}] ${note}`;
|
|
2271
|
+
const contact = updateContact(contactId, { notes: updatedNotes });
|
|
2272
|
+
return { content: [{ type: "text", text: JSON.stringify(contact, null, 2) }] };
|
|
2273
|
+
}
|
|
2274
|
+
case "list_contacts_by_company": {
|
|
2275
|
+
const result = listContacts({
|
|
2276
|
+
company_id: a.company_id,
|
|
2277
|
+
limit: a.limit,
|
|
2278
|
+
offset: a.offset
|
|
2279
|
+
});
|
|
2280
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
2281
|
+
}
|
|
2282
|
+
case "list_contacts_by_tag": {
|
|
2283
|
+
const tagInput = a.tag;
|
|
2284
|
+
const db = getDatabase();
|
|
2285
|
+
const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(tagInput);
|
|
2286
|
+
let tagId = isUuid ? tagInput : null;
|
|
2287
|
+
if (!tagId) {
|
|
2288
|
+
const tag = getTagByName(tagInput, db);
|
|
2289
|
+
if (!tag)
|
|
2290
|
+
return { content: [{ type: "text", text: `Tag not found: ${tagInput}` }], isError: true };
|
|
2291
|
+
tagId = tag.id;
|
|
2292
|
+
}
|
|
2293
|
+
const result = listContacts({
|
|
2294
|
+
tag_id: tagId,
|
|
2295
|
+
limit: a.limit,
|
|
2296
|
+
offset: a.offset
|
|
2297
|
+
});
|
|
2298
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
2299
|
+
}
|
|
2300
|
+
case "create_group": {
|
|
2301
|
+
const db = getDatabase();
|
|
2302
|
+
const group = createGroup(db, { name: a.name, description: a.description });
|
|
2303
|
+
return { content: [{ type: "text", text: JSON.stringify(group, null, 2) }] };
|
|
2304
|
+
}
|
|
2305
|
+
case "list_groups": {
|
|
2306
|
+
const db = getDatabase();
|
|
2307
|
+
const groups = listGroups(db);
|
|
2308
|
+
return { content: [{ type: "text", text: JSON.stringify(groups, null, 2) }] };
|
|
2309
|
+
}
|
|
2310
|
+
case "get_group": {
|
|
2311
|
+
const db = getDatabase();
|
|
2312
|
+
const group = getGroup(db, a.id);
|
|
2313
|
+
if (!group)
|
|
2314
|
+
return { content: [{ type: "text", text: `Group not found: ${a.id}` }], isError: true };
|
|
2315
|
+
return { content: [{ type: "text", text: JSON.stringify(group, null, 2) }] };
|
|
2316
|
+
}
|
|
2317
|
+
case "update_group": {
|
|
2318
|
+
const db = getDatabase();
|
|
2319
|
+
const { id: groupId, ...groupRest } = a;
|
|
2320
|
+
const group = updateGroup(db, groupId, { name: groupRest.name, description: groupRest.description });
|
|
2321
|
+
return { content: [{ type: "text", text: JSON.stringify(group, null, 2) }] };
|
|
2322
|
+
}
|
|
2323
|
+
case "delete_group": {
|
|
2324
|
+
const db = getDatabase();
|
|
2325
|
+
deleteGroup(db, a.id);
|
|
2326
|
+
return { content: [{ type: "text", text: `Group ${a.id} deleted successfully` }] };
|
|
2327
|
+
}
|
|
2328
|
+
case "add_contact_to_group": {
|
|
2329
|
+
const db = getDatabase();
|
|
2330
|
+
addContactToGroup(db, a.contact_id, a.group_id);
|
|
2331
|
+
return { content: [{ type: "text", text: `Contact ${a.contact_id} added to group ${a.group_id}` }] };
|
|
2332
|
+
}
|
|
2333
|
+
case "remove_contact_from_group": {
|
|
2334
|
+
const db = getDatabase();
|
|
2335
|
+
removeContactFromGroup(db, a.contact_id, a.group_id);
|
|
2336
|
+
return { content: [{ type: "text", text: `Contact ${a.contact_id} removed from group ${a.group_id}` }] };
|
|
2337
|
+
}
|
|
2338
|
+
case "list_contacts_in_group": {
|
|
2339
|
+
const db = getDatabase();
|
|
2340
|
+
const contactIds = listContactsInGroup(db, a.group_id);
|
|
2341
|
+
return { content: [{ type: "text", text: JSON.stringify(contactIds, null, 2) }] };
|
|
2342
|
+
}
|
|
2343
|
+
case "list_groups_for_contact": {
|
|
2344
|
+
const db = getDatabase();
|
|
2345
|
+
const groups = listGroupsForContact(db, a.contact_id);
|
|
2346
|
+
return { content: [{ type: "text", text: JSON.stringify(groups, null, 2) }] };
|
|
2347
|
+
}
|
|
1857
2348
|
default:
|
|
1858
2349
|
return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
|
|
1859
2350
|
}
|