@hasna/contacts 0.4.3 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +1176 -21
- package/dist/db/contacts.d.ts +4 -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/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 +978 -85
- 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 +1276 -94
- package/dist/server/index.js +136 -0
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -5,43 +5,25 @@ var __getProtoOf = Object.getPrototypeOf;
|
|
|
5
5
|
var __defProp = Object.defineProperty;
|
|
6
6
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
-
function __accessProp(key) {
|
|
9
|
-
return this[key];
|
|
10
|
-
}
|
|
11
|
-
var __toESMCache_node;
|
|
12
|
-
var __toESMCache_esm;
|
|
13
8
|
var __toESM = (mod, isNodeMode, target) => {
|
|
14
|
-
var canCache = mod != null && typeof mod === "object";
|
|
15
|
-
if (canCache) {
|
|
16
|
-
var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
|
|
17
|
-
var cached = cache.get(mod);
|
|
18
|
-
if (cached)
|
|
19
|
-
return cached;
|
|
20
|
-
}
|
|
21
9
|
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
22
10
|
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
23
11
|
for (let key of __getOwnPropNames(mod))
|
|
24
12
|
if (!__hasOwnProp.call(to, key))
|
|
25
13
|
__defProp(to, key, {
|
|
26
|
-
get:
|
|
14
|
+
get: () => mod[key],
|
|
27
15
|
enumerable: true
|
|
28
16
|
});
|
|
29
|
-
if (canCache)
|
|
30
|
-
cache.set(mod, to);
|
|
31
17
|
return to;
|
|
32
18
|
};
|
|
33
19
|
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
|
34
|
-
var __returnValue = (v) => v;
|
|
35
|
-
function __exportSetter(name, newValue) {
|
|
36
|
-
this[name] = __returnValue.bind(null, newValue);
|
|
37
|
-
}
|
|
38
20
|
var __export = (target, all) => {
|
|
39
21
|
for (var name in all)
|
|
40
22
|
__defProp(target, name, {
|
|
41
23
|
get: all[name],
|
|
42
24
|
enumerable: true,
|
|
43
25
|
configurable: true,
|
|
44
|
-
set:
|
|
26
|
+
set: (newValue) => all[name] = () => newValue
|
|
45
27
|
});
|
|
46
28
|
};
|
|
47
29
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
@@ -2514,6 +2496,142 @@ var init_database = __esm(() => {
|
|
|
2514
2496
|
deal_id TEXT REFERENCES deals(id) ON DELETE SET NULL,
|
|
2515
2497
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
2516
2498
|
);
|
|
2499
|
+
`,
|
|
2500
|
+
`
|
|
2501
|
+
-- CON-00069: temporal field history
|
|
2502
|
+
CREATE TABLE IF NOT EXISTS contact_field_history (
|
|
2503
|
+
id TEXT PRIMARY KEY,
|
|
2504
|
+
contact_id TEXT NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
|
|
2505
|
+
field_name TEXT NOT NULL,
|
|
2506
|
+
old_value TEXT,
|
|
2507
|
+
new_value TEXT,
|
|
2508
|
+
valid_from TEXT NOT NULL DEFAULT (datetime('now')),
|
|
2509
|
+
source TEXT,
|
|
2510
|
+
confidence TEXT NOT NULL DEFAULT 'imported' CHECK(confidence IN ('verified','inferred','imported','stale')),
|
|
2511
|
+
created_by TEXT,
|
|
2512
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
2513
|
+
);
|
|
2514
|
+
|
|
2515
|
+
-- CON-00070: job history
|
|
2516
|
+
CREATE TABLE IF NOT EXISTS job_history (
|
|
2517
|
+
id TEXT PRIMARY KEY,
|
|
2518
|
+
contact_id TEXT NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
|
|
2519
|
+
company_id TEXT REFERENCES companies(id) ON DELETE SET NULL,
|
|
2520
|
+
company_name TEXT NOT NULL,
|
|
2521
|
+
title TEXT,
|
|
2522
|
+
start_date TEXT,
|
|
2523
|
+
end_date TEXT,
|
|
2524
|
+
is_current INTEGER NOT NULL DEFAULT 0,
|
|
2525
|
+
inferred INTEGER NOT NULL DEFAULT 0,
|
|
2526
|
+
source TEXT,
|
|
2527
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
2528
|
+
);
|
|
2529
|
+
|
|
2530
|
+
-- CON-00071: learnings
|
|
2531
|
+
CREATE TABLE IF NOT EXISTS contact_learnings (
|
|
2532
|
+
id TEXT PRIMARY KEY,
|
|
2533
|
+
contact_id TEXT NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
|
|
2534
|
+
content TEXT NOT NULL,
|
|
2535
|
+
type TEXT NOT NULL DEFAULT 'fact' CHECK(type IN ('preference','fact','inference','warning','signal')),
|
|
2536
|
+
confidence INTEGER NOT NULL DEFAULT 70 CHECK(confidence BETWEEN 0 AND 100),
|
|
2537
|
+
importance INTEGER NOT NULL DEFAULT 5 CHECK(importance BETWEEN 1 AND 10),
|
|
2538
|
+
learned_by TEXT,
|
|
2539
|
+
session_id TEXT,
|
|
2540
|
+
visibility TEXT NOT NULL DEFAULT 'shared' CHECK(visibility IN ('private','shared','human')),
|
|
2541
|
+
tags TEXT NOT NULL DEFAULT '[]',
|
|
2542
|
+
confirmed_count INTEGER NOT NULL DEFAULT 0,
|
|
2543
|
+
contradicts_id TEXT REFERENCES contact_learnings(id) ON DELETE SET NULL,
|
|
2544
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
2545
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
2546
|
+
);
|
|
2547
|
+
|
|
2548
|
+
-- CON-00072: coordination
|
|
2549
|
+
CREATE TABLE IF NOT EXISTS contact_locks (
|
|
2550
|
+
id TEXT PRIMARY KEY,
|
|
2551
|
+
contact_id TEXT NOT NULL UNIQUE REFERENCES contacts(id) ON DELETE CASCADE,
|
|
2552
|
+
agent_name TEXT NOT NULL,
|
|
2553
|
+
reason TEXT,
|
|
2554
|
+
acquired_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
2555
|
+
expires_at TEXT NOT NULL,
|
|
2556
|
+
session_id TEXT
|
|
2557
|
+
);
|
|
2558
|
+
CREATE TABLE IF NOT EXISTS contact_agent_activity (
|
|
2559
|
+
id TEXT PRIMARY KEY,
|
|
2560
|
+
contact_id TEXT NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
|
|
2561
|
+
agent_name TEXT NOT NULL,
|
|
2562
|
+
action TEXT NOT NULL,
|
|
2563
|
+
details TEXT,
|
|
2564
|
+
session_id TEXT,
|
|
2565
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
2566
|
+
);
|
|
2567
|
+
|
|
2568
|
+
-- CON-00073: relationship graph extra columns
|
|
2569
|
+
ALTER TABLE contact_relationships ADD COLUMN strength_score INTEGER NOT NULL DEFAULT 50;
|
|
2570
|
+
ALTER TABLE contact_relationships ADD COLUMN interaction_count INTEGER NOT NULL DEFAULT 0;
|
|
2571
|
+
ALTER TABLE contact_relationships ADD COLUMN last_interaction TEXT;
|
|
2572
|
+
ALTER TABLE contact_relationships ADD COLUMN relationship_status TEXT NOT NULL DEFAULT 'stable' CHECK(relationship_status IN ('warming','stable','cooling','ghost'));
|
|
2573
|
+
|
|
2574
|
+
-- CON-00074: identity resolution
|
|
2575
|
+
CREATE TABLE IF NOT EXISTS contact_identities (
|
|
2576
|
+
id TEXT PRIMARY KEY,
|
|
2577
|
+
contact_id TEXT NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
|
|
2578
|
+
system TEXT NOT NULL,
|
|
2579
|
+
external_id TEXT NOT NULL,
|
|
2580
|
+
external_url TEXT,
|
|
2581
|
+
confidence TEXT NOT NULL DEFAULT 'inferred' CHECK(confidence IN ('verified','inferred')),
|
|
2582
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
2583
|
+
UNIQUE(system, external_id)
|
|
2584
|
+
);
|
|
2585
|
+
ALTER TABLE contacts ADD COLUMN canonical_id TEXT;
|
|
2586
|
+
|
|
2587
|
+
-- CON-00076: relationship signals
|
|
2588
|
+
ALTER TABLE contacts ADD COLUMN relationship_health INTEGER NOT NULL DEFAULT 50;
|
|
2589
|
+
ALTER TABLE contacts ADD COLUMN avg_response_hours REAL;
|
|
2590
|
+
ALTER TABLE contacts ADD COLUMN preferred_channel TEXT;
|
|
2591
|
+
ALTER TABLE contacts ADD COLUMN engagement_status TEXT NOT NULL DEFAULT 'new' CHECK(engagement_status IN ('warming','stable','cooling','ghost','new'));
|
|
2592
|
+
ALTER TABLE contacts ADD COLUMN interaction_count_30d INTEGER NOT NULL DEFAULT 0;
|
|
2593
|
+
ALTER TABLE contacts ADD COLUMN interaction_count_90d INTEGER NOT NULL DEFAULT 0;
|
|
2594
|
+
|
|
2595
|
+
-- CON-00079: freshness scoring
|
|
2596
|
+
CREATE TABLE IF NOT EXISTS contact_field_confidence (
|
|
2597
|
+
id TEXT PRIMARY KEY,
|
|
2598
|
+
contact_id TEXT NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
|
|
2599
|
+
field_name TEXT NOT NULL,
|
|
2600
|
+
confidence TEXT NOT NULL DEFAULT 'imported' CHECK(confidence IN ('verified','inferred','imported','stale')),
|
|
2601
|
+
source TEXT,
|
|
2602
|
+
last_verified_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
2603
|
+
UNIQUE(contact_id, field_name)
|
|
2604
|
+
);
|
|
2605
|
+
|
|
2606
|
+
-- CON-00080: org chart
|
|
2607
|
+
CREATE TABLE IF NOT EXISTS org_chart_edges (
|
|
2608
|
+
id TEXT PRIMARY KEY,
|
|
2609
|
+
company_id TEXT NOT NULL REFERENCES companies(id) ON DELETE CASCADE,
|
|
2610
|
+
contact_a_id TEXT NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
|
|
2611
|
+
contact_b_id TEXT NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
|
|
2612
|
+
edge_type TEXT NOT NULL CHECK(edge_type IN ('reports_to','manages','collaborates_with','peer')),
|
|
2613
|
+
inferred INTEGER NOT NULL DEFAULT 0,
|
|
2614
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
2615
|
+
UNIQUE(company_id, contact_a_id, contact_b_id, edge_type)
|
|
2616
|
+
);
|
|
2617
|
+
CREATE TABLE IF NOT EXISTS deal_contact_roles (
|
|
2618
|
+
id TEXT PRIMARY KEY,
|
|
2619
|
+
deal_id TEXT NOT NULL REFERENCES deals(id) ON DELETE CASCADE,
|
|
2620
|
+
contact_id TEXT NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
|
|
2621
|
+
account_role TEXT NOT NULL CHECK(account_role IN ('economic_buyer','technical_evaluator','champion','blocker','influencer','user','sponsor','other')),
|
|
2622
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
2623
|
+
UNIQUE(deal_id, contact_id)
|
|
2624
|
+
);
|
|
2625
|
+
|
|
2626
|
+
-- CON-00075: embeddings
|
|
2627
|
+
CREATE TABLE IF NOT EXISTS contact_embeddings (
|
|
2628
|
+
contact_id TEXT PRIMARY KEY REFERENCES contacts(id) ON DELETE CASCADE,
|
|
2629
|
+
embedding TEXT NOT NULL,
|
|
2630
|
+
model TEXT NOT NULL DEFAULT 'tfidf',
|
|
2631
|
+
embedded_text TEXT,
|
|
2632
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
2633
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
2634
|
+
);
|
|
2517
2635
|
`
|
|
2518
2636
|
];
|
|
2519
2637
|
});
|
|
@@ -2545,6 +2663,7 @@ __export(exports_contacts, {
|
|
|
2545
2663
|
getContactProjectIds: () => getContactProjectIds,
|
|
2546
2664
|
getContactByEmail: () => getContactByEmail,
|
|
2547
2665
|
getContact: () => getContact,
|
|
2666
|
+
findOrCreateContact: () => findOrCreateContact,
|
|
2548
2667
|
deleteContact: () => deleteContact,
|
|
2549
2668
|
createContact: () => createContact,
|
|
2550
2669
|
autoLinkContactToCompany: () => autoLinkContactToCompany,
|
|
@@ -3074,6 +3193,25 @@ function listColdContacts(days, db) {
|
|
|
3074
3193
|
LIMIT 100`).all(`-${days}`);
|
|
3075
3194
|
return rows.map((row) => loadContactDetails(d, rowToContact(row)));
|
|
3076
3195
|
}
|
|
3196
|
+
async function findOrCreateContact(input, db) {
|
|
3197
|
+
const d = db || getDatabase();
|
|
3198
|
+
const emailAddresses = (input.emails ?? []).map((e) => e.address);
|
|
3199
|
+
for (const addr of emailAddresses) {
|
|
3200
|
+
const emailRow = d.query(`SELECT contact_id FROM emails WHERE LOWER(address) = LOWER(?) AND contact_id IS NOT NULL LIMIT 1`).get(addr);
|
|
3201
|
+
if (emailRow) {
|
|
3202
|
+
return { contact: getContact(emailRow.contact_id, d), created: false };
|
|
3203
|
+
}
|
|
3204
|
+
}
|
|
3205
|
+
const nameQuery = input.display_name ?? (input.first_name || input.last_name ? `${input.first_name ?? ""} ${input.last_name ?? ""}`.trim() : null);
|
|
3206
|
+
if (nameQuery) {
|
|
3207
|
+
const results = searchContacts(nameQuery, d);
|
|
3208
|
+
if (results.length > 0 && results[0]) {
|
|
3209
|
+
return { contact: results[0], created: false };
|
|
3210
|
+
}
|
|
3211
|
+
}
|
|
3212
|
+
const contact = createContact(input, d);
|
|
3213
|
+
return { contact, created: true };
|
|
3214
|
+
}
|
|
3077
3215
|
function autoLinkContactToCompany(contactId, db) {
|
|
3078
3216
|
const d = db || getDatabase();
|
|
3079
3217
|
const row = d.query(`SELECT * FROM contacts WHERE id = ?`).get(contactId);
|
|
@@ -3354,6 +3492,61 @@ var init_companies = __esm(() => {
|
|
|
3354
3492
|
});
|
|
3355
3493
|
|
|
3356
3494
|
// src/db/relationships.ts
|
|
3495
|
+
var exports_relationships = {};
|
|
3496
|
+
__export(exports_relationships, {
|
|
3497
|
+
listRelationships: () => listRelationships,
|
|
3498
|
+
listCompanyRelationships: () => listCompanyRelationships,
|
|
3499
|
+
getRelationship: () => getRelationship,
|
|
3500
|
+
getEntityTeam: () => getEntityTeam,
|
|
3501
|
+
deleteRelationship: () => deleteRelationship,
|
|
3502
|
+
deleteCompanyRelationship: () => deleteCompanyRelationship,
|
|
3503
|
+
createRelationship: () => createRelationship,
|
|
3504
|
+
createCompanyRelationship: () => createCompanyRelationship
|
|
3505
|
+
});
|
|
3506
|
+
function rowToRelationship(row) {
|
|
3507
|
+
return {
|
|
3508
|
+
...row,
|
|
3509
|
+
relationship_type: row.relationship_type
|
|
3510
|
+
};
|
|
3511
|
+
}
|
|
3512
|
+
function createRelationship(input, db) {
|
|
3513
|
+
const d = db || getDatabase();
|
|
3514
|
+
const a = d.query(`SELECT id FROM contacts WHERE id = ?`).get(input.contact_a_id);
|
|
3515
|
+
if (!a)
|
|
3516
|
+
throw new ContactNotFoundError(input.contact_a_id);
|
|
3517
|
+
const b = d.query(`SELECT id FROM contacts WHERE id = ?`).get(input.contact_b_id);
|
|
3518
|
+
if (!b)
|
|
3519
|
+
throw new ContactNotFoundError(input.contact_b_id);
|
|
3520
|
+
const id = uuid();
|
|
3521
|
+
d.run(`INSERT INTO contact_relationships (id, contact_a_id, contact_b_id, relationship_type, notes) VALUES (?, ?, ?, ?, ?)`, [id, input.contact_a_id, input.contact_b_id, input.relationship_type, input.notes ?? null]);
|
|
3522
|
+
return rowToRelationship(d.query(`SELECT * FROM contact_relationships WHERE id = ?`).get(id));
|
|
3523
|
+
}
|
|
3524
|
+
function listRelationships(opts = {}, db) {
|
|
3525
|
+
const d = db || getDatabase();
|
|
3526
|
+
const { contact_id, relationship_type } = opts;
|
|
3527
|
+
const conditions = [];
|
|
3528
|
+
const params = [];
|
|
3529
|
+
if (contact_id) {
|
|
3530
|
+
conditions.push("(contact_a_id = ? OR contact_b_id = ?)");
|
|
3531
|
+
params.push(contact_id, contact_id);
|
|
3532
|
+
}
|
|
3533
|
+
if (relationship_type) {
|
|
3534
|
+
conditions.push("relationship_type = ?");
|
|
3535
|
+
params.push(relationship_type);
|
|
3536
|
+
}
|
|
3537
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
3538
|
+
const rows = d.query(`SELECT * FROM contact_relationships ${where} ORDER BY created_at DESC`).all(...params);
|
|
3539
|
+
return rows.map(rowToRelationship);
|
|
3540
|
+
}
|
|
3541
|
+
function getRelationship(id, db) {
|
|
3542
|
+
const d = db || getDatabase();
|
|
3543
|
+
const row = d.query(`SELECT * FROM contact_relationships WHERE id = ?`).get(id);
|
|
3544
|
+
return row ? rowToRelationship(row) : null;
|
|
3545
|
+
}
|
|
3546
|
+
function deleteRelationship(id, db) {
|
|
3547
|
+
const d = db || getDatabase();
|
|
3548
|
+
d.run(`DELETE FROM contact_relationships WHERE id = ?`, [id]);
|
|
3549
|
+
}
|
|
3357
3550
|
function rowToCompanyRelationship(row) {
|
|
3358
3551
|
return {
|
|
3359
3552
|
...row,
|
|
@@ -3406,6 +3599,25 @@ function listCompanyRelationships(opts = {}, db) {
|
|
|
3406
3599
|
const rows = d.query(`SELECT * FROM company_relationships ${where} ORDER BY created_at DESC`).all(...params);
|
|
3407
3600
|
return rows.map(rowToCompanyRelationship);
|
|
3408
3601
|
}
|
|
3602
|
+
function deleteCompanyRelationship(id, db) {
|
|
3603
|
+
const d = db || getDatabase();
|
|
3604
|
+
d.run(`DELETE FROM company_relationships WHERE id = ?`, [id]);
|
|
3605
|
+
}
|
|
3606
|
+
function getEntityTeam(companyId, db) {
|
|
3607
|
+
const d = db || getDatabase();
|
|
3608
|
+
const rows = d.query(`SELECT id, contact_id, relationship_type, is_primary, status, start_date, end_date, notes
|
|
3609
|
+
FROM company_relationships WHERE company_id = ? ORDER BY is_primary DESC, created_at ASC`).all(companyId);
|
|
3610
|
+
return rows.map((r) => ({
|
|
3611
|
+
contact_id: r.contact_id,
|
|
3612
|
+
relationship_id: r.id,
|
|
3613
|
+
relationship_type: r.relationship_type,
|
|
3614
|
+
is_primary: !!r.is_primary,
|
|
3615
|
+
status: r.status,
|
|
3616
|
+
start_date: r.start_date,
|
|
3617
|
+
end_date: r.end_date,
|
|
3618
|
+
notes: r.notes
|
|
3619
|
+
}));
|
|
3620
|
+
}
|
|
3409
3621
|
var init_relationships = __esm(() => {
|
|
3410
3622
|
init_types();
|
|
3411
3623
|
init_database();
|
|
@@ -4976,6 +5188,590 @@ var init_events = __esm(() => {
|
|
|
4976
5188
|
init_database();
|
|
4977
5189
|
});
|
|
4978
5190
|
|
|
5191
|
+
// src/db/field-history.ts
|
|
5192
|
+
var exports_field_history = {};
|
|
5193
|
+
__export(exports_field_history, {
|
|
5194
|
+
recordFieldChange: () => recordFieldChange,
|
|
5195
|
+
getFieldHistory: () => getFieldHistory,
|
|
5196
|
+
getContactAt: () => getContactAt
|
|
5197
|
+
});
|
|
5198
|
+
function recordFieldChange(contactId, fieldName, oldValue, newValue, source, createdBy, db) {
|
|
5199
|
+
const _db2 = db || getDatabase();
|
|
5200
|
+
_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());
|
|
5201
|
+
}
|
|
5202
|
+
function getFieldHistory(contactId, fieldName, db) {
|
|
5203
|
+
const _db2 = db || getDatabase();
|
|
5204
|
+
if (fieldName) {
|
|
5205
|
+
return _db2.query(`SELECT * FROM contact_field_history WHERE contact_id=? AND field_name=? ORDER BY valid_from DESC`).all(contactId, fieldName);
|
|
5206
|
+
}
|
|
5207
|
+
return _db2.query(`SELECT * FROM contact_field_history WHERE contact_id=? ORDER BY valid_from DESC`).all(contactId);
|
|
5208
|
+
}
|
|
5209
|
+
function getContactAt(contactId, timestamp, db) {
|
|
5210
|
+
const _db2 = db || getDatabase();
|
|
5211
|
+
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);
|
|
5212
|
+
const result = {};
|
|
5213
|
+
for (const r of rows) {
|
|
5214
|
+
if (r.new_value != null)
|
|
5215
|
+
result[r.field_name] = r.new_value;
|
|
5216
|
+
}
|
|
5217
|
+
return result;
|
|
5218
|
+
}
|
|
5219
|
+
var init_field_history = __esm(() => {
|
|
5220
|
+
init_database();
|
|
5221
|
+
});
|
|
5222
|
+
|
|
5223
|
+
// src/db/learnings.ts
|
|
5224
|
+
var exports_learnings = {};
|
|
5225
|
+
__export(exports_learnings, {
|
|
5226
|
+
searchLearnings: () => searchLearnings,
|
|
5227
|
+
saveLearning: () => saveLearning,
|
|
5228
|
+
getLearnings: () => getLearnings,
|
|
5229
|
+
deleteLearning: () => deleteLearning,
|
|
5230
|
+
decayLearnings: () => decayLearnings,
|
|
5231
|
+
confirmLearning: () => confirmLearning
|
|
5232
|
+
});
|
|
5233
|
+
function rowToLearning(r) {
|
|
5234
|
+
return { ...r, tags: JSON.parse(r["tags"] || "[]") };
|
|
5235
|
+
}
|
|
5236
|
+
function saveLearning(contactId, input, db) {
|
|
5237
|
+
const _db2 = db || getDatabase();
|
|
5238
|
+
const id = uuid();
|
|
5239
|
+
_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());
|
|
5240
|
+
return rowToLearning(_db2.query(`SELECT * FROM contact_learnings WHERE id=?`).get(id));
|
|
5241
|
+
}
|
|
5242
|
+
function getLearnings(contactId, opts = {}, db) {
|
|
5243
|
+
const _db2 = db || getDatabase();
|
|
5244
|
+
let sql = `SELECT * FROM contact_learnings WHERE contact_id=?`;
|
|
5245
|
+
const params = [contactId];
|
|
5246
|
+
if (opts.type) {
|
|
5247
|
+
sql += ` AND type=?`;
|
|
5248
|
+
params.push(opts.type);
|
|
5249
|
+
}
|
|
5250
|
+
if (opts.min_importance) {
|
|
5251
|
+
sql += ` AND importance>=?`;
|
|
5252
|
+
params.push(opts.min_importance);
|
|
5253
|
+
}
|
|
5254
|
+
if (opts.visibility) {
|
|
5255
|
+
sql += ` AND visibility=?`;
|
|
5256
|
+
params.push(opts.visibility);
|
|
5257
|
+
}
|
|
5258
|
+
sql += ` ORDER BY importance DESC, confidence DESC`;
|
|
5259
|
+
return _db2.query(sql).all(...params).map(rowToLearning);
|
|
5260
|
+
}
|
|
5261
|
+
function searchLearnings(query, opts = {}, db) {
|
|
5262
|
+
const _db2 = db || getDatabase();
|
|
5263
|
+
let sql = `SELECT * FROM contact_learnings WHERE content LIKE ?`;
|
|
5264
|
+
const params = [`%${query}%`];
|
|
5265
|
+
if (opts.type) {
|
|
5266
|
+
sql += ` AND type=?`;
|
|
5267
|
+
params.push(opts.type);
|
|
5268
|
+
}
|
|
5269
|
+
if (opts.contact_id) {
|
|
5270
|
+
sql += ` AND contact_id=?`;
|
|
5271
|
+
params.push(opts.contact_id);
|
|
5272
|
+
}
|
|
5273
|
+
sql += ` ORDER BY importance DESC, confidence DESC LIMIT 50`;
|
|
5274
|
+
return _db2.query(sql).all(...params).map(rowToLearning);
|
|
5275
|
+
}
|
|
5276
|
+
function confirmLearning(learningId, _agentName, db) {
|
|
5277
|
+
const _db2 = db || getDatabase();
|
|
5278
|
+
_db2.query(`UPDATE contact_learnings SET confirmed_count=confirmed_count+1, confidence=MIN(100,confidence+10), updated_at=? WHERE id=?`).run(now(), learningId);
|
|
5279
|
+
}
|
|
5280
|
+
function decayLearnings(db) {
|
|
5281
|
+
const _db2 = db || getDatabase();
|
|
5282
|
+
const cutoff = new Date(Date.now() - 30 * 86400000).toISOString();
|
|
5283
|
+
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);
|
|
5284
|
+
return result.changes || 0;
|
|
5285
|
+
}
|
|
5286
|
+
function deleteLearning(learningId, db) {
|
|
5287
|
+
const _db2 = db || getDatabase();
|
|
5288
|
+
_db2.query(`DELETE FROM contact_learnings WHERE id=?`).run(learningId);
|
|
5289
|
+
}
|
|
5290
|
+
var init_learnings = __esm(() => {
|
|
5291
|
+
init_database();
|
|
5292
|
+
});
|
|
5293
|
+
|
|
5294
|
+
// src/db/graph.ts
|
|
5295
|
+
var exports_graph = {};
|
|
5296
|
+
__export(exports_graph, {
|
|
5297
|
+
findWarmPath: () => findWarmPath,
|
|
5298
|
+
findConnectionsAtCompany: () => findConnectionsAtCompany,
|
|
5299
|
+
detectCoolingRelationships: () => detectCoolingRelationships,
|
|
5300
|
+
computeRelationshipStrength: () => computeRelationshipStrength
|
|
5301
|
+
});
|
|
5302
|
+
function computeRelationshipStrength(contactId, db) {
|
|
5303
|
+
const _db2 = db || getDatabase();
|
|
5304
|
+
const contact = _db2.query(`SELECT last_contacted_at, interaction_count_30d, interaction_count_90d FROM contacts WHERE id=?`).get(contactId);
|
|
5305
|
+
if (!contact)
|
|
5306
|
+
return 0;
|
|
5307
|
+
let score = 50;
|
|
5308
|
+
if (contact.last_contacted_at) {
|
|
5309
|
+
const days = Math.floor((Date.now() - new Date(contact.last_contacted_at).getTime()) / 86400000);
|
|
5310
|
+
score += days < 7 ? 30 : days < 30 ? 20 : days < 90 ? 5 : -20;
|
|
5311
|
+
} else {
|
|
5312
|
+
score -= 20;
|
|
5313
|
+
}
|
|
5314
|
+
score += Math.min(20, (contact.interaction_count_30d || 0) * 4);
|
|
5315
|
+
return Math.max(0, Math.min(100, score));
|
|
5316
|
+
}
|
|
5317
|
+
function findWarmPath(fromContactId, toContactId, db) {
|
|
5318
|
+
const _db2 = db || getDatabase();
|
|
5319
|
+
const visited = new Set([fromContactId]);
|
|
5320
|
+
const queue = [{ id: fromContactId, path: [] }];
|
|
5321
|
+
while (queue.length) {
|
|
5322
|
+
const item = queue.shift();
|
|
5323
|
+
const { id, path } = item;
|
|
5324
|
+
if (id === toContactId)
|
|
5325
|
+
return path;
|
|
5326
|
+
if (path.length >= 4)
|
|
5327
|
+
continue;
|
|
5328
|
+
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);
|
|
5329
|
+
for (const n of neighbors) {
|
|
5330
|
+
const nextId = n.contact_a_id === id ? n.contact_b_id : n.contact_a_id;
|
|
5331
|
+
if (visited.has(nextId))
|
|
5332
|
+
continue;
|
|
5333
|
+
visited.add(nextId);
|
|
5334
|
+
queue.push({
|
|
5335
|
+
id: nextId,
|
|
5336
|
+
path: [
|
|
5337
|
+
...path,
|
|
5338
|
+
{ contact_id: nextId, display_name: n.display_name, strength: n.strength_score || 50 }
|
|
5339
|
+
]
|
|
5340
|
+
});
|
|
5341
|
+
}
|
|
5342
|
+
}
|
|
5343
|
+
return [];
|
|
5344
|
+
}
|
|
5345
|
+
function findConnectionsAtCompany(companyId, db) {
|
|
5346
|
+
const _db2 = db || getDatabase();
|
|
5347
|
+
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);
|
|
5348
|
+
}
|
|
5349
|
+
function detectCoolingRelationships(db) {
|
|
5350
|
+
const _db2 = db || getDatabase();
|
|
5351
|
+
const cutoff = new Date(Date.now() - 45 * 86400000).toISOString();
|
|
5352
|
+
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);
|
|
5353
|
+
}
|
|
5354
|
+
var init_graph = __esm(() => {
|
|
5355
|
+
init_database();
|
|
5356
|
+
});
|
|
5357
|
+
|
|
5358
|
+
// src/db/signals.ts
|
|
5359
|
+
var exports_signals = {};
|
|
5360
|
+
__export(exports_signals, {
|
|
5361
|
+
recomputeAllSignals: () => recomputeAllSignals,
|
|
5362
|
+
getWarmingContacts: () => getWarmingContacts,
|
|
5363
|
+
getRelationshipSignals: () => getRelationshipSignals,
|
|
5364
|
+
getGhostContacts: () => getGhostContacts
|
|
5365
|
+
});
|
|
5366
|
+
function getRelationshipSignals(contactId, db) {
|
|
5367
|
+
const _db2 = db || getDatabase();
|
|
5368
|
+
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);
|
|
5369
|
+
if (!row)
|
|
5370
|
+
return [];
|
|
5371
|
+
const daysSince = row.last_contacted_at ? Math.floor((Date.now() - new Date(row.last_contacted_at).getTime()) / 86400000) : null;
|
|
5372
|
+
const signals = [];
|
|
5373
|
+
const cnt = row.interaction_count_30d || 0;
|
|
5374
|
+
const health = row.relationship_health ?? 50;
|
|
5375
|
+
if (daysSince === null || daysSince > 180) {
|
|
5376
|
+
signals.push({ ...row, signal_type: "ghost", days_since_contact: daysSince, reason: "No contact in 180+ days or never contacted" });
|
|
5377
|
+
} else if (daysSince > 60 && cnt === 0) {
|
|
5378
|
+
signals.push({ ...row, signal_type: "cooling", days_since_contact: daysSince, reason: `No contact in ${daysSince} days, no recent interactions` });
|
|
5379
|
+
} else if (cnt > 3 && health > 70) {
|
|
5380
|
+
signals.push({ ...row, signal_type: "warming", days_since_contact: daysSince, reason: `${cnt} interactions in last 30 days, health score ${health}` });
|
|
5381
|
+
} else {
|
|
5382
|
+
signals.push({ ...row, signal_type: "healthy", days_since_contact: daysSince, reason: `Last contact ${daysSince}d ago, ${cnt} interactions in 30d` });
|
|
5383
|
+
}
|
|
5384
|
+
return signals;
|
|
5385
|
+
}
|
|
5386
|
+
function getGhostContacts(db) {
|
|
5387
|
+
const _db2 = db || getDatabase();
|
|
5388
|
+
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();
|
|
5389
|
+
return rows.map((r) => ({
|
|
5390
|
+
...r,
|
|
5391
|
+
signal_type: "ghost",
|
|
5392
|
+
days_since_contact: r.last_contacted_at ? Math.floor((Date.now() - new Date(r.last_contacted_at).getTime()) / 86400000) : null,
|
|
5393
|
+
reason: "No contact in 180+ days or never contacted"
|
|
5394
|
+
}));
|
|
5395
|
+
}
|
|
5396
|
+
function getWarmingContacts(db) {
|
|
5397
|
+
const _db2 = db || getDatabase();
|
|
5398
|
+
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();
|
|
5399
|
+
return rows.map((r) => ({
|
|
5400
|
+
...r,
|
|
5401
|
+
signal_type: "warming",
|
|
5402
|
+
days_since_contact: r.last_contacted_at ? Math.floor((Date.now() - new Date(r.last_contacted_at).getTime()) / 86400000) : null,
|
|
5403
|
+
reason: `${r.interaction_count_30d} interactions in last 30 days`
|
|
5404
|
+
}));
|
|
5405
|
+
}
|
|
5406
|
+
function recomputeAllSignals(db) {
|
|
5407
|
+
const _db2 = db || getDatabase();
|
|
5408
|
+
_db2.query(`
|
|
5409
|
+
UPDATE contacts SET
|
|
5410
|
+
engagement_status = CASE
|
|
5411
|
+
WHEN interaction_count_30d > 3 THEN 'warm'
|
|
5412
|
+
WHEN last_contacted_at IS NULL OR julianday('now') - julianday(last_contacted_at) > 180 THEN 'ghost'
|
|
5413
|
+
WHEN julianday('now') - julianday(last_contacted_at) > 60 THEN 'cooling'
|
|
5414
|
+
ELSE 'active'
|
|
5415
|
+
END,
|
|
5416
|
+
updated_at = datetime('now')
|
|
5417
|
+
WHERE archived = 0
|
|
5418
|
+
`).run();
|
|
5419
|
+
const result = _db2.query(`SELECT changes() as n`).get();
|
|
5420
|
+
return { updated: result?.n ?? 0 };
|
|
5421
|
+
}
|
|
5422
|
+
var init_signals = __esm(() => {
|
|
5423
|
+
init_database();
|
|
5424
|
+
});
|
|
5425
|
+
|
|
5426
|
+
// src/db/identity.ts
|
|
5427
|
+
var exports_identity = {};
|
|
5428
|
+
__export(exports_identity, {
|
|
5429
|
+
resolveIdentity: () => resolveIdentity,
|
|
5430
|
+
resolveByPartial: () => resolveByPartial,
|
|
5431
|
+
getIdentities: () => getIdentities,
|
|
5432
|
+
addIdentity: () => addIdentity
|
|
5433
|
+
});
|
|
5434
|
+
function addIdentity(contactId, system, externalId, externalUrl, confidence = "inferred", db) {
|
|
5435
|
+
const _db2 = db || getDatabase();
|
|
5436
|
+
const id = uuid();
|
|
5437
|
+
_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());
|
|
5438
|
+
return _db2.query(`SELECT * FROM contact_identities WHERE id=?`).get(id);
|
|
5439
|
+
}
|
|
5440
|
+
function resolveIdentity(system, externalId, db) {
|
|
5441
|
+
const _db2 = db || getDatabase();
|
|
5442
|
+
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);
|
|
5443
|
+
return row || null;
|
|
5444
|
+
}
|
|
5445
|
+
function resolveByPartial(partial, db) {
|
|
5446
|
+
const _db2 = db || getDatabase();
|
|
5447
|
+
const matches = new Map;
|
|
5448
|
+
const addMatch = (id, name, title, score, reason) => {
|
|
5449
|
+
const existing = matches.get(id);
|
|
5450
|
+
if (existing) {
|
|
5451
|
+
existing.confidence_score = Math.min(100, existing.confidence_score + score);
|
|
5452
|
+
existing.match_reasons.push(reason);
|
|
5453
|
+
} else {
|
|
5454
|
+
matches.set(id, {
|
|
5455
|
+
contact: { id, display_name: name, job_title: title },
|
|
5456
|
+
confidence_score: score,
|
|
5457
|
+
match_reasons: [reason]
|
|
5458
|
+
});
|
|
5459
|
+
}
|
|
5460
|
+
};
|
|
5461
|
+
if (partial.email) {
|
|
5462
|
+
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);
|
|
5463
|
+
rows.forEach((r) => addMatch(r.id, r.display_name, r.job_title, 90, `email match: ${partial.email}`));
|
|
5464
|
+
}
|
|
5465
|
+
if (partial.linkedin_url) {
|
|
5466
|
+
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()}%`);
|
|
5467
|
+
rows.forEach((r) => addMatch(r.id, r.display_name, r.job_title, 85, `linkedin match`));
|
|
5468
|
+
}
|
|
5469
|
+
if (partial.name) {
|
|
5470
|
+
const rows = _db2.query(`SELECT id, display_name, job_title FROM contacts WHERE display_name LIKE ? AND archived=0 LIMIT 10`).all(`%${partial.name}%`);
|
|
5471
|
+
rows.forEach((r) => addMatch(r.id, r.display_name, r.job_title, 40, `name match: ${partial.name}`));
|
|
5472
|
+
}
|
|
5473
|
+
return Array.from(matches.values()).sort((a, b) => b.confidence_score - a.confidence_score);
|
|
5474
|
+
}
|
|
5475
|
+
function getIdentities(contactId, db) {
|
|
5476
|
+
const _db2 = db || getDatabase();
|
|
5477
|
+
return _db2.query(`SELECT * FROM contact_identities WHERE contact_id=? ORDER BY created_at DESC`).all(contactId);
|
|
5478
|
+
}
|
|
5479
|
+
var init_identity = __esm(() => {
|
|
5480
|
+
init_database();
|
|
5481
|
+
});
|
|
5482
|
+
|
|
5483
|
+
// src/lib/embeddings.ts
|
|
5484
|
+
var exports_embeddings = {};
|
|
5485
|
+
__export(exports_embeddings, {
|
|
5486
|
+
semanticSearch: () => semanticSearch,
|
|
5487
|
+
embedContact: () => embedContact,
|
|
5488
|
+
embedAllContacts: () => embedAllContacts,
|
|
5489
|
+
buildContactEmbeddingText: () => buildContactEmbeddingText
|
|
5490
|
+
});
|
|
5491
|
+
function tokenize(text) {
|
|
5492
|
+
return text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t) => t.length > 2);
|
|
5493
|
+
}
|
|
5494
|
+
function buildTfIdf(tokens) {
|
|
5495
|
+
const freq = new Map;
|
|
5496
|
+
for (const t of tokens)
|
|
5497
|
+
freq.set(t, (freq.get(t) || 0) + 1);
|
|
5498
|
+
const max = Math.max(...freq.values(), 1);
|
|
5499
|
+
const result = new Map;
|
|
5500
|
+
freq.forEach((v, k) => result.set(k, v / max));
|
|
5501
|
+
return result;
|
|
5502
|
+
}
|
|
5503
|
+
function cosineSimilarity(a, b) {
|
|
5504
|
+
let dot = 0, normA = 0, normB = 0;
|
|
5505
|
+
a.forEach((v, k) => {
|
|
5506
|
+
if (b.has(k))
|
|
5507
|
+
dot += v * b.get(k);
|
|
5508
|
+
normA += v * v;
|
|
5509
|
+
});
|
|
5510
|
+
b.forEach((v) => normB += v * v);
|
|
5511
|
+
return normA && normB ? dot / (Math.sqrt(normA) * Math.sqrt(normB)) : 0;
|
|
5512
|
+
}
|
|
5513
|
+
function buildContactEmbeddingText(contact) {
|
|
5514
|
+
const tags = contact.tags ?? [];
|
|
5515
|
+
const socialProfiles = contact.social_profiles ?? [];
|
|
5516
|
+
const company = contact.company;
|
|
5517
|
+
const parts = [
|
|
5518
|
+
contact.display_name,
|
|
5519
|
+
contact.job_title,
|
|
5520
|
+
contact.notes,
|
|
5521
|
+
company?.name,
|
|
5522
|
+
company?.industry,
|
|
5523
|
+
...tags.map((t) => t.name),
|
|
5524
|
+
...socialProfiles.map((s) => s.platform)
|
|
5525
|
+
].filter(Boolean);
|
|
5526
|
+
return parts.join(" ");
|
|
5527
|
+
}
|
|
5528
|
+
async function embedContact(contactId, db) {
|
|
5529
|
+
const { getContact: getContact2 } = await Promise.resolve().then(() => (init_contacts(), exports_contacts));
|
|
5530
|
+
const _db2 = db || getDatabase();
|
|
5531
|
+
const contact = getContact2(contactId, _db2);
|
|
5532
|
+
const text = buildContactEmbeddingText(contact);
|
|
5533
|
+
const tokens = tokenize(text);
|
|
5534
|
+
const tfidf = buildTfIdf(tokens);
|
|
5535
|
+
const embedding = JSON.stringify(Array.from(tfidf.entries()).sort((a, b) => b[1] - a[1]).slice(0, 100));
|
|
5536
|
+
_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());
|
|
5537
|
+
}
|
|
5538
|
+
async function embedAllContacts(db) {
|
|
5539
|
+
const _db2 = db || getDatabase();
|
|
5540
|
+
const contacts = _db2.query(`SELECT id FROM contacts WHERE archived=0`).all();
|
|
5541
|
+
for (const c of contacts) {
|
|
5542
|
+
try {
|
|
5543
|
+
await embedContact(c.id, _db2);
|
|
5544
|
+
} catch {}
|
|
5545
|
+
}
|
|
5546
|
+
return contacts.length;
|
|
5547
|
+
}
|
|
5548
|
+
function semanticSearch(query, limit = 10, db) {
|
|
5549
|
+
const _db2 = db || getDatabase();
|
|
5550
|
+
const queryTokens = buildTfIdf(tokenize(query));
|
|
5551
|
+
let embeddings = [];
|
|
5552
|
+
try {
|
|
5553
|
+
embeddings = _db2.query(`SELECT contact_id, embedding FROM contact_embeddings`).all();
|
|
5554
|
+
} catch {
|
|
5555
|
+
return [];
|
|
5556
|
+
}
|
|
5557
|
+
const results = embeddings.map((e) => {
|
|
5558
|
+
try {
|
|
5559
|
+
const emb = new Map(JSON.parse(e.embedding));
|
|
5560
|
+
return { contact_id: e.contact_id, score: cosineSimilarity(queryTokens, emb) };
|
|
5561
|
+
} catch {
|
|
5562
|
+
return { contact_id: e.contact_id, score: 0 };
|
|
5563
|
+
}
|
|
5564
|
+
}).filter((r) => r.score > 0).sort((a, b) => b.score - a.score).slice(0, limit);
|
|
5565
|
+
return results;
|
|
5566
|
+
}
|
|
5567
|
+
var init_embeddings = __esm(() => {
|
|
5568
|
+
init_database();
|
|
5569
|
+
});
|
|
5570
|
+
|
|
5571
|
+
// src/db/freshness.ts
|
|
5572
|
+
var exports_freshness = {};
|
|
5573
|
+
__export(exports_freshness, {
|
|
5574
|
+
markFieldVerified: () => markFieldVerified,
|
|
5575
|
+
getStaleContacts: () => getStaleContacts,
|
|
5576
|
+
getFreshnessScore: () => getFreshnessScore
|
|
5577
|
+
});
|
|
5578
|
+
function getFreshnessScore(contactId, db) {
|
|
5579
|
+
const _db2 = db || getDatabase();
|
|
5580
|
+
const contact = _db2.query(`SELECT * FROM contacts WHERE id=?`).get(contactId);
|
|
5581
|
+
if (!contact)
|
|
5582
|
+
throw new Error(`Contact not found: ${contactId}`);
|
|
5583
|
+
let historyRows = [];
|
|
5584
|
+
try {
|
|
5585
|
+
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);
|
|
5586
|
+
} catch {}
|
|
5587
|
+
let verifiedRows = [];
|
|
5588
|
+
try {
|
|
5589
|
+
verifiedRows = _db2.query(`SELECT field_name, verified_at, source FROM field_verifications WHERE contact_id=?`).all(contactId);
|
|
5590
|
+
} catch {}
|
|
5591
|
+
const verifiedMap = new Map(verifiedRows.map((r) => [r.field_name, r]));
|
|
5592
|
+
const historyMap = new Map;
|
|
5593
|
+
for (const r of historyRows) {
|
|
5594
|
+
if (!historyMap.has(r.field_name))
|
|
5595
|
+
historyMap.set(r.field_name, r);
|
|
5596
|
+
}
|
|
5597
|
+
const fields = SCORED_FIELDS.map((field) => {
|
|
5598
|
+
let value = null;
|
|
5599
|
+
if (field === "emails") {
|
|
5600
|
+
const emailRow = _db2.query(`SELECT address FROM emails WHERE contact_id=? LIMIT 1`).get(contactId);
|
|
5601
|
+
value = emailRow?.address ?? null;
|
|
5602
|
+
} else if (field === "phones") {
|
|
5603
|
+
const phoneRow = _db2.query(`SELECT number FROM phones WHERE contact_id=? LIMIT 1`).get(contactId);
|
|
5604
|
+
value = phoneRow?.number ?? null;
|
|
5605
|
+
} else {
|
|
5606
|
+
value = contact[field] != null ? String(contact[field]) : null;
|
|
5607
|
+
}
|
|
5608
|
+
const verified = verifiedMap.get(field);
|
|
5609
|
+
const history = historyMap.get(field);
|
|
5610
|
+
let confidence = "unknown";
|
|
5611
|
+
let days_old = null;
|
|
5612
|
+
let last_verified_at = null;
|
|
5613
|
+
let source = null;
|
|
5614
|
+
if (verified) {
|
|
5615
|
+
confidence = "verified";
|
|
5616
|
+
last_verified_at = verified.verified_at;
|
|
5617
|
+
source = verified.source;
|
|
5618
|
+
days_old = Math.floor((Date.now() - new Date(verified.verified_at).getTime()) / 86400000);
|
|
5619
|
+
} else if (history) {
|
|
5620
|
+
confidence = history.source === "import" ? "imported" : "inferred";
|
|
5621
|
+
last_verified_at = history.created_at;
|
|
5622
|
+
source = history.source;
|
|
5623
|
+
days_old = Math.floor((Date.now() - new Date(history.created_at).getTime()) / 86400000);
|
|
5624
|
+
if (days_old > 365)
|
|
5625
|
+
confidence = "stale";
|
|
5626
|
+
} else if (value) {
|
|
5627
|
+
confidence = "inferred";
|
|
5628
|
+
}
|
|
5629
|
+
return { field_name: field, value, last_verified_at, source, confidence, days_old };
|
|
5630
|
+
});
|
|
5631
|
+
const fieldScore = fields.reduce((acc, f) => {
|
|
5632
|
+
if (!f.value)
|
|
5633
|
+
return acc;
|
|
5634
|
+
if (f.confidence === "verified")
|
|
5635
|
+
return acc + 20;
|
|
5636
|
+
if (f.confidence === "imported" || f.confidence === "inferred")
|
|
5637
|
+
return acc + 10;
|
|
5638
|
+
return acc + 5;
|
|
5639
|
+
}, 0);
|
|
5640
|
+
const overall_score = Math.min(100, fieldScore);
|
|
5641
|
+
return {
|
|
5642
|
+
contact_id: contactId,
|
|
5643
|
+
overall_score,
|
|
5644
|
+
fields,
|
|
5645
|
+
stale_fields: fields.filter((f) => f.confidence === "stale" || !f.value && f.field_name !== "phones").map((f) => f.field_name),
|
|
5646
|
+
verified_fields: fields.filter((f) => f.confidence === "verified").map((f) => f.field_name)
|
|
5647
|
+
};
|
|
5648
|
+
}
|
|
5649
|
+
function getStaleContacts(threshold = 40, db) {
|
|
5650
|
+
const _db2 = db || getDatabase();
|
|
5651
|
+
const rows = _db2.query(`SELECT c.id as contact_id, c.display_name,
|
|
5652
|
+
(CASE WHEN c.job_title IS NOT NULL THEN 15 ELSE 0 END +
|
|
5653
|
+
CASE WHEN c.company_id IS NOT NULL THEN 15 ELSE 0 END +
|
|
5654
|
+
CASE WHEN c.last_contacted_at IS NOT NULL THEN 20 ELSE 0 END +
|
|
5655
|
+
CASE WHEN EXISTS(SELECT 1 FROM emails WHERE contact_id=c.id) THEN 20 ELSE 0 END +
|
|
5656
|
+
CASE WHEN EXISTS(SELECT 1 FROM phones WHERE contact_id=c.id) THEN 15 ELSE 0 END +
|
|
5657
|
+
CASE WHEN c.notes IS NOT NULL THEN 10 ELSE 0 END +
|
|
5658
|
+
CASE WHEN EXISTS(SELECT 1 FROM contact_tags WHERE contact_id=c.id) THEN 5 ELSE 0 END
|
|
5659
|
+
) as score
|
|
5660
|
+
FROM contacts c WHERE c.archived=0 HAVING score < ? ORDER BY score ASC LIMIT 100`).all(threshold);
|
|
5661
|
+
return rows;
|
|
5662
|
+
}
|
|
5663
|
+
function markFieldVerified(contactId, fieldName, source, db) {
|
|
5664
|
+
const _db2 = db || getDatabase();
|
|
5665
|
+
try {
|
|
5666
|
+
_db2.query(`INSERT OR REPLACE INTO field_verifications(contact_id,field_name,verified_at,source) VALUES(?,?,?,?)`).run(contactId, fieldName, now(), source || null);
|
|
5667
|
+
} catch {
|
|
5668
|
+
_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());
|
|
5669
|
+
}
|
|
5670
|
+
}
|
|
5671
|
+
var SCORED_FIELDS;
|
|
5672
|
+
var init_freshness = __esm(() => {
|
|
5673
|
+
init_database();
|
|
5674
|
+
SCORED_FIELDS = ["display_name", "job_title", "company_id", "emails", "phones", "last_contacted_at"];
|
|
5675
|
+
});
|
|
5676
|
+
|
|
5677
|
+
// src/lib/meeting-capture.ts
|
|
5678
|
+
var exports_meeting_capture = {};
|
|
5679
|
+
__export(exports_meeting_capture, {
|
|
5680
|
+
ingestMeetingParticipants: () => ingestMeetingParticipants
|
|
5681
|
+
});
|
|
5682
|
+
async function ingestMeetingParticipants(event, db) {
|
|
5683
|
+
const { findOrCreateContact: findOrCreateContact2 } = await Promise.resolve().then(() => (init_contacts(), exports_contacts));
|
|
5684
|
+
const { logEvent: logEvent2 } = await Promise.resolve().then(() => (init_events(), exports_events));
|
|
5685
|
+
const _db2 = db || getDatabase();
|
|
5686
|
+
let created = 0;
|
|
5687
|
+
let updated = 0;
|
|
5688
|
+
const ids = [];
|
|
5689
|
+
for (const a of event.attendees) {
|
|
5690
|
+
try {
|
|
5691
|
+
const nameParts = a.name.split(" ");
|
|
5692
|
+
const result = await findOrCreateContact2({
|
|
5693
|
+
display_name: a.name,
|
|
5694
|
+
first_name: nameParts[0],
|
|
5695
|
+
last_name: nameParts.slice(1).join(" ") || undefined,
|
|
5696
|
+
emails: [{ address: a.email, type: "work", is_primary: true }],
|
|
5697
|
+
source: "import"
|
|
5698
|
+
}, _db2);
|
|
5699
|
+
ids.push(result.contact.id);
|
|
5700
|
+
if (result.created)
|
|
5701
|
+
created++;
|
|
5702
|
+
else
|
|
5703
|
+
updated++;
|
|
5704
|
+
} catch {}
|
|
5705
|
+
}
|
|
5706
|
+
if (ids.length) {
|
|
5707
|
+
try {
|
|
5708
|
+
logEvent2({
|
|
5709
|
+
title: event.title,
|
|
5710
|
+
type: "meeting",
|
|
5711
|
+
event_date: event.event_date,
|
|
5712
|
+
contact_ids: ids,
|
|
5713
|
+
notes: event.context
|
|
5714
|
+
}, _db2);
|
|
5715
|
+
} catch {}
|
|
5716
|
+
}
|
|
5717
|
+
return { created, updated, contact_ids: ids };
|
|
5718
|
+
}
|
|
5719
|
+
var init_meeting_capture = __esm(() => {
|
|
5720
|
+
init_database();
|
|
5721
|
+
});
|
|
5722
|
+
|
|
5723
|
+
// src/db/org-chart.ts
|
|
5724
|
+
var exports_org_chart = {};
|
|
5725
|
+
__export(exports_org_chart, {
|
|
5726
|
+
setDealContactRole: () => setDealContactRole,
|
|
5727
|
+
listOrgChart: () => listOrgChart,
|
|
5728
|
+
getDealTeam: () => getDealTeam,
|
|
5729
|
+
getCoverageGaps: () => getCoverageGaps,
|
|
5730
|
+
addOrgChartEdge: () => addOrgChartEdge
|
|
5731
|
+
});
|
|
5732
|
+
function addOrgChartEdge(companyId, contactAId, contactBId, edgeType, inferred = false, db) {
|
|
5733
|
+
const _db2 = db || getDatabase();
|
|
5734
|
+
const id = uuid();
|
|
5735
|
+
_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());
|
|
5736
|
+
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);
|
|
5737
|
+
}
|
|
5738
|
+
function listOrgChart(companyId, db) {
|
|
5739
|
+
const _db2 = db || getDatabase();
|
|
5740
|
+
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);
|
|
5741
|
+
}
|
|
5742
|
+
function setDealContactRole(dealId, contactId, accountRole, db) {
|
|
5743
|
+
const _db2 = db || getDatabase();
|
|
5744
|
+
const id = uuid();
|
|
5745
|
+
_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());
|
|
5746
|
+
return _db2.query(`SELECT * FROM deal_contact_roles WHERE deal_id=? AND contact_id=?`).get(dealId, contactId);
|
|
5747
|
+
}
|
|
5748
|
+
function getDealTeam(dealId, db) {
|
|
5749
|
+
const _db2 = db || getDatabase();
|
|
5750
|
+
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);
|
|
5751
|
+
}
|
|
5752
|
+
function getCoverageGaps(companyId, db) {
|
|
5753
|
+
const _db2 = db || getDatabase();
|
|
5754
|
+
const total = _db2.query(`SELECT COUNT(*) c FROM contacts WHERE company_id=? AND archived=0`).get(companyId).c;
|
|
5755
|
+
const hasManager = _db2.query(`SELECT COUNT(*) c FROM org_chart_edges WHERE company_id=? AND edge_type='manages'`).get(companyId).c > 0;
|
|
5756
|
+
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;
|
|
5757
|
+
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;
|
|
5758
|
+
const missing = [
|
|
5759
|
+
!hasManager && "org chart relationships",
|
|
5760
|
+
!hasEco && "economic buyer",
|
|
5761
|
+
!hasTech && "technical evaluator"
|
|
5762
|
+
].filter(Boolean);
|
|
5763
|
+
return {
|
|
5764
|
+
total_contacts: total,
|
|
5765
|
+
has_manager: hasManager,
|
|
5766
|
+
has_technical: hasTech,
|
|
5767
|
+
has_economic_buyer: hasEco,
|
|
5768
|
+
suggestion: missing.length ? `Missing: ${missing.join(", ")}` : "Good coverage"
|
|
5769
|
+
};
|
|
5770
|
+
}
|
|
5771
|
+
var init_org_chart = __esm(() => {
|
|
5772
|
+
init_database();
|
|
5773
|
+
});
|
|
5774
|
+
|
|
4979
5775
|
// node_modules/commander/esm.mjs
|
|
4980
5776
|
var import__ = __toESM(require_commander(), 1);
|
|
4981
5777
|
var {
|
|
@@ -5382,7 +6178,7 @@ async function confirm(question) {
|
|
|
5382
6178
|
const answer = await prompt(question + " [y/N]");
|
|
5383
6179
|
return answer.toLowerCase() === "y" || answer.toLowerCase() === "yes";
|
|
5384
6180
|
}
|
|
5385
|
-
program.name("contacts").description("Open Contacts \u2014 contact management for AI coding agents").version("0.
|
|
6181
|
+
program.name("contacts").description("Open Contacts \u2014 contact management for AI coding agents").version("0.5.0");
|
|
5386
6182
|
function collect(val, prev) {
|
|
5387
6183
|
return [...prev, val];
|
|
5388
6184
|
}
|
|
@@ -6943,4 +7739,363 @@ program.command("dnc <id>").description("Mark contact as do-not-contact").option
|
|
|
6943
7739
|
`));
|
|
6944
7740
|
}
|
|
6945
7741
|
});
|
|
7742
|
+
program.command("history <id>").description("Show field change timeline for a contact").option("--field <name>", "Filter to a specific field").action(async (id, opts) => {
|
|
7743
|
+
const { getFieldHistory: getFieldHistory2 } = await Promise.resolve().then(() => (init_field_history(), exports_field_history));
|
|
7744
|
+
const db = getDatabase();
|
|
7745
|
+
const history = getFieldHistory2(id, opts.field, db);
|
|
7746
|
+
if (!history.length) {
|
|
7747
|
+
console.log(chalk.gray(`
|
|
7748
|
+
No field history found.
|
|
7749
|
+
`));
|
|
7750
|
+
return;
|
|
7751
|
+
}
|
|
7752
|
+
const contact = getContact(id);
|
|
7753
|
+
console.log(chalk.bold(`
|
|
7754
|
+
Field History: ${contact.display_name}
|
|
7755
|
+
`));
|
|
7756
|
+
renderTable(["Field", "Old Value", "New Value", "When", "Source"], history.map((h) => ({
|
|
7757
|
+
Field: h.field_name,
|
|
7758
|
+
"Old Value": h.old_value ?? "",
|
|
7759
|
+
"New Value": h.new_value ?? "",
|
|
7760
|
+
When: h.valid_from.slice(0, 16),
|
|
7761
|
+
Source: h.source ?? ""
|
|
7762
|
+
})));
|
|
7763
|
+
console.log();
|
|
7764
|
+
});
|
|
7765
|
+
program.command("learnings [id]").description("Show or search learnings for a contact").option("--search <query>", "Cross-contact search across all learnings").option("--type <type>", "Filter by type: preference|fact|inference|warning|signal").option("--min-importance <n>", "Minimum importance (1-10)").action(async (id, opts) => {
|
|
7766
|
+
const { getLearnings: getLearnings2, searchLearnings: searchLearnings2 } = await Promise.resolve().then(() => (init_learnings(), exports_learnings));
|
|
7767
|
+
const db = getDatabase();
|
|
7768
|
+
if (opts.search) {
|
|
7769
|
+
const results = searchLearnings2(opts.search, { type: opts.type, contact_id: id }, db);
|
|
7770
|
+
if (!results.length) {
|
|
7771
|
+
console.log(chalk.gray(`
|
|
7772
|
+
No learnings found for: "${opts.search}"
|
|
7773
|
+
`));
|
|
7774
|
+
return;
|
|
7775
|
+
}
|
|
7776
|
+
console.log(chalk.bold(`
|
|
7777
|
+
Learnings matching "${opts.search}":
|
|
7778
|
+
`));
|
|
7779
|
+
renderTable(["Contact", "Type", "Confidence", "Content"], results.map((r) => {
|
|
7780
|
+
let name = r.contact_id;
|
|
7781
|
+
try {
|
|
7782
|
+
name = getContact(r.contact_id).display_name;
|
|
7783
|
+
} catch {}
|
|
7784
|
+
return { Contact: name, Type: r.type, Confidence: String(r.confidence) + "%", Content: r.content };
|
|
7785
|
+
}));
|
|
7786
|
+
console.log(chalk.gray(`
|
|
7787
|
+
${results.length} learning(s)
|
|
7788
|
+
`));
|
|
7789
|
+
return;
|
|
7790
|
+
}
|
|
7791
|
+
if (!id) {
|
|
7792
|
+
console.error(chalk.red("Provide a contact ID or use --search <query>"));
|
|
7793
|
+
process.exit(1);
|
|
7794
|
+
}
|
|
7795
|
+
const learnings = getLearnings2(id, {
|
|
7796
|
+
type: opts.type,
|
|
7797
|
+
min_importance: opts.minImportance ? parseInt(opts.minImportance, 10) : undefined
|
|
7798
|
+
}, db);
|
|
7799
|
+
const contact = getContact(id);
|
|
7800
|
+
if (!learnings.length) {
|
|
7801
|
+
console.log(chalk.gray(`
|
|
7802
|
+
No learnings for ${contact.display_name}.
|
|
7803
|
+
`));
|
|
7804
|
+
return;
|
|
7805
|
+
}
|
|
7806
|
+
console.log(chalk.bold(`
|
|
7807
|
+
Learnings: ${contact.display_name} (${learnings.length})
|
|
7808
|
+
`));
|
|
7809
|
+
for (const l of learnings) {
|
|
7810
|
+
const conf = l.confidence >= 80 ? chalk.green : l.confidence >= 50 ? chalk.yellow : chalk.red;
|
|
7811
|
+
console.log(` ${conf(String(l.confidence).padStart(3) + "%")} ${chalk.cyan(l.type.padEnd(12))} ${l.content}`);
|
|
7812
|
+
}
|
|
7813
|
+
console.log();
|
|
7814
|
+
});
|
|
7815
|
+
program.command("graph <id>").description("Show relationship network and strength score for a contact").action(async (id) => {
|
|
7816
|
+
const { computeRelationshipStrength: computeRelationshipStrength2 } = await Promise.resolve().then(() => (init_graph(), exports_graph));
|
|
7817
|
+
const { listRelationships: listRelationships2 } = await Promise.resolve().then(() => (init_relationships(), exports_relationships));
|
|
7818
|
+
const db = getDatabase();
|
|
7819
|
+
const contact = getContact(id);
|
|
7820
|
+
const strength = computeRelationshipStrength2(id, db);
|
|
7821
|
+
const rels = listRelationships2({ contact_id: id });
|
|
7822
|
+
console.log(chalk.bold(`
|
|
7823
|
+
Network: ${contact.display_name}`));
|
|
7824
|
+
const strengthColor = strength >= 70 ? chalk.green : strength >= 40 ? chalk.yellow : chalk.red;
|
|
7825
|
+
console.log(` Relationship Strength: ${strengthColor(String(strength) + "/100")}
|
|
7826
|
+
`);
|
|
7827
|
+
if (!rels.length) {
|
|
7828
|
+
console.log(chalk.gray(` No relationships mapped.
|
|
7829
|
+
`));
|
|
7830
|
+
return;
|
|
7831
|
+
}
|
|
7832
|
+
for (const r of rels) {
|
|
7833
|
+
const otherId = r.contact_a_id === id ? r.contact_b_id : r.contact_a_id;
|
|
7834
|
+
let otherName = otherId;
|
|
7835
|
+
try {
|
|
7836
|
+
otherName = getContact(otherId).display_name;
|
|
7837
|
+
} catch {}
|
|
7838
|
+
console.log(` ${chalk.cyan(r.relationship_type.padEnd(14))} ${otherName}`);
|
|
7839
|
+
}
|
|
7840
|
+
console.log();
|
|
7841
|
+
});
|
|
7842
|
+
program.command("cooling").description("Show warming, cooling, and ghost contacts").action(async () => {
|
|
7843
|
+
const { detectCoolingRelationships: detectCoolingRelationships2 } = await Promise.resolve().then(() => (init_graph(), exports_graph));
|
|
7844
|
+
const { getGhostContacts: getGhostContacts2, getWarmingContacts: getWarmingContacts2 } = await Promise.resolve().then(() => (init_signals(), exports_signals));
|
|
7845
|
+
const db = getDatabase();
|
|
7846
|
+
const cooling = detectCoolingRelationships2(db);
|
|
7847
|
+
const ghosts = getGhostContacts2(db);
|
|
7848
|
+
const warming = getWarmingContacts2(db);
|
|
7849
|
+
if (warming.length) {
|
|
7850
|
+
console.log(chalk.bold.green(`
|
|
7851
|
+
Warming (${warming.length}):`));
|
|
7852
|
+
for (const c of warming.slice(0, 10)) {
|
|
7853
|
+
console.log(` ${chalk.green("\u2191")} ${c.display_name} ${chalk.gray(c.days_since_contact !== null ? c.days_since_contact + "d ago" : "never")}`);
|
|
7854
|
+
}
|
|
7855
|
+
}
|
|
7856
|
+
if (cooling.length) {
|
|
7857
|
+
console.log(chalk.bold.yellow(`
|
|
7858
|
+
Cooling (${cooling.length}):`));
|
|
7859
|
+
for (const c of cooling.slice(0, 10)) {
|
|
7860
|
+
console.log(` ${chalk.yellow("\u2193")} ${c.display_name} ${chalk.gray(String(c.days_since) + "d ago")}`);
|
|
7861
|
+
}
|
|
7862
|
+
}
|
|
7863
|
+
if (ghosts.length) {
|
|
7864
|
+
console.log(chalk.bold.red(`
|
|
7865
|
+
Ghost (${ghosts.length}):`));
|
|
7866
|
+
for (const c of ghosts.slice(0, 10)) {
|
|
7867
|
+
console.log(` ${chalk.red("\u2620")} ${c.display_name} ${chalk.gray(c.days_since_contact !== null ? c.days_since_contact + "d ago" : "never contacted")}`);
|
|
7868
|
+
}
|
|
7869
|
+
}
|
|
7870
|
+
if (!warming.length && !cooling.length && !ghosts.length) {
|
|
7871
|
+
console.log(chalk.green(`
|
|
7872
|
+
All relationships look healthy!
|
|
7873
|
+
`));
|
|
7874
|
+
}
|
|
7875
|
+
console.log();
|
|
7876
|
+
});
|
|
7877
|
+
program.command("resolve").description("Resolve contact identity before creating (check for existing contact)").option("--email <email>", "Email to search").option("--name <name>", "Name to search").option("--linkedin <url>", "LinkedIn URL to search").action(async (opts) => {
|
|
7878
|
+
const { resolveByPartial: resolveByPartial2 } = await Promise.resolve().then(() => (init_identity(), exports_identity));
|
|
7879
|
+
const db = getDatabase();
|
|
7880
|
+
const matches = resolveByPartial2({ email: opts.email, name: opts.name, linkedin_url: opts.linkedin }, db);
|
|
7881
|
+
if (!matches.length) {
|
|
7882
|
+
console.log(chalk.gray(`
|
|
7883
|
+
No matches found \u2014 safe to create.
|
|
7884
|
+
`));
|
|
7885
|
+
return;
|
|
7886
|
+
}
|
|
7887
|
+
console.log(chalk.bold(`
|
|
7888
|
+
Potential matches (${matches.length}):
|
|
7889
|
+
`));
|
|
7890
|
+
renderTable(["Name", "Job Title", "Confidence", "Match Reasons"], matches.map((m) => ({
|
|
7891
|
+
Name: m.contact.display_name,
|
|
7892
|
+
"Job Title": m.contact.job_title ?? "",
|
|
7893
|
+
Confidence: String(m.confidence_score) + "%",
|
|
7894
|
+
"Match Reasons": m.match_reasons.join("; ")
|
|
7895
|
+
})));
|
|
7896
|
+
console.log();
|
|
7897
|
+
});
|
|
7898
|
+
program.command("search-semantic <query>").description('Semantic capability search using TF-IDF embeddings (run "contacts embed --all" first)').option("--limit <n>", "Max results", "10").action(async (query, opts) => {
|
|
7899
|
+
const { semanticSearch: semanticSearch2 } = await Promise.resolve().then(() => (init_embeddings(), exports_embeddings));
|
|
7900
|
+
const db = getDatabase();
|
|
7901
|
+
const results = semanticSearch2(query, parseInt(opts.limit, 10), db);
|
|
7902
|
+
if (!results.length) {
|
|
7903
|
+
console.log(chalk.gray(`
|
|
7904
|
+
No semantic matches for "${query}". Try running: contacts embed --all
|
|
7905
|
+
`));
|
|
7906
|
+
return;
|
|
7907
|
+
}
|
|
7908
|
+
console.log(chalk.bold(`
|
|
7909
|
+
Semantic search: "${query}"
|
|
7910
|
+
`));
|
|
7911
|
+
renderTable(["Name", "Score", "Company", "Title"], results.map((r) => {
|
|
7912
|
+
let name = r.contact_id;
|
|
7913
|
+
let company = "";
|
|
7914
|
+
let title = "";
|
|
7915
|
+
try {
|
|
7916
|
+
const c = getContact(r.contact_id);
|
|
7917
|
+
name = c.display_name;
|
|
7918
|
+
company = c.company?.name ?? "";
|
|
7919
|
+
title = c.job_title ?? "";
|
|
7920
|
+
} catch {}
|
|
7921
|
+
return { Name: name, Score: (r.score * 100).toFixed(1) + "%", Company: company, Title: title };
|
|
7922
|
+
}));
|
|
7923
|
+
console.log();
|
|
7924
|
+
});
|
|
7925
|
+
program.command("signals <id>").description("Show relationship health signals for a contact").action(async (id) => {
|
|
7926
|
+
const { getRelationshipSignals: getRelationshipSignals2 } = await Promise.resolve().then(() => (init_signals(), exports_signals));
|
|
7927
|
+
const db = getDatabase();
|
|
7928
|
+
const contact = getContact(id);
|
|
7929
|
+
const signals = getRelationshipSignals2(id, db);
|
|
7930
|
+
console.log(chalk.bold(`
|
|
7931
|
+
Signals: ${contact.display_name}
|
|
7932
|
+
`));
|
|
7933
|
+
for (const s of signals) {
|
|
7934
|
+
const color = s.signal_type === "warming" ? chalk.green : s.signal_type === "ghost" ? chalk.red : s.signal_type === "cooling" ? chalk.yellow : chalk.cyan;
|
|
7935
|
+
const icon = s.signal_type === "warming" ? "\u2191" : s.signal_type === "ghost" ? "\u2620" : s.signal_type === "cooling" ? "\u2193" : "\u2713";
|
|
7936
|
+
console.log(` ${color(icon + " " + s.signal_type.toUpperCase())} ${chalk.gray(s.reason)}`);
|
|
7937
|
+
if (s.days_since_contact !== null) {
|
|
7938
|
+
console.log(` Last contact: ${s.days_since_contact} days ago`);
|
|
7939
|
+
}
|
|
7940
|
+
}
|
|
7941
|
+
console.log();
|
|
7942
|
+
});
|
|
7943
|
+
program.command("stale").description("List contacts with low data completeness scores").option("--threshold <n>", "Score threshold 0-100 (default 40)", "40").action(async (opts) => {
|
|
7944
|
+
const { getStaleContacts: getStaleContacts2 } = await Promise.resolve().then(() => (init_freshness(), exports_freshness));
|
|
7945
|
+
const db = getDatabase();
|
|
7946
|
+
const threshold = parseInt(opts.threshold, 10);
|
|
7947
|
+
const contacts = getStaleContacts2(threshold, db);
|
|
7948
|
+
if (!contacts.length) {
|
|
7949
|
+
console.log(chalk.green(`
|
|
7950
|
+
No contacts below ${threshold}% completeness!
|
|
7951
|
+
`));
|
|
7952
|
+
return;
|
|
7953
|
+
}
|
|
7954
|
+
console.log(chalk.bold(`
|
|
7955
|
+
Stale contacts (below ${threshold}% completeness):
|
|
7956
|
+
`));
|
|
7957
|
+
renderTable(["Name", "Score"], contacts.map((c) => ({
|
|
7958
|
+
Name: c.display_name,
|
|
7959
|
+
Score: String(c.score) + "%"
|
|
7960
|
+
})));
|
|
7961
|
+
console.log(chalk.gray(`
|
|
7962
|
+
${contacts.length} contact(s) need enrichment
|
|
7963
|
+
`));
|
|
7964
|
+
});
|
|
7965
|
+
program.command("freshness <id>").description("Show per-field confidence and freshness breakdown for a contact").action(async (id) => {
|
|
7966
|
+
const { getFreshnessScore: getFreshnessScore2 } = await Promise.resolve().then(() => (init_freshness(), exports_freshness));
|
|
7967
|
+
const db = getDatabase();
|
|
7968
|
+
const contact = getContact(id);
|
|
7969
|
+
const score = getFreshnessScore2(id, db);
|
|
7970
|
+
console.log(chalk.bold(`
|
|
7971
|
+
Freshness: ${contact.display_name}`));
|
|
7972
|
+
const scoreColor = score.overall_score >= 70 ? chalk.green : score.overall_score >= 40 ? chalk.yellow : chalk.red;
|
|
7973
|
+
console.log(` Overall: ${scoreColor(String(score.overall_score) + "/100")}
|
|
7974
|
+
`);
|
|
7975
|
+
renderTable(["Field", "Value", "Confidence", "Last Verified", "Days Old"], score.fields.map((f) => ({
|
|
7976
|
+
Field: f.field_name,
|
|
7977
|
+
Value: f.value ? f.value.slice(0, 20) : chalk.gray("(missing)"),
|
|
7978
|
+
Confidence: f.confidence,
|
|
7979
|
+
"Last Verified": f.last_verified_at ? f.last_verified_at.slice(0, 10) : "",
|
|
7980
|
+
"Days Old": f.days_old !== null ? String(f.days_old) + "d" : ""
|
|
7981
|
+
})));
|
|
7982
|
+
console.log();
|
|
7983
|
+
});
|
|
7984
|
+
program.command("embed").description("Build semantic embeddings for contacts").option("--all", "Embed all contacts in the database").option("--contact <id>", "Embed a single contact").action(async (opts) => {
|
|
7985
|
+
const { embedAllContacts: embedAllContacts2, embedContact: embedContact2 } = await Promise.resolve().then(() => (init_embeddings(), exports_embeddings));
|
|
7986
|
+
const db = getDatabase();
|
|
7987
|
+
if (opts.all) {
|
|
7988
|
+
console.log(chalk.blue(`
|
|
7989
|
+
Building embeddings for all contacts...
|
|
7990
|
+
`));
|
|
7991
|
+
const count = await embedAllContacts2(db);
|
|
7992
|
+
console.log(chalk.green(`\u2713 Embedded ${count} contact(s)
|
|
7993
|
+
`));
|
|
7994
|
+
} else if (opts.contact) {
|
|
7995
|
+
await embedContact2(opts.contact, db);
|
|
7996
|
+
const contact = getContact(opts.contact);
|
|
7997
|
+
console.log(chalk.green(`\u2713 Embedded: ${contact.display_name}
|
|
7998
|
+
`));
|
|
7999
|
+
} else {
|
|
8000
|
+
console.error(chalk.red("Use --all or --contact <id>"));
|
|
8001
|
+
process.exit(1);
|
|
8002
|
+
}
|
|
8003
|
+
});
|
|
8004
|
+
program.command("capture-meeting").description("Ingest meeting participants as contacts and log the event").option("--title <title>", "Meeting title (required)").option("--date <date>", "Meeting date (YYYY-MM-DD, default today)").option("--attendee <name:email>", "Attendee (format: Name:email@example.com), repeatable", collect, []).option("--context <text>", "Meeting context or agenda").action(async (opts) => {
|
|
8005
|
+
const { ingestMeetingParticipants: ingestMeetingParticipants2 } = await Promise.resolve().then(() => (init_meeting_capture(), exports_meeting_capture));
|
|
8006
|
+
const db = getDatabase();
|
|
8007
|
+
let title = opts.title;
|
|
8008
|
+
if (!title) {
|
|
8009
|
+
title = await prompt("Meeting title (required):");
|
|
8010
|
+
if (!title) {
|
|
8011
|
+
console.error(chalk.red("Title is required."));
|
|
8012
|
+
process.exit(1);
|
|
8013
|
+
}
|
|
8014
|
+
}
|
|
8015
|
+
const eventDate = opts.date ?? new Date().toISOString().slice(0, 10);
|
|
8016
|
+
const attendees = opts.attendee.map((a) => {
|
|
8017
|
+
const colonIdx = a.lastIndexOf(":");
|
|
8018
|
+
if (colonIdx > 0) {
|
|
8019
|
+
return { name: a.slice(0, colonIdx), email: a.slice(colonIdx + 1) };
|
|
8020
|
+
}
|
|
8021
|
+
return { name: a, email: a };
|
|
8022
|
+
});
|
|
8023
|
+
if (!attendees.length) {
|
|
8024
|
+
console.log(chalk.yellow(`
|
|
8025
|
+
No attendees provided. Use --attendee "Name:email@example.com"
|
|
8026
|
+
`));
|
|
8027
|
+
return;
|
|
8028
|
+
}
|
|
8029
|
+
const result = await ingestMeetingParticipants2({ title, event_date: eventDate, attendees, context: opts.context }, db);
|
|
8030
|
+
console.log(chalk.green(`
|
|
8031
|
+
\u2713 Meeting captured: ${title}`));
|
|
8032
|
+
console.log(` ${chalk.cyan(String(result.created))} contacts created`);
|
|
8033
|
+
console.log(` ${chalk.cyan(String(result.updated))} contacts found (existing)
|
|
8034
|
+
`);
|
|
8035
|
+
});
|
|
8036
|
+
var orgCmd = program.command("org").description("Org chart and deal team management");
|
|
8037
|
+
orgCmd.command("chart <company-id>").description("Show ASCII org chart for a company").action(async (companyId) => {
|
|
8038
|
+
const { listOrgChart: listOrgChart2 } = await Promise.resolve().then(() => (init_org_chart(), exports_org_chart));
|
|
8039
|
+
const db = getDatabase();
|
|
8040
|
+
const company = getCompany(companyId);
|
|
8041
|
+
if (!company) {
|
|
8042
|
+
console.error(chalk.red(`
|
|
8043
|
+
Company not found: ${companyId}
|
|
8044
|
+
`));
|
|
8045
|
+
process.exit(1);
|
|
8046
|
+
}
|
|
8047
|
+
const edges = listOrgChart2(companyId, db);
|
|
8048
|
+
console.log(chalk.bold(`
|
|
8049
|
+
Org Chart: ${company.name}
|
|
8050
|
+
`));
|
|
8051
|
+
if (!edges.length) {
|
|
8052
|
+
console.log(chalk.gray(` No org chart edges defined. Use: contacts org add-edge
|
|
8053
|
+
`));
|
|
8054
|
+
return;
|
|
8055
|
+
}
|
|
8056
|
+
for (const e of edges) {
|
|
8057
|
+
const arrow = e.edge_type === "manages" ? "\u2192" : e.edge_type === "reports_to" ? "\u2190" : "\u2194";
|
|
8058
|
+
console.log(` ${chalk.cyan(e.contact_a_name)} ${chalk.gray(arrow + " " + e.edge_type + " \u2192")} ${chalk.cyan(e.contact_b_name)}`);
|
|
8059
|
+
}
|
|
8060
|
+
console.log();
|
|
8061
|
+
});
|
|
8062
|
+
orgCmd.command("add-edge <company-id> <contact-a> <contact-b>").description("Add an org chart edge between two contacts at a company").option("--type <type>", "Edge type: reports_to|manages|peer|collaborates_with", "reports_to").action(async (companyId, contactAId, contactBId, opts) => {
|
|
8063
|
+
const { addOrgChartEdge: addOrgChartEdge2 } = await Promise.resolve().then(() => (init_org_chart(), exports_org_chart));
|
|
8064
|
+
const db = getDatabase();
|
|
8065
|
+
const contactA = getContact(contactAId);
|
|
8066
|
+
const contactB = getContact(contactBId);
|
|
8067
|
+
addOrgChartEdge2(companyId, contactAId, contactBId, opts.type || "reports_to", false, db);
|
|
8068
|
+
console.log(chalk.green(`
|
|
8069
|
+
\u2713 ${contactA.display_name} ${opts.type || "reports_to"} ${contactB.display_name}
|
|
8070
|
+
`));
|
|
8071
|
+
});
|
|
8072
|
+
var dealsTeamCmd = program.command("deals-team").description("Deal team and buying committee management");
|
|
8073
|
+
dealsTeamCmd.command("show <deal-id>").description("Show buying committee for a deal").action(async (dealId) => {
|
|
8074
|
+
const { getDealTeam: getDealTeam2 } = await Promise.resolve().then(() => (init_org_chart(), exports_org_chart));
|
|
8075
|
+
const db = getDatabase();
|
|
8076
|
+
const team = getDealTeam2(dealId, db);
|
|
8077
|
+
console.log(chalk.bold(`
|
|
8078
|
+
Deal Team: ${dealId}
|
|
8079
|
+
`));
|
|
8080
|
+
if (!team.length) {
|
|
8081
|
+
console.log(chalk.gray(` No contacts assigned to this deal team.
|
|
8082
|
+
`));
|
|
8083
|
+
return;
|
|
8084
|
+
}
|
|
8085
|
+
renderTable(["Contact", "Role", "Title"], team.map((m) => ({
|
|
8086
|
+
Contact: m.display_name,
|
|
8087
|
+
Role: m.account_role,
|
|
8088
|
+
Title: m.job_title ?? ""
|
|
8089
|
+
})));
|
|
8090
|
+
console.log();
|
|
8091
|
+
});
|
|
8092
|
+
dealsTeamCmd.command("assign <deal-id> <contact-id>").description("Assign a contact a role in a deal (buying committee)").option("--role <role>", "Role: economic_buyer|technical_evaluator|champion|blocker|influencer|user|sponsor|other", "other").action(async (dealId, contactId, opts) => {
|
|
8093
|
+
const { setDealContactRole: setDealContactRole2 } = await Promise.resolve().then(() => (init_org_chart(), exports_org_chart));
|
|
8094
|
+
const db = getDatabase();
|
|
8095
|
+
const contact = getContact(contactId);
|
|
8096
|
+
setDealContactRole2(dealId, contactId, opts.role || "other", db);
|
|
8097
|
+
console.log(chalk.green(`
|
|
8098
|
+
\u2713 ${contact.display_name} assigned as ${opts.role || "other"} in deal ${dealId}
|
|
8099
|
+
`));
|
|
8100
|
+
});
|
|
6946
8101
|
program.parse(process.argv);
|