@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/index.js
CHANGED
|
@@ -269,6 +269,21 @@ var init_database = __esm(() => {
|
|
|
269
269
|
|
|
270
270
|
CREATE INDEX IF NOT EXISTS idx_company_relationships_contact ON company_relationships(contact_id);
|
|
271
271
|
CREATE INDEX IF NOT EXISTS idx_company_relationships_company ON company_relationships(company_id);
|
|
272
|
+
`,
|
|
273
|
+
`
|
|
274
|
+
ALTER TABLE groups ADD COLUMN project_id TEXT;
|
|
275
|
+
|
|
276
|
+
CREATE TABLE IF NOT EXISTS contact_projects (
|
|
277
|
+
contact_id TEXT NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
|
|
278
|
+
project_id TEXT NOT NULL,
|
|
279
|
+
PRIMARY KEY (contact_id, project_id)
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
CREATE INDEX IF NOT EXISTS idx_contact_projects_project ON contact_projects(project_id);
|
|
283
|
+
CREATE INDEX IF NOT EXISTS idx_contact_projects_contact ON contact_projects(contact_id);
|
|
284
|
+
|
|
285
|
+
INSERT OR IGNORE INTO contact_projects (contact_id, project_id)
|
|
286
|
+
SELECT id, project_id FROM contacts WHERE project_id IS NOT NULL;
|
|
272
287
|
`,
|
|
273
288
|
`
|
|
274
289
|
CREATE TABLE IF NOT EXISTS contact_notes (
|
|
@@ -392,6 +407,142 @@ var init_database = __esm(() => {
|
|
|
392
407
|
deal_id TEXT REFERENCES deals(id) ON DELETE SET NULL,
|
|
393
408
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
394
409
|
);
|
|
410
|
+
`,
|
|
411
|
+
`
|
|
412
|
+
-- CON-00069: temporal field history
|
|
413
|
+
CREATE TABLE IF NOT EXISTS contact_field_history (
|
|
414
|
+
id TEXT PRIMARY KEY,
|
|
415
|
+
contact_id TEXT NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
|
|
416
|
+
field_name TEXT NOT NULL,
|
|
417
|
+
old_value TEXT,
|
|
418
|
+
new_value TEXT,
|
|
419
|
+
valid_from TEXT NOT NULL DEFAULT (datetime('now')),
|
|
420
|
+
source TEXT,
|
|
421
|
+
confidence TEXT NOT NULL DEFAULT 'imported' CHECK(confidence IN ('verified','inferred','imported','stale')),
|
|
422
|
+
created_by TEXT,
|
|
423
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
-- CON-00070: job history
|
|
427
|
+
CREATE TABLE IF NOT EXISTS job_history (
|
|
428
|
+
id TEXT PRIMARY KEY,
|
|
429
|
+
contact_id TEXT NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
|
|
430
|
+
company_id TEXT REFERENCES companies(id) ON DELETE SET NULL,
|
|
431
|
+
company_name TEXT NOT NULL,
|
|
432
|
+
title TEXT,
|
|
433
|
+
start_date TEXT,
|
|
434
|
+
end_date TEXT,
|
|
435
|
+
is_current INTEGER NOT NULL DEFAULT 0,
|
|
436
|
+
inferred INTEGER NOT NULL DEFAULT 0,
|
|
437
|
+
source TEXT,
|
|
438
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
439
|
+
);
|
|
440
|
+
|
|
441
|
+
-- CON-00071: learnings
|
|
442
|
+
CREATE TABLE IF NOT EXISTS contact_learnings (
|
|
443
|
+
id TEXT PRIMARY KEY,
|
|
444
|
+
contact_id TEXT NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
|
|
445
|
+
content TEXT NOT NULL,
|
|
446
|
+
type TEXT NOT NULL DEFAULT 'fact' CHECK(type IN ('preference','fact','inference','warning','signal')),
|
|
447
|
+
confidence INTEGER NOT NULL DEFAULT 70 CHECK(confidence BETWEEN 0 AND 100),
|
|
448
|
+
importance INTEGER NOT NULL DEFAULT 5 CHECK(importance BETWEEN 1 AND 10),
|
|
449
|
+
learned_by TEXT,
|
|
450
|
+
session_id TEXT,
|
|
451
|
+
visibility TEXT NOT NULL DEFAULT 'shared' CHECK(visibility IN ('private','shared','human')),
|
|
452
|
+
tags TEXT NOT NULL DEFAULT '[]',
|
|
453
|
+
confirmed_count INTEGER NOT NULL DEFAULT 0,
|
|
454
|
+
contradicts_id TEXT REFERENCES contact_learnings(id) ON DELETE SET NULL,
|
|
455
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
456
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
457
|
+
);
|
|
458
|
+
|
|
459
|
+
-- CON-00072: coordination
|
|
460
|
+
CREATE TABLE IF NOT EXISTS contact_locks (
|
|
461
|
+
id TEXT PRIMARY KEY,
|
|
462
|
+
contact_id TEXT NOT NULL UNIQUE REFERENCES contacts(id) ON DELETE CASCADE,
|
|
463
|
+
agent_name TEXT NOT NULL,
|
|
464
|
+
reason TEXT,
|
|
465
|
+
acquired_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
466
|
+
expires_at TEXT NOT NULL,
|
|
467
|
+
session_id TEXT
|
|
468
|
+
);
|
|
469
|
+
CREATE TABLE IF NOT EXISTS contact_agent_activity (
|
|
470
|
+
id TEXT PRIMARY KEY,
|
|
471
|
+
contact_id TEXT NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
|
|
472
|
+
agent_name TEXT NOT NULL,
|
|
473
|
+
action TEXT NOT NULL,
|
|
474
|
+
details TEXT,
|
|
475
|
+
session_id TEXT,
|
|
476
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
477
|
+
);
|
|
478
|
+
|
|
479
|
+
-- CON-00073: relationship graph extra columns
|
|
480
|
+
ALTER TABLE contact_relationships ADD COLUMN strength_score INTEGER NOT NULL DEFAULT 50;
|
|
481
|
+
ALTER TABLE contact_relationships ADD COLUMN interaction_count INTEGER NOT NULL DEFAULT 0;
|
|
482
|
+
ALTER TABLE contact_relationships ADD COLUMN last_interaction TEXT;
|
|
483
|
+
ALTER TABLE contact_relationships ADD COLUMN relationship_status TEXT NOT NULL DEFAULT 'stable' CHECK(relationship_status IN ('warming','stable','cooling','ghost'));
|
|
484
|
+
|
|
485
|
+
-- CON-00074: identity resolution
|
|
486
|
+
CREATE TABLE IF NOT EXISTS contact_identities (
|
|
487
|
+
id TEXT PRIMARY KEY,
|
|
488
|
+
contact_id TEXT NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
|
|
489
|
+
system TEXT NOT NULL,
|
|
490
|
+
external_id TEXT NOT NULL,
|
|
491
|
+
external_url TEXT,
|
|
492
|
+
confidence TEXT NOT NULL DEFAULT 'inferred' CHECK(confidence IN ('verified','inferred')),
|
|
493
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
494
|
+
UNIQUE(system, external_id)
|
|
495
|
+
);
|
|
496
|
+
ALTER TABLE contacts ADD COLUMN canonical_id TEXT;
|
|
497
|
+
|
|
498
|
+
-- CON-00076: relationship signals
|
|
499
|
+
ALTER TABLE contacts ADD COLUMN relationship_health INTEGER NOT NULL DEFAULT 50;
|
|
500
|
+
ALTER TABLE contacts ADD COLUMN avg_response_hours REAL;
|
|
501
|
+
ALTER TABLE contacts ADD COLUMN preferred_channel TEXT;
|
|
502
|
+
ALTER TABLE contacts ADD COLUMN engagement_status TEXT NOT NULL DEFAULT 'new' CHECK(engagement_status IN ('warming','stable','cooling','ghost','new'));
|
|
503
|
+
ALTER TABLE contacts ADD COLUMN interaction_count_30d INTEGER NOT NULL DEFAULT 0;
|
|
504
|
+
ALTER TABLE contacts ADD COLUMN interaction_count_90d INTEGER NOT NULL DEFAULT 0;
|
|
505
|
+
|
|
506
|
+
-- CON-00079: freshness scoring
|
|
507
|
+
CREATE TABLE IF NOT EXISTS contact_field_confidence (
|
|
508
|
+
id TEXT PRIMARY KEY,
|
|
509
|
+
contact_id TEXT NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
|
|
510
|
+
field_name TEXT NOT NULL,
|
|
511
|
+
confidence TEXT NOT NULL DEFAULT 'imported' CHECK(confidence IN ('verified','inferred','imported','stale')),
|
|
512
|
+
source TEXT,
|
|
513
|
+
last_verified_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
514
|
+
UNIQUE(contact_id, field_name)
|
|
515
|
+
);
|
|
516
|
+
|
|
517
|
+
-- CON-00080: org chart
|
|
518
|
+
CREATE TABLE IF NOT EXISTS org_chart_edges (
|
|
519
|
+
id TEXT PRIMARY KEY,
|
|
520
|
+
company_id TEXT NOT NULL REFERENCES companies(id) ON DELETE CASCADE,
|
|
521
|
+
contact_a_id TEXT NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
|
|
522
|
+
contact_b_id TEXT NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
|
|
523
|
+
edge_type TEXT NOT NULL CHECK(edge_type IN ('reports_to','manages','collaborates_with','peer')),
|
|
524
|
+
inferred INTEGER NOT NULL DEFAULT 0,
|
|
525
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
526
|
+
UNIQUE(company_id, contact_a_id, contact_b_id, edge_type)
|
|
527
|
+
);
|
|
528
|
+
CREATE TABLE IF NOT EXISTS deal_contact_roles (
|
|
529
|
+
id TEXT PRIMARY KEY,
|
|
530
|
+
deal_id TEXT NOT NULL REFERENCES deals(id) ON DELETE CASCADE,
|
|
531
|
+
contact_id TEXT NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
|
|
532
|
+
account_role TEXT NOT NULL CHECK(account_role IN ('economic_buyer','technical_evaluator','champion','blocker','influencer','user','sponsor','other')),
|
|
533
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
534
|
+
UNIQUE(deal_id, contact_id)
|
|
535
|
+
);
|
|
536
|
+
|
|
537
|
+
-- CON-00075: embeddings
|
|
538
|
+
CREATE TABLE IF NOT EXISTS contact_embeddings (
|
|
539
|
+
contact_id TEXT PRIMARY KEY REFERENCES contacts(id) ON DELETE CASCADE,
|
|
540
|
+
embedding TEXT NOT NULL,
|
|
541
|
+
model TEXT NOT NULL DEFAULT 'tfidf',
|
|
542
|
+
embedded_text TEXT,
|
|
543
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
544
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
545
|
+
);
|
|
395
546
|
`
|
|
396
547
|
];
|
|
397
548
|
});
|
|
@@ -465,14 +616,20 @@ var init_activity = __esm(() => {
|
|
|
465
616
|
var exports_contacts = {};
|
|
466
617
|
__export(exports_contacts, {
|
|
467
618
|
updateContact: () => updateContact,
|
|
619
|
+
unlinkContactFromProject: () => unlinkContactFromProject,
|
|
468
620
|
unarchiveContact: () => unarchiveContact,
|
|
621
|
+
setContactProjects: () => setContactProjects,
|
|
469
622
|
searchContacts: () => searchContacts,
|
|
470
623
|
mergeContacts: () => mergeContacts,
|
|
471
624
|
listRecentContacts: () => listRecentContacts,
|
|
472
625
|
listContacts: () => listContacts,
|
|
626
|
+
listContactIdsByProject: () => listContactIdsByProject,
|
|
473
627
|
listColdContacts: () => listColdContacts,
|
|
628
|
+
linkContactToProject: () => linkContactToProject,
|
|
629
|
+
getContactProjectIds: () => getContactProjectIds,
|
|
474
630
|
getContactByEmail: () => getContactByEmail,
|
|
475
631
|
getContact: () => getContact,
|
|
632
|
+
findOrCreateContact: () => findOrCreateContact,
|
|
476
633
|
deleteContact: () => deleteContact,
|
|
477
634
|
createContact: () => createContact,
|
|
478
635
|
autoLinkContactToCompany: () => autoLinkContactToCompany,
|
|
@@ -1002,6 +1159,25 @@ function listColdContacts(days, db) {
|
|
|
1002
1159
|
LIMIT 100`).all(`-${days}`);
|
|
1003
1160
|
return rows.map((row) => loadContactDetails(d, rowToContact(row)));
|
|
1004
1161
|
}
|
|
1162
|
+
async function findOrCreateContact(input, db) {
|
|
1163
|
+
const d = db || getDatabase();
|
|
1164
|
+
const emailAddresses = (input.emails ?? []).map((e) => e.address);
|
|
1165
|
+
for (const addr of emailAddresses) {
|
|
1166
|
+
const emailRow = d.query(`SELECT contact_id FROM emails WHERE LOWER(address) = LOWER(?) AND contact_id IS NOT NULL LIMIT 1`).get(addr);
|
|
1167
|
+
if (emailRow) {
|
|
1168
|
+
return { contact: getContact(emailRow.contact_id, d), created: false };
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
const nameQuery = input.display_name ?? (input.first_name || input.last_name ? `${input.first_name ?? ""} ${input.last_name ?? ""}`.trim() : null);
|
|
1172
|
+
if (nameQuery) {
|
|
1173
|
+
const results = searchContacts(nameQuery, d);
|
|
1174
|
+
if (results.length > 0 && results[0]) {
|
|
1175
|
+
return { contact: results[0], created: false };
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
const contact = createContact(input, d);
|
|
1179
|
+
return { contact, created: true };
|
|
1180
|
+
}
|
|
1005
1181
|
function autoLinkContactToCompany(contactId, db) {
|
|
1006
1182
|
const d = db || getDatabase();
|
|
1007
1183
|
const row = d.query(`SELECT * FROM contacts WHERE id = ?`).get(contactId);
|
|
@@ -1021,12 +1197,127 @@ function autoLinkContactToCompany(contactId, db) {
|
|
|
1021
1197
|
const updated = d.query(`SELECT * FROM contacts WHERE id = ?`).get(contactId);
|
|
1022
1198
|
return loadContactDetails(d, rowToContact(updated));
|
|
1023
1199
|
}
|
|
1200
|
+
function linkContactToProject(contactId, projectId, db) {
|
|
1201
|
+
const d = db || getDatabase();
|
|
1202
|
+
d.run(`INSERT OR IGNORE INTO contact_projects (contact_id, project_id) VALUES (?, ?)`, [contactId, projectId]);
|
|
1203
|
+
}
|
|
1204
|
+
function unlinkContactFromProject(contactId, projectId, db) {
|
|
1205
|
+
const d = db || getDatabase();
|
|
1206
|
+
d.run(`DELETE FROM contact_projects WHERE contact_id = ? AND project_id = ?`, [contactId, projectId]);
|
|
1207
|
+
}
|
|
1208
|
+
function getContactProjectIds(contactId, db) {
|
|
1209
|
+
const d = db || getDatabase();
|
|
1210
|
+
const rows = d.query(`SELECT project_id FROM contact_projects WHERE contact_id = ?`).all(contactId);
|
|
1211
|
+
return rows.map((r) => r.project_id);
|
|
1212
|
+
}
|
|
1213
|
+
function listContactIdsByProject(projectId, db) {
|
|
1214
|
+
const d = db || getDatabase();
|
|
1215
|
+
const rows = d.query(`SELECT contact_id FROM contact_projects WHERE project_id = ?`).all(projectId);
|
|
1216
|
+
return rows.map((r) => r.contact_id);
|
|
1217
|
+
}
|
|
1218
|
+
function setContactProjects(contactId, projectIds, db) {
|
|
1219
|
+
const d = db || getDatabase();
|
|
1220
|
+
d.run(`DELETE FROM contact_projects WHERE contact_id = ?`, [contactId]);
|
|
1221
|
+
for (const pid of projectIds) {
|
|
1222
|
+
d.run(`INSERT OR IGNORE INTO contact_projects (contact_id, project_id) VALUES (?, ?)`, [contactId, pid]);
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1024
1225
|
var init_contacts = __esm(() => {
|
|
1025
1226
|
init_types();
|
|
1026
1227
|
init_database();
|
|
1027
1228
|
init_activity();
|
|
1028
1229
|
});
|
|
1029
1230
|
|
|
1231
|
+
// src/db/events.ts
|
|
1232
|
+
var exports_events = {};
|
|
1233
|
+
__export(exports_events, {
|
|
1234
|
+
logEvent: () => logEvent,
|
|
1235
|
+
listEvents: () => listEvents,
|
|
1236
|
+
getEvent: () => getEvent,
|
|
1237
|
+
deleteEvent: () => deleteEvent
|
|
1238
|
+
});
|
|
1239
|
+
function rowToEvent(row) {
|
|
1240
|
+
let contact_ids = [];
|
|
1241
|
+
try {
|
|
1242
|
+
contact_ids = JSON.parse(row.contact_ids);
|
|
1243
|
+
} catch {
|
|
1244
|
+
contact_ids = [];
|
|
1245
|
+
}
|
|
1246
|
+
return {
|
|
1247
|
+
id: row.id,
|
|
1248
|
+
title: row.title,
|
|
1249
|
+
type: row.type,
|
|
1250
|
+
event_date: row.event_date,
|
|
1251
|
+
duration_min: row.duration_min,
|
|
1252
|
+
contact_ids,
|
|
1253
|
+
company_id: row.company_id,
|
|
1254
|
+
notes: row.notes,
|
|
1255
|
+
outcome: row.outcome,
|
|
1256
|
+
deal_id: row.deal_id,
|
|
1257
|
+
created_at: row.created_at
|
|
1258
|
+
};
|
|
1259
|
+
}
|
|
1260
|
+
function logEvent(input, db) {
|
|
1261
|
+
const d = db || getDatabase();
|
|
1262
|
+
const id = uuid();
|
|
1263
|
+
const timestamp = now();
|
|
1264
|
+
d.run(`INSERT INTO events (id, title, type, event_date, duration_min, contact_ids, company_id, notes, outcome, deal_id, created_at)
|
|
1265
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
1266
|
+
id,
|
|
1267
|
+
input.title,
|
|
1268
|
+
input.type ?? "meeting",
|
|
1269
|
+
input.event_date,
|
|
1270
|
+
input.duration_min ?? null,
|
|
1271
|
+
JSON.stringify(input.contact_ids ?? []),
|
|
1272
|
+
input.company_id ?? null,
|
|
1273
|
+
input.notes ?? null,
|
|
1274
|
+
input.outcome ?? null,
|
|
1275
|
+
input.deal_id ?? null,
|
|
1276
|
+
timestamp
|
|
1277
|
+
]);
|
|
1278
|
+
return rowToEvent(d.query(`SELECT * FROM events WHERE id = ?`).get(id));
|
|
1279
|
+
}
|
|
1280
|
+
function getEvent(id, db) {
|
|
1281
|
+
const d = db || getDatabase();
|
|
1282
|
+
const row = d.query(`SELECT * FROM events WHERE id = ?`).get(id);
|
|
1283
|
+
return row ? rowToEvent(row) : null;
|
|
1284
|
+
}
|
|
1285
|
+
function listEvents(opts = {}, db) {
|
|
1286
|
+
const d = db || getDatabase();
|
|
1287
|
+
const conditions = [];
|
|
1288
|
+
const params = [];
|
|
1289
|
+
if (opts.contact_id) {
|
|
1290
|
+
conditions.push("contact_ids LIKE ?");
|
|
1291
|
+
params.push(`%${opts.contact_id}%`);
|
|
1292
|
+
}
|
|
1293
|
+
if (opts.company_id) {
|
|
1294
|
+
conditions.push("company_id = ?");
|
|
1295
|
+
params.push(opts.company_id);
|
|
1296
|
+
}
|
|
1297
|
+
if (opts.type) {
|
|
1298
|
+
conditions.push("type = ?");
|
|
1299
|
+
params.push(opts.type);
|
|
1300
|
+
}
|
|
1301
|
+
if (opts.date_from) {
|
|
1302
|
+
conditions.push("event_date >= ?");
|
|
1303
|
+
params.push(opts.date_from);
|
|
1304
|
+
}
|
|
1305
|
+
if (opts.date_to) {
|
|
1306
|
+
conditions.push("event_date <= ?");
|
|
1307
|
+
params.push(opts.date_to);
|
|
1308
|
+
}
|
|
1309
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1310
|
+
const rows = d.query(`SELECT * FROM events ${where} ORDER BY event_date DESC`).all(...params);
|
|
1311
|
+
return rows.map(rowToEvent);
|
|
1312
|
+
}
|
|
1313
|
+
function deleteEvent(id, db) {
|
|
1314
|
+
const d = db || getDatabase();
|
|
1315
|
+
d.run(`DELETE FROM events WHERE id = ?`, [id]);
|
|
1316
|
+
}
|
|
1317
|
+
var init_events = __esm(() => {
|
|
1318
|
+
init_database();
|
|
1319
|
+
});
|
|
1320
|
+
|
|
1030
1321
|
// src/index.ts
|
|
1031
1322
|
init_database();
|
|
1032
1323
|
init_contacts();
|
|
@@ -2204,86 +2495,10 @@ function getDealsByStage(db) {
|
|
|
2204
2495
|
}
|
|
2205
2496
|
return result;
|
|
2206
2497
|
}
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
try {
|
|
2212
|
-
contact_ids = JSON.parse(row.contact_ids);
|
|
2213
|
-
} catch {
|
|
2214
|
-
contact_ids = [];
|
|
2215
|
-
}
|
|
2216
|
-
return {
|
|
2217
|
-
id: row.id,
|
|
2218
|
-
title: row.title,
|
|
2219
|
-
type: row.type,
|
|
2220
|
-
event_date: row.event_date,
|
|
2221
|
-
duration_min: row.duration_min,
|
|
2222
|
-
contact_ids,
|
|
2223
|
-
company_id: row.company_id,
|
|
2224
|
-
notes: row.notes,
|
|
2225
|
-
outcome: row.outcome,
|
|
2226
|
-
deal_id: row.deal_id,
|
|
2227
|
-
created_at: row.created_at
|
|
2228
|
-
};
|
|
2229
|
-
}
|
|
2230
|
-
function logEvent(input, db) {
|
|
2231
|
-
const d = db || getDatabase();
|
|
2232
|
-
const id = uuid();
|
|
2233
|
-
const timestamp = now();
|
|
2234
|
-
d.run(`INSERT INTO events (id, title, type, event_date, duration_min, contact_ids, company_id, notes, outcome, deal_id, created_at)
|
|
2235
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
2236
|
-
id,
|
|
2237
|
-
input.title,
|
|
2238
|
-
input.type ?? "meeting",
|
|
2239
|
-
input.event_date,
|
|
2240
|
-
input.duration_min ?? null,
|
|
2241
|
-
JSON.stringify(input.contact_ids ?? []),
|
|
2242
|
-
input.company_id ?? null,
|
|
2243
|
-
input.notes ?? null,
|
|
2244
|
-
input.outcome ?? null,
|
|
2245
|
-
input.deal_id ?? null,
|
|
2246
|
-
timestamp
|
|
2247
|
-
]);
|
|
2248
|
-
return rowToEvent(d.query(`SELECT * FROM events WHERE id = ?`).get(id));
|
|
2249
|
-
}
|
|
2250
|
-
function getEvent(id, db) {
|
|
2251
|
-
const d = db || getDatabase();
|
|
2252
|
-
const row = d.query(`SELECT * FROM events WHERE id = ?`).get(id);
|
|
2253
|
-
return row ? rowToEvent(row) : null;
|
|
2254
|
-
}
|
|
2255
|
-
function listEvents(opts = {}, db) {
|
|
2256
|
-
const d = db || getDatabase();
|
|
2257
|
-
const conditions = [];
|
|
2258
|
-
const params = [];
|
|
2259
|
-
if (opts.contact_id) {
|
|
2260
|
-
conditions.push("contact_ids LIKE ?");
|
|
2261
|
-
params.push(`%${opts.contact_id}%`);
|
|
2262
|
-
}
|
|
2263
|
-
if (opts.company_id) {
|
|
2264
|
-
conditions.push("company_id = ?");
|
|
2265
|
-
params.push(opts.company_id);
|
|
2266
|
-
}
|
|
2267
|
-
if (opts.type) {
|
|
2268
|
-
conditions.push("type = ?");
|
|
2269
|
-
params.push(opts.type);
|
|
2270
|
-
}
|
|
2271
|
-
if (opts.date_from) {
|
|
2272
|
-
conditions.push("event_date >= ?");
|
|
2273
|
-
params.push(opts.date_from);
|
|
2274
|
-
}
|
|
2275
|
-
if (opts.date_to) {
|
|
2276
|
-
conditions.push("event_date <= ?");
|
|
2277
|
-
params.push(opts.date_to);
|
|
2278
|
-
}
|
|
2279
|
-
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
2280
|
-
const rows = d.query(`SELECT * FROM events ${where} ORDER BY event_date DESC`).all(...params);
|
|
2281
|
-
return rows.map(rowToEvent);
|
|
2282
|
-
}
|
|
2283
|
-
function deleteEvent(id, db) {
|
|
2284
|
-
const d = db || getDatabase();
|
|
2285
|
-
d.run(`DELETE FROM events WHERE id = ?`, [id]);
|
|
2286
|
-
}
|
|
2498
|
+
|
|
2499
|
+
// src/index.ts
|
|
2500
|
+
init_events();
|
|
2501
|
+
|
|
2287
2502
|
// src/db/notes.ts
|
|
2288
2503
|
init_types();
|
|
2289
2504
|
init_database();
|
|
@@ -2316,17 +2531,19 @@ function getNote(noteId, db) {
|
|
|
2316
2531
|
init_database();
|
|
2317
2532
|
function createGroup(db, input) {
|
|
2318
2533
|
const id = uuid();
|
|
2319
|
-
db.query(`INSERT INTO groups(id, name, description, created_at, updated_at) VALUES(
|
|
2534
|
+
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());
|
|
2320
2535
|
return getGroup(db, id);
|
|
2321
2536
|
}
|
|
2322
2537
|
function getGroup(db, id) {
|
|
2323
2538
|
return db.query(`SELECT * FROM groups WHERE id = ?`).get(id);
|
|
2324
2539
|
}
|
|
2325
|
-
function listGroups(db) {
|
|
2540
|
+
function listGroups(db, projectId) {
|
|
2541
|
+
const where = projectId ? "WHERE g.project_id = ?" : "";
|
|
2542
|
+
const params = projectId ? [projectId] : [];
|
|
2326
2543
|
return db.query(`SELECT g.*,
|
|
2327
2544
|
(SELECT COUNT(*) FROM contact_groups cg WHERE cg.group_id = g.id) as member_count,
|
|
2328
2545
|
(SELECT COUNT(*) FROM company_groups cog WHERE cog.group_id = g.id) as company_count
|
|
2329
|
-
FROM groups g ORDER BY g.name`).all();
|
|
2546
|
+
FROM groups g ${where} ORDER BY g.name`).all(...params);
|
|
2330
2547
|
}
|
|
2331
2548
|
function updateGroup(db, id, input) {
|
|
2332
2549
|
const fields = [];
|
|
@@ -2339,6 +2556,10 @@ function updateGroup(db, id, input) {
|
|
|
2339
2556
|
fields.push("description = ?");
|
|
2340
2557
|
vals.push(input.description ?? null);
|
|
2341
2558
|
}
|
|
2559
|
+
if (input.project_id !== undefined) {
|
|
2560
|
+
fields.push("project_id = ?");
|
|
2561
|
+
vals.push(input.project_id ?? null);
|
|
2562
|
+
}
|
|
2342
2563
|
fields.push("updated_at = ?");
|
|
2343
2564
|
vals.push(now());
|
|
2344
2565
|
vals.push(id);
|
|
@@ -3476,6 +3697,683 @@ async function pullGoogleContactsAsInputs(opts = {}) {
|
|
|
3476
3697
|
|
|
3477
3698
|
// src/index.ts
|
|
3478
3699
|
init_types();
|
|
3700
|
+
|
|
3701
|
+
// src/db/field-history.ts
|
|
3702
|
+
init_database();
|
|
3703
|
+
function recordFieldChange(contactId, fieldName, oldValue, newValue, source, createdBy, db) {
|
|
3704
|
+
const _db2 = db || getDatabase();
|
|
3705
|
+
_db2.query(`INSERT INTO contact_field_history(id,contact_id,field_name,old_value,new_value,valid_from,source,created_by,created_at) VALUES(?,?,?,?,?,?,?,?,?)`).run(uuid(), contactId, fieldName, oldValue != null ? String(oldValue) : null, newValue != null ? String(newValue) : null, now(), source || null, createdBy || null, now());
|
|
3706
|
+
}
|
|
3707
|
+
function getFieldHistory(contactId, fieldName, db) {
|
|
3708
|
+
const _db2 = db || getDatabase();
|
|
3709
|
+
if (fieldName) {
|
|
3710
|
+
return _db2.query(`SELECT * FROM contact_field_history WHERE contact_id=? AND field_name=? ORDER BY valid_from DESC`).all(contactId, fieldName);
|
|
3711
|
+
}
|
|
3712
|
+
return _db2.query(`SELECT * FROM contact_field_history WHERE contact_id=? ORDER BY valid_from DESC`).all(contactId);
|
|
3713
|
+
}
|
|
3714
|
+
function getContactAt(contactId, timestamp, db) {
|
|
3715
|
+
const _db2 = db || getDatabase();
|
|
3716
|
+
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);
|
|
3717
|
+
const result = {};
|
|
3718
|
+
for (const r of rows) {
|
|
3719
|
+
if (r.new_value != null)
|
|
3720
|
+
result[r.field_name] = r.new_value;
|
|
3721
|
+
}
|
|
3722
|
+
return result;
|
|
3723
|
+
}
|
|
3724
|
+
// src/db/job-history.ts
|
|
3725
|
+
init_database();
|
|
3726
|
+
function rowToJob(r) {
|
|
3727
|
+
return { ...r, is_current: !!r["is_current"], inferred: !!r["inferred"] };
|
|
3728
|
+
}
|
|
3729
|
+
function addJobEntry(contactId, input, db) {
|
|
3730
|
+
const _db2 = db || getDatabase();
|
|
3731
|
+
if (input.is_current) {
|
|
3732
|
+
_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);
|
|
3733
|
+
}
|
|
3734
|
+
const id = uuid();
|
|
3735
|
+
_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());
|
|
3736
|
+
return rowToJob(_db2.query(`SELECT * FROM job_history WHERE id=?`).get(id));
|
|
3737
|
+
}
|
|
3738
|
+
function getJobHistory(contactId, db) {
|
|
3739
|
+
const _db2 = db || getDatabase();
|
|
3740
|
+
return _db2.query(`SELECT * FROM job_history WHERE contact_id=? ORDER BY is_current DESC, start_date DESC`).all(contactId).map(rowToJob);
|
|
3741
|
+
}
|
|
3742
|
+
function getCurrentRole(contactId, db) {
|
|
3743
|
+
const _db2 = db || getDatabase();
|
|
3744
|
+
const r = _db2.query(`SELECT * FROM job_history WHERE contact_id=? AND is_current=1`).get(contactId);
|
|
3745
|
+
return r ? rowToJob(r) : null;
|
|
3746
|
+
}
|
|
3747
|
+
function getPreviousEmployers(contactId, db) {
|
|
3748
|
+
const _db2 = db || getDatabase();
|
|
3749
|
+
return _db2.query(`SELECT * FROM job_history WHERE contact_id=? AND is_current=0 ORDER BY start_date DESC`).all(contactId).map(rowToJob);
|
|
3750
|
+
}
|
|
3751
|
+
// src/db/learnings.ts
|
|
3752
|
+
init_database();
|
|
3753
|
+
function rowToLearning(r) {
|
|
3754
|
+
return { ...r, tags: JSON.parse(r["tags"] || "[]") };
|
|
3755
|
+
}
|
|
3756
|
+
function saveLearning(contactId, input, db) {
|
|
3757
|
+
const _db2 = db || getDatabase();
|
|
3758
|
+
const id = uuid();
|
|
3759
|
+
_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());
|
|
3760
|
+
return rowToLearning(_db2.query(`SELECT * FROM contact_learnings WHERE id=?`).get(id));
|
|
3761
|
+
}
|
|
3762
|
+
function getLearnings(contactId, opts = {}, db) {
|
|
3763
|
+
const _db2 = db || getDatabase();
|
|
3764
|
+
let sql = `SELECT * FROM contact_learnings WHERE contact_id=?`;
|
|
3765
|
+
const params = [contactId];
|
|
3766
|
+
if (opts.type) {
|
|
3767
|
+
sql += ` AND type=?`;
|
|
3768
|
+
params.push(opts.type);
|
|
3769
|
+
}
|
|
3770
|
+
if (opts.min_importance) {
|
|
3771
|
+
sql += ` AND importance>=?`;
|
|
3772
|
+
params.push(opts.min_importance);
|
|
3773
|
+
}
|
|
3774
|
+
if (opts.visibility) {
|
|
3775
|
+
sql += ` AND visibility=?`;
|
|
3776
|
+
params.push(opts.visibility);
|
|
3777
|
+
}
|
|
3778
|
+
sql += ` ORDER BY importance DESC, confidence DESC`;
|
|
3779
|
+
return _db2.query(sql).all(...params).map(rowToLearning);
|
|
3780
|
+
}
|
|
3781
|
+
function searchLearnings(query, opts = {}, db) {
|
|
3782
|
+
const _db2 = db || getDatabase();
|
|
3783
|
+
let sql = `SELECT * FROM contact_learnings WHERE content LIKE ?`;
|
|
3784
|
+
const params = [`%${query}%`];
|
|
3785
|
+
if (opts.type) {
|
|
3786
|
+
sql += ` AND type=?`;
|
|
3787
|
+
params.push(opts.type);
|
|
3788
|
+
}
|
|
3789
|
+
if (opts.contact_id) {
|
|
3790
|
+
sql += ` AND contact_id=?`;
|
|
3791
|
+
params.push(opts.contact_id);
|
|
3792
|
+
}
|
|
3793
|
+
sql += ` ORDER BY importance DESC, confidence DESC LIMIT 50`;
|
|
3794
|
+
return _db2.query(sql).all(...params).map(rowToLearning);
|
|
3795
|
+
}
|
|
3796
|
+
function confirmLearning(learningId, _agentName, db) {
|
|
3797
|
+
const _db2 = db || getDatabase();
|
|
3798
|
+
_db2.query(`UPDATE contact_learnings SET confirmed_count=confirmed_count+1, confidence=MIN(100,confidence+10), updated_at=? WHERE id=?`).run(now(), learningId);
|
|
3799
|
+
}
|
|
3800
|
+
function decayLearnings(db) {
|
|
3801
|
+
const _db2 = db || getDatabase();
|
|
3802
|
+
const cutoff = new Date(Date.now() - 30 * 86400000).toISOString();
|
|
3803
|
+
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);
|
|
3804
|
+
return result.changes || 0;
|
|
3805
|
+
}
|
|
3806
|
+
function deleteLearning(learningId, db) {
|
|
3807
|
+
const _db2 = db || getDatabase();
|
|
3808
|
+
_db2.query(`DELETE FROM contact_learnings WHERE id=?`).run(learningId);
|
|
3809
|
+
}
|
|
3810
|
+
// src/db/coordination.ts
|
|
3811
|
+
init_database();
|
|
3812
|
+
function acquireLock(contactId, agentName, ttlSeconds = 300, reason, sessionId, db) {
|
|
3813
|
+
const _db2 = db || getDatabase();
|
|
3814
|
+
cleanExpiredLocks(_db2);
|
|
3815
|
+
const existing = _db2.query(`SELECT * FROM contact_locks WHERE contact_id=?`).get(contactId);
|
|
3816
|
+
if (existing)
|
|
3817
|
+
return { acquired: false, held_by: existing.agent_name, lock: existing };
|
|
3818
|
+
const id = uuid();
|
|
3819
|
+
const expiresAt = new Date(Date.now() + ttlSeconds * 1000).toISOString();
|
|
3820
|
+
_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);
|
|
3821
|
+
return {
|
|
3822
|
+
acquired: true,
|
|
3823
|
+
lock: _db2.query(`SELECT * FROM contact_locks WHERE id=?`).get(id)
|
|
3824
|
+
};
|
|
3825
|
+
}
|
|
3826
|
+
function releaseLock(contactId, agentName, db) {
|
|
3827
|
+
const _db2 = db || getDatabase();
|
|
3828
|
+
const result = _db2.query(`DELETE FROM contact_locks WHERE contact_id=? AND agent_name=?`).run(contactId, agentName);
|
|
3829
|
+
return (result.changes || 0) > 0;
|
|
3830
|
+
}
|
|
3831
|
+
function checkLock(contactId, db) {
|
|
3832
|
+
const _db2 = db || getDatabase();
|
|
3833
|
+
cleanExpiredLocks(_db2);
|
|
3834
|
+
return _db2.query(`SELECT * FROM contact_locks WHERE contact_id=?`).get(contactId);
|
|
3835
|
+
}
|
|
3836
|
+
function cleanExpiredLocks(db) {
|
|
3837
|
+
const _db2 = db || getDatabase();
|
|
3838
|
+
_db2.query(`DELETE FROM contact_locks WHERE expires_at<?`).run(now());
|
|
3839
|
+
}
|
|
3840
|
+
function logAgentActivity(contactId, agentName, action, details, sessionId, db) {
|
|
3841
|
+
const _db2 = db || getDatabase();
|
|
3842
|
+
_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());
|
|
3843
|
+
}
|
|
3844
|
+
function getAgentActivity(contactId, limit = 20, db) {
|
|
3845
|
+
const _db2 = db || getDatabase();
|
|
3846
|
+
return _db2.query(`SELECT * FROM contact_agent_activity WHERE contact_id=? ORDER BY created_at DESC LIMIT ?`).all(contactId, limit);
|
|
3847
|
+
}
|
|
3848
|
+
// src/db/graph.ts
|
|
3849
|
+
init_database();
|
|
3850
|
+
function computeRelationshipStrength(contactId, db) {
|
|
3851
|
+
const _db2 = db || getDatabase();
|
|
3852
|
+
const contact = _db2.query(`SELECT last_contacted_at, interaction_count_30d, interaction_count_90d FROM contacts WHERE id=?`).get(contactId);
|
|
3853
|
+
if (!contact)
|
|
3854
|
+
return 0;
|
|
3855
|
+
let score = 50;
|
|
3856
|
+
if (contact.last_contacted_at) {
|
|
3857
|
+
const days = Math.floor((Date.now() - new Date(contact.last_contacted_at).getTime()) / 86400000);
|
|
3858
|
+
score += days < 7 ? 30 : days < 30 ? 20 : days < 90 ? 5 : -20;
|
|
3859
|
+
} else {
|
|
3860
|
+
score -= 20;
|
|
3861
|
+
}
|
|
3862
|
+
score += Math.min(20, (contact.interaction_count_30d || 0) * 4);
|
|
3863
|
+
return Math.max(0, Math.min(100, score));
|
|
3864
|
+
}
|
|
3865
|
+
function findWarmPath(fromContactId, toContactId, db) {
|
|
3866
|
+
const _db2 = db || getDatabase();
|
|
3867
|
+
const visited = new Set([fromContactId]);
|
|
3868
|
+
const queue = [{ id: fromContactId, path: [] }];
|
|
3869
|
+
while (queue.length) {
|
|
3870
|
+
const item = queue.shift();
|
|
3871
|
+
const { id, path } = item;
|
|
3872
|
+
if (id === toContactId)
|
|
3873
|
+
return path;
|
|
3874
|
+
if (path.length >= 4)
|
|
3875
|
+
continue;
|
|
3876
|
+
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);
|
|
3877
|
+
for (const n of neighbors) {
|
|
3878
|
+
const nextId = n.contact_a_id === id ? n.contact_b_id : n.contact_a_id;
|
|
3879
|
+
if (visited.has(nextId))
|
|
3880
|
+
continue;
|
|
3881
|
+
visited.add(nextId);
|
|
3882
|
+
queue.push({
|
|
3883
|
+
id: nextId,
|
|
3884
|
+
path: [
|
|
3885
|
+
...path,
|
|
3886
|
+
{ contact_id: nextId, display_name: n.display_name, strength: n.strength_score || 50 }
|
|
3887
|
+
]
|
|
3888
|
+
});
|
|
3889
|
+
}
|
|
3890
|
+
}
|
|
3891
|
+
return [];
|
|
3892
|
+
}
|
|
3893
|
+
function findConnectionsAtCompany(companyId, db) {
|
|
3894
|
+
const _db2 = db || getDatabase();
|
|
3895
|
+
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);
|
|
3896
|
+
}
|
|
3897
|
+
function detectCoolingRelationships(db) {
|
|
3898
|
+
const _db2 = db || getDatabase();
|
|
3899
|
+
const cutoff = new Date(Date.now() - 45 * 86400000).toISOString();
|
|
3900
|
+
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);
|
|
3901
|
+
}
|
|
3902
|
+
// src/db/identity.ts
|
|
3903
|
+
init_database();
|
|
3904
|
+
function addIdentity(contactId, system, externalId, externalUrl, confidence = "inferred", db) {
|
|
3905
|
+
const _db2 = db || getDatabase();
|
|
3906
|
+
const id = uuid();
|
|
3907
|
+
_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());
|
|
3908
|
+
return _db2.query(`SELECT * FROM contact_identities WHERE id=?`).get(id);
|
|
3909
|
+
}
|
|
3910
|
+
function resolveIdentity(system, externalId, db) {
|
|
3911
|
+
const _db2 = db || getDatabase();
|
|
3912
|
+
const row = _db2.query(`SELECT c.id, c.display_name FROM contacts c JOIN contact_identities ci ON c.id=ci.contact_id WHERE ci.system=? AND ci.external_id=?`).get(system, externalId);
|
|
3913
|
+
return row || null;
|
|
3914
|
+
}
|
|
3915
|
+
function resolveByPartial(partial, db) {
|
|
3916
|
+
const _db2 = db || getDatabase();
|
|
3917
|
+
const matches = new Map;
|
|
3918
|
+
const addMatch = (id, name, title, score, reason) => {
|
|
3919
|
+
const existing = matches.get(id);
|
|
3920
|
+
if (existing) {
|
|
3921
|
+
existing.confidence_score = Math.min(100, existing.confidence_score + score);
|
|
3922
|
+
existing.match_reasons.push(reason);
|
|
3923
|
+
} else {
|
|
3924
|
+
matches.set(id, {
|
|
3925
|
+
contact: { id, display_name: name, job_title: title },
|
|
3926
|
+
confidence_score: score,
|
|
3927
|
+
match_reasons: [reason]
|
|
3928
|
+
});
|
|
3929
|
+
}
|
|
3930
|
+
};
|
|
3931
|
+
if (partial.email) {
|
|
3932
|
+
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);
|
|
3933
|
+
rows.forEach((r) => addMatch(r.id, r.display_name, r.job_title, 90, `email match: ${partial.email}`));
|
|
3934
|
+
}
|
|
3935
|
+
if (partial.linkedin_url) {
|
|
3936
|
+
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()}%`);
|
|
3937
|
+
rows.forEach((r) => addMatch(r.id, r.display_name, r.job_title, 85, `linkedin match`));
|
|
3938
|
+
}
|
|
3939
|
+
if (partial.name) {
|
|
3940
|
+
const rows = _db2.query(`SELECT id, display_name, job_title FROM contacts WHERE display_name LIKE ? AND archived=0 LIMIT 10`).all(`%${partial.name}%`);
|
|
3941
|
+
rows.forEach((r) => addMatch(r.id, r.display_name, r.job_title, 40, `name match: ${partial.name}`));
|
|
3942
|
+
}
|
|
3943
|
+
return Array.from(matches.values()).sort((a, b) => b.confidence_score - a.confidence_score);
|
|
3944
|
+
}
|
|
3945
|
+
function getIdentities(contactId, db) {
|
|
3946
|
+
const _db2 = db || getDatabase();
|
|
3947
|
+
return _db2.query(`SELECT * FROM contact_identities WHERE contact_id=? ORDER BY created_at DESC`).all(contactId);
|
|
3948
|
+
}
|
|
3949
|
+
// src/db/org-chart.ts
|
|
3950
|
+
init_database();
|
|
3951
|
+
function addOrgChartEdge(companyId, contactAId, contactBId, edgeType, inferred = false, db) {
|
|
3952
|
+
const _db2 = db || getDatabase();
|
|
3953
|
+
const id = uuid();
|
|
3954
|
+
_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());
|
|
3955
|
+
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);
|
|
3956
|
+
}
|
|
3957
|
+
function listOrgChart(companyId, db) {
|
|
3958
|
+
const _db2 = db || getDatabase();
|
|
3959
|
+
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);
|
|
3960
|
+
}
|
|
3961
|
+
function setDealContactRole(dealId, contactId, accountRole, db) {
|
|
3962
|
+
const _db2 = db || getDatabase();
|
|
3963
|
+
const id = uuid();
|
|
3964
|
+
_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());
|
|
3965
|
+
return _db2.query(`SELECT * FROM deal_contact_roles WHERE deal_id=? AND contact_id=?`).get(dealId, contactId);
|
|
3966
|
+
}
|
|
3967
|
+
function getDealTeam(dealId, db) {
|
|
3968
|
+
const _db2 = db || getDatabase();
|
|
3969
|
+
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);
|
|
3970
|
+
}
|
|
3971
|
+
function getCoverageGaps(companyId, db) {
|
|
3972
|
+
const _db2 = db || getDatabase();
|
|
3973
|
+
const total = _db2.query(`SELECT COUNT(*) c FROM contacts WHERE company_id=? AND archived=0`).get(companyId).c;
|
|
3974
|
+
const hasManager = _db2.query(`SELECT COUNT(*) c FROM org_chart_edges WHERE company_id=? AND edge_type='manages'`).get(companyId).c > 0;
|
|
3975
|
+
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;
|
|
3976
|
+
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;
|
|
3977
|
+
const missing = [
|
|
3978
|
+
!hasManager && "org chart relationships",
|
|
3979
|
+
!hasEco && "economic buyer",
|
|
3980
|
+
!hasTech && "technical evaluator"
|
|
3981
|
+
].filter(Boolean);
|
|
3982
|
+
return {
|
|
3983
|
+
total_contacts: total,
|
|
3984
|
+
has_manager: hasManager,
|
|
3985
|
+
has_technical: hasTech,
|
|
3986
|
+
has_economic_buyer: hasEco,
|
|
3987
|
+
suggestion: missing.length ? `Missing: ${missing.join(", ")}` : "Good coverage"
|
|
3988
|
+
};
|
|
3989
|
+
}
|
|
3990
|
+
// src/db/signals.ts
|
|
3991
|
+
init_database();
|
|
3992
|
+
function getRelationshipSignals(contactId, db) {
|
|
3993
|
+
const _db2 = db || getDatabase();
|
|
3994
|
+
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);
|
|
3995
|
+
if (!row)
|
|
3996
|
+
return [];
|
|
3997
|
+
const daysSince = row.last_contacted_at ? Math.floor((Date.now() - new Date(row.last_contacted_at).getTime()) / 86400000) : null;
|
|
3998
|
+
const signals = [];
|
|
3999
|
+
const cnt = row.interaction_count_30d || 0;
|
|
4000
|
+
const health = row.relationship_health ?? 50;
|
|
4001
|
+
if (daysSince === null || daysSince > 180) {
|
|
4002
|
+
signals.push({ ...row, signal_type: "ghost", days_since_contact: daysSince, reason: "No contact in 180+ days or never contacted" });
|
|
4003
|
+
} else if (daysSince > 60 && cnt === 0) {
|
|
4004
|
+
signals.push({ ...row, signal_type: "cooling", days_since_contact: daysSince, reason: `No contact in ${daysSince} days, no recent interactions` });
|
|
4005
|
+
} else if (cnt > 3 && health > 70) {
|
|
4006
|
+
signals.push({ ...row, signal_type: "warming", days_since_contact: daysSince, reason: `${cnt} interactions in last 30 days, health score ${health}` });
|
|
4007
|
+
} else {
|
|
4008
|
+
signals.push({ ...row, signal_type: "healthy", days_since_contact: daysSince, reason: `Last contact ${daysSince}d ago, ${cnt} interactions in 30d` });
|
|
4009
|
+
}
|
|
4010
|
+
return signals;
|
|
4011
|
+
}
|
|
4012
|
+
function getGhostContacts(db) {
|
|
4013
|
+
const _db2 = db || getDatabase();
|
|
4014
|
+
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();
|
|
4015
|
+
return rows.map((r) => ({
|
|
4016
|
+
...r,
|
|
4017
|
+
signal_type: "ghost",
|
|
4018
|
+
days_since_contact: r.last_contacted_at ? Math.floor((Date.now() - new Date(r.last_contacted_at).getTime()) / 86400000) : null,
|
|
4019
|
+
reason: "No contact in 180+ days or never contacted"
|
|
4020
|
+
}));
|
|
4021
|
+
}
|
|
4022
|
+
function getWarmingContacts(db) {
|
|
4023
|
+
const _db2 = db || getDatabase();
|
|
4024
|
+
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();
|
|
4025
|
+
return rows.map((r) => ({
|
|
4026
|
+
...r,
|
|
4027
|
+
signal_type: "warming",
|
|
4028
|
+
days_since_contact: r.last_contacted_at ? Math.floor((Date.now() - new Date(r.last_contacted_at).getTime()) / 86400000) : null,
|
|
4029
|
+
reason: `${r.interaction_count_30d} interactions in last 30 days`
|
|
4030
|
+
}));
|
|
4031
|
+
}
|
|
4032
|
+
function recomputeAllSignals(db) {
|
|
4033
|
+
const _db2 = db || getDatabase();
|
|
4034
|
+
_db2.query(`
|
|
4035
|
+
UPDATE contacts SET
|
|
4036
|
+
engagement_status = CASE
|
|
4037
|
+
WHEN interaction_count_30d > 3 THEN 'warm'
|
|
4038
|
+
WHEN last_contacted_at IS NULL OR julianday('now') - julianday(last_contacted_at) > 180 THEN 'ghost'
|
|
4039
|
+
WHEN julianday('now') - julianday(last_contacted_at) > 60 THEN 'cooling'
|
|
4040
|
+
ELSE 'active'
|
|
4041
|
+
END,
|
|
4042
|
+
updated_at = datetime('now')
|
|
4043
|
+
WHERE archived = 0
|
|
4044
|
+
`).run();
|
|
4045
|
+
const result = _db2.query(`SELECT changes() as n`).get();
|
|
4046
|
+
return { updated: result?.n ?? 0 };
|
|
4047
|
+
}
|
|
4048
|
+
// src/db/freshness.ts
|
|
4049
|
+
init_database();
|
|
4050
|
+
var SCORED_FIELDS = ["display_name", "job_title", "company_id", "emails", "phones", "last_contacted_at"];
|
|
4051
|
+
function getFreshnessScore(contactId, db) {
|
|
4052
|
+
const _db2 = db || getDatabase();
|
|
4053
|
+
const contact = _db2.query(`SELECT * FROM contacts WHERE id=?`).get(contactId);
|
|
4054
|
+
if (!contact)
|
|
4055
|
+
throw new Error(`Contact not found: ${contactId}`);
|
|
4056
|
+
let historyRows = [];
|
|
4057
|
+
try {
|
|
4058
|
+
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);
|
|
4059
|
+
} catch {}
|
|
4060
|
+
let verifiedRows = [];
|
|
4061
|
+
try {
|
|
4062
|
+
verifiedRows = _db2.query(`SELECT field_name, verified_at, source FROM field_verifications WHERE contact_id=?`).all(contactId);
|
|
4063
|
+
} catch {}
|
|
4064
|
+
const verifiedMap = new Map(verifiedRows.map((r) => [r.field_name, r]));
|
|
4065
|
+
const historyMap = new Map;
|
|
4066
|
+
for (const r of historyRows) {
|
|
4067
|
+
if (!historyMap.has(r.field_name))
|
|
4068
|
+
historyMap.set(r.field_name, r);
|
|
4069
|
+
}
|
|
4070
|
+
const fields = SCORED_FIELDS.map((field) => {
|
|
4071
|
+
let value = null;
|
|
4072
|
+
if (field === "emails") {
|
|
4073
|
+
const emailRow = _db2.query(`SELECT address FROM emails WHERE contact_id=? LIMIT 1`).get(contactId);
|
|
4074
|
+
value = emailRow?.address ?? null;
|
|
4075
|
+
} else if (field === "phones") {
|
|
4076
|
+
const phoneRow = _db2.query(`SELECT number FROM phones WHERE contact_id=? LIMIT 1`).get(contactId);
|
|
4077
|
+
value = phoneRow?.number ?? null;
|
|
4078
|
+
} else {
|
|
4079
|
+
value = contact[field] != null ? String(contact[field]) : null;
|
|
4080
|
+
}
|
|
4081
|
+
const verified = verifiedMap.get(field);
|
|
4082
|
+
const history = historyMap.get(field);
|
|
4083
|
+
let confidence = "unknown";
|
|
4084
|
+
let days_old = null;
|
|
4085
|
+
let last_verified_at = null;
|
|
4086
|
+
let source = null;
|
|
4087
|
+
if (verified) {
|
|
4088
|
+
confidence = "verified";
|
|
4089
|
+
last_verified_at = verified.verified_at;
|
|
4090
|
+
source = verified.source;
|
|
4091
|
+
days_old = Math.floor((Date.now() - new Date(verified.verified_at).getTime()) / 86400000);
|
|
4092
|
+
} else if (history) {
|
|
4093
|
+
confidence = history.source === "import" ? "imported" : "inferred";
|
|
4094
|
+
last_verified_at = history.created_at;
|
|
4095
|
+
source = history.source;
|
|
4096
|
+
days_old = Math.floor((Date.now() - new Date(history.created_at).getTime()) / 86400000);
|
|
4097
|
+
if (days_old > 365)
|
|
4098
|
+
confidence = "stale";
|
|
4099
|
+
} else if (value) {
|
|
4100
|
+
confidence = "inferred";
|
|
4101
|
+
}
|
|
4102
|
+
return { field_name: field, value, last_verified_at, source, confidence, days_old };
|
|
4103
|
+
});
|
|
4104
|
+
const fieldScore = fields.reduce((acc, f) => {
|
|
4105
|
+
if (!f.value)
|
|
4106
|
+
return acc;
|
|
4107
|
+
if (f.confidence === "verified")
|
|
4108
|
+
return acc + 20;
|
|
4109
|
+
if (f.confidence === "imported" || f.confidence === "inferred")
|
|
4110
|
+
return acc + 10;
|
|
4111
|
+
return acc + 5;
|
|
4112
|
+
}, 0);
|
|
4113
|
+
const overall_score = Math.min(100, fieldScore);
|
|
4114
|
+
return {
|
|
4115
|
+
contact_id: contactId,
|
|
4116
|
+
overall_score,
|
|
4117
|
+
fields,
|
|
4118
|
+
stale_fields: fields.filter((f) => f.confidence === "stale" || !f.value && f.field_name !== "phones").map((f) => f.field_name),
|
|
4119
|
+
verified_fields: fields.filter((f) => f.confidence === "verified").map((f) => f.field_name)
|
|
4120
|
+
};
|
|
4121
|
+
}
|
|
4122
|
+
function getStaleContacts(threshold = 40, db) {
|
|
4123
|
+
const _db2 = db || getDatabase();
|
|
4124
|
+
const rows = _db2.query(`SELECT c.id as contact_id, c.display_name,
|
|
4125
|
+
(CASE WHEN c.job_title IS NOT NULL THEN 15 ELSE 0 END +
|
|
4126
|
+
CASE WHEN c.company_id IS NOT NULL THEN 15 ELSE 0 END +
|
|
4127
|
+
CASE WHEN c.last_contacted_at IS NOT NULL THEN 20 ELSE 0 END +
|
|
4128
|
+
CASE WHEN EXISTS(SELECT 1 FROM emails WHERE contact_id=c.id) THEN 20 ELSE 0 END +
|
|
4129
|
+
CASE WHEN EXISTS(SELECT 1 FROM phones WHERE contact_id=c.id) THEN 15 ELSE 0 END +
|
|
4130
|
+
CASE WHEN c.notes IS NOT NULL THEN 10 ELSE 0 END +
|
|
4131
|
+
CASE WHEN EXISTS(SELECT 1 FROM contact_tags WHERE contact_id=c.id) THEN 5 ELSE 0 END
|
|
4132
|
+
) as score
|
|
4133
|
+
FROM contacts c WHERE c.archived=0 HAVING score < ? ORDER BY score ASC LIMIT 100`).all(threshold);
|
|
4134
|
+
return rows;
|
|
4135
|
+
}
|
|
4136
|
+
function markFieldVerified(contactId, fieldName, source, db) {
|
|
4137
|
+
const _db2 = db || getDatabase();
|
|
4138
|
+
try {
|
|
4139
|
+
_db2.query(`INSERT OR REPLACE INTO field_verifications(contact_id,field_name,verified_at,source) VALUES(?,?,?,?)`).run(contactId, fieldName, now(), source || null);
|
|
4140
|
+
} catch {
|
|
4141
|
+
_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());
|
|
4142
|
+
}
|
|
4143
|
+
}
|
|
4144
|
+
// src/lib/context.ts
|
|
4145
|
+
init_database();
|
|
4146
|
+
init_contacts();
|
|
4147
|
+
function getContactCard(contactId, db) {
|
|
4148
|
+
const _db2 = db || getDatabase();
|
|
4149
|
+
const c = getContact(contactId, _db2);
|
|
4150
|
+
const emails = c.emails;
|
|
4151
|
+
const phones = c.phones;
|
|
4152
|
+
const company = c.company;
|
|
4153
|
+
return {
|
|
4154
|
+
id: c.id,
|
|
4155
|
+
display_name: c.display_name,
|
|
4156
|
+
job_title: c.job_title,
|
|
4157
|
+
company: company?.name,
|
|
4158
|
+
primary_email: emails?.find((e) => e.is_primary)?.address || emails?.[0]?.address,
|
|
4159
|
+
primary_phone: phones?.find((p) => p.is_primary)?.number || phones?.[0]?.number
|
|
4160
|
+
};
|
|
4161
|
+
}
|
|
4162
|
+
function getContactBrief(contactId, taskContext, db) {
|
|
4163
|
+
const _db2 = db || getDatabase();
|
|
4164
|
+
const c = getContact(contactId, _db2);
|
|
4165
|
+
const notes = listNotes(contactId, _db2).slice(0, 3);
|
|
4166
|
+
const learnings = getLearnings(contactId, { min_importance: 7 }, _db2).slice(0, 5);
|
|
4167
|
+
const ctx = (taskContext ?? "").toLowerCase();
|
|
4168
|
+
const lastContactedAt = c.last_contacted_at;
|
|
4169
|
+
const daysSince = lastContactedAt ? Math.floor((Date.now() - new Date(lastContactedAt).getTime()) / 86400000) : null;
|
|
4170
|
+
const company = c.company;
|
|
4171
|
+
const brief = {
|
|
4172
|
+
id: c.id,
|
|
4173
|
+
display_name: c.display_name,
|
|
4174
|
+
job_title: c.job_title,
|
|
4175
|
+
company: company?.name,
|
|
4176
|
+
status: c.status,
|
|
4177
|
+
last_contacted: daysSince !== null ? `${daysSince}d ago` : "never",
|
|
4178
|
+
relationship_health: c.relationship_health,
|
|
4179
|
+
engagement_status: c.engagement_status,
|
|
4180
|
+
preferred_contact: c.preferred_contact_method || c.preferred_channel
|
|
4181
|
+
};
|
|
4182
|
+
if (ctx.includes("meeting") || ctx.includes("call") || ctx.includes("prep")) {
|
|
4183
|
+
brief.recent_notes = notes.map((n) => ({ date: n.created_at?.slice(0, 10), content: n.body }));
|
|
4184
|
+
brief.key_learnings = learnings.map((l) => l.content);
|
|
4185
|
+
}
|
|
4186
|
+
if (ctx.includes("outreach") || ctx.includes("email")) {
|
|
4187
|
+
brief.preferred_channel = c.preferred_channel;
|
|
4188
|
+
brief.follow_up_at = c.follow_up_at;
|
|
4189
|
+
}
|
|
4190
|
+
if (ctx.includes("deal")) {
|
|
4191
|
+
const dealCompany = c.company;
|
|
4192
|
+
brief.company_details = dealCompany ? { name: dealCompany.name, domain: dealCompany.domain } : null;
|
|
4193
|
+
}
|
|
4194
|
+
if (learnings.length)
|
|
4195
|
+
brief.top_learnings = learnings.map((l) => l.content);
|
|
4196
|
+
return brief;
|
|
4197
|
+
}
|
|
4198
|
+
async function assembleContext(contactIds, format = "meeting_prep", db) {
|
|
4199
|
+
const _db2 = db || getDatabase();
|
|
4200
|
+
const briefs = contactIds.map((id) => {
|
|
4201
|
+
try {
|
|
4202
|
+
return getContactBrief(id, format, _db2);
|
|
4203
|
+
} catch {
|
|
4204
|
+
return { id, error: "not found" };
|
|
4205
|
+
}
|
|
4206
|
+
});
|
|
4207
|
+
return { format, contact_count: contactIds.length, assembled_at: new Date().toISOString(), contacts: briefs };
|
|
4208
|
+
}
|
|
4209
|
+
// src/lib/embeddings.ts
|
|
4210
|
+
init_database();
|
|
4211
|
+
function tokenize(text) {
|
|
4212
|
+
return text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t) => t.length > 2);
|
|
4213
|
+
}
|
|
4214
|
+
function buildTfIdf(tokens) {
|
|
4215
|
+
const freq = new Map;
|
|
4216
|
+
for (const t of tokens)
|
|
4217
|
+
freq.set(t, (freq.get(t) || 0) + 1);
|
|
4218
|
+
const max = Math.max(...freq.values(), 1);
|
|
4219
|
+
const result = new Map;
|
|
4220
|
+
freq.forEach((v, k) => result.set(k, v / max));
|
|
4221
|
+
return result;
|
|
4222
|
+
}
|
|
4223
|
+
function cosineSimilarity(a, b) {
|
|
4224
|
+
let dot = 0, normA = 0, normB = 0;
|
|
4225
|
+
a.forEach((v, k) => {
|
|
4226
|
+
if (b.has(k))
|
|
4227
|
+
dot += v * b.get(k);
|
|
4228
|
+
normA += v * v;
|
|
4229
|
+
});
|
|
4230
|
+
b.forEach((v) => normB += v * v);
|
|
4231
|
+
return normA && normB ? dot / (Math.sqrt(normA) * Math.sqrt(normB)) : 0;
|
|
4232
|
+
}
|
|
4233
|
+
function buildContactEmbeddingText(contact) {
|
|
4234
|
+
const tags = contact.tags ?? [];
|
|
4235
|
+
const socialProfiles = contact.social_profiles ?? [];
|
|
4236
|
+
const company = contact.company;
|
|
4237
|
+
const parts = [
|
|
4238
|
+
contact.display_name,
|
|
4239
|
+
contact.job_title,
|
|
4240
|
+
contact.notes,
|
|
4241
|
+
company?.name,
|
|
4242
|
+
company?.industry,
|
|
4243
|
+
...tags.map((t) => t.name),
|
|
4244
|
+
...socialProfiles.map((s) => s.platform)
|
|
4245
|
+
].filter(Boolean);
|
|
4246
|
+
return parts.join(" ");
|
|
4247
|
+
}
|
|
4248
|
+
async function embedContact(contactId, db) {
|
|
4249
|
+
const { getContact: getContact2 } = await Promise.resolve().then(() => (init_contacts(), exports_contacts));
|
|
4250
|
+
const _db2 = db || getDatabase();
|
|
4251
|
+
const contact = getContact2(contactId, _db2);
|
|
4252
|
+
const text = buildContactEmbeddingText(contact);
|
|
4253
|
+
const tokens = tokenize(text);
|
|
4254
|
+
const tfidf = buildTfIdf(tokens);
|
|
4255
|
+
const embedding = JSON.stringify(Array.from(tfidf.entries()).sort((a, b) => b[1] - a[1]).slice(0, 100));
|
|
4256
|
+
_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());
|
|
4257
|
+
}
|
|
4258
|
+
async function embedAllContacts(db) {
|
|
4259
|
+
const _db2 = db || getDatabase();
|
|
4260
|
+
const contacts = _db2.query(`SELECT id FROM contacts WHERE archived=0`).all();
|
|
4261
|
+
for (const c of contacts) {
|
|
4262
|
+
try {
|
|
4263
|
+
await embedContact(c.id, _db2);
|
|
4264
|
+
} catch {}
|
|
4265
|
+
}
|
|
4266
|
+
return contacts.length;
|
|
4267
|
+
}
|
|
4268
|
+
function semanticSearch(query, limit = 10, db) {
|
|
4269
|
+
const _db2 = db || getDatabase();
|
|
4270
|
+
const queryTokens = buildTfIdf(tokenize(query));
|
|
4271
|
+
let embeddings = [];
|
|
4272
|
+
try {
|
|
4273
|
+
embeddings = _db2.query(`SELECT contact_id, embedding FROM contact_embeddings`).all();
|
|
4274
|
+
} catch {
|
|
4275
|
+
return [];
|
|
4276
|
+
}
|
|
4277
|
+
const results = embeddings.map((e) => {
|
|
4278
|
+
try {
|
|
4279
|
+
const emb = new Map(JSON.parse(e.embedding));
|
|
4280
|
+
return { contact_id: e.contact_id, score: cosineSimilarity(queryTokens, emb) };
|
|
4281
|
+
} catch {
|
|
4282
|
+
return { contact_id: e.contact_id, score: 0 };
|
|
4283
|
+
}
|
|
4284
|
+
}).filter((r) => r.score > 0).sort((a, b) => b.score - a.score).slice(0, limit);
|
|
4285
|
+
return results;
|
|
4286
|
+
}
|
|
4287
|
+
// src/lib/signature-parser.ts
|
|
4288
|
+
function parseEmailSignature(text) {
|
|
4289
|
+
const result = {};
|
|
4290
|
+
const phoneMatch = text.match(/(\+?[\d\s\-\(\)]{7,20})/);
|
|
4291
|
+
if (phoneMatch)
|
|
4292
|
+
result.phone = phoneMatch[1]?.trim();
|
|
4293
|
+
const emailMatch = text.match(/[\w.+-]+@[\w-]+\.[a-z]{2,}/i);
|
|
4294
|
+
if (emailMatch)
|
|
4295
|
+
result.email = emailMatch[0];
|
|
4296
|
+
const linkedinMatch = text.match(/(?:linkedin\.com\/in\/)([\w-]+)/i);
|
|
4297
|
+
if (linkedinMatch)
|
|
4298
|
+
result.linkedin = `https://linkedin.com/in/${linkedinMatch[1]}`;
|
|
4299
|
+
const websiteMatch = text.match(/https?:\/\/(?!linkedin)(?!twitter)[\w.-]+\.[a-z]{2,}/i);
|
|
4300
|
+
if (websiteMatch)
|
|
4301
|
+
result.website = websiteMatch[0];
|
|
4302
|
+
const lines = text.split(`
|
|
4303
|
+
`).map((l) => l.trim()).filter((l) => l.length > 2 && l.length < 80);
|
|
4304
|
+
if (lines[0])
|
|
4305
|
+
result.name = lines[0];
|
|
4306
|
+
for (const line of lines.slice(1)) {
|
|
4307
|
+
if (line.match(/\b(CEO|CTO|VP|Director|Manager|Engineer|Partner|Associate|Consultant|Analyst|President|Founder)\b/i)) {
|
|
4308
|
+
result.title = line;
|
|
4309
|
+
} else if (!result.company && line.match(/^[A-Z][A-Za-z\s,\.]+$/) && !line.includes("@")) {
|
|
4310
|
+
result.company = line;
|
|
4311
|
+
}
|
|
4312
|
+
}
|
|
4313
|
+
return result;
|
|
4314
|
+
}
|
|
4315
|
+
function extractContactsFromEmailThread(participants) {
|
|
4316
|
+
return participants.map((p) => {
|
|
4317
|
+
const sig = p.signature ? parseEmailSignature(p.signature) : {};
|
|
4318
|
+
const name = p.name || sig.name || p.email.split("@")[0] || "Unknown";
|
|
4319
|
+
const contact = {
|
|
4320
|
+
display_name: name,
|
|
4321
|
+
emails: [{ address: p.email, type: "work", is_primary: true }],
|
|
4322
|
+
source: "import"
|
|
4323
|
+
};
|
|
4324
|
+
if (sig.title)
|
|
4325
|
+
contact.job_title = sig.title;
|
|
4326
|
+
if (sig.phone)
|
|
4327
|
+
contact.phones = [{ number: sig.phone, type: "work", is_primary: true }];
|
|
4328
|
+
if (sig.linkedin)
|
|
4329
|
+
contact.social_profiles = [{ platform: "linkedin", url: sig.linkedin, is_primary: true }];
|
|
4330
|
+
if (sig.website)
|
|
4331
|
+
contact.website = sig.website;
|
|
4332
|
+
return contact;
|
|
4333
|
+
});
|
|
4334
|
+
}
|
|
4335
|
+
// src/lib/meeting-capture.ts
|
|
4336
|
+
init_database();
|
|
4337
|
+
async function ingestMeetingParticipants(event, db) {
|
|
4338
|
+
const { findOrCreateContact: findOrCreateContact2 } = await Promise.resolve().then(() => (init_contacts(), exports_contacts));
|
|
4339
|
+
const { logEvent: logEvent2 } = await Promise.resolve().then(() => (init_events(), exports_events));
|
|
4340
|
+
const _db2 = db || getDatabase();
|
|
4341
|
+
let created = 0;
|
|
4342
|
+
let updated = 0;
|
|
4343
|
+
const ids = [];
|
|
4344
|
+
for (const a of event.attendees) {
|
|
4345
|
+
try {
|
|
4346
|
+
const nameParts = a.name.split(" ");
|
|
4347
|
+
const result = await findOrCreateContact2({
|
|
4348
|
+
display_name: a.name,
|
|
4349
|
+
first_name: nameParts[0],
|
|
4350
|
+
last_name: nameParts.slice(1).join(" ") || undefined,
|
|
4351
|
+
emails: [{ address: a.email, type: "work", is_primary: true }],
|
|
4352
|
+
source: "import"
|
|
4353
|
+
}, _db2);
|
|
4354
|
+
ids.push(result.contact.id);
|
|
4355
|
+
if (result.created)
|
|
4356
|
+
created++;
|
|
4357
|
+
else
|
|
4358
|
+
updated++;
|
|
4359
|
+
} catch {}
|
|
4360
|
+
}
|
|
4361
|
+
if (ids.length) {
|
|
4362
|
+
try {
|
|
4363
|
+
logEvent2({
|
|
4364
|
+
title: event.title,
|
|
4365
|
+
type: "meeting",
|
|
4366
|
+
event_date: event.event_date,
|
|
4367
|
+
contact_ids: ids,
|
|
4368
|
+
notes: event.context
|
|
4369
|
+
}, _db2);
|
|
4370
|
+
} catch {}
|
|
4371
|
+
}
|
|
4372
|
+
return { created, updated, contact_ids: ids };
|
|
4373
|
+
}
|
|
4374
|
+
|
|
4375
|
+
// src/index.ts
|
|
4376
|
+
init_contacts();
|
|
3479
4377
|
export {
|
|
3480
4378
|
updateVendorCommunication,
|
|
3481
4379
|
updateTag,
|
|
@@ -3488,26 +4386,38 @@ export {
|
|
|
3488
4386
|
updateApplication,
|
|
3489
4387
|
unarchiveContact,
|
|
3490
4388
|
unarchiveCompany,
|
|
4389
|
+
setDealContactRole,
|
|
4390
|
+
semanticSearch,
|
|
4391
|
+
searchLearnings,
|
|
3491
4392
|
searchGoogleContacts,
|
|
3492
4393
|
searchContacts,
|
|
3493
4394
|
searchCompanies,
|
|
4395
|
+
saveLearning,
|
|
3494
4396
|
runConnector,
|
|
4397
|
+
resolveIdentity,
|
|
4398
|
+
resolveByPartial,
|
|
3495
4399
|
resetDatabase,
|
|
3496
4400
|
removeTagFromContact,
|
|
3497
4401
|
removeTagFromCompany,
|
|
3498
4402
|
removeOrgMember,
|
|
3499
4403
|
removeContactFromGroup,
|
|
3500
4404
|
removeCompanyFromGroup,
|
|
4405
|
+
releaseLock,
|
|
4406
|
+
recordFieldChange,
|
|
4407
|
+
recomputeAllSignals,
|
|
3501
4408
|
readConnectorTokens,
|
|
3502
4409
|
pushContactToGoogle,
|
|
3503
4410
|
pullGoogleContactsAsInputs,
|
|
3504
4411
|
parseName,
|
|
3505
4412
|
parseLinkedIn,
|
|
4413
|
+
parseEmailSignature,
|
|
3506
4414
|
parseAddressHeader,
|
|
3507
4415
|
mergeContacts,
|
|
3508
4416
|
markFollowUpDone,
|
|
4417
|
+
markFieldVerified,
|
|
3509
4418
|
logVendorCommunication,
|
|
3510
4419
|
logEvent,
|
|
4420
|
+
logAgentActivity,
|
|
3511
4421
|
logActivity,
|
|
3512
4422
|
listVendorCommunications,
|
|
3513
4423
|
listTags,
|
|
@@ -3519,6 +4429,7 @@ export {
|
|
|
3519
4429
|
listOverdueTasks,
|
|
3520
4430
|
listOrgMembersForContact,
|
|
3521
4431
|
listOrgMembers,
|
|
4432
|
+
listOrgChart,
|
|
3522
4433
|
listNotesForContactAtCompany,
|
|
3523
4434
|
listNotes,
|
|
3524
4435
|
listMissingInvoices,
|
|
@@ -3541,40 +4452,66 @@ export {
|
|
|
3541
4452
|
listColdContacts,
|
|
3542
4453
|
listApplications,
|
|
3543
4454
|
listActivity,
|
|
4455
|
+
ingestMeetingParticipants,
|
|
3544
4456
|
importToApple,
|
|
3545
4457
|
importFromCsv,
|
|
3546
4458
|
importContacts,
|
|
3547
4459
|
googlePersonToContactInput,
|
|
4460
|
+
getWarmingContacts,
|
|
3548
4461
|
getUpcomingItems,
|
|
3549
4462
|
getTagByName,
|
|
3550
4463
|
getTag,
|
|
4464
|
+
getStaleContacts,
|
|
4465
|
+
getRelationshipSignals,
|
|
3551
4466
|
getRelationship,
|
|
4467
|
+
getPreviousEmployers,
|
|
3552
4468
|
getOrgMember,
|
|
3553
4469
|
getNote,
|
|
3554
4470
|
getNetworkStats,
|
|
4471
|
+
getLearnings,
|
|
4472
|
+
getJobHistory,
|
|
4473
|
+
getIdentities,
|
|
3555
4474
|
getGroup,
|
|
4475
|
+
getGhostContacts,
|
|
4476
|
+
getFreshnessScore,
|
|
4477
|
+
getFieldHistory,
|
|
3556
4478
|
getEvent,
|
|
3557
4479
|
getEntityTeam,
|
|
3558
4480
|
getDealsByStage,
|
|
4481
|
+
getDealTeam,
|
|
3559
4482
|
getDeal,
|
|
3560
4483
|
getDatabase,
|
|
4484
|
+
getCurrentRole,
|
|
4485
|
+
getCoverageGaps,
|
|
3561
4486
|
getContactTimeline,
|
|
3562
4487
|
getContactTask,
|
|
4488
|
+
getContactCard,
|
|
3563
4489
|
getContactByEmail,
|
|
4490
|
+
getContactBrief,
|
|
4491
|
+
getContactAt,
|
|
3564
4492
|
getContact,
|
|
3565
4493
|
getConnectorTokenPath,
|
|
3566
4494
|
getCompany,
|
|
3567
4495
|
getApplication,
|
|
4496
|
+
getAgentActivity,
|
|
3568
4497
|
getActivity,
|
|
3569
4498
|
generateBrief,
|
|
4499
|
+
findWarmPath,
|
|
4500
|
+
findOrCreateContact,
|
|
4501
|
+
findConnectionsAtCompany,
|
|
3570
4502
|
extractContactsFromGmail,
|
|
4503
|
+
extractContactsFromEmailThread,
|
|
3571
4504
|
exportFromApple,
|
|
3572
4505
|
exportContacts,
|
|
4506
|
+
embedContact,
|
|
4507
|
+
embedAllContacts,
|
|
3573
4508
|
domainToCompany,
|
|
4509
|
+
detectCoolingRelationships,
|
|
3574
4510
|
deleteVendorCommunication,
|
|
3575
4511
|
deleteTag,
|
|
3576
4512
|
deleteRelationship,
|
|
3577
4513
|
deleteNote,
|
|
4514
|
+
deleteLearning,
|
|
3578
4515
|
deleteGroup,
|
|
3579
4516
|
deleteEvent,
|
|
3580
4517
|
deleteDeal,
|
|
@@ -3583,6 +4520,7 @@ export {
|
|
|
3583
4520
|
deleteCompanyRelationship,
|
|
3584
4521
|
deleteCompany,
|
|
3585
4522
|
deleteApplication,
|
|
4523
|
+
decayLearnings,
|
|
3586
4524
|
createTag,
|
|
3587
4525
|
createRelationship,
|
|
3588
4526
|
createGroup,
|
|
@@ -3593,19 +4531,29 @@ export {
|
|
|
3593
4531
|
createCompany,
|
|
3594
4532
|
createApplication,
|
|
3595
4533
|
contactToGoogleArgs,
|
|
4534
|
+
confirmLearning,
|
|
4535
|
+
computeRelationshipStrength,
|
|
4536
|
+
cleanExpiredLocks,
|
|
4537
|
+
checkLock,
|
|
3596
4538
|
checkEscalations,
|
|
4539
|
+
buildContactEmbeddingText,
|
|
3597
4540
|
autoLinkContactToCompany,
|
|
3598
4541
|
auditContact,
|
|
4542
|
+
assembleContext,
|
|
3599
4543
|
archiveContact,
|
|
3600
4544
|
archiveCompany,
|
|
3601
4545
|
addTagToContact,
|
|
3602
4546
|
addTagToCompany,
|
|
3603
4547
|
addPhoneToContact,
|
|
3604
4548
|
addOrgMember,
|
|
4549
|
+
addOrgChartEdge,
|
|
3605
4550
|
addNote,
|
|
4551
|
+
addJobEntry,
|
|
4552
|
+
addIdentity,
|
|
3606
4553
|
addEmailToContact,
|
|
3607
4554
|
addContactToGroup,
|
|
3608
4555
|
addCompanyToGroup,
|
|
4556
|
+
acquireLock,
|
|
3609
4557
|
TagNotFoundError,
|
|
3610
4558
|
DuplicateTagNameError,
|
|
3611
4559
|
ContactNotFoundError,
|