@hasna/contacts 0.4.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +1223 -3
- package/dist/db/contacts.d.ts +9 -0
- package/dist/db/contacts.d.ts.map +1 -1
- package/dist/db/coordination.d.ts +30 -0
- package/dist/db/coordination.d.ts.map +1 -0
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/field-history.d.ts +17 -0
- package/dist/db/field-history.d.ts.map +1 -0
- package/dist/db/freshness.d.ts +24 -0
- package/dist/db/freshness.d.ts.map +1 -0
- package/dist/db/graph.d.ts +21 -0
- package/dist/db/graph.d.ts.map +1 -0
- package/dist/db/groups.d.ts +1 -1
- package/dist/db/groups.d.ts.map +1 -1
- package/dist/db/identity.d.ts +32 -0
- package/dist/db/identity.d.ts.map +1 -0
- package/dist/db/job-history.d.ts +29 -0
- package/dist/db/job-history.d.ts.map +1 -0
- package/dist/db/learnings.d.ts +43 -0
- package/dist/db/learnings.d.ts.map +1 -0
- package/dist/db/org-chart.d.ts +37 -0
- package/dist/db/org-chart.d.ts.map +1 -0
- package/dist/db/signals.d.ts +18 -0
- package/dist/db/signals.d.ts.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1031 -83
- package/dist/lib/context.d.ts +5 -0
- package/dist/lib/context.d.ts.map +1 -0
- package/dist/lib/embeddings.d.ts +9 -0
- package/dist/lib/embeddings.d.ts.map +1 -0
- package/dist/lib/freshness.d.ts +19 -0
- package/dist/lib/freshness.d.ts.map +1 -0
- package/dist/lib/learning-maintenance.d.ts +8 -0
- package/dist/lib/learning-maintenance.d.ts.map +1 -0
- package/dist/lib/meeting-capture.d.ts +15 -0
- package/dist/lib/meeting-capture.d.ts.map +1 -0
- package/dist/lib/signals.d.ts +9 -0
- package/dist/lib/signals.d.ts.map +1 -0
- package/dist/lib/signature-parser.d.ts +36 -0
- package/dist/lib/signature-parser.d.ts.map +1 -0
- package/dist/mcp/index.js +1387 -84
- package/dist/server/index.js +151 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/mcp/index.js
CHANGED
|
@@ -283,6 +283,21 @@ var init_database = __esm(() => {
|
|
|
283
283
|
|
|
284
284
|
CREATE INDEX IF NOT EXISTS idx_company_relationships_contact ON company_relationships(contact_id);
|
|
285
285
|
CREATE INDEX IF NOT EXISTS idx_company_relationships_company ON company_relationships(company_id);
|
|
286
|
+
`,
|
|
287
|
+
`
|
|
288
|
+
ALTER TABLE groups ADD COLUMN project_id TEXT;
|
|
289
|
+
|
|
290
|
+
CREATE TABLE IF NOT EXISTS contact_projects (
|
|
291
|
+
contact_id TEXT NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
|
|
292
|
+
project_id TEXT NOT NULL,
|
|
293
|
+
PRIMARY KEY (contact_id, project_id)
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
CREATE INDEX IF NOT EXISTS idx_contact_projects_project ON contact_projects(project_id);
|
|
297
|
+
CREATE INDEX IF NOT EXISTS idx_contact_projects_contact ON contact_projects(contact_id);
|
|
298
|
+
|
|
299
|
+
INSERT OR IGNORE INTO contact_projects (contact_id, project_id)
|
|
300
|
+
SELECT id, project_id FROM contacts WHERE project_id IS NOT NULL;
|
|
286
301
|
`,
|
|
287
302
|
`
|
|
288
303
|
CREATE TABLE IF NOT EXISTS contact_notes (
|
|
@@ -406,6 +421,142 @@ var init_database = __esm(() => {
|
|
|
406
421
|
deal_id TEXT REFERENCES deals(id) ON DELETE SET NULL,
|
|
407
422
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
408
423
|
);
|
|
424
|
+
`,
|
|
425
|
+
`
|
|
426
|
+
-- CON-00069: temporal field history
|
|
427
|
+
CREATE TABLE IF NOT EXISTS contact_field_history (
|
|
428
|
+
id TEXT PRIMARY KEY,
|
|
429
|
+
contact_id TEXT NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
|
|
430
|
+
field_name TEXT NOT NULL,
|
|
431
|
+
old_value TEXT,
|
|
432
|
+
new_value TEXT,
|
|
433
|
+
valid_from TEXT NOT NULL DEFAULT (datetime('now')),
|
|
434
|
+
source TEXT,
|
|
435
|
+
confidence TEXT NOT NULL DEFAULT 'imported' CHECK(confidence IN ('verified','inferred','imported','stale')),
|
|
436
|
+
created_by TEXT,
|
|
437
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
-- CON-00070: job history
|
|
441
|
+
CREATE TABLE IF NOT EXISTS job_history (
|
|
442
|
+
id TEXT PRIMARY KEY,
|
|
443
|
+
contact_id TEXT NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
|
|
444
|
+
company_id TEXT REFERENCES companies(id) ON DELETE SET NULL,
|
|
445
|
+
company_name TEXT NOT NULL,
|
|
446
|
+
title TEXT,
|
|
447
|
+
start_date TEXT,
|
|
448
|
+
end_date TEXT,
|
|
449
|
+
is_current INTEGER NOT NULL DEFAULT 0,
|
|
450
|
+
inferred INTEGER NOT NULL DEFAULT 0,
|
|
451
|
+
source TEXT,
|
|
452
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
-- CON-00071: learnings
|
|
456
|
+
CREATE TABLE IF NOT EXISTS contact_learnings (
|
|
457
|
+
id TEXT PRIMARY KEY,
|
|
458
|
+
contact_id TEXT NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
|
|
459
|
+
content TEXT NOT NULL,
|
|
460
|
+
type TEXT NOT NULL DEFAULT 'fact' CHECK(type IN ('preference','fact','inference','warning','signal')),
|
|
461
|
+
confidence INTEGER NOT NULL DEFAULT 70 CHECK(confidence BETWEEN 0 AND 100),
|
|
462
|
+
importance INTEGER NOT NULL DEFAULT 5 CHECK(importance BETWEEN 1 AND 10),
|
|
463
|
+
learned_by TEXT,
|
|
464
|
+
session_id TEXT,
|
|
465
|
+
visibility TEXT NOT NULL DEFAULT 'shared' CHECK(visibility IN ('private','shared','human')),
|
|
466
|
+
tags TEXT NOT NULL DEFAULT '[]',
|
|
467
|
+
confirmed_count INTEGER NOT NULL DEFAULT 0,
|
|
468
|
+
contradicts_id TEXT REFERENCES contact_learnings(id) ON DELETE SET NULL,
|
|
469
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
470
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
-- CON-00072: coordination
|
|
474
|
+
CREATE TABLE IF NOT EXISTS contact_locks (
|
|
475
|
+
id TEXT PRIMARY KEY,
|
|
476
|
+
contact_id TEXT NOT NULL UNIQUE REFERENCES contacts(id) ON DELETE CASCADE,
|
|
477
|
+
agent_name TEXT NOT NULL,
|
|
478
|
+
reason TEXT,
|
|
479
|
+
acquired_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
480
|
+
expires_at TEXT NOT NULL,
|
|
481
|
+
session_id TEXT
|
|
482
|
+
);
|
|
483
|
+
CREATE TABLE IF NOT EXISTS contact_agent_activity (
|
|
484
|
+
id TEXT PRIMARY KEY,
|
|
485
|
+
contact_id TEXT NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
|
|
486
|
+
agent_name TEXT NOT NULL,
|
|
487
|
+
action TEXT NOT NULL,
|
|
488
|
+
details TEXT,
|
|
489
|
+
session_id TEXT,
|
|
490
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
491
|
+
);
|
|
492
|
+
|
|
493
|
+
-- CON-00073: relationship graph extra columns
|
|
494
|
+
ALTER TABLE contact_relationships ADD COLUMN strength_score INTEGER NOT NULL DEFAULT 50;
|
|
495
|
+
ALTER TABLE contact_relationships ADD COLUMN interaction_count INTEGER NOT NULL DEFAULT 0;
|
|
496
|
+
ALTER TABLE contact_relationships ADD COLUMN last_interaction TEXT;
|
|
497
|
+
ALTER TABLE contact_relationships ADD COLUMN relationship_status TEXT NOT NULL DEFAULT 'stable' CHECK(relationship_status IN ('warming','stable','cooling','ghost'));
|
|
498
|
+
|
|
499
|
+
-- CON-00074: identity resolution
|
|
500
|
+
CREATE TABLE IF NOT EXISTS contact_identities (
|
|
501
|
+
id TEXT PRIMARY KEY,
|
|
502
|
+
contact_id TEXT NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
|
|
503
|
+
system TEXT NOT NULL,
|
|
504
|
+
external_id TEXT NOT NULL,
|
|
505
|
+
external_url TEXT,
|
|
506
|
+
confidence TEXT NOT NULL DEFAULT 'inferred' CHECK(confidence IN ('verified','inferred')),
|
|
507
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
508
|
+
UNIQUE(system, external_id)
|
|
509
|
+
);
|
|
510
|
+
ALTER TABLE contacts ADD COLUMN canonical_id TEXT;
|
|
511
|
+
|
|
512
|
+
-- CON-00076: relationship signals
|
|
513
|
+
ALTER TABLE contacts ADD COLUMN relationship_health INTEGER NOT NULL DEFAULT 50;
|
|
514
|
+
ALTER TABLE contacts ADD COLUMN avg_response_hours REAL;
|
|
515
|
+
ALTER TABLE contacts ADD COLUMN preferred_channel TEXT;
|
|
516
|
+
ALTER TABLE contacts ADD COLUMN engagement_status TEXT NOT NULL DEFAULT 'new' CHECK(engagement_status IN ('warming','stable','cooling','ghost','new'));
|
|
517
|
+
ALTER TABLE contacts ADD COLUMN interaction_count_30d INTEGER NOT NULL DEFAULT 0;
|
|
518
|
+
ALTER TABLE contacts ADD COLUMN interaction_count_90d INTEGER NOT NULL DEFAULT 0;
|
|
519
|
+
|
|
520
|
+
-- CON-00079: freshness scoring
|
|
521
|
+
CREATE TABLE IF NOT EXISTS contact_field_confidence (
|
|
522
|
+
id TEXT PRIMARY KEY,
|
|
523
|
+
contact_id TEXT NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
|
|
524
|
+
field_name TEXT NOT NULL,
|
|
525
|
+
confidence TEXT NOT NULL DEFAULT 'imported' CHECK(confidence IN ('verified','inferred','imported','stale')),
|
|
526
|
+
source TEXT,
|
|
527
|
+
last_verified_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
528
|
+
UNIQUE(contact_id, field_name)
|
|
529
|
+
);
|
|
530
|
+
|
|
531
|
+
-- CON-00080: org chart
|
|
532
|
+
CREATE TABLE IF NOT EXISTS org_chart_edges (
|
|
533
|
+
id TEXT PRIMARY KEY,
|
|
534
|
+
company_id TEXT NOT NULL REFERENCES companies(id) ON DELETE CASCADE,
|
|
535
|
+
contact_a_id TEXT NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
|
|
536
|
+
contact_b_id TEXT NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
|
|
537
|
+
edge_type TEXT NOT NULL CHECK(edge_type IN ('reports_to','manages','collaborates_with','peer')),
|
|
538
|
+
inferred INTEGER NOT NULL DEFAULT 0,
|
|
539
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
540
|
+
UNIQUE(company_id, contact_a_id, contact_b_id, edge_type)
|
|
541
|
+
);
|
|
542
|
+
CREATE TABLE IF NOT EXISTS deal_contact_roles (
|
|
543
|
+
id TEXT PRIMARY KEY,
|
|
544
|
+
deal_id TEXT NOT NULL REFERENCES deals(id) ON DELETE CASCADE,
|
|
545
|
+
contact_id TEXT NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
|
|
546
|
+
account_role TEXT NOT NULL CHECK(account_role IN ('economic_buyer','technical_evaluator','champion','blocker','influencer','user','sponsor','other')),
|
|
547
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
548
|
+
UNIQUE(deal_id, contact_id)
|
|
549
|
+
);
|
|
550
|
+
|
|
551
|
+
-- CON-00075: embeddings
|
|
552
|
+
CREATE TABLE IF NOT EXISTS contact_embeddings (
|
|
553
|
+
contact_id TEXT PRIMARY KEY REFERENCES contacts(id) ON DELETE CASCADE,
|
|
554
|
+
embedding TEXT NOT NULL,
|
|
555
|
+
model TEXT NOT NULL DEFAULT 'tfidf',
|
|
556
|
+
embedded_text TEXT,
|
|
557
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
558
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
559
|
+
);
|
|
409
560
|
`
|
|
410
561
|
];
|
|
411
562
|
});
|
|
@@ -474,14 +625,20 @@ var init_activity = __esm(() => {
|
|
|
474
625
|
var exports_contacts = {};
|
|
475
626
|
__export(exports_contacts, {
|
|
476
627
|
updateContact: () => updateContact,
|
|
628
|
+
unlinkContactFromProject: () => unlinkContactFromProject,
|
|
477
629
|
unarchiveContact: () => unarchiveContact,
|
|
630
|
+
setContactProjects: () => setContactProjects,
|
|
478
631
|
searchContacts: () => searchContacts,
|
|
479
632
|
mergeContacts: () => mergeContacts,
|
|
480
633
|
listRecentContacts: () => listRecentContacts,
|
|
481
634
|
listContacts: () => listContacts,
|
|
635
|
+
listContactIdsByProject: () => listContactIdsByProject,
|
|
482
636
|
listColdContacts: () => listColdContacts,
|
|
637
|
+
linkContactToProject: () => linkContactToProject,
|
|
638
|
+
getContactProjectIds: () => getContactProjectIds,
|
|
483
639
|
getContactByEmail: () => getContactByEmail,
|
|
484
640
|
getContact: () => getContact,
|
|
641
|
+
findOrCreateContact: () => findOrCreateContact,
|
|
485
642
|
deleteContact: () => deleteContact,
|
|
486
643
|
createContact: () => createContact,
|
|
487
644
|
autoLinkContactToCompany: () => autoLinkContactToCompany,
|
|
@@ -1011,6 +1168,25 @@ function listColdContacts(days, db) {
|
|
|
1011
1168
|
LIMIT 100`).all(`-${days}`);
|
|
1012
1169
|
return rows.map((row) => loadContactDetails(d, rowToContact(row)));
|
|
1013
1170
|
}
|
|
1171
|
+
async function findOrCreateContact(input, db) {
|
|
1172
|
+
const d = db || getDatabase();
|
|
1173
|
+
const emailAddresses = (input.emails ?? []).map((e) => e.address);
|
|
1174
|
+
for (const addr of emailAddresses) {
|
|
1175
|
+
const emailRow = d.query(`SELECT contact_id FROM emails WHERE LOWER(address) = LOWER(?) AND contact_id IS NOT NULL LIMIT 1`).get(addr);
|
|
1176
|
+
if (emailRow) {
|
|
1177
|
+
return { contact: getContact(emailRow.contact_id, d), created: false };
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
const nameQuery = input.display_name ?? (input.first_name || input.last_name ? `${input.first_name ?? ""} ${input.last_name ?? ""}`.trim() : null);
|
|
1181
|
+
if (nameQuery) {
|
|
1182
|
+
const results = searchContacts(nameQuery, d);
|
|
1183
|
+
if (results.length > 0 && results[0]) {
|
|
1184
|
+
return { contact: results[0], created: false };
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
const contact = createContact(input, d);
|
|
1188
|
+
return { contact, created: true };
|
|
1189
|
+
}
|
|
1014
1190
|
function autoLinkContactToCompany(contactId, db) {
|
|
1015
1191
|
const d = db || getDatabase();
|
|
1016
1192
|
const row = d.query(`SELECT * FROM contacts WHERE id = ?`).get(contactId);
|
|
@@ -1030,12 +1206,127 @@ function autoLinkContactToCompany(contactId, db) {
|
|
|
1030
1206
|
const updated = d.query(`SELECT * FROM contacts WHERE id = ?`).get(contactId);
|
|
1031
1207
|
return loadContactDetails(d, rowToContact(updated));
|
|
1032
1208
|
}
|
|
1209
|
+
function linkContactToProject(contactId, projectId, db) {
|
|
1210
|
+
const d = db || getDatabase();
|
|
1211
|
+
d.run(`INSERT OR IGNORE INTO contact_projects (contact_id, project_id) VALUES (?, ?)`, [contactId, projectId]);
|
|
1212
|
+
}
|
|
1213
|
+
function unlinkContactFromProject(contactId, projectId, db) {
|
|
1214
|
+
const d = db || getDatabase();
|
|
1215
|
+
d.run(`DELETE FROM contact_projects WHERE contact_id = ? AND project_id = ?`, [contactId, projectId]);
|
|
1216
|
+
}
|
|
1217
|
+
function getContactProjectIds(contactId, db) {
|
|
1218
|
+
const d = db || getDatabase();
|
|
1219
|
+
const rows = d.query(`SELECT project_id FROM contact_projects WHERE contact_id = ?`).all(contactId);
|
|
1220
|
+
return rows.map((r) => r.project_id);
|
|
1221
|
+
}
|
|
1222
|
+
function listContactIdsByProject(projectId, db) {
|
|
1223
|
+
const d = db || getDatabase();
|
|
1224
|
+
const rows = d.query(`SELECT contact_id FROM contact_projects WHERE project_id = ?`).all(projectId);
|
|
1225
|
+
return rows.map((r) => r.contact_id);
|
|
1226
|
+
}
|
|
1227
|
+
function setContactProjects(contactId, projectIds, db) {
|
|
1228
|
+
const d = db || getDatabase();
|
|
1229
|
+
d.run(`DELETE FROM contact_projects WHERE contact_id = ?`, [contactId]);
|
|
1230
|
+
for (const pid of projectIds) {
|
|
1231
|
+
d.run(`INSERT OR IGNORE INTO contact_projects (contact_id, project_id) VALUES (?, ?)`, [contactId, pid]);
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1033
1234
|
var init_contacts = __esm(() => {
|
|
1034
1235
|
init_types();
|
|
1035
1236
|
init_database();
|
|
1036
1237
|
init_activity();
|
|
1037
1238
|
});
|
|
1038
1239
|
|
|
1240
|
+
// src/db/events.ts
|
|
1241
|
+
var exports_events = {};
|
|
1242
|
+
__export(exports_events, {
|
|
1243
|
+
logEvent: () => logEvent,
|
|
1244
|
+
listEvents: () => listEvents,
|
|
1245
|
+
getEvent: () => getEvent,
|
|
1246
|
+
deleteEvent: () => deleteEvent
|
|
1247
|
+
});
|
|
1248
|
+
function rowToEvent(row) {
|
|
1249
|
+
let contact_ids = [];
|
|
1250
|
+
try {
|
|
1251
|
+
contact_ids = JSON.parse(row.contact_ids);
|
|
1252
|
+
} catch {
|
|
1253
|
+
contact_ids = [];
|
|
1254
|
+
}
|
|
1255
|
+
return {
|
|
1256
|
+
id: row.id,
|
|
1257
|
+
title: row.title,
|
|
1258
|
+
type: row.type,
|
|
1259
|
+
event_date: row.event_date,
|
|
1260
|
+
duration_min: row.duration_min,
|
|
1261
|
+
contact_ids,
|
|
1262
|
+
company_id: row.company_id,
|
|
1263
|
+
notes: row.notes,
|
|
1264
|
+
outcome: row.outcome,
|
|
1265
|
+
deal_id: row.deal_id,
|
|
1266
|
+
created_at: row.created_at
|
|
1267
|
+
};
|
|
1268
|
+
}
|
|
1269
|
+
function logEvent(input, db) {
|
|
1270
|
+
const d = db || getDatabase();
|
|
1271
|
+
const id = uuid();
|
|
1272
|
+
const timestamp = now();
|
|
1273
|
+
d.run(`INSERT INTO events (id, title, type, event_date, duration_min, contact_ids, company_id, notes, outcome, deal_id, created_at)
|
|
1274
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
1275
|
+
id,
|
|
1276
|
+
input.title,
|
|
1277
|
+
input.type ?? "meeting",
|
|
1278
|
+
input.event_date,
|
|
1279
|
+
input.duration_min ?? null,
|
|
1280
|
+
JSON.stringify(input.contact_ids ?? []),
|
|
1281
|
+
input.company_id ?? null,
|
|
1282
|
+
input.notes ?? null,
|
|
1283
|
+
input.outcome ?? null,
|
|
1284
|
+
input.deal_id ?? null,
|
|
1285
|
+
timestamp
|
|
1286
|
+
]);
|
|
1287
|
+
return rowToEvent(d.query(`SELECT * FROM events WHERE id = ?`).get(id));
|
|
1288
|
+
}
|
|
1289
|
+
function getEvent(id, db) {
|
|
1290
|
+
const d = db || getDatabase();
|
|
1291
|
+
const row = d.query(`SELECT * FROM events WHERE id = ?`).get(id);
|
|
1292
|
+
return row ? rowToEvent(row) : null;
|
|
1293
|
+
}
|
|
1294
|
+
function listEvents(opts = {}, db) {
|
|
1295
|
+
const d = db || getDatabase();
|
|
1296
|
+
const conditions = [];
|
|
1297
|
+
const params = [];
|
|
1298
|
+
if (opts.contact_id) {
|
|
1299
|
+
conditions.push("contact_ids LIKE ?");
|
|
1300
|
+
params.push(`%${opts.contact_id}%`);
|
|
1301
|
+
}
|
|
1302
|
+
if (opts.company_id) {
|
|
1303
|
+
conditions.push("company_id = ?");
|
|
1304
|
+
params.push(opts.company_id);
|
|
1305
|
+
}
|
|
1306
|
+
if (opts.type) {
|
|
1307
|
+
conditions.push("type = ?");
|
|
1308
|
+
params.push(opts.type);
|
|
1309
|
+
}
|
|
1310
|
+
if (opts.date_from) {
|
|
1311
|
+
conditions.push("event_date >= ?");
|
|
1312
|
+
params.push(opts.date_from);
|
|
1313
|
+
}
|
|
1314
|
+
if (opts.date_to) {
|
|
1315
|
+
conditions.push("event_date <= ?");
|
|
1316
|
+
params.push(opts.date_to);
|
|
1317
|
+
}
|
|
1318
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1319
|
+
const rows = d.query(`SELECT * FROM events ${where} ORDER BY event_date DESC`).all(...params);
|
|
1320
|
+
return rows.map(rowToEvent);
|
|
1321
|
+
}
|
|
1322
|
+
function deleteEvent(id, db) {
|
|
1323
|
+
const d = db || getDatabase();
|
|
1324
|
+
d.run(`DELETE FROM events WHERE id = ?`, [id]);
|
|
1325
|
+
}
|
|
1326
|
+
var init_events = __esm(() => {
|
|
1327
|
+
init_database();
|
|
1328
|
+
});
|
|
1329
|
+
|
|
1039
1330
|
// src/mcp/index.ts
|
|
1040
1331
|
init_database();
|
|
1041
1332
|
init_contacts();
|
|
@@ -1050,17 +1341,19 @@ import {
|
|
|
1050
1341
|
init_database();
|
|
1051
1342
|
function createGroup(db, input) {
|
|
1052
1343
|
const id = uuid();
|
|
1053
|
-
db.query(`INSERT INTO groups(id, name, description, created_at, updated_at) VALUES(
|
|
1344
|
+
db.query(`INSERT INTO groups(id, name, description, project_id, created_at, updated_at) VALUES(?,?,?,?,?,?)`).run(id, input.name, input.description ?? null, input.project_id ?? null, now(), now());
|
|
1054
1345
|
return getGroup(db, id);
|
|
1055
1346
|
}
|
|
1056
1347
|
function getGroup(db, id) {
|
|
1057
1348
|
return db.query(`SELECT * FROM groups WHERE id = ?`).get(id);
|
|
1058
1349
|
}
|
|
1059
|
-
function listGroups(db) {
|
|
1350
|
+
function listGroups(db, projectId) {
|
|
1351
|
+
const where = projectId ? "WHERE g.project_id = ?" : "";
|
|
1352
|
+
const params = projectId ? [projectId] : [];
|
|
1060
1353
|
return db.query(`SELECT g.*,
|
|
1061
1354
|
(SELECT COUNT(*) FROM contact_groups cg WHERE cg.group_id = g.id) as member_count,
|
|
1062
1355
|
(SELECT COUNT(*) FROM company_groups cog WHERE cog.group_id = g.id) as company_count
|
|
1063
|
-
FROM groups g ORDER BY g.name`).all();
|
|
1356
|
+
FROM groups g ${where} ORDER BY g.name`).all(...params);
|
|
1064
1357
|
}
|
|
1065
1358
|
function updateGroup(db, id, input) {
|
|
1066
1359
|
const fields = [];
|
|
@@ -1073,6 +1366,10 @@ function updateGroup(db, id, input) {
|
|
|
1073
1366
|
fields.push("description = ?");
|
|
1074
1367
|
vals.push(input.description ?? null);
|
|
1075
1368
|
}
|
|
1369
|
+
if (input.project_id !== undefined) {
|
|
1370
|
+
fields.push("project_id = ?");
|
|
1371
|
+
vals.push(input.project_id ?? null);
|
|
1372
|
+
}
|
|
1076
1373
|
fields.push("updated_at = ?");
|
|
1077
1374
|
vals.push(now());
|
|
1078
1375
|
vals.push(id);
|
|
@@ -3239,84 +3536,675 @@ function deleteDeal(id, db) {
|
|
|
3239
3536
|
d.run(`DELETE FROM deals WHERE id = ?`, [id]);
|
|
3240
3537
|
}
|
|
3241
3538
|
|
|
3242
|
-
// src/
|
|
3539
|
+
// src/mcp/index.ts
|
|
3540
|
+
init_events();
|
|
3541
|
+
|
|
3542
|
+
// src/db/field-history.ts
|
|
3243
3543
|
init_database();
|
|
3244
|
-
function
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
} catch {
|
|
3249
|
-
contact_ids = [];
|
|
3544
|
+
function getFieldHistory(contactId, fieldName, db) {
|
|
3545
|
+
const _db2 = db || getDatabase();
|
|
3546
|
+
if (fieldName) {
|
|
3547
|
+
return _db2.query(`SELECT * FROM contact_field_history WHERE contact_id=? AND field_name=? ORDER BY valid_from DESC`).all(contactId, fieldName);
|
|
3250
3548
|
}
|
|
3251
|
-
return
|
|
3252
|
-
id: row.id,
|
|
3253
|
-
title: row.title,
|
|
3254
|
-
type: row.type,
|
|
3255
|
-
event_date: row.event_date,
|
|
3256
|
-
duration_min: row.duration_min,
|
|
3257
|
-
contact_ids,
|
|
3258
|
-
company_id: row.company_id,
|
|
3259
|
-
notes: row.notes,
|
|
3260
|
-
outcome: row.outcome,
|
|
3261
|
-
deal_id: row.deal_id,
|
|
3262
|
-
created_at: row.created_at
|
|
3263
|
-
};
|
|
3549
|
+
return _db2.query(`SELECT * FROM contact_field_history WHERE contact_id=? ORDER BY valid_from DESC`).all(contactId);
|
|
3264
3550
|
}
|
|
3265
|
-
function
|
|
3266
|
-
const
|
|
3551
|
+
function getContactAt(contactId, timestamp, db) {
|
|
3552
|
+
const _db2 = db || getDatabase();
|
|
3553
|
+
const rows = _db2.query(`SELECT field_name, new_value FROM contact_field_history WHERE contact_id=? AND valid_from<=? ORDER BY valid_from ASC`).all(contactId, timestamp);
|
|
3554
|
+
const result = {};
|
|
3555
|
+
for (const r of rows) {
|
|
3556
|
+
if (r.new_value != null)
|
|
3557
|
+
result[r.field_name] = r.new_value;
|
|
3558
|
+
}
|
|
3559
|
+
return result;
|
|
3560
|
+
}
|
|
3561
|
+
|
|
3562
|
+
// src/db/job-history.ts
|
|
3563
|
+
init_database();
|
|
3564
|
+
function rowToJob(r) {
|
|
3565
|
+
return { ...r, is_current: !!r["is_current"], inferred: !!r["inferred"] };
|
|
3566
|
+
}
|
|
3567
|
+
function addJobEntry(contactId, input, db) {
|
|
3568
|
+
const _db2 = db || getDatabase();
|
|
3569
|
+
if (input.is_current) {
|
|
3570
|
+
_db2.query(`UPDATE job_history SET is_current=0, end_date=COALESCE(end_date,?) WHERE contact_id=? AND is_current=1`).run(new Date().toISOString().slice(0, 10), contactId);
|
|
3571
|
+
}
|
|
3267
3572
|
const id = uuid();
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
3271
|
-
id,
|
|
3272
|
-
input.title,
|
|
3273
|
-
input.type ?? "meeting",
|
|
3274
|
-
input.event_date,
|
|
3275
|
-
input.duration_min ?? null,
|
|
3276
|
-
JSON.stringify(input.contact_ids ?? []),
|
|
3277
|
-
input.company_id ?? null,
|
|
3278
|
-
input.notes ?? null,
|
|
3279
|
-
input.outcome ?? null,
|
|
3280
|
-
input.deal_id ?? null,
|
|
3281
|
-
timestamp
|
|
3282
|
-
]);
|
|
3283
|
-
return rowToEvent(d.query(`SELECT * FROM events WHERE id = ?`).get(id));
|
|
3573
|
+
_db2.query(`INSERT INTO job_history(id,contact_id,company_id,company_name,title,start_date,end_date,is_current,inferred,source,created_at) VALUES(?,?,?,?,?,?,?,?,?,?,?)`).run(id, contactId, input.company_id || null, input.company_name, input.title || null, input.start_date || null, input.end_date || null, input.is_current ? 1 : 0, input.inferred ? 1 : 0, input.source || null, now());
|
|
3574
|
+
return rowToJob(_db2.query(`SELECT * FROM job_history WHERE id=?`).get(id));
|
|
3284
3575
|
}
|
|
3285
|
-
function
|
|
3286
|
-
const
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3576
|
+
function getJobHistory(contactId, db) {
|
|
3577
|
+
const _db2 = db || getDatabase();
|
|
3578
|
+
return _db2.query(`SELECT * FROM job_history WHERE contact_id=? ORDER BY is_current DESC, start_date DESC`).all(contactId).map(rowToJob);
|
|
3579
|
+
}
|
|
3580
|
+
|
|
3581
|
+
// src/db/learnings.ts
|
|
3582
|
+
init_database();
|
|
3583
|
+
function rowToLearning(r) {
|
|
3584
|
+
return { ...r, tags: JSON.parse(r["tags"] || "[]") };
|
|
3585
|
+
}
|
|
3586
|
+
function saveLearning(contactId, input, db) {
|
|
3587
|
+
const _db2 = db || getDatabase();
|
|
3588
|
+
const id = uuid();
|
|
3589
|
+
_db2.query(`INSERT INTO contact_learnings(id,contact_id,content,type,confidence,importance,learned_by,session_id,visibility,tags,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)`).run(id, contactId, input.content, input.type || "fact", input.confidence ?? 70, input.importance ?? 5, input.learned_by || null, input.session_id || null, input.visibility || "shared", JSON.stringify(input.tags || []), now(), now());
|
|
3590
|
+
return rowToLearning(_db2.query(`SELECT * FROM contact_learnings WHERE id=?`).get(id));
|
|
3591
|
+
}
|
|
3592
|
+
function getLearnings(contactId, opts = {}, db) {
|
|
3593
|
+
const _db2 = db || getDatabase();
|
|
3594
|
+
let sql = `SELECT * FROM contact_learnings WHERE contact_id=?`;
|
|
3595
|
+
const params = [contactId];
|
|
3596
|
+
if (opts.type) {
|
|
3597
|
+
sql += ` AND type=?`;
|
|
3598
|
+
params.push(opts.type);
|
|
3292
3599
|
}
|
|
3293
|
-
if (opts.
|
|
3294
|
-
|
|
3295
|
-
params.push(opts.
|
|
3600
|
+
if (opts.min_importance) {
|
|
3601
|
+
sql += ` AND importance>=?`;
|
|
3602
|
+
params.push(opts.min_importance);
|
|
3296
3603
|
}
|
|
3604
|
+
if (opts.visibility) {
|
|
3605
|
+
sql += ` AND visibility=?`;
|
|
3606
|
+
params.push(opts.visibility);
|
|
3607
|
+
}
|
|
3608
|
+
sql += ` ORDER BY importance DESC, confidence DESC`;
|
|
3609
|
+
return _db2.query(sql).all(...params).map(rowToLearning);
|
|
3610
|
+
}
|
|
3611
|
+
function searchLearnings(query, opts = {}, db) {
|
|
3612
|
+
const _db2 = db || getDatabase();
|
|
3613
|
+
let sql = `SELECT * FROM contact_learnings WHERE content LIKE ?`;
|
|
3614
|
+
const params = [`%${query}%`];
|
|
3297
3615
|
if (opts.type) {
|
|
3298
|
-
|
|
3616
|
+
sql += ` AND type=?`;
|
|
3299
3617
|
params.push(opts.type);
|
|
3300
3618
|
}
|
|
3301
|
-
if (opts.
|
|
3302
|
-
|
|
3303
|
-
params.push(opts.
|
|
3304
|
-
}
|
|
3305
|
-
if (opts.date_to) {
|
|
3306
|
-
conditions.push("event_date <= ?");
|
|
3307
|
-
params.push(opts.date_to);
|
|
3619
|
+
if (opts.contact_id) {
|
|
3620
|
+
sql += ` AND contact_id=?`;
|
|
3621
|
+
params.push(opts.contact_id);
|
|
3308
3622
|
}
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
return rows.map(rowToEvent);
|
|
3623
|
+
sql += ` ORDER BY importance DESC, confidence DESC LIMIT 50`;
|
|
3624
|
+
return _db2.query(sql).all(...params).map(rowToLearning);
|
|
3312
3625
|
}
|
|
3313
|
-
function
|
|
3314
|
-
const
|
|
3315
|
-
|
|
3626
|
+
function confirmLearning(learningId, _agentName, db) {
|
|
3627
|
+
const _db2 = db || getDatabase();
|
|
3628
|
+
_db2.query(`UPDATE contact_learnings SET confirmed_count=confirmed_count+1, confidence=MIN(100,confidence+10), updated_at=? WHERE id=?`).run(now(), learningId);
|
|
3629
|
+
}
|
|
3630
|
+
function decayLearnings(db) {
|
|
3631
|
+
const _db2 = db || getDatabase();
|
|
3632
|
+
const cutoff = new Date(Date.now() - 30 * 86400000).toISOString();
|
|
3633
|
+
const result = _db2.query(`UPDATE contact_learnings SET confidence=MAX(10,confidence-5), updated_at=? WHERE confirmed_count=0 AND created_at<? AND confidence>10`).run(now(), cutoff);
|
|
3634
|
+
return result.changes || 0;
|
|
3316
3635
|
}
|
|
3317
3636
|
|
|
3318
|
-
// src/
|
|
3319
|
-
|
|
3637
|
+
// src/db/coordination.ts
|
|
3638
|
+
init_database();
|
|
3639
|
+
function acquireLock(contactId, agentName, ttlSeconds = 300, reason, sessionId, db) {
|
|
3640
|
+
const _db2 = db || getDatabase();
|
|
3641
|
+
cleanExpiredLocks(_db2);
|
|
3642
|
+
const existing = _db2.query(`SELECT * FROM contact_locks WHERE contact_id=?`).get(contactId);
|
|
3643
|
+
if (existing)
|
|
3644
|
+
return { acquired: false, held_by: existing.agent_name, lock: existing };
|
|
3645
|
+
const id = uuid();
|
|
3646
|
+
const expiresAt = new Date(Date.now() + ttlSeconds * 1000).toISOString();
|
|
3647
|
+
_db2.query(`INSERT INTO contact_locks(id,contact_id,agent_name,reason,acquired_at,expires_at,session_id) VALUES(?,?,?,?,?,?,?)`).run(id, contactId, agentName, reason || null, now(), expiresAt, sessionId || null);
|
|
3648
|
+
return {
|
|
3649
|
+
acquired: true,
|
|
3650
|
+
lock: _db2.query(`SELECT * FROM contact_locks WHERE id=?`).get(id)
|
|
3651
|
+
};
|
|
3652
|
+
}
|
|
3653
|
+
function releaseLock(contactId, agentName, db) {
|
|
3654
|
+
const _db2 = db || getDatabase();
|
|
3655
|
+
const result = _db2.query(`DELETE FROM contact_locks WHERE contact_id=? AND agent_name=?`).run(contactId, agentName);
|
|
3656
|
+
return (result.changes || 0) > 0;
|
|
3657
|
+
}
|
|
3658
|
+
function checkLock(contactId, db) {
|
|
3659
|
+
const _db2 = db || getDatabase();
|
|
3660
|
+
cleanExpiredLocks(_db2);
|
|
3661
|
+
return _db2.query(`SELECT * FROM contact_locks WHERE contact_id=?`).get(contactId);
|
|
3662
|
+
}
|
|
3663
|
+
function cleanExpiredLocks(db) {
|
|
3664
|
+
const _db2 = db || getDatabase();
|
|
3665
|
+
_db2.query(`DELETE FROM contact_locks WHERE expires_at<?`).run(now());
|
|
3666
|
+
}
|
|
3667
|
+
function logAgentActivity(contactId, agentName, action, details, sessionId, db) {
|
|
3668
|
+
const _db2 = db || getDatabase();
|
|
3669
|
+
_db2.query(`INSERT INTO contact_agent_activity(id,contact_id,agent_name,action,details,session_id,created_at) VALUES(?,?,?,?,?,?,?)`).run(uuid(), contactId, agentName, action, details || null, sessionId || null, now());
|
|
3670
|
+
}
|
|
3671
|
+
function getAgentActivity(contactId, limit = 20, db) {
|
|
3672
|
+
const _db2 = db || getDatabase();
|
|
3673
|
+
return _db2.query(`SELECT * FROM contact_agent_activity WHERE contact_id=? ORDER BY created_at DESC LIMIT ?`).all(contactId, limit);
|
|
3674
|
+
}
|
|
3675
|
+
|
|
3676
|
+
// src/db/graph.ts
|
|
3677
|
+
init_database();
|
|
3678
|
+
function computeRelationshipStrength(contactId, db) {
|
|
3679
|
+
const _db2 = db || getDatabase();
|
|
3680
|
+
const contact = _db2.query(`SELECT last_contacted_at, interaction_count_30d, interaction_count_90d FROM contacts WHERE id=?`).get(contactId);
|
|
3681
|
+
if (!contact)
|
|
3682
|
+
return 0;
|
|
3683
|
+
let score = 50;
|
|
3684
|
+
if (contact.last_contacted_at) {
|
|
3685
|
+
const days = Math.floor((Date.now() - new Date(contact.last_contacted_at).getTime()) / 86400000);
|
|
3686
|
+
score += days < 7 ? 30 : days < 30 ? 20 : days < 90 ? 5 : -20;
|
|
3687
|
+
} else {
|
|
3688
|
+
score -= 20;
|
|
3689
|
+
}
|
|
3690
|
+
score += Math.min(20, (contact.interaction_count_30d || 0) * 4);
|
|
3691
|
+
return Math.max(0, Math.min(100, score));
|
|
3692
|
+
}
|
|
3693
|
+
function findWarmPath(fromContactId, toContactId, db) {
|
|
3694
|
+
const _db2 = db || getDatabase();
|
|
3695
|
+
const visited = new Set([fromContactId]);
|
|
3696
|
+
const queue = [{ id: fromContactId, path: [] }];
|
|
3697
|
+
while (queue.length) {
|
|
3698
|
+
const item = queue.shift();
|
|
3699
|
+
const { id, path } = item;
|
|
3700
|
+
if (id === toContactId)
|
|
3701
|
+
return path;
|
|
3702
|
+
if (path.length >= 4)
|
|
3703
|
+
continue;
|
|
3704
|
+
const neighbors = _db2.query(`SELECT cr.*, c.display_name FROM contact_relationships cr JOIN contacts c ON (CASE WHEN cr.contact_a_id=? THEN cr.contact_b_id ELSE cr.contact_a_id END)=c.id WHERE (cr.contact_a_id=? OR cr.contact_b_id=?) LIMIT 20`).all(id, id, id);
|
|
3705
|
+
for (const n of neighbors) {
|
|
3706
|
+
const nextId = n.contact_a_id === id ? n.contact_b_id : n.contact_a_id;
|
|
3707
|
+
if (visited.has(nextId))
|
|
3708
|
+
continue;
|
|
3709
|
+
visited.add(nextId);
|
|
3710
|
+
queue.push({
|
|
3711
|
+
id: nextId,
|
|
3712
|
+
path: [
|
|
3713
|
+
...path,
|
|
3714
|
+
{ contact_id: nextId, display_name: n.display_name, strength: n.strength_score || 50 }
|
|
3715
|
+
]
|
|
3716
|
+
});
|
|
3717
|
+
}
|
|
3718
|
+
}
|
|
3719
|
+
return [];
|
|
3720
|
+
}
|
|
3721
|
+
function findConnectionsAtCompany(companyId, db) {
|
|
3722
|
+
const _db2 = db || getDatabase();
|
|
3723
|
+
return _db2.query(`SELECT c.id as contact_id, c.display_name, c.job_title, c.relationship_health as strength FROM contacts c WHERE c.company_id=? AND c.archived=0 ORDER BY c.relationship_health DESC`).all(companyId);
|
|
3724
|
+
}
|
|
3725
|
+
function detectCoolingRelationships(db) {
|
|
3726
|
+
const _db2 = db || getDatabase();
|
|
3727
|
+
const cutoff = new Date(Date.now() - 45 * 86400000).toISOString();
|
|
3728
|
+
return _db2.query(`SELECT id as contact_id, display_name, CAST((julianday('now') - julianday(last_contacted_at)) AS INTEGER) as days_since FROM contacts WHERE last_contacted_at IS NOT NULL AND last_contacted_at < ? AND engagement_status != 'ghost' AND archived=0 ORDER BY last_contacted_at ASC LIMIT 50`).all(cutoff);
|
|
3729
|
+
}
|
|
3730
|
+
|
|
3731
|
+
// src/db/identity.ts
|
|
3732
|
+
init_database();
|
|
3733
|
+
function addIdentity(contactId, system, externalId, externalUrl, confidence = "inferred", db) {
|
|
3734
|
+
const _db2 = db || getDatabase();
|
|
3735
|
+
const id = uuid();
|
|
3736
|
+
_db2.query(`INSERT OR REPLACE INTO contact_identities(id,contact_id,system,external_id,external_url,confidence,created_at) VALUES(?,?,?,?,?,?,?)`).run(id, contactId, system, externalId, externalUrl || null, confidence, now());
|
|
3737
|
+
return _db2.query(`SELECT * FROM contact_identities WHERE id=?`).get(id);
|
|
3738
|
+
}
|
|
3739
|
+
function resolveByPartial(partial, db) {
|
|
3740
|
+
const _db2 = db || getDatabase();
|
|
3741
|
+
const matches = new Map;
|
|
3742
|
+
const addMatch = (id, name, title, score, reason) => {
|
|
3743
|
+
const existing = matches.get(id);
|
|
3744
|
+
if (existing) {
|
|
3745
|
+
existing.confidence_score = Math.min(100, existing.confidence_score + score);
|
|
3746
|
+
existing.match_reasons.push(reason);
|
|
3747
|
+
} else {
|
|
3748
|
+
matches.set(id, {
|
|
3749
|
+
contact: { id, display_name: name, job_title: title },
|
|
3750
|
+
confidence_score: score,
|
|
3751
|
+
match_reasons: [reason]
|
|
3752
|
+
});
|
|
3753
|
+
}
|
|
3754
|
+
};
|
|
3755
|
+
if (partial.email) {
|
|
3756
|
+
const rows = _db2.query(`SELECT c.id, c.display_name, c.job_title FROM contacts c JOIN emails e ON c.id=e.contact_id WHERE LOWER(e.address)=LOWER(?)`).all(partial.email);
|
|
3757
|
+
rows.forEach((r) => addMatch(r.id, r.display_name, r.job_title, 90, `email match: ${partial.email}`));
|
|
3758
|
+
}
|
|
3759
|
+
if (partial.linkedin_url) {
|
|
3760
|
+
const rows = _db2.query(`SELECT c.id, c.display_name, c.job_title FROM contacts c JOIN social_profiles sp ON c.id=sp.contact_id WHERE sp.platform='linkedin' AND sp.url LIKE ?`).all(`%${partial.linkedin_url.split("/").pop()}%`);
|
|
3761
|
+
rows.forEach((r) => addMatch(r.id, r.display_name, r.job_title, 85, `linkedin match`));
|
|
3762
|
+
}
|
|
3763
|
+
if (partial.name) {
|
|
3764
|
+
const rows = _db2.query(`SELECT id, display_name, job_title FROM contacts WHERE display_name LIKE ? AND archived=0 LIMIT 10`).all(`%${partial.name}%`);
|
|
3765
|
+
rows.forEach((r) => addMatch(r.id, r.display_name, r.job_title, 40, `name match: ${partial.name}`));
|
|
3766
|
+
}
|
|
3767
|
+
return Array.from(matches.values()).sort((a, b) => b.confidence_score - a.confidence_score);
|
|
3768
|
+
}
|
|
3769
|
+
function getIdentities(contactId, db) {
|
|
3770
|
+
const _db2 = db || getDatabase();
|
|
3771
|
+
return _db2.query(`SELECT * FROM contact_identities WHERE contact_id=? ORDER BY created_at DESC`).all(contactId);
|
|
3772
|
+
}
|
|
3773
|
+
|
|
3774
|
+
// src/lib/embeddings.ts
|
|
3775
|
+
init_database();
|
|
3776
|
+
function tokenize(text) {
|
|
3777
|
+
return text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t) => t.length > 2);
|
|
3778
|
+
}
|
|
3779
|
+
function buildTfIdf(tokens) {
|
|
3780
|
+
const freq = new Map;
|
|
3781
|
+
for (const t of tokens)
|
|
3782
|
+
freq.set(t, (freq.get(t) || 0) + 1);
|
|
3783
|
+
const max = Math.max(...freq.values(), 1);
|
|
3784
|
+
const result = new Map;
|
|
3785
|
+
freq.forEach((v, k) => result.set(k, v / max));
|
|
3786
|
+
return result;
|
|
3787
|
+
}
|
|
3788
|
+
function cosineSimilarity(a, b) {
|
|
3789
|
+
let dot = 0, normA = 0, normB = 0;
|
|
3790
|
+
a.forEach((v, k) => {
|
|
3791
|
+
if (b.has(k))
|
|
3792
|
+
dot += v * b.get(k);
|
|
3793
|
+
normA += v * v;
|
|
3794
|
+
});
|
|
3795
|
+
b.forEach((v) => normB += v * v);
|
|
3796
|
+
return normA && normB ? dot / (Math.sqrt(normA) * Math.sqrt(normB)) : 0;
|
|
3797
|
+
}
|
|
3798
|
+
function buildContactEmbeddingText(contact) {
|
|
3799
|
+
const tags = contact.tags ?? [];
|
|
3800
|
+
const socialProfiles = contact.social_profiles ?? [];
|
|
3801
|
+
const company = contact.company;
|
|
3802
|
+
const parts = [
|
|
3803
|
+
contact.display_name,
|
|
3804
|
+
contact.job_title,
|
|
3805
|
+
contact.notes,
|
|
3806
|
+
company?.name,
|
|
3807
|
+
company?.industry,
|
|
3808
|
+
...tags.map((t) => t.name),
|
|
3809
|
+
...socialProfiles.map((s) => s.platform)
|
|
3810
|
+
].filter(Boolean);
|
|
3811
|
+
return parts.join(" ");
|
|
3812
|
+
}
|
|
3813
|
+
async function embedContact(contactId, db) {
|
|
3814
|
+
const { getContact: getContact2 } = await Promise.resolve().then(() => (init_contacts(), exports_contacts));
|
|
3815
|
+
const _db2 = db || getDatabase();
|
|
3816
|
+
const contact = getContact2(contactId, _db2);
|
|
3817
|
+
const text = buildContactEmbeddingText(contact);
|
|
3818
|
+
const tokens = tokenize(text);
|
|
3819
|
+
const tfidf = buildTfIdf(tokens);
|
|
3820
|
+
const embedding = JSON.stringify(Array.from(tfidf.entries()).sort((a, b) => b[1] - a[1]).slice(0, 100));
|
|
3821
|
+
_db2.query(`INSERT OR REPLACE INTO contact_embeddings(contact_id, embedding, model, embedded_text, created_at, updated_at) VALUES(?,?,'tfidf',?,?,?)`).run(contactId, embedding, text.slice(0, 500), now(), now());
|
|
3822
|
+
}
|
|
3823
|
+
async function embedAllContacts(db) {
|
|
3824
|
+
const _db2 = db || getDatabase();
|
|
3825
|
+
const contacts = _db2.query(`SELECT id FROM contacts WHERE archived=0`).all();
|
|
3826
|
+
for (const c of contacts) {
|
|
3827
|
+
try {
|
|
3828
|
+
await embedContact(c.id, _db2);
|
|
3829
|
+
} catch {}
|
|
3830
|
+
}
|
|
3831
|
+
return contacts.length;
|
|
3832
|
+
}
|
|
3833
|
+
function semanticSearch(query, limit = 10, db) {
|
|
3834
|
+
const _db2 = db || getDatabase();
|
|
3835
|
+
const queryTokens = buildTfIdf(tokenize(query));
|
|
3836
|
+
let embeddings = [];
|
|
3837
|
+
try {
|
|
3838
|
+
embeddings = _db2.query(`SELECT contact_id, embedding FROM contact_embeddings`).all();
|
|
3839
|
+
} catch {
|
|
3840
|
+
return [];
|
|
3841
|
+
}
|
|
3842
|
+
const results = embeddings.map((e) => {
|
|
3843
|
+
try {
|
|
3844
|
+
const emb = new Map(JSON.parse(e.embedding));
|
|
3845
|
+
return { contact_id: e.contact_id, score: cosineSimilarity(queryTokens, emb) };
|
|
3846
|
+
} catch {
|
|
3847
|
+
return { contact_id: e.contact_id, score: 0 };
|
|
3848
|
+
}
|
|
3849
|
+
}).filter((r) => r.score > 0).sort((a, b) => b.score - a.score).slice(0, limit);
|
|
3850
|
+
return results;
|
|
3851
|
+
}
|
|
3852
|
+
|
|
3853
|
+
// src/db/signals.ts
|
|
3854
|
+
init_database();
|
|
3855
|
+
function getRelationshipSignals(contactId, db) {
|
|
3856
|
+
const _db2 = db || getDatabase();
|
|
3857
|
+
const row = _db2.query(`SELECT id as contact_id, display_name, last_contacted_at, interaction_count_30d, engagement_status, relationship_health FROM contacts WHERE id=?`).get(contactId);
|
|
3858
|
+
if (!row)
|
|
3859
|
+
return [];
|
|
3860
|
+
const daysSince = row.last_contacted_at ? Math.floor((Date.now() - new Date(row.last_contacted_at).getTime()) / 86400000) : null;
|
|
3861
|
+
const signals = [];
|
|
3862
|
+
const cnt = row.interaction_count_30d || 0;
|
|
3863
|
+
const health = row.relationship_health ?? 50;
|
|
3864
|
+
if (daysSince === null || daysSince > 180) {
|
|
3865
|
+
signals.push({ ...row, signal_type: "ghost", days_since_contact: daysSince, reason: "No contact in 180+ days or never contacted" });
|
|
3866
|
+
} else if (daysSince > 60 && cnt === 0) {
|
|
3867
|
+
signals.push({ ...row, signal_type: "cooling", days_since_contact: daysSince, reason: `No contact in ${daysSince} days, no recent interactions` });
|
|
3868
|
+
} else if (cnt > 3 && health > 70) {
|
|
3869
|
+
signals.push({ ...row, signal_type: "warming", days_since_contact: daysSince, reason: `${cnt} interactions in last 30 days, health score ${health}` });
|
|
3870
|
+
} else {
|
|
3871
|
+
signals.push({ ...row, signal_type: "healthy", days_since_contact: daysSince, reason: `Last contact ${daysSince}d ago, ${cnt} interactions in 30d` });
|
|
3872
|
+
}
|
|
3873
|
+
return signals;
|
|
3874
|
+
}
|
|
3875
|
+
function getGhostContacts(db) {
|
|
3876
|
+
const _db2 = db || getDatabase();
|
|
3877
|
+
const rows = _db2.query(`SELECT id as contact_id, display_name, last_contacted_at, interaction_count_30d, engagement_status, relationship_health FROM contacts WHERE (last_contacted_at IS NULL OR julianday('now') - julianday(last_contacted_at) > 180) AND archived=0 ORDER BY last_contacted_at ASC LIMIT 50`).all();
|
|
3878
|
+
return rows.map((r) => ({
|
|
3879
|
+
...r,
|
|
3880
|
+
signal_type: "ghost",
|
|
3881
|
+
days_since_contact: r.last_contacted_at ? Math.floor((Date.now() - new Date(r.last_contacted_at).getTime()) / 86400000) : null,
|
|
3882
|
+
reason: "No contact in 180+ days or never contacted"
|
|
3883
|
+
}));
|
|
3884
|
+
}
|
|
3885
|
+
function getWarmingContacts(db) {
|
|
3886
|
+
const _db2 = db || getDatabase();
|
|
3887
|
+
const rows = _db2.query(`SELECT id as contact_id, display_name, last_contacted_at, interaction_count_30d, engagement_status, relationship_health FROM contacts WHERE interaction_count_30d > 2 AND relationship_health > 60 AND archived=0 ORDER BY relationship_health DESC LIMIT 50`).all();
|
|
3888
|
+
return rows.map((r) => ({
|
|
3889
|
+
...r,
|
|
3890
|
+
signal_type: "warming",
|
|
3891
|
+
days_since_contact: r.last_contacted_at ? Math.floor((Date.now() - new Date(r.last_contacted_at).getTime()) / 86400000) : null,
|
|
3892
|
+
reason: `${r.interaction_count_30d} interactions in last 30 days`
|
|
3893
|
+
}));
|
|
3894
|
+
}
|
|
3895
|
+
function recomputeAllSignals(db) {
|
|
3896
|
+
const _db2 = db || getDatabase();
|
|
3897
|
+
_db2.query(`
|
|
3898
|
+
UPDATE contacts SET
|
|
3899
|
+
engagement_status = CASE
|
|
3900
|
+
WHEN interaction_count_30d > 3 THEN 'warm'
|
|
3901
|
+
WHEN last_contacted_at IS NULL OR julianday('now') - julianday(last_contacted_at) > 180 THEN 'ghost'
|
|
3902
|
+
WHEN julianday('now') - julianday(last_contacted_at) > 60 THEN 'cooling'
|
|
3903
|
+
ELSE 'active'
|
|
3904
|
+
END,
|
|
3905
|
+
updated_at = datetime('now')
|
|
3906
|
+
WHERE archived = 0
|
|
3907
|
+
`).run();
|
|
3908
|
+
const result = _db2.query(`SELECT changes() as n`).get();
|
|
3909
|
+
return { updated: result?.n ?? 0 };
|
|
3910
|
+
}
|
|
3911
|
+
|
|
3912
|
+
// src/lib/context.ts
|
|
3913
|
+
init_database();
|
|
3914
|
+
init_contacts();
|
|
3915
|
+
function getContactCard(contactId, db) {
|
|
3916
|
+
const _db2 = db || getDatabase();
|
|
3917
|
+
const c = getContact(contactId, _db2);
|
|
3918
|
+
const emails = c.emails;
|
|
3919
|
+
const phones = c.phones;
|
|
3920
|
+
const company = c.company;
|
|
3921
|
+
return {
|
|
3922
|
+
id: c.id,
|
|
3923
|
+
display_name: c.display_name,
|
|
3924
|
+
job_title: c.job_title,
|
|
3925
|
+
company: company?.name,
|
|
3926
|
+
primary_email: emails?.find((e) => e.is_primary)?.address || emails?.[0]?.address,
|
|
3927
|
+
primary_phone: phones?.find((p) => p.is_primary)?.number || phones?.[0]?.number
|
|
3928
|
+
};
|
|
3929
|
+
}
|
|
3930
|
+
function getContactBrief(contactId, taskContext, db) {
|
|
3931
|
+
const _db2 = db || getDatabase();
|
|
3932
|
+
const c = getContact(contactId, _db2);
|
|
3933
|
+
const notes = listNotes(contactId, _db2).slice(0, 3);
|
|
3934
|
+
const learnings = getLearnings(contactId, { min_importance: 7 }, _db2).slice(0, 5);
|
|
3935
|
+
const ctx = (taskContext ?? "").toLowerCase();
|
|
3936
|
+
const lastContactedAt = c.last_contacted_at;
|
|
3937
|
+
const daysSince = lastContactedAt ? Math.floor((Date.now() - new Date(lastContactedAt).getTime()) / 86400000) : null;
|
|
3938
|
+
const company = c.company;
|
|
3939
|
+
const brief = {
|
|
3940
|
+
id: c.id,
|
|
3941
|
+
display_name: c.display_name,
|
|
3942
|
+
job_title: c.job_title,
|
|
3943
|
+
company: company?.name,
|
|
3944
|
+
status: c.status,
|
|
3945
|
+
last_contacted: daysSince !== null ? `${daysSince}d ago` : "never",
|
|
3946
|
+
relationship_health: c.relationship_health,
|
|
3947
|
+
engagement_status: c.engagement_status,
|
|
3948
|
+
preferred_contact: c.preferred_contact_method || c.preferred_channel
|
|
3949
|
+
};
|
|
3950
|
+
if (ctx.includes("meeting") || ctx.includes("call") || ctx.includes("prep")) {
|
|
3951
|
+
brief.recent_notes = notes.map((n) => ({ date: n.created_at?.slice(0, 10), content: n.body }));
|
|
3952
|
+
brief.key_learnings = learnings.map((l) => l.content);
|
|
3953
|
+
}
|
|
3954
|
+
if (ctx.includes("outreach") || ctx.includes("email")) {
|
|
3955
|
+
brief.preferred_channel = c.preferred_channel;
|
|
3956
|
+
brief.follow_up_at = c.follow_up_at;
|
|
3957
|
+
}
|
|
3958
|
+
if (ctx.includes("deal")) {
|
|
3959
|
+
const dealCompany = c.company;
|
|
3960
|
+
brief.company_details = dealCompany ? { name: dealCompany.name, domain: dealCompany.domain } : null;
|
|
3961
|
+
}
|
|
3962
|
+
if (learnings.length)
|
|
3963
|
+
brief.top_learnings = learnings.map((l) => l.content);
|
|
3964
|
+
return brief;
|
|
3965
|
+
}
|
|
3966
|
+
async function assembleContext(contactIds, format = "meeting_prep", db) {
|
|
3967
|
+
const _db2 = db || getDatabase();
|
|
3968
|
+
const briefs = contactIds.map((id) => {
|
|
3969
|
+
try {
|
|
3970
|
+
return getContactBrief(id, format, _db2);
|
|
3971
|
+
} catch {
|
|
3972
|
+
return { id, error: "not found" };
|
|
3973
|
+
}
|
|
3974
|
+
});
|
|
3975
|
+
return { format, contact_count: contactIds.length, assembled_at: new Date().toISOString(), contacts: briefs };
|
|
3976
|
+
}
|
|
3977
|
+
|
|
3978
|
+
// src/lib/signature-parser.ts
|
|
3979
|
+
function parseEmailSignature(text) {
|
|
3980
|
+
const result = {};
|
|
3981
|
+
const phoneMatch = text.match(/(\+?[\d\s\-\(\)]{7,20})/);
|
|
3982
|
+
if (phoneMatch)
|
|
3983
|
+
result.phone = phoneMatch[1]?.trim();
|
|
3984
|
+
const emailMatch = text.match(/[\w.+-]+@[\w-]+\.[a-z]{2,}/i);
|
|
3985
|
+
if (emailMatch)
|
|
3986
|
+
result.email = emailMatch[0];
|
|
3987
|
+
const linkedinMatch = text.match(/(?:linkedin\.com\/in\/)([\w-]+)/i);
|
|
3988
|
+
if (linkedinMatch)
|
|
3989
|
+
result.linkedin = `https://linkedin.com/in/${linkedinMatch[1]}`;
|
|
3990
|
+
const websiteMatch = text.match(/https?:\/\/(?!linkedin)(?!twitter)[\w.-]+\.[a-z]{2,}/i);
|
|
3991
|
+
if (websiteMatch)
|
|
3992
|
+
result.website = websiteMatch[0];
|
|
3993
|
+
const lines = text.split(`
|
|
3994
|
+
`).map((l) => l.trim()).filter((l) => l.length > 2 && l.length < 80);
|
|
3995
|
+
if (lines[0])
|
|
3996
|
+
result.name = lines[0];
|
|
3997
|
+
for (const line of lines.slice(1)) {
|
|
3998
|
+
if (line.match(/\b(CEO|CTO|VP|Director|Manager|Engineer|Partner|Associate|Consultant|Analyst|President|Founder)\b/i)) {
|
|
3999
|
+
result.title = line;
|
|
4000
|
+
} else if (!result.company && line.match(/^[A-Z][A-Za-z\s,\.]+$/) && !line.includes("@")) {
|
|
4001
|
+
result.company = line;
|
|
4002
|
+
}
|
|
4003
|
+
}
|
|
4004
|
+
return result;
|
|
4005
|
+
}
|
|
4006
|
+
function extractContactsFromEmailThread(participants) {
|
|
4007
|
+
return participants.map((p) => {
|
|
4008
|
+
const sig = p.signature ? parseEmailSignature(p.signature) : {};
|
|
4009
|
+
const name = p.name || sig.name || p.email.split("@")[0] || "Unknown";
|
|
4010
|
+
const contact = {
|
|
4011
|
+
display_name: name,
|
|
4012
|
+
emails: [{ address: p.email, type: "work", is_primary: true }],
|
|
4013
|
+
source: "import"
|
|
4014
|
+
};
|
|
4015
|
+
if (sig.title)
|
|
4016
|
+
contact.job_title = sig.title;
|
|
4017
|
+
if (sig.phone)
|
|
4018
|
+
contact.phones = [{ number: sig.phone, type: "work", is_primary: true }];
|
|
4019
|
+
if (sig.linkedin)
|
|
4020
|
+
contact.social_profiles = [{ platform: "linkedin", url: sig.linkedin, is_primary: true }];
|
|
4021
|
+
if (sig.website)
|
|
4022
|
+
contact.website = sig.website;
|
|
4023
|
+
return contact;
|
|
4024
|
+
});
|
|
4025
|
+
}
|
|
4026
|
+
|
|
4027
|
+
// src/lib/meeting-capture.ts
|
|
4028
|
+
init_database();
|
|
4029
|
+
async function ingestMeetingParticipants(event, db) {
|
|
4030
|
+
const { findOrCreateContact: findOrCreateContact2 } = await Promise.resolve().then(() => (init_contacts(), exports_contacts));
|
|
4031
|
+
const { logEvent: logEvent2 } = await Promise.resolve().then(() => (init_events(), exports_events));
|
|
4032
|
+
const _db2 = db || getDatabase();
|
|
4033
|
+
let created = 0;
|
|
4034
|
+
let updated = 0;
|
|
4035
|
+
const ids = [];
|
|
4036
|
+
for (const a of event.attendees) {
|
|
4037
|
+
try {
|
|
4038
|
+
const nameParts = a.name.split(" ");
|
|
4039
|
+
const result = await findOrCreateContact2({
|
|
4040
|
+
display_name: a.name,
|
|
4041
|
+
first_name: nameParts[0],
|
|
4042
|
+
last_name: nameParts.slice(1).join(" ") || undefined,
|
|
4043
|
+
emails: [{ address: a.email, type: "work", is_primary: true }],
|
|
4044
|
+
source: "import"
|
|
4045
|
+
}, _db2);
|
|
4046
|
+
ids.push(result.contact.id);
|
|
4047
|
+
if (result.created)
|
|
4048
|
+
created++;
|
|
4049
|
+
else
|
|
4050
|
+
updated++;
|
|
4051
|
+
} catch {}
|
|
4052
|
+
}
|
|
4053
|
+
if (ids.length) {
|
|
4054
|
+
try {
|
|
4055
|
+
logEvent2({
|
|
4056
|
+
title: event.title,
|
|
4057
|
+
type: "meeting",
|
|
4058
|
+
event_date: event.event_date,
|
|
4059
|
+
contact_ids: ids,
|
|
4060
|
+
notes: event.context
|
|
4061
|
+
}, _db2);
|
|
4062
|
+
} catch {}
|
|
4063
|
+
}
|
|
4064
|
+
return { created, updated, contact_ids: ids };
|
|
4065
|
+
}
|
|
4066
|
+
|
|
4067
|
+
// src/db/freshness.ts
|
|
4068
|
+
init_database();
|
|
4069
|
+
var SCORED_FIELDS = ["display_name", "job_title", "company_id", "emails", "phones", "last_contacted_at"];
|
|
4070
|
+
function getFreshnessScore(contactId, db) {
|
|
4071
|
+
const _db2 = db || getDatabase();
|
|
4072
|
+
const contact = _db2.query(`SELECT * FROM contacts WHERE id=?`).get(contactId);
|
|
4073
|
+
if (!contact)
|
|
4074
|
+
throw new Error(`Contact not found: ${contactId}`);
|
|
4075
|
+
let historyRows = [];
|
|
4076
|
+
try {
|
|
4077
|
+
historyRows = _db2.query(`SELECT field_name, new_value, source, created_at FROM contact_field_history WHERE contact_id=? ORDER BY created_at DESC`).all(contactId);
|
|
4078
|
+
} catch {}
|
|
4079
|
+
let verifiedRows = [];
|
|
4080
|
+
try {
|
|
4081
|
+
verifiedRows = _db2.query(`SELECT field_name, verified_at, source FROM field_verifications WHERE contact_id=?`).all(contactId);
|
|
4082
|
+
} catch {}
|
|
4083
|
+
const verifiedMap = new Map(verifiedRows.map((r) => [r.field_name, r]));
|
|
4084
|
+
const historyMap = new Map;
|
|
4085
|
+
for (const r of historyRows) {
|
|
4086
|
+
if (!historyMap.has(r.field_name))
|
|
4087
|
+
historyMap.set(r.field_name, r);
|
|
4088
|
+
}
|
|
4089
|
+
const fields = SCORED_FIELDS.map((field) => {
|
|
4090
|
+
let value = null;
|
|
4091
|
+
if (field === "emails") {
|
|
4092
|
+
const emailRow = _db2.query(`SELECT address FROM emails WHERE contact_id=? LIMIT 1`).get(contactId);
|
|
4093
|
+
value = emailRow?.address ?? null;
|
|
4094
|
+
} else if (field === "phones") {
|
|
4095
|
+
const phoneRow = _db2.query(`SELECT number FROM phones WHERE contact_id=? LIMIT 1`).get(contactId);
|
|
4096
|
+
value = phoneRow?.number ?? null;
|
|
4097
|
+
} else {
|
|
4098
|
+
value = contact[field] != null ? String(contact[field]) : null;
|
|
4099
|
+
}
|
|
4100
|
+
const verified = verifiedMap.get(field);
|
|
4101
|
+
const history = historyMap.get(field);
|
|
4102
|
+
let confidence = "unknown";
|
|
4103
|
+
let days_old = null;
|
|
4104
|
+
let last_verified_at = null;
|
|
4105
|
+
let source = null;
|
|
4106
|
+
if (verified) {
|
|
4107
|
+
confidence = "verified";
|
|
4108
|
+
last_verified_at = verified.verified_at;
|
|
4109
|
+
source = verified.source;
|
|
4110
|
+
days_old = Math.floor((Date.now() - new Date(verified.verified_at).getTime()) / 86400000);
|
|
4111
|
+
} else if (history) {
|
|
4112
|
+
confidence = history.source === "import" ? "imported" : "inferred";
|
|
4113
|
+
last_verified_at = history.created_at;
|
|
4114
|
+
source = history.source;
|
|
4115
|
+
days_old = Math.floor((Date.now() - new Date(history.created_at).getTime()) / 86400000);
|
|
4116
|
+
if (days_old > 365)
|
|
4117
|
+
confidence = "stale";
|
|
4118
|
+
} else if (value) {
|
|
4119
|
+
confidence = "inferred";
|
|
4120
|
+
}
|
|
4121
|
+
return { field_name: field, value, last_verified_at, source, confidence, days_old };
|
|
4122
|
+
});
|
|
4123
|
+
const fieldScore = fields.reduce((acc, f) => {
|
|
4124
|
+
if (!f.value)
|
|
4125
|
+
return acc;
|
|
4126
|
+
if (f.confidence === "verified")
|
|
4127
|
+
return acc + 20;
|
|
4128
|
+
if (f.confidence === "imported" || f.confidence === "inferred")
|
|
4129
|
+
return acc + 10;
|
|
4130
|
+
return acc + 5;
|
|
4131
|
+
}, 0);
|
|
4132
|
+
const overall_score = Math.min(100, fieldScore);
|
|
4133
|
+
return {
|
|
4134
|
+
contact_id: contactId,
|
|
4135
|
+
overall_score,
|
|
4136
|
+
fields,
|
|
4137
|
+
stale_fields: fields.filter((f) => f.confidence === "stale" || !f.value && f.field_name !== "phones").map((f) => f.field_name),
|
|
4138
|
+
verified_fields: fields.filter((f) => f.confidence === "verified").map((f) => f.field_name)
|
|
4139
|
+
};
|
|
4140
|
+
}
|
|
4141
|
+
function getStaleContacts(threshold = 40, db) {
|
|
4142
|
+
const _db2 = db || getDatabase();
|
|
4143
|
+
const rows = _db2.query(`SELECT c.id as contact_id, c.display_name,
|
|
4144
|
+
(CASE WHEN c.job_title IS NOT NULL THEN 15 ELSE 0 END +
|
|
4145
|
+
CASE WHEN c.company_id IS NOT NULL THEN 15 ELSE 0 END +
|
|
4146
|
+
CASE WHEN c.last_contacted_at IS NOT NULL THEN 20 ELSE 0 END +
|
|
4147
|
+
CASE WHEN EXISTS(SELECT 1 FROM emails WHERE contact_id=c.id) THEN 20 ELSE 0 END +
|
|
4148
|
+
CASE WHEN EXISTS(SELECT 1 FROM phones WHERE contact_id=c.id) THEN 15 ELSE 0 END +
|
|
4149
|
+
CASE WHEN c.notes IS NOT NULL THEN 10 ELSE 0 END +
|
|
4150
|
+
CASE WHEN EXISTS(SELECT 1 FROM contact_tags WHERE contact_id=c.id) THEN 5 ELSE 0 END
|
|
4151
|
+
) as score
|
|
4152
|
+
FROM contacts c WHERE c.archived=0 HAVING score < ? ORDER BY score ASC LIMIT 100`).all(threshold);
|
|
4153
|
+
return rows;
|
|
4154
|
+
}
|
|
4155
|
+
function markFieldVerified(contactId, fieldName, source, db) {
|
|
4156
|
+
const _db2 = db || getDatabase();
|
|
4157
|
+
try {
|
|
4158
|
+
_db2.query(`INSERT OR REPLACE INTO field_verifications(contact_id,field_name,verified_at,source) VALUES(?,?,?,?)`).run(contactId, fieldName, now(), source || null);
|
|
4159
|
+
} catch {
|
|
4160
|
+
_db2.query(`INSERT INTO activity_log(id,contact_id,action,details,created_at) VALUES(?,?,?,?,?)`).run(crypto.randomUUID(), contactId, "field.verified", JSON.stringify({ field_name: fieldName, source }), now());
|
|
4161
|
+
}
|
|
4162
|
+
}
|
|
4163
|
+
|
|
4164
|
+
// src/db/org-chart.ts
|
|
4165
|
+
init_database();
|
|
4166
|
+
function addOrgChartEdge(companyId, contactAId, contactBId, edgeType, inferred = false, db) {
|
|
4167
|
+
const _db2 = db || getDatabase();
|
|
4168
|
+
const id = uuid();
|
|
4169
|
+
_db2.query(`INSERT OR IGNORE INTO org_chart_edges(id,company_id,contact_a_id,contact_b_id,edge_type,inferred,created_at) VALUES(?,?,?,?,?,?,?)`).run(id, companyId, contactAId, contactBId, edgeType, inferred ? 1 : 0, now());
|
|
4170
|
+
return _db2.query(`SELECT * FROM org_chart_edges WHERE company_id=? AND contact_a_id=? AND contact_b_id=? AND edge_type=?`).get(companyId, contactAId, contactBId, edgeType);
|
|
4171
|
+
}
|
|
4172
|
+
function listOrgChart(companyId, db) {
|
|
4173
|
+
const _db2 = db || getDatabase();
|
|
4174
|
+
return _db2.query(`SELECT oe.*, ca.display_name as contact_a_name, cb.display_name as contact_b_name FROM org_chart_edges oe JOIN contacts ca ON oe.contact_a_id=ca.id JOIN contacts cb ON oe.contact_b_id=cb.id WHERE oe.company_id=?`).all(companyId);
|
|
4175
|
+
}
|
|
4176
|
+
function setDealContactRole(dealId, contactId, accountRole, db) {
|
|
4177
|
+
const _db2 = db || getDatabase();
|
|
4178
|
+
const id = uuid();
|
|
4179
|
+
_db2.query(`INSERT OR REPLACE INTO deal_contact_roles(id,deal_id,contact_id,account_role,created_at) VALUES(?,?,?,?,?)`).run(id, dealId, contactId, accountRole, now());
|
|
4180
|
+
return _db2.query(`SELECT * FROM deal_contact_roles WHERE deal_id=? AND contact_id=?`).get(dealId, contactId);
|
|
4181
|
+
}
|
|
4182
|
+
function getDealTeam(dealId, db) {
|
|
4183
|
+
const _db2 = db || getDatabase();
|
|
4184
|
+
return _db2.query(`SELECT dr.*, c.display_name, c.job_title FROM deal_contact_roles dr JOIN contacts c ON dr.contact_id=c.id WHERE dr.deal_id=?`).all(dealId);
|
|
4185
|
+
}
|
|
4186
|
+
function getCoverageGaps(companyId, db) {
|
|
4187
|
+
const _db2 = db || getDatabase();
|
|
4188
|
+
const total = _db2.query(`SELECT COUNT(*) c FROM contacts WHERE company_id=? AND archived=0`).get(companyId).c;
|
|
4189
|
+
const hasManager = _db2.query(`SELECT COUNT(*) c FROM org_chart_edges WHERE company_id=? AND edge_type='manages'`).get(companyId).c > 0;
|
|
4190
|
+
const hasEco = _db2.query(`SELECT COUNT(*) c FROM deal_contact_roles dr JOIN deals d ON dr.deal_id=d.id WHERE d.company_id=? AND dr.account_role='economic_buyer'`).get(companyId).c > 0;
|
|
4191
|
+
const hasTech = _db2.query(`SELECT COUNT(*) c FROM deal_contact_roles dr JOIN deals d ON dr.deal_id=d.id WHERE d.company_id=? AND dr.account_role='technical_evaluator'`).get(companyId).c > 0;
|
|
4192
|
+
const missing = [
|
|
4193
|
+
!hasManager && "org chart relationships",
|
|
4194
|
+
!hasEco && "economic buyer",
|
|
4195
|
+
!hasTech && "technical evaluator"
|
|
4196
|
+
].filter(Boolean);
|
|
4197
|
+
return {
|
|
4198
|
+
total_contacts: total,
|
|
4199
|
+
has_manager: hasManager,
|
|
4200
|
+
has_technical: hasTech,
|
|
4201
|
+
has_economic_buyer: hasEco,
|
|
4202
|
+
suggestion: missing.length ? `Missing: ${missing.join(", ")}` : "Good coverage"
|
|
4203
|
+
};
|
|
4204
|
+
}
|
|
4205
|
+
|
|
4206
|
+
// src/mcp/index.ts
|
|
4207
|
+
var server = new Server({ name: "contacts", version: "0.1.0" }, { capabilities: { tools: {} } });
|
|
3320
4208
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
3321
4209
|
tools: [
|
|
3322
4210
|
{
|
|
@@ -3338,7 +4226,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
3338
4226
|
preferred_contact_method: { type: "string", enum: ["email", "phone", "telegram", "whatsapp", "linkedin", "twitter", "other"] },
|
|
3339
4227
|
status: { type: "string", enum: ["active", "pending_reply", "converted", "closed", "other"], description: "Contact lifecycle status (default: active)" },
|
|
3340
4228
|
follow_up_at: { type: "string", description: "ISO 8601 datetime to follow up with this contact" },
|
|
3341
|
-
project_id: { type: "string", description: "
|
|
4229
|
+
project_id: { type: "string", description: "Primary project ID (single). Use project_ids for multiple." },
|
|
4230
|
+
project_ids: { type: "array", items: { type: "string" }, description: "Associate contact with multiple todos project IDs" },
|
|
3342
4231
|
emails: {
|
|
3343
4232
|
type: "array",
|
|
3344
4233
|
items: {
|
|
@@ -3425,7 +4314,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
3425
4314
|
preferred_contact_method: { type: "string", enum: ["email", "phone", "telegram", "whatsapp", "linkedin", "twitter", "other"] },
|
|
3426
4315
|
status: { type: "string", enum: ["active", "pending_reply", "converted", "closed", "other"] },
|
|
3427
4316
|
follow_up_at: { type: "string", description: "ISO 8601 datetime for follow-up reminder (null to clear)" },
|
|
3428
|
-
project_id: { type: "string", description: "
|
|
4317
|
+
project_id: { type: "string", description: "Primary project ID (single, null to clear)" },
|
|
4318
|
+
project_ids: { type: "array", items: { type: "string" }, description: "Replace all project links with this array of todos project IDs" },
|
|
3429
4319
|
source: { type: "string", enum: ["manual", "import", "linkedin", "github", "twitter", "email", "calendar", "crm", "other"] },
|
|
3430
4320
|
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)" },
|
|
3431
4321
|
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)" }
|
|
@@ -3785,6 +4675,43 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
3785
4675
|
required: ["note_id"]
|
|
3786
4676
|
}
|
|
3787
4677
|
},
|
|
4678
|
+
{
|
|
4679
|
+
name: "link_contact_to_project",
|
|
4680
|
+
description: "Associate a contact with a todos project ID. Contacts can belong to multiple projects.",
|
|
4681
|
+
inputSchema: {
|
|
4682
|
+
type: "object",
|
|
4683
|
+
properties: {
|
|
4684
|
+
contact_id: { type: "string" },
|
|
4685
|
+
project_id: { type: "string", description: "Todos project ID" }
|
|
4686
|
+
},
|
|
4687
|
+
required: ["contact_id", "project_id"]
|
|
4688
|
+
}
|
|
4689
|
+
},
|
|
4690
|
+
{
|
|
4691
|
+
name: "unlink_contact_from_project",
|
|
4692
|
+
description: "Remove the association between a contact and a todos project.",
|
|
4693
|
+
inputSchema: {
|
|
4694
|
+
type: "object",
|
|
4695
|
+
properties: {
|
|
4696
|
+
contact_id: { type: "string" },
|
|
4697
|
+
project_id: { type: "string" }
|
|
4698
|
+
},
|
|
4699
|
+
required: ["contact_id", "project_id"]
|
|
4700
|
+
}
|
|
4701
|
+
},
|
|
4702
|
+
{
|
|
4703
|
+
name: "list_contacts_by_project",
|
|
4704
|
+
description: "List all contacts linked to a specific todos project ID.",
|
|
4705
|
+
inputSchema: {
|
|
4706
|
+
type: "object",
|
|
4707
|
+
properties: {
|
|
4708
|
+
project_id: { type: "string" },
|
|
4709
|
+
limit: { type: "number", description: "Max results (default 100)" },
|
|
4710
|
+
offset: { type: "number" }
|
|
4711
|
+
},
|
|
4712
|
+
required: ["project_id"]
|
|
4713
|
+
}
|
|
4714
|
+
},
|
|
3788
4715
|
{
|
|
3789
4716
|
name: "list_contacts_by_company",
|
|
3790
4717
|
description: "List all contacts belonging to a specific company. Equivalent to list_contacts with company_id filter but more ergonomic.",
|
|
@@ -3818,7 +4745,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
3818
4745
|
type: "object",
|
|
3819
4746
|
properties: {
|
|
3820
4747
|
name: { type: "string" },
|
|
3821
|
-
description: { type: "string" }
|
|
4748
|
+
description: { type: "string" },
|
|
4749
|
+
project_id: { type: "string", description: "Associate this group with a todos project ID" }
|
|
3822
4750
|
},
|
|
3823
4751
|
required: ["name"]
|
|
3824
4752
|
}
|
|
@@ -3826,7 +4754,12 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
3826
4754
|
{
|
|
3827
4755
|
name: "list_groups",
|
|
3828
4756
|
description: "List all groups with their member counts.",
|
|
3829
|
-
inputSchema: {
|
|
4757
|
+
inputSchema: {
|
|
4758
|
+
type: "object",
|
|
4759
|
+
properties: {
|
|
4760
|
+
project_id: { type: "string", description: "Filter groups by todos project ID" }
|
|
4761
|
+
}
|
|
4762
|
+
}
|
|
3830
4763
|
},
|
|
3831
4764
|
{
|
|
3832
4765
|
name: "get_group",
|
|
@@ -3839,13 +4772,14 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
3839
4772
|
},
|
|
3840
4773
|
{
|
|
3841
4774
|
name: "update_group",
|
|
3842
|
-
description: "Update a group's name or
|
|
4775
|
+
description: "Update a group's name, description, or project association.",
|
|
3843
4776
|
inputSchema: {
|
|
3844
4777
|
type: "object",
|
|
3845
4778
|
properties: {
|
|
3846
4779
|
id: { type: "string" },
|
|
3847
4780
|
name: { type: "string" },
|
|
3848
|
-
description: { type: "string" }
|
|
4781
|
+
description: { type: "string" },
|
|
4782
|
+
project_id: { type: "string", description: "Associate with a todos project ID (null to clear)" }
|
|
3849
4783
|
},
|
|
3850
4784
|
required: ["id"]
|
|
3851
4785
|
}
|
|
@@ -4687,7 +5621,49 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
4687
5621
|
},
|
|
4688
5622
|
required: ["contact_id", "do_not_contact"]
|
|
4689
5623
|
}
|
|
4690
|
-
}
|
|
5624
|
+
},
|
|
5625
|
+
{ name: "get_field_history", description: "Get the change history for one or all fields of a contact (temporal audit trail).", inputSchema: { type: "object", properties: { contact_id: { type: "string" }, field_name: { type: "string", description: "Optional \u2014 filter to a single field" } }, required: ["contact_id"] } },
|
|
5626
|
+
{ name: "get_contact_at", description: "Reconstruct a contact's profile as it was at a specific point in time, using field history.", inputSchema: { type: "object", properties: { contact_id: { type: "string" }, timestamp: { type: "string", description: "ISO 8601 datetime \u2014 reconstruct profile at this point in time" } }, required: ["contact_id", "timestamp"] } },
|
|
5627
|
+
{ name: "get_job_history", description: "Get the employment timeline for a contact \u2014 all past and current job entries in reverse chronological order.", inputSchema: { type: "object", properties: { contact_id: { type: "string" } }, required: ["contact_id"] } },
|
|
5628
|
+
{ name: "add_job_entry", description: "Add a job history entry to a contact's employment timeline.", inputSchema: { type: "object", properties: { contact_id: { type: "string" }, company_name: { type: "string" }, title: { type: "string" }, start_date: { type: "string" }, end_date: { type: "string" }, is_current: { type: "boolean" } }, required: ["contact_id", "company_name"] } },
|
|
5629
|
+
{ name: "save_learning", description: "Save a structured learning about a contact \u2014 preferences, facts, inferences, warnings, or signals. Include confidence (0-100) and importance (1-10).", inputSchema: { type: "object", properties: { contact_id: { type: "string" }, content: { type: "string" }, type: { type: "string", enum: ["preference", "fact", "inference", "warning", "signal"] }, confidence: { type: "number" }, importance: { type: "number" }, learned_by: { type: "string" }, visibility: { type: "string", enum: ["private", "shared", "human"] }, tags: { type: "array", items: { type: "string" } } }, required: ["contact_id", "content"] } },
|
|
5630
|
+
{ name: "get_learnings", description: "Get all learnings for a contact, optionally filtered by type and minimum importance.", inputSchema: { type: "object", properties: { contact_id: { type: "string" }, type: { type: "string", enum: ["preference", "fact", "inference", "warning", "signal"] }, min_importance: { type: "number" } }, required: ["contact_id"] } },
|
|
5631
|
+
{ name: "search_learnings", description: "Cross-contact search across all learnings for a keyword or phrase.", inputSchema: { type: "object", properties: { query: { type: "string" }, type: { type: "string" }, contact_id: { type: "string", description: "Optional \u2014 limit to a specific contact" } }, required: ["query"] } },
|
|
5632
|
+
{ name: "confirm_learning", description: "Confirm a learning as correct, boosting its confidence score.", inputSchema: { type: "object", properties: { learning_id: { type: "string" }, agent_name: { type: "string" } }, required: ["learning_id", "agent_name"] } },
|
|
5633
|
+
{ name: "get_stale_learnings", description: "Find learnings that haven't been confirmed recently and may need review.", inputSchema: { type: "object", properties: { days_old: { type: "number" }, min_confidence: { type: "number" } } } },
|
|
5634
|
+
{ name: "run_learning_maintenance", description: "Run decay (reduce confidence on old unconfirmed learnings) and contradiction detection across all learnings.", inputSchema: { type: "object", properties: {} } },
|
|
5635
|
+
{ name: "acquire_contact_lock", description: "Acquire a write lock on a contact to prevent conflicts when multiple agents edit the same record. Returns {acquired, lock, held_by}.", inputSchema: { type: "object", properties: { contact_id: { type: "string" }, agent_name: { type: "string" }, ttl_seconds: { type: "number" }, reason: { type: "string" }, session_id: { type: "string" } }, required: ["contact_id", "agent_name"] } },
|
|
5636
|
+
{ name: "release_contact_lock", description: "Release a contact write lock previously acquired by this agent.", inputSchema: { type: "object", properties: { contact_id: { type: "string" }, agent_name: { type: "string" } }, required: ["contact_id", "agent_name"] } },
|
|
5637
|
+
{ name: "check_contact_lock", description: "Check if a contact is currently locked by any agent.", inputSchema: { type: "object", properties: { contact_id: { type: "string" } }, required: ["contact_id"] } },
|
|
5638
|
+
{ name: "log_agent_activity", description: "Log an agent action against a contact for audit/coordination purposes.", inputSchema: { type: "object", properties: { contact_id: { type: "string" }, agent_name: { type: "string" }, action: { type: "string" }, details: { type: "string" }, session_id: { type: "string" } }, required: ["contact_id", "agent_name", "action"] } },
|
|
5639
|
+
{ name: "get_contact_agent_activity", description: "Get the recent agent activity log for a contact.", inputSchema: { type: "object", properties: { contact_id: { type: "string" }, limit: { type: "number" } }, required: ["contact_id"] } },
|
|
5640
|
+
{ name: "get_relationship_strength", description: "Compute and return the relationship strength score (0-100) for a contact based on interaction frequency and recency.", inputSchema: { type: "object", properties: { contact_id: { type: "string" } }, required: ["contact_id"] } },
|
|
5641
|
+
{ name: "find_warm_path", description: "Find the shortest warm introduction path between two contacts through the relationship graph.", inputSchema: { type: "object", properties: { from_contact_id: { type: "string" }, to_contact_id: { type: "string" } }, required: ["from_contact_id", "to_contact_id"] } },
|
|
5642
|
+
{ name: "find_connections_at_company", description: "Find all contacts linked to a specific company, with relationship strength scores.", inputSchema: { type: "object", properties: { company_id: { type: "string" } }, required: ["company_id"] } },
|
|
5643
|
+
{ name: "get_cooling_relationships", description: "Get all relationships that are cooling (no contact in 45+ days) \u2014 use to prioritize re-engagement outreach.", inputSchema: { type: "object", properties: {} } },
|
|
5644
|
+
{ name: "resolve_contact_identity", description: "Resolve a contact's identity from partial signals (email, name, LinkedIn URL, phone, or external system ID). Returns ranked matches with confidence scores.", inputSchema: { type: "object", properties: { email: { type: "string" }, name: { type: "string" }, linkedin_url: { type: "string" }, phone: { type: "string" }, system: { type: "string" }, external_id: { type: "string" } } } },
|
|
5645
|
+
{ name: "add_contact_identity", description: "Register an external system identity (e.g. Salesforce ID, LinkedIn URL) for a contact.", inputSchema: { type: "object", properties: { contact_id: { type: "string" }, system: { type: "string" }, external_id: { type: "string" }, external_url: { type: "string" }, confidence: { type: "string", enum: ["verified", "inferred"] } }, required: ["contact_id", "system", "external_id"] } },
|
|
5646
|
+
{ name: "get_contact_identities", description: "Get all registered external system identities for a contact.", inputSchema: { type: "object", properties: { contact_id: { type: "string" } }, required: ["contact_id"] } },
|
|
5647
|
+
{ name: "semantic_search_contacts", description: "Search contacts by capability or context using TF-IDF semantic similarity \u2014 finds contacts based on meaning, not just keyword match.", inputSchema: { type: "object", properties: { query: { type: "string" }, limit: { type: "number" } }, required: ["query"] } },
|
|
5648
|
+
{ name: "embed_all_contacts", description: "Build TF-IDF embeddings for all contacts in the database \u2014 run once to enable semantic_search_contacts.", inputSchema: { type: "object", properties: {} } },
|
|
5649
|
+
{ name: "get_relationship_signals", description: "Get relationship health signals for a contact: warming/cooling/ghost/healthy status with reasons.", inputSchema: { type: "object", properties: { contact_id: { type: "string" } }, required: ["contact_id"] } },
|
|
5650
|
+
{ name: "get_ghost_contacts", description: "List contacts you haven't been in touch with for 180+ days \u2014 relationships at risk of becoming permanently cold.", inputSchema: { type: "object", properties: {} } },
|
|
5651
|
+
{ name: "get_warming_contacts", description: "List contacts with rising interaction frequency \u2014 relationships gaining momentum.", inputSchema: { type: "object", properties: {} } },
|
|
5652
|
+
{ name: "recompute_signals", description: "Recompute engagement_status for all contacts based on interaction counts and recency.", inputSchema: { type: "object", properties: {} } },
|
|
5653
|
+
{ name: "get_contact_card", description: "Get a minimal ~50-token contact summary: name, title, company, primary email and phone. Ideal for lists and agent context injection.", inputSchema: { type: "object", properties: { contact_id: { type: "string" } }, required: ["contact_id"] } },
|
|
5654
|
+
{ name: "assemble_context", description: "Assemble a multi-contact context package for meetings, deals, outreach, or research. Returns task-relevant briefs for each contact.", inputSchema: { type: "object", properties: { contact_ids: { type: "array", items: { type: "string" } }, format: { type: "string", enum: ["meeting_prep", "deal_review", "outreach", "research"] } }, required: ["contact_ids"] } },
|
|
5655
|
+
{ name: "parse_email_signature", description: "Parse an email signature text to extract contact fields (name, title, company, phone, email, LinkedIn, website). Does NOT create a contact.", inputSchema: { type: "object", properties: { signature_text: { type: "string" } }, required: ["signature_text"] } },
|
|
5656
|
+
{ name: "ingest_email_participants", description: "Find or create contacts from email thread participants (with optional signatures). Returns { created, updated, contacts }.", inputSchema: { type: "object", properties: { participants: { type: "array", items: { type: "object", properties: { name: { type: "string" }, email: { type: "string" }, signature: { type: "string" } }, required: ["email"] } }, context: { type: "string" } }, required: ["participants"] } },
|
|
5657
|
+
{ name: "ingest_meeting_participants", description: "Ingest meeting attendees: find-or-create contacts and log the meeting as an event. Returns { created, updated, contact_ids }.", inputSchema: { type: "object", properties: { title: { type: "string" }, event_date: { type: "string" }, attendees: { type: "array", items: { type: "object", properties: { name: { type: "string" }, email: { type: "string" } }, required: ["name", "email"] } }, context: { type: "string" } }, required: ["title", "event_date", "attendees"] } },
|
|
5658
|
+
{ name: "get_freshness_score", description: "Get a per-field freshness and confidence breakdown for a contact \u2014 shows which fields are verified, stale, or missing.", inputSchema: { type: "object", properties: { contact_id: { type: "string" } }, required: ["contact_id"] } },
|
|
5659
|
+
{ name: "get_stale_contacts", description: "List contacts with low data completeness scores (below threshold). Default threshold: 40.", inputSchema: { type: "object", properties: { threshold: { type: "number", description: "Score threshold 0-100 (default 40)" } } } },
|
|
5660
|
+
{ name: "mark_field_verified", description: "Mark a specific contact field as verified by a human or trusted source.", inputSchema: { type: "object", properties: { contact_id: { type: "string" }, field_name: { type: "string" }, source: { type: "string" } }, required: ["contact_id", "field_name"] } },
|
|
5661
|
+
{ name: "add_org_chart_edge", description: "Add a relationship edge to the org chart for a company (reports_to, manages, peer, collaborates_with).", inputSchema: { type: "object", properties: { company_id: { type: "string" }, contact_a_id: { type: "string" }, contact_b_id: { type: "string" }, edge_type: { type: "string", enum: ["reports_to", "manages", "collaborates_with", "peer"] } }, required: ["company_id", "contact_a_id", "contact_b_id", "edge_type"] } },
|
|
5662
|
+
{ name: "get_org_chart", description: "Get the org chart for a company as a list of directed edges with contact names.", inputSchema: { type: "object", properties: { company_id: { type: "string" } }, required: ["company_id"] } },
|
|
5663
|
+
{ name: "set_deal_contact_role", description: "Assign a contact a buying committee role in a deal (economic_buyer, technical_evaluator, champion, blocker, influencer, user, sponsor, other).", inputSchema: { type: "object", properties: { deal_id: { type: "string" }, contact_id: { type: "string" }, account_role: { type: "string", enum: ["economic_buyer", "technical_evaluator", "champion", "blocker", "influencer", "user", "sponsor", "other"] } }, required: ["deal_id", "contact_id", "account_role"] } },
|
|
5664
|
+
{ name: "get_deal_team", description: "Get the full buying committee for a deal with contact names and roles.", inputSchema: { type: "object", properties: { deal_id: { type: "string" } }, required: ["deal_id"] } },
|
|
5665
|
+
{ name: "get_coverage_gaps", description: "Identify coverage gaps in a company account \u2014 missing economic buyer, technical evaluator, or org chart relationships.", inputSchema: { type: "object", properties: { company_id: { type: "string" } }, required: ["company_id"] } },
|
|
5666
|
+
{ name: "get_recent_contact_events", description: "Polling fallback for change events \u2014 returns recent activity log entries, optionally filtered by event type or date.", inputSchema: { type: "object", properties: { since: { type: "string", description: "ISO 8601 datetime \u2014 only events after this date" }, event_types: { type: "array", items: { type: "string" } } } } }
|
|
4691
5667
|
]
|
|
4692
5668
|
}));
|
|
4693
5669
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
@@ -4719,10 +5695,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4719
5695
|
source: a.source
|
|
4720
5696
|
};
|
|
4721
5697
|
const contact = createContact(input);
|
|
4722
|
-
|
|
5698
|
+
if (Array.isArray(a.project_ids) && a.project_ids.length > 0) {
|
|
5699
|
+
setContactProjects(contact.id, a.project_ids);
|
|
5700
|
+
}
|
|
5701
|
+
const projectIds = getContactProjectIds(contact.id);
|
|
5702
|
+
return { content: [{ type: "text", text: JSON.stringify({ ...contact, project_ids: projectIds }, null, 2) }] };
|
|
4723
5703
|
}
|
|
4724
5704
|
case "get_contact": {
|
|
4725
5705
|
const contact = getContact(a.id);
|
|
5706
|
+
if (contact) {
|
|
5707
|
+
const projectIds = getContactProjectIds(contact.id);
|
|
5708
|
+
return { content: [{ type: "text", text: JSON.stringify({ ...contact, project_ids: projectIds }, null, 2) }] };
|
|
5709
|
+
}
|
|
4726
5710
|
return { content: [{ type: "text", text: JSON.stringify(contact, null, 2) }] };
|
|
4727
5711
|
}
|
|
4728
5712
|
case "update_contact": {
|
|
@@ -4747,7 +5731,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4747
5731
|
phones_add: rest.phones_add
|
|
4748
5732
|
};
|
|
4749
5733
|
const contact = updateContact(id, input);
|
|
4750
|
-
|
|
5734
|
+
if (Array.isArray(rest.project_ids)) {
|
|
5735
|
+
setContactProjects(id, rest.project_ids);
|
|
5736
|
+
}
|
|
5737
|
+
const projectIds = getContactProjectIds(id);
|
|
5738
|
+
return { content: [{ type: "text", text: JSON.stringify({ ...contact, project_ids: projectIds }, null, 2) }] };
|
|
4751
5739
|
}
|
|
4752
5740
|
case "delete_contact": {
|
|
4753
5741
|
deleteContact(a.id);
|
|
@@ -5047,6 +6035,25 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
5047
6035
|
deleteNote(a.note_id);
|
|
5048
6036
|
return { content: [{ type: "text", text: JSON.stringify({ deleted: true }) }] };
|
|
5049
6037
|
}
|
|
6038
|
+
case "link_contact_to_project": {
|
|
6039
|
+
linkContactToProject(a.contact_id, a.project_id);
|
|
6040
|
+
const projectIds = getContactProjectIds(a.contact_id);
|
|
6041
|
+
return { content: [{ type: "text", text: JSON.stringify({ contact_id: a.contact_id, project_ids: projectIds }) }] };
|
|
6042
|
+
}
|
|
6043
|
+
case "unlink_contact_from_project": {
|
|
6044
|
+
unlinkContactFromProject(a.contact_id, a.project_id);
|
|
6045
|
+
const projectIds = getContactProjectIds(a.contact_id);
|
|
6046
|
+
return { content: [{ type: "text", text: JSON.stringify({ contact_id: a.contact_id, project_ids: projectIds }) }] };
|
|
6047
|
+
}
|
|
6048
|
+
case "list_contacts_by_project": {
|
|
6049
|
+
const db = getDatabase();
|
|
6050
|
+
const contactIds = listContactIdsByProject(a.project_id);
|
|
6051
|
+
const limit = a.limit ?? 100;
|
|
6052
|
+
const offset = a.offset ?? 0;
|
|
6053
|
+
const paged = contactIds.slice(offset, offset + limit);
|
|
6054
|
+
const contacts = paged.map((id) => getContact(id, db)).filter(Boolean);
|
|
6055
|
+
return { content: [{ type: "text", text: JSON.stringify({ contacts, total: contactIds.length, project_id: a.project_id }, null, 2) }] };
|
|
6056
|
+
}
|
|
5050
6057
|
case "list_contacts_by_company": {
|
|
5051
6058
|
const result = listContacts({
|
|
5052
6059
|
company_id: a.company_id,
|
|
@@ -5075,12 +6082,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
5075
6082
|
}
|
|
5076
6083
|
case "create_group": {
|
|
5077
6084
|
const db = getDatabase();
|
|
5078
|
-
const group = createGroup(db, { name: a.name, description: a.description });
|
|
6085
|
+
const group = createGroup(db, { name: a.name, description: a.description, project_id: a.project_id });
|
|
5079
6086
|
return { content: [{ type: "text", text: JSON.stringify(group, null, 2) }] };
|
|
5080
6087
|
}
|
|
5081
6088
|
case "list_groups": {
|
|
5082
6089
|
const db = getDatabase();
|
|
5083
|
-
const groups = listGroups(db);
|
|
6090
|
+
const groups = listGroups(db, a.project_id);
|
|
5084
6091
|
return { content: [{ type: "text", text: JSON.stringify(groups, null, 2) }] };
|
|
5085
6092
|
}
|
|
5086
6093
|
case "get_group": {
|
|
@@ -5093,7 +6100,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
5093
6100
|
case "update_group": {
|
|
5094
6101
|
const db = getDatabase();
|
|
5095
6102
|
const { id: groupId, ...groupRest } = a;
|
|
5096
|
-
const group = updateGroup(db, groupId, {
|
|
6103
|
+
const group = updateGroup(db, groupId, {
|
|
6104
|
+
name: groupRest.name,
|
|
6105
|
+
description: groupRest.description,
|
|
6106
|
+
project_id: groupRest.project_id
|
|
6107
|
+
});
|
|
5097
6108
|
return { content: [{ type: "text", text: JSON.stringify(group, null, 2) }] };
|
|
5098
6109
|
}
|
|
5099
6110
|
case "delete_group": {
|
|
@@ -5599,11 +6610,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
5599
6610
|
const team = listCompanyRelationships({ company_id: a.company_id }, db);
|
|
5600
6611
|
return { content: [{ type: "text", text: JSON.stringify({ company, team }, null, 2) }] };
|
|
5601
6612
|
}
|
|
5602
|
-
case "get_contact_brief": {
|
|
5603
|
-
const db = getDatabase();
|
|
5604
|
-
const brief = generateBrief(a.contact_id, db);
|
|
5605
|
-
return { content: [{ type: "text", text: JSON.stringify({ brief }, null, 2) }] };
|
|
5606
|
-
}
|
|
5607
6613
|
case "list_cold_contacts": {
|
|
5608
6614
|
const db = getDatabase();
|
|
5609
6615
|
const contacts = listColdContacts(a.days ?? 30, db);
|
|
@@ -5851,6 +6857,303 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
5851
6857
|
const output = await exportContacts(format, contactList);
|
|
5852
6858
|
return { content: [{ type: "text", text: output }] };
|
|
5853
6859
|
}
|
|
6860
|
+
case "get_field_history": {
|
|
6861
|
+
const db = getDatabase();
|
|
6862
|
+
const history = getFieldHistory(a.contact_id, a.field_name, db);
|
|
6863
|
+
return { content: [{ type: "text", text: JSON.stringify({ history }, null, 2) }] };
|
|
6864
|
+
}
|
|
6865
|
+
case "get_contact_at": {
|
|
6866
|
+
const db = getDatabase();
|
|
6867
|
+
const snapshot = getContactAt(a.contact_id, a.timestamp, db);
|
|
6868
|
+
return { content: [{ type: "text", text: JSON.stringify({ contact_id: a.contact_id, timestamp: a.timestamp, snapshot }, null, 2) }] };
|
|
6869
|
+
}
|
|
6870
|
+
case "get_job_history": {
|
|
6871
|
+
const db = getDatabase();
|
|
6872
|
+
const history = getJobHistory(a.contact_id, db);
|
|
6873
|
+
return { content: [{ type: "text", text: JSON.stringify({ history }, null, 2) }] };
|
|
6874
|
+
}
|
|
6875
|
+
case "add_job_entry": {
|
|
6876
|
+
const db = getDatabase();
|
|
6877
|
+
const entry = addJobEntry(a.contact_id, {
|
|
6878
|
+
company_name: a.company_name,
|
|
6879
|
+
title: a.title,
|
|
6880
|
+
start_date: a.start_date,
|
|
6881
|
+
end_date: a.end_date,
|
|
6882
|
+
is_current: a.is_current
|
|
6883
|
+
}, db);
|
|
6884
|
+
return { content: [{ type: "text", text: JSON.stringify(entry, null, 2) }] };
|
|
6885
|
+
}
|
|
6886
|
+
case "save_learning": {
|
|
6887
|
+
const db = getDatabase();
|
|
6888
|
+
const input = {
|
|
6889
|
+
content: a.content,
|
|
6890
|
+
type: a.type,
|
|
6891
|
+
confidence: a.confidence,
|
|
6892
|
+
importance: a.importance,
|
|
6893
|
+
learned_by: a.learned_by,
|
|
6894
|
+
visibility: a.visibility,
|
|
6895
|
+
tags: a.tags
|
|
6896
|
+
};
|
|
6897
|
+
const learning = saveLearning(a.contact_id, input, db);
|
|
6898
|
+
return { content: [{ type: "text", text: JSON.stringify(learning, null, 2) }] };
|
|
6899
|
+
}
|
|
6900
|
+
case "get_learnings": {
|
|
6901
|
+
const db = getDatabase();
|
|
6902
|
+
const learnings = getLearnings(a.contact_id, {
|
|
6903
|
+
type: a.type,
|
|
6904
|
+
min_importance: a.min_importance
|
|
6905
|
+
}, db);
|
|
6906
|
+
return { content: [{ type: "text", text: JSON.stringify({ learnings }, null, 2) }] };
|
|
6907
|
+
}
|
|
6908
|
+
case "search_learnings": {
|
|
6909
|
+
const db = getDatabase();
|
|
6910
|
+
const results = searchLearnings(a.query, {
|
|
6911
|
+
type: a.type,
|
|
6912
|
+
contact_id: a.contact_id
|
|
6913
|
+
}, db);
|
|
6914
|
+
return { content: [{ type: "text", text: JSON.stringify({ results }, null, 2) }] };
|
|
6915
|
+
}
|
|
6916
|
+
case "confirm_learning": {
|
|
6917
|
+
const db = getDatabase();
|
|
6918
|
+
confirmLearning(a.learning_id, a.agent_name, db);
|
|
6919
|
+
return { content: [{ type: "text", text: JSON.stringify({ confirmed: true }) }] };
|
|
6920
|
+
}
|
|
6921
|
+
case "get_stale_learnings": {
|
|
6922
|
+
const db = getDatabase();
|
|
6923
|
+
const daysOld = a.days_old ?? 30;
|
|
6924
|
+
const minConf = a.min_confidence ?? 0;
|
|
6925
|
+
const cutoff = new Date(Date.now() - daysOld * 86400000).toISOString();
|
|
6926
|
+
const rows = db.query(`SELECT * FROM contact_learnings WHERE confirmed_count=0 AND created_at<? AND confidence>=? ORDER BY confidence ASC LIMIT 50`).all(cutoff, minConf);
|
|
6927
|
+
return { content: [{ type: "text", text: JSON.stringify({ stale_learnings: rows }, null, 2) }] };
|
|
6928
|
+
}
|
|
6929
|
+
case "run_learning_maintenance": {
|
|
6930
|
+
const db = getDatabase();
|
|
6931
|
+
const decayed = decayLearnings(db);
|
|
6932
|
+
const duplicates = db.query(`SELECT contact_id, COUNT(*) as cnt FROM contact_learnings GROUP BY contact_id, LOWER(SUBSTR(content,1,30)) HAVING cnt > 1`).all();
|
|
6933
|
+
return { content: [{ type: "text", text: JSON.stringify({ decayed_count: decayed, potential_contradictions: duplicates }, null, 2) }] };
|
|
6934
|
+
}
|
|
6935
|
+
case "acquire_contact_lock": {
|
|
6936
|
+
const db = getDatabase();
|
|
6937
|
+
const result = acquireLock(a.contact_id, a.agent_name, a.ttl_seconds, a.reason, a.session_id, db);
|
|
6938
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
6939
|
+
}
|
|
6940
|
+
case "release_contact_lock": {
|
|
6941
|
+
const db = getDatabase();
|
|
6942
|
+
const released = releaseLock(a.contact_id, a.agent_name, db);
|
|
6943
|
+
return { content: [{ type: "text", text: JSON.stringify({ released }, null, 2) }] };
|
|
6944
|
+
}
|
|
6945
|
+
case "check_contact_lock": {
|
|
6946
|
+
const db = getDatabase();
|
|
6947
|
+
const lock = checkLock(a.contact_id, db);
|
|
6948
|
+
return { content: [{ type: "text", text: JSON.stringify({ locked: !!lock, lock }, null, 2) }] };
|
|
6949
|
+
}
|
|
6950
|
+
case "log_agent_activity": {
|
|
6951
|
+
const db = getDatabase();
|
|
6952
|
+
logAgentActivity(a.contact_id, a.agent_name, a.action, a.details, a.session_id, db);
|
|
6953
|
+
return { content: [{ type: "text", text: JSON.stringify({ logged: true }) }] };
|
|
6954
|
+
}
|
|
6955
|
+
case "get_contact_agent_activity": {
|
|
6956
|
+
const db = getDatabase();
|
|
6957
|
+
const activity = getAgentActivity(a.contact_id, a.limit ?? 20, db);
|
|
6958
|
+
return { content: [{ type: "text", text: JSON.stringify({ activity }, null, 2) }] };
|
|
6959
|
+
}
|
|
6960
|
+
case "get_relationship_strength": {
|
|
6961
|
+
const db = getDatabase();
|
|
6962
|
+
const score = computeRelationshipStrength(a.contact_id, db);
|
|
6963
|
+
return { content: [{ type: "text", text: JSON.stringify({ contact_id: a.contact_id, strength_score: score }, null, 2) }] };
|
|
6964
|
+
}
|
|
6965
|
+
case "find_warm_path": {
|
|
6966
|
+
const db = getDatabase();
|
|
6967
|
+
const path = findWarmPath(a.from_contact_id, a.to_contact_id, db);
|
|
6968
|
+
return { content: [{ type: "text", text: JSON.stringify({ path, hops: path.length }, null, 2) }] };
|
|
6969
|
+
}
|
|
6970
|
+
case "find_connections_at_company": {
|
|
6971
|
+
const db = getDatabase();
|
|
6972
|
+
const connections = findConnectionsAtCompany(a.company_id, db);
|
|
6973
|
+
return { content: [{ type: "text", text: JSON.stringify({ connections }, null, 2) }] };
|
|
6974
|
+
}
|
|
6975
|
+
case "get_cooling_relationships": {
|
|
6976
|
+
const db = getDatabase();
|
|
6977
|
+
const cooling = detectCoolingRelationships(db);
|
|
6978
|
+
return { content: [{ type: "text", text: JSON.stringify({ cooling }, null, 2) }] };
|
|
6979
|
+
}
|
|
6980
|
+
case "resolve_contact_identity": {
|
|
6981
|
+
const db = getDatabase();
|
|
6982
|
+
const matches = resolveByPartial({
|
|
6983
|
+
email: a.email,
|
|
6984
|
+
name: a.name,
|
|
6985
|
+
linkedin_url: a.linkedin_url,
|
|
6986
|
+
phone: a.phone
|
|
6987
|
+
}, db);
|
|
6988
|
+
return { content: [{ type: "text", text: JSON.stringify({ matches }, null, 2) }] };
|
|
6989
|
+
}
|
|
6990
|
+
case "add_contact_identity": {
|
|
6991
|
+
const db = getDatabase();
|
|
6992
|
+
const identity = addIdentity(a.contact_id, a.system, a.external_id, a.external_url, a.confidence ?? "inferred", db);
|
|
6993
|
+
return { content: [{ type: "text", text: JSON.stringify(identity, null, 2) }] };
|
|
6994
|
+
}
|
|
6995
|
+
case "get_contact_identities": {
|
|
6996
|
+
const db = getDatabase();
|
|
6997
|
+
const identities = getIdentities(a.contact_id, db);
|
|
6998
|
+
return { content: [{ type: "text", text: JSON.stringify({ identities }, null, 2) }] };
|
|
6999
|
+
}
|
|
7000
|
+
case "semantic_search_contacts": {
|
|
7001
|
+
const db = getDatabase();
|
|
7002
|
+
const results = semanticSearch(a.query, a.limit ?? 10, db);
|
|
7003
|
+
const enriched = results.map((r) => {
|
|
7004
|
+
try {
|
|
7005
|
+
return { ...r, contact: getContact(r.contact_id) };
|
|
7006
|
+
} catch {
|
|
7007
|
+
return r;
|
|
7008
|
+
}
|
|
7009
|
+
});
|
|
7010
|
+
return { content: [{ type: "text", text: JSON.stringify({ results: enriched }, null, 2) }] };
|
|
7011
|
+
}
|
|
7012
|
+
case "embed_all_contacts": {
|
|
7013
|
+
const db = getDatabase();
|
|
7014
|
+
const count = await embedAllContacts(db);
|
|
7015
|
+
return { content: [{ type: "text", text: JSON.stringify({ embedded: count }) }] };
|
|
7016
|
+
}
|
|
7017
|
+
case "get_relationship_signals": {
|
|
7018
|
+
const db = getDatabase();
|
|
7019
|
+
const signals = getRelationshipSignals(a.contact_id, db);
|
|
7020
|
+
return { content: [{ type: "text", text: JSON.stringify({ signals }, null, 2) }] };
|
|
7021
|
+
}
|
|
7022
|
+
case "get_ghost_contacts": {
|
|
7023
|
+
const db = getDatabase();
|
|
7024
|
+
const ghosts = getGhostContacts(db);
|
|
7025
|
+
return { content: [{ type: "text", text: JSON.stringify({ ghosts }, null, 2) }] };
|
|
7026
|
+
}
|
|
7027
|
+
case "get_warming_contacts": {
|
|
7028
|
+
const db = getDatabase();
|
|
7029
|
+
const warming = getWarmingContacts(db);
|
|
7030
|
+
return { content: [{ type: "text", text: JSON.stringify({ warming }, null, 2) }] };
|
|
7031
|
+
}
|
|
7032
|
+
case "recompute_signals": {
|
|
7033
|
+
const db = getDatabase();
|
|
7034
|
+
const result = recomputeAllSignals(db);
|
|
7035
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
7036
|
+
}
|
|
7037
|
+
case "get_contact_card": {
|
|
7038
|
+
const db = getDatabase();
|
|
7039
|
+
const card = getContactCard(a.contact_id, db);
|
|
7040
|
+
return { content: [{ type: "text", text: JSON.stringify(card, null, 2) }] };
|
|
7041
|
+
}
|
|
7042
|
+
case "get_contact_brief": {
|
|
7043
|
+
const db = getDatabase();
|
|
7044
|
+
const taskContext = a.task_context ?? a.format;
|
|
7045
|
+
if (taskContext) {
|
|
7046
|
+
const brief2 = getContactBrief(a.contact_id, taskContext, db);
|
|
7047
|
+
return { content: [{ type: "text", text: JSON.stringify(brief2, null, 2) }] };
|
|
7048
|
+
}
|
|
7049
|
+
const brief = generateBrief(a.contact_id, db);
|
|
7050
|
+
return { content: [{ type: "text", text: JSON.stringify({ brief }, null, 2) }] };
|
|
7051
|
+
}
|
|
7052
|
+
case "assemble_context": {
|
|
7053
|
+
const db = getDatabase();
|
|
7054
|
+
const ctx = await assembleContext(a.contact_ids, a.format ?? "meeting_prep", db);
|
|
7055
|
+
return { content: [{ type: "text", text: JSON.stringify(ctx, null, 2) }] };
|
|
7056
|
+
}
|
|
7057
|
+
case "parse_email_signature": {
|
|
7058
|
+
const parsed = parseEmailSignature(a.signature_text);
|
|
7059
|
+
return { content: [{ type: "text", text: JSON.stringify(parsed, null, 2) }] };
|
|
7060
|
+
}
|
|
7061
|
+
case "ingest_email_participants": {
|
|
7062
|
+
const db = getDatabase();
|
|
7063
|
+
const participants = a.participants;
|
|
7064
|
+
const extracted = extractContactsFromEmailThread(participants);
|
|
7065
|
+
let created = 0;
|
|
7066
|
+
let updated = 0;
|
|
7067
|
+
const contacts = [];
|
|
7068
|
+
const { findOrCreateContact: findOrCreate } = await Promise.resolve().then(() => (init_contacts(), exports_contacts));
|
|
7069
|
+
for (const ci of extracted) {
|
|
7070
|
+
try {
|
|
7071
|
+
const result = await findOrCreate({
|
|
7072
|
+
display_name: ci.display_name,
|
|
7073
|
+
job_title: ci.job_title,
|
|
7074
|
+
website: ci.website,
|
|
7075
|
+
emails: ci.emails?.map((e) => ({ address: e.address, type: e.type, is_primary: e.is_primary })),
|
|
7076
|
+
phones: ci.phones?.map((p) => ({ number: p.number, type: p.type, is_primary: p.is_primary })),
|
|
7077
|
+
social_profiles: ci.social_profiles?.map((s) => ({ platform: "linkedin", url: s.url, is_primary: s.is_primary })),
|
|
7078
|
+
source: "import"
|
|
7079
|
+
}, db);
|
|
7080
|
+
contacts.push(result.contact);
|
|
7081
|
+
if (result.created)
|
|
7082
|
+
created++;
|
|
7083
|
+
else
|
|
7084
|
+
updated++;
|
|
7085
|
+
} catch {}
|
|
7086
|
+
}
|
|
7087
|
+
return { content: [{ type: "text", text: JSON.stringify({ created, updated, contacts }, null, 2) }] };
|
|
7088
|
+
}
|
|
7089
|
+
case "ingest_meeting_participants": {
|
|
7090
|
+
const db = getDatabase();
|
|
7091
|
+
const result = await ingestMeetingParticipants({
|
|
7092
|
+
title: a.title,
|
|
7093
|
+
event_date: a.event_date,
|
|
7094
|
+
attendees: a.attendees,
|
|
7095
|
+
context: a.context
|
|
7096
|
+
}, db);
|
|
7097
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
7098
|
+
}
|
|
7099
|
+
case "get_freshness_score": {
|
|
7100
|
+
const db = getDatabase();
|
|
7101
|
+
const score = getFreshnessScore(a.contact_id, db);
|
|
7102
|
+
return { content: [{ type: "text", text: JSON.stringify(score, null, 2) }] };
|
|
7103
|
+
}
|
|
7104
|
+
case "get_stale_contacts": {
|
|
7105
|
+
const db = getDatabase();
|
|
7106
|
+
const contacts = getStaleContacts(a.threshold ?? 40, db);
|
|
7107
|
+
return { content: [{ type: "text", text: JSON.stringify({ contacts }, null, 2) }] };
|
|
7108
|
+
}
|
|
7109
|
+
case "mark_field_verified": {
|
|
7110
|
+
const db = getDatabase();
|
|
7111
|
+
markFieldVerified(a.contact_id, a.field_name, a.source, db);
|
|
7112
|
+
return { content: [{ type: "text", text: JSON.stringify({ verified: true }) }] };
|
|
7113
|
+
}
|
|
7114
|
+
case "add_org_chart_edge": {
|
|
7115
|
+
const db = getDatabase();
|
|
7116
|
+
const edge = addOrgChartEdge(a.company_id, a.contact_a_id, a.contact_b_id, a.edge_type, false, db);
|
|
7117
|
+
return { content: [{ type: "text", text: JSON.stringify(edge, null, 2) }] };
|
|
7118
|
+
}
|
|
7119
|
+
case "get_org_chart": {
|
|
7120
|
+
const db = getDatabase();
|
|
7121
|
+
const edges = listOrgChart(a.company_id, db);
|
|
7122
|
+
return { content: [{ type: "text", text: JSON.stringify({ company_id: a.company_id, edges }, null, 2) }] };
|
|
7123
|
+
}
|
|
7124
|
+
case "set_deal_contact_role": {
|
|
7125
|
+
const db = getDatabase();
|
|
7126
|
+
const role = setDealContactRole(a.deal_id, a.contact_id, a.account_role, db);
|
|
7127
|
+
return { content: [{ type: "text", text: JSON.stringify(role, null, 2) }] };
|
|
7128
|
+
}
|
|
7129
|
+
case "get_deal_team": {
|
|
7130
|
+
const db = getDatabase();
|
|
7131
|
+
const team = getDealTeam(a.deal_id, db);
|
|
7132
|
+
return { content: [{ type: "text", text: JSON.stringify({ deal_id: a.deal_id, team }, null, 2) }] };
|
|
7133
|
+
}
|
|
7134
|
+
case "get_coverage_gaps": {
|
|
7135
|
+
const db = getDatabase();
|
|
7136
|
+
const gaps = getCoverageGaps(a.company_id, db);
|
|
7137
|
+
return { content: [{ type: "text", text: JSON.stringify(gaps, null, 2) }] };
|
|
7138
|
+
}
|
|
7139
|
+
case "get_recent_contact_events": {
|
|
7140
|
+
const db = getDatabase();
|
|
7141
|
+
const since = a.since;
|
|
7142
|
+
const eventTypes = a.event_types;
|
|
7143
|
+
let sql = `SELECT * FROM activity_log WHERE 1=1`;
|
|
7144
|
+
const params = [];
|
|
7145
|
+
if (since) {
|
|
7146
|
+
sql += ` AND created_at >= ?`;
|
|
7147
|
+
params.push(since);
|
|
7148
|
+
}
|
|
7149
|
+
if (eventTypes?.length) {
|
|
7150
|
+
sql += ` AND action IN (${eventTypes.map(() => "?").join(",")})`;
|
|
7151
|
+
params.push(...eventTypes);
|
|
7152
|
+
}
|
|
7153
|
+
sql += ` ORDER BY created_at DESC LIMIT 100`;
|
|
7154
|
+
const events = db.query(sql).all(...params);
|
|
7155
|
+
return { content: [{ type: "text", text: JSON.stringify({ events }, null, 2) }] };
|
|
7156
|
+
}
|
|
5854
7157
|
default:
|
|
5855
7158
|
return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
|
|
5856
7159
|
}
|