@hasna/contacts 0.6.29 → 0.6.31

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 CHANGED
@@ -7456,6 +7456,30 @@ class ApiStore {
7456
7456
  constructor(client) {
7457
7457
  this.client = client;
7458
7458
  }
7459
+ g(path, query) {
7460
+ return this.client.transport.get(path, query ? { query } : undefined);
7461
+ }
7462
+ post(path, body) {
7463
+ return this.client.transport.post(path, body);
7464
+ }
7465
+ patch(path, body) {
7466
+ return this.client.transport.patch(path, body);
7467
+ }
7468
+ del(path, query) {
7469
+ return this.client.transport.del(path, undefined, query ? { query } : undefined);
7470
+ }
7471
+ async gMaybe(path) {
7472
+ try {
7473
+ return await this.client.transport.get(path);
7474
+ } catch (e) {
7475
+ if (e && typeof e === "object" && e.status === 404)
7476
+ return null;
7477
+ throw e;
7478
+ }
7479
+ }
7480
+ enc(id) {
7481
+ return encodeURIComponent(String(id));
7482
+ }
7459
7483
  async createContact(input) {
7460
7484
  const res = await this.client.create("contacts", stripUndefined(input));
7461
7485
  return pick(res, "contact") ?? res;
@@ -7586,74 +7610,74 @@ class ApiStore {
7586
7610
  async removeTagFromCompany() {
7587
7611
  return unavailable("removeTagFromCompany");
7588
7612
  }
7589
- async createGroup() {
7590
- return unavailable("createGroup");
7613
+ async createGroup(input) {
7614
+ return pick(await this.post("/groups", input), "group");
7591
7615
  }
7592
- async getGroup() {
7593
- return unavailable("getGroup");
7616
+ async getGroup(id) {
7617
+ return pick(await this.gMaybe(`/groups/${this.enc(id)}`), "group") ?? null;
7594
7618
  }
7595
- async listGroups() {
7596
- return unavailable("listGroups");
7619
+ async listGroups(projectId) {
7620
+ return pick(await this.g("/groups", projectId ? { project_id: projectId } : undefined), "groups") ?? [];
7597
7621
  }
7598
- async updateGroup() {
7599
- return unavailable("updateGroup");
7622
+ async updateGroup(id, input) {
7623
+ return pick(await this.patch(`/groups/${this.enc(id)}`, input), "group");
7600
7624
  }
7601
- async deleteGroup() {
7602
- return unavailable("deleteGroup");
7625
+ async deleteGroup(id) {
7626
+ await this.del(`/groups/${this.enc(id)}`);
7603
7627
  }
7604
- async addContactToGroup() {
7605
- return unavailable("addContactToGroup");
7628
+ async addContactToGroup(contactId, groupId) {
7629
+ return this.post(`/groups/${this.enc(groupId)}/contacts`, { contact_id: contactId });
7606
7630
  }
7607
- async removeContactFromGroup() {
7608
- return unavailable("removeContactFromGroup");
7631
+ async removeContactFromGroup(contactId, groupId) {
7632
+ await this.del(`/groups/${this.enc(groupId)}/contacts/${this.enc(contactId)}`);
7609
7633
  }
7610
- async listContactsInGroup() {
7611
- return unavailable("listContactsInGroup");
7634
+ async listContactsInGroup(groupId) {
7635
+ return pick(await this.g(`/groups/${this.enc(groupId)}/contacts`), "contact_ids") ?? [];
7612
7636
  }
7613
- async listGroupsForContact() {
7614
- return unavailable("listGroupsForContact");
7637
+ async listGroupsForContact(contactId) {
7638
+ return pick(await this.g(`/groups/for-contact/${this.enc(contactId)}`), "groups") ?? [];
7615
7639
  }
7616
- async addCompanyToGroup() {
7617
- return unavailable("addCompanyToGroup");
7640
+ async addCompanyToGroup(companyId, groupId) {
7641
+ return this.post(`/groups/${this.enc(groupId)}/companies`, { company_id: companyId });
7618
7642
  }
7619
- async removeCompanyFromGroup() {
7620
- return unavailable("removeCompanyFromGroup");
7643
+ async removeCompanyFromGroup(companyId, groupId) {
7644
+ await this.del(`/groups/${this.enc(groupId)}/companies/${this.enc(companyId)}`);
7621
7645
  }
7622
- async listCompaniesInGroup() {
7623
- return unavailable("listCompaniesInGroup");
7646
+ async listCompaniesInGroup(groupId) {
7647
+ return pick(await this.g(`/groups/${this.enc(groupId)}/companies`), "company_ids") ?? [];
7624
7648
  }
7625
- async listGroupsForCompany() {
7626
- return unavailable("listGroupsForCompany");
7649
+ async listGroupsForCompany(companyId) {
7650
+ return pick(await this.g(`/groups/for-company/${this.enc(companyId)}`), "groups") ?? [];
7627
7651
  }
7628
- async createRelationship() {
7629
- return unavailable("createRelationship");
7652
+ async createRelationship(input) {
7653
+ return pick(await this.post("/relationships", input), "relationship");
7630
7654
  }
7631
- async listRelationships() {
7632
- return unavailable("listRelationships");
7655
+ async listRelationships(opts = {}) {
7656
+ return pick(await this.g("/relationships", stripUndefined(opts)), "relationships") ?? [];
7633
7657
  }
7634
- async deleteRelationship() {
7635
- return unavailable("deleteRelationship");
7658
+ async deleteRelationship(id) {
7659
+ await this.del(`/relationships/${this.enc(id)}`);
7636
7660
  }
7637
- async createCompanyRelationship() {
7638
- return unavailable("createCompanyRelationship");
7661
+ async createCompanyRelationship(input) {
7662
+ return pick(await this.post("/company-relationships", input), "relationship");
7639
7663
  }
7640
- async listCompanyRelationships() {
7641
- return unavailable("listCompanyRelationships");
7664
+ async listCompanyRelationships(opts = {}) {
7665
+ return pick(await this.g("/company-relationships", stripUndefined(opts)), "relationships") ?? [];
7642
7666
  }
7643
- async deleteCompanyRelationship() {
7644
- return unavailable("deleteCompanyRelationship");
7667
+ async deleteCompanyRelationship(id) {
7668
+ await this.del(`/company-relationships/${this.enc(id)}`);
7645
7669
  }
7646
- async addNote() {
7647
- return unavailable("addNote");
7670
+ async addNote(contactId, body, createdBy, companyId) {
7671
+ return pick(await this.post("/notes", { contact_id: contactId, body, created_by: createdBy, company_id: companyId }), "note");
7648
7672
  }
7649
- async listNotes() {
7650
- return unavailable("listNotes");
7673
+ async listNotes(contactId) {
7674
+ return pick(await this.g("/notes", { contact_id: contactId }), "notes") ?? [];
7651
7675
  }
7652
- async listNotesForContactAtCompany() {
7653
- return unavailable("listNotesForContactAtCompany");
7676
+ async listNotesForContactAtCompany(contactId, companyId) {
7677
+ return pick(await this.g("/notes", { contact_id: contactId, company_id: companyId }), "notes") ?? [];
7654
7678
  }
7655
- async deleteNote() {
7656
- return unavailable("deleteNote");
7679
+ async deleteNote(noteId) {
7680
+ await this.del(`/notes/${this.enc(noteId)}`);
7657
7681
  }
7658
7682
  async listActivity() {
7659
7683
  return unavailable("listActivity");
@@ -7668,178 +7692,191 @@ class ApiStore {
7668
7692
  };
7669
7693
  }
7670
7694
  async findEmailDuplicates() {
7671
- return unavailable("findEmailDuplicates");
7695
+ return pick(await this.g("/email-duplicates"), "duplicates") ?? [];
7672
7696
  }
7673
7697
  async findNameDuplicates() {
7674
- return unavailable("findNameDuplicates");
7675
- }
7676
- async flushForBackup() {
7677
- return unavailable("flushForBackup");
7698
+ return pick(await this.g("/name-duplicates"), "duplicates") ?? [];
7678
7699
  }
7679
- async listColdContacts() {
7680
- return unavailable("listColdContacts");
7700
+ async flushForBackup() {}
7701
+ async listColdContacts(days) {
7702
+ return pick(await this.g("/cold-contacts", { days }), "contacts") ?? [];
7681
7703
  }
7682
- async findOrCreateContact() {
7683
- return unavailable("findOrCreateContact");
7704
+ async findOrCreateContact(input) {
7705
+ const emails = (input.emails ?? []).map((e) => e.address).filter(Boolean);
7706
+ for (const addr of emails) {
7707
+ const c = await this.getContactByEmail(addr);
7708
+ if (c)
7709
+ return { contact: c, created: false };
7710
+ }
7711
+ const nameQuery = input.display_name ?? (input.first_name || input.last_name ? `${input.first_name ?? ""} ${input.last_name ?? ""}`.trim() : null);
7712
+ if (nameQuery) {
7713
+ const results = await this.searchContacts(nameQuery);
7714
+ if (results[0])
7715
+ return { contact: results[0], created: false };
7716
+ }
7717
+ return { contact: await this.createContact(input), created: true };
7684
7718
  }
7685
- async findContactsForContext() {
7686
- return unavailable("findContactsForContext");
7719
+ async findContactsForContext(topic, limit) {
7720
+ return pick(await this.g("/contacts-for-context", { topic, limit }), "contacts") ?? [];
7687
7721
  }
7688
- async listContactsNotContactedSince() {
7689
- return unavailable("listContactsNotContactedSince");
7722
+ async listContactsNotContactedSince(days, limit) {
7723
+ return pick(await this.g("/not-contacted", { days, limit }), "contacts") ?? [];
7690
7724
  }
7691
- async listFollowupDueContacts() {
7692
- return unavailable("listFollowupDueContacts");
7725
+ async listFollowupDueContacts(onOrBefore) {
7726
+ return pick(await this.g("/followup-due-contacts", { on_or_before: onOrBefore }), "contacts") ?? [];
7693
7727
  }
7694
- async logVendorCommunication() {
7695
- return unavailable("logVendorCommunication");
7728
+ async logVendorCommunication(input) {
7729
+ return pick(await this.post("/vendor-comms", input), "communication");
7696
7730
  }
7697
- async listVendorCommunications() {
7698
- return unavailable("listVendorCommunications");
7731
+ async listVendorCommunications(companyId, opts = {}) {
7732
+ return pick(await this.g("/vendor-comms", { company_id: companyId, ...stripUndefined(opts) }), "communications") ?? [];
7699
7733
  }
7700
7734
  async listMissingInvoices() {
7701
- return unavailable("listMissingInvoices");
7735
+ return pick(await this.g("/vendor-comms/missing-invoices"), "communications") ?? [];
7702
7736
  }
7703
7737
  async listPendingFollowUps() {
7704
- return unavailable("listPendingFollowUps");
7738
+ return pick(await this.g("/vendor-comms/pending-follow-ups"), "communications") ?? [];
7705
7739
  }
7706
- async markFollowUpDone() {
7707
- return unavailable("markFollowUpDone");
7740
+ async markFollowUpDone(id) {
7741
+ return pick(await this.post(`/vendor-comms/${this.enc(id)}/mark-done`), "communication");
7708
7742
  }
7709
- async createContactTask() {
7710
- return unavailable("createContactTask");
7743
+ async createContactTask(input) {
7744
+ return pick(await this.post("/tasks", input), "task");
7711
7745
  }
7712
- async listContactTasks() {
7713
- return unavailable("listContactTasks");
7746
+ async listContactTasks(opts = {}) {
7747
+ return pick(await this.g("/tasks", stripUndefined(opts)), "tasks") ?? [];
7714
7748
  }
7715
- async updateContactTask() {
7716
- return unavailable("updateContactTask");
7749
+ async updateContactTask(id, input) {
7750
+ return pick(await this.patch(`/tasks/${this.enc(id)}`, input), "task");
7717
7751
  }
7718
- async deleteContactTask() {
7719
- return unavailable("deleteContactTask");
7752
+ async deleteContactTask(id) {
7753
+ await this.del(`/tasks/${this.enc(id)}`);
7720
7754
  }
7721
7755
  async listOverdueTasks() {
7722
- return unavailable("listOverdueTasks");
7756
+ return pick(await this.g("/tasks/overdue"), "tasks") ?? [];
7723
7757
  }
7724
7758
  async checkEscalations() {
7725
- return unavailable("checkEscalations");
7759
+ return pick(await this.g("/tasks/escalations"), "escalations") ?? [];
7726
7760
  }
7727
- async createApplication() {
7728
- return unavailable("createApplication");
7761
+ async createApplication(input) {
7762
+ return pick(await this.post("/applications", input), "application");
7729
7763
  }
7730
- async listApplications() {
7731
- return unavailable("listApplications");
7764
+ async listApplications(opts = {}) {
7765
+ return pick(await this.g("/applications", stripUndefined(opts)), "applications") ?? [];
7732
7766
  }
7733
- async updateApplication() {
7734
- return unavailable("updateApplication");
7767
+ async updateApplication(id, input) {
7768
+ return pick(await this.patch(`/applications/${this.enc(id)}`, input), "application");
7735
7769
  }
7736
7770
  async listFollowUpDueApplications() {
7737
- return unavailable("listFollowUpDueApplications");
7771
+ return pick(await this.g("/applications/follow-up-due"), "applications") ?? [];
7738
7772
  }
7739
- async addOrgMember() {
7740
- return unavailable("addOrgMember");
7773
+ async addOrgMember(input) {
7774
+ return pick(await this.post("/org-members", input), "org_member");
7741
7775
  }
7742
- async listOrgMembers() {
7743
- return unavailable("listOrgMembers");
7776
+ async listOrgMembers(companyId) {
7777
+ return pick(await this.g("/org-members", { company_id: companyId }), "org_members") ?? [];
7744
7778
  }
7745
- async updateOrgMember() {
7746
- return unavailable("updateOrgMember");
7779
+ async updateOrgMember(id, input) {
7780
+ return pick(await this.patch(`/org-members/${this.enc(id)}`, input), "org_member");
7747
7781
  }
7748
- async removeOrgMember() {
7749
- return unavailable("removeOrgMember");
7782
+ async removeOrgMember(id) {
7783
+ await this.del(`/org-members/${this.enc(id)}`);
7750
7784
  }
7751
- async listOrgMembersForContact() {
7752
- return unavailable("listOrgMembersForContact");
7785
+ async listOrgMembersForContact(contactId) {
7786
+ return pick(await this.g("/org-members", { contact_id: contactId }), "org_members") ?? [];
7753
7787
  }
7754
- async createDeal() {
7755
- return unavailable("createDeal");
7788
+ async createDeal(input) {
7789
+ return pick(await this.post("/deals", input), "deal");
7756
7790
  }
7757
- async getDeal() {
7758
- return unavailable("getDeal");
7791
+ async getDeal(id) {
7792
+ return pick(await this.gMaybe(`/deals/${this.enc(id)}`), "deal") ?? null;
7759
7793
  }
7760
- async listDeals() {
7761
- return unavailable("listDeals");
7794
+ async listDeals(opts = {}) {
7795
+ return pick(await this.g("/deals", stripUndefined(opts)), "deals") ?? [];
7762
7796
  }
7763
- async updateDeal() {
7764
- return unavailable("updateDeal");
7797
+ async updateDeal(id, input) {
7798
+ return pick(await this.patch(`/deals/${this.enc(id)}`, input), "deal") ?? null;
7765
7799
  }
7766
- async deleteDeal() {
7767
- return unavailable("deleteDeal");
7800
+ async deleteDeal(id) {
7801
+ await this.del(`/deals/${this.enc(id)}`);
7768
7802
  }
7769
- async logEvent() {
7770
- return unavailable("logEvent");
7803
+ async logEvent(input) {
7804
+ return pick(await this.post("/events", input), "event");
7771
7805
  }
7772
- async listEvents() {
7773
- return unavailable("listEvents");
7806
+ async listEvents(opts = {}) {
7807
+ return pick(await this.g("/events", stripUndefined(opts)), "events") ?? [];
7774
7808
  }
7775
- async deleteEvent() {
7776
- return unavailable("deleteEvent");
7809
+ async deleteEvent(id) {
7810
+ await this.del(`/events/${this.enc(id)}`);
7777
7811
  }
7778
- async getFieldHistory() {
7779
- return unavailable("getFieldHistory");
7812
+ async getFieldHistory(contactId, fieldName) {
7813
+ return pick(await this.g(`/contacts/${this.enc(contactId)}/field-history`, fieldName ? { field_name: fieldName } : undefined), "history") ?? [];
7780
7814
  }
7781
- async getContactAt() {
7782
- return unavailable("getContactAt");
7815
+ async getContactAt(contactId, timestamp) {
7816
+ return pick(await this.g(`/contacts/${this.enc(contactId)}/field-at`, { timestamp }), "fields") ?? {};
7783
7817
  }
7784
- async addJobEntry() {
7785
- return unavailable("addJobEntry");
7818
+ async addJobEntry(contactId, input) {
7819
+ return pick(await this.post(`/contacts/${this.enc(contactId)}/job-history`, input), "job");
7786
7820
  }
7787
- async getJobHistory() {
7788
- return unavailable("getJobHistory");
7821
+ async getJobHistory(contactId) {
7822
+ return pick(await this.g(`/contacts/${this.enc(contactId)}/job-history`), "job_history") ?? [];
7789
7823
  }
7790
- async saveLearning() {
7791
- return unavailable("saveLearning");
7824
+ async saveLearning(contactId, input) {
7825
+ return pick(await this.post(`/contacts/${this.enc(contactId)}/learnings`, input), "learning");
7792
7826
  }
7793
- async getLearnings() {
7794
- return unavailable("getLearnings");
7827
+ async getLearnings(contactId, opts = {}) {
7828
+ return pick(await this.g(`/contacts/${this.enc(contactId)}/learnings`, stripUndefined(opts)), "learnings") ?? [];
7795
7829
  }
7796
- async searchLearnings() {
7797
- return unavailable("searchLearnings");
7830
+ async searchLearnings(query, opts = {}) {
7831
+ return pick(await this.g("/learnings/search", { q: query, ...stripUndefined(opts) }), "learnings") ?? [];
7798
7832
  }
7799
- async confirmLearning() {
7800
- return unavailable("confirmLearning");
7833
+ async confirmLearning(learningId) {
7834
+ await this.post(`/learnings/${this.enc(learningId)}/confirm`);
7801
7835
  }
7802
- async getStaleLearnings() {
7803
- return unavailable("getStaleLearnings");
7836
+ async getStaleLearnings(daysOld, minConfidence) {
7837
+ return pick(await this.g("/learnings/stale", { days_old: daysOld, min_confidence: minConfidence }), "learnings") ?? [];
7804
7838
  }
7805
7839
  async runLearningMaintenance() {
7806
- return unavailable("runLearningMaintenance");
7840
+ const r = await this.post("/learnings/maintenance");
7841
+ return { decayed_count: Number(r?.decayed_count ?? 0), potential_contradictions: r?.potential_contradictions ?? [] };
7807
7842
  }
7808
- async acquireContactLock() {
7809
- return unavailable("acquireContactLock");
7843
+ async acquireContactLock(contactId, agentName, ttlSeconds, reason, sessionId) {
7844
+ return this.post("/locks", { contact_id: contactId, agent_name: agentName, ttl_seconds: ttlSeconds, reason, session_id: sessionId });
7810
7845
  }
7811
- async releaseContactLock() {
7812
- return unavailable("releaseContactLock");
7846
+ async releaseContactLock(contactId, agentName) {
7847
+ const r = await this.del(`/locks/${this.enc(contactId)}`, { agent_name: agentName });
7848
+ return Boolean(r?.released);
7813
7849
  }
7814
- async checkContactLock() {
7815
- return unavailable("checkContactLock");
7850
+ async checkContactLock(contactId) {
7851
+ return pick(await this.g(`/locks/${this.enc(contactId)}`), "lock") ?? null;
7816
7852
  }
7817
- async logAgentActivity() {
7818
- return unavailable("logAgentActivity");
7853
+ async logAgentActivity(contactId, agentName, action, details, sessionId) {
7854
+ await this.post("/activity", { contact_id: contactId, agent_name: agentName, action, details, session_id: sessionId });
7819
7855
  }
7820
- async getAgentActivity() {
7821
- return unavailable("getAgentActivity");
7856
+ async getAgentActivity(contactId, limit) {
7857
+ return pick(await this.g("/activity", { contact_id: contactId, limit }), "activity") ?? [];
7822
7858
  }
7823
- async computeRelationshipStrength() {
7824
- return unavailable("computeRelationshipStrength");
7859
+ async computeRelationshipStrength(contactId) {
7860
+ const r = await this.g(`/graph/strength/${this.enc(contactId)}`);
7861
+ return Number(r?.strength ?? 0);
7825
7862
  }
7826
- async findWarmPath() {
7827
- return unavailable("findWarmPath");
7863
+ async findWarmPath(fromContactId, toContactId) {
7864
+ return pick(await this.g("/graph/warm-path", { from: fromContactId, to: toContactId }), "path") ?? [];
7828
7865
  }
7829
- async findConnectionsAtCompany() {
7830
- return unavailable("findConnectionsAtCompany");
7866
+ async findConnectionsAtCompany(companyId) {
7867
+ return pick(await this.g(`/graph/company/${this.enc(companyId)}`), "connections") ?? [];
7831
7868
  }
7832
7869
  async detectCoolingRelationships() {
7833
- return unavailable("detectCoolingRelationships");
7870
+ return pick(await this.g("/graph/cooling"), "cooling") ?? [];
7834
7871
  }
7835
- async resolveContactIdentity() {
7836
- return unavailable("resolveContactIdentity");
7872
+ async resolveContactIdentity(partial) {
7873
+ return pick(await this.post("/identity/resolve", partial), "matches") ?? [];
7837
7874
  }
7838
- async addContactIdentity() {
7839
- return unavailable("addContactIdentity");
7875
+ async addContactIdentity(contactId, system, externalId, externalUrl, confidence = "inferred") {
7876
+ return pick(await this.post("/identity", { contact_id: contactId, system, external_id: externalId, external_url: externalUrl, confidence }), "identity");
7840
7877
  }
7841
- async getContactIdentities() {
7842
- return unavailable("getContactIdentities");
7878
+ async getContactIdentities(contactId) {
7879
+ return pick(await this.g("/identity", { contact_id: contactId }), "identities") ?? [];
7843
7880
  }
7844
7881
  async semanticSearch() {
7845
7882
  return unavailable("semanticSearch");
@@ -7850,44 +7887,45 @@ class ApiStore {
7850
7887
  async embedAllContacts() {
7851
7888
  return unavailable("embedAllContacts");
7852
7889
  }
7853
- async getRelationshipSignals() {
7854
- return unavailable("getRelationshipSignals");
7890
+ async getRelationshipSignals(contactId) {
7891
+ return pick(await this.g("/signals", { contact_id: contactId }), "signals") ?? [];
7855
7892
  }
7856
7893
  async getGhostContacts() {
7857
- return unavailable("getGhostContacts");
7894
+ return pick(await this.g("/signals/ghost"), "signals") ?? [];
7858
7895
  }
7859
7896
  async getWarmingContacts() {
7860
- return unavailable("getWarmingContacts");
7897
+ return pick(await this.g("/signals/warming"), "signals") ?? [];
7861
7898
  }
7862
7899
  async recomputeSignals() {
7863
- return unavailable("recomputeSignals");
7900
+ const r = await this.post("/signals/recompute");
7901
+ return { updated: Number(r?.updated ?? 0) };
7864
7902
  }
7865
- async getFreshnessScore() {
7866
- return unavailable("getFreshnessScore");
7903
+ async getFreshnessScore(contactId) {
7904
+ return pick(await this.g(`/freshness/${this.enc(contactId)}`), "freshness");
7867
7905
  }
7868
- async getStaleContacts() {
7869
- return unavailable("getStaleContacts");
7906
+ async getStaleContacts(threshold) {
7907
+ return pick(await this.g("/freshness/stale", { threshold }), "contacts") ?? [];
7870
7908
  }
7871
- async markFieldVerified() {
7872
- return unavailable("markFieldVerified");
7909
+ async markFieldVerified(contactId, fieldName, source) {
7910
+ await this.post("/freshness/verify", { contact_id: contactId, field_name: fieldName, source });
7873
7911
  }
7874
- async addOrgChartEdge() {
7875
- return unavailable("addOrgChartEdge");
7912
+ async addOrgChartEdge(companyId, contactAId, contactBId, edgeType, inferred = false) {
7913
+ return pick(await this.post("/org-chart", { company_id: companyId, contact_a_id: contactAId, contact_b_id: contactBId, edge_type: edgeType, inferred }), "edge");
7876
7914
  }
7877
- async listOrgChart() {
7878
- return unavailable("listOrgChart");
7915
+ async listOrgChart(companyId) {
7916
+ return pick(await this.g("/org-chart", { company_id: companyId }), "edges") ?? [];
7879
7917
  }
7880
- async setDealContactRole() {
7881
- return unavailable("setDealContactRole");
7918
+ async setDealContactRole(dealId, contactId, accountRole) {
7919
+ return pick(await this.post(`/deals/${this.enc(dealId)}/roles`, { contact_id: contactId, account_role: accountRole }), "role");
7882
7920
  }
7883
- async getDealTeam() {
7884
- return unavailable("getDealTeam");
7921
+ async getDealTeam(dealId) {
7922
+ return pick(await this.g(`/deals/${this.enc(dealId)}/team`), "team") ?? [];
7885
7923
  }
7886
- async getCoverageGaps() {
7887
- return unavailable("getCoverageGaps");
7924
+ async getCoverageGaps(companyId) {
7925
+ return pick(await this.g(`/org-chart/coverage/${this.enc(companyId)}`), "coverage");
7888
7926
  }
7889
- async getRecentContactEvents() {
7890
- return unavailable("getRecentContactEvents");
7927
+ async getRecentContactEvents(since, eventTypes) {
7928
+ return pick(await this.g("/recent-events", { since, types: eventTypes?.length ? eventTypes.join(",") : undefined }), "events") ?? [];
7891
7929
  }
7892
7930
  async addDocument() {
7893
7931
  return unavailable("addDocument");
@@ -7913,65 +7951,66 @@ class ApiStore {
7913
7951
  async deleteHealthData() {
7914
7952
  return unavailable("deleteHealthData");
7915
7953
  }
7916
- async createAudience() {
7917
- return unavailable("createAudience");
7954
+ async createAudience(input) {
7955
+ return pick(await this.post("/audiences", input), "audience");
7918
7956
  }
7919
- async getAudience() {
7920
- return unavailable("getAudience");
7957
+ async getAudience(idOrSlug) {
7958
+ return pick(await this.g(`/audiences/${this.enc(idOrSlug)}`), "audience");
7921
7959
  }
7922
7960
  async listAudiences() {
7923
- return unavailable("listAudiences");
7961
+ return pick(await this.g("/audiences"), "audiences") ?? [];
7924
7962
  }
7925
- async updateAudience() {
7926
- return unavailable("updateAudience");
7963
+ async updateAudience(idOrSlug, input) {
7964
+ return pick(await this.patch(`/audiences/${this.enc(idOrSlug)}`, input), "audience");
7927
7965
  }
7928
- async deleteAudience() {
7929
- return unavailable("deleteAudience");
7966
+ async deleteAudience(idOrSlug) {
7967
+ await this.del(`/audiences/${this.enc(idOrSlug)}`);
7930
7968
  }
7931
- async resolveAudience() {
7932
- return unavailable("resolveAudience");
7969
+ async resolveAudience(idOrSlug, channel) {
7970
+ return pick(await this.g(`/audiences/${this.enc(idOrSlug)}/resolve`, { channel }), "resolution");
7933
7971
  }
7934
- async setContactConsent() {
7935
- return unavailable("setContactConsent");
7972
+ async setContactConsent(contactId, channel, status, source) {
7973
+ return pick(await this.post("/consent", { contact_id: contactId, channel, status, source }), "consent");
7936
7974
  }
7937
- async listContactConsent() {
7938
- return unavailable("listContactConsent");
7975
+ async listContactConsent(contactId) {
7976
+ return pick(await this.g("/consent", { contact_id: contactId }), "consent") ?? [];
7939
7977
  }
7940
- async suppressAddress() {
7941
- return unavailable("suppressAddress");
7978
+ async suppressAddress(input) {
7979
+ return pick(await this.post("/suppressions", input), "suppression");
7942
7980
  }
7943
- async unsuppressAddress() {
7944
- return unavailable("unsuppressAddress");
7981
+ async unsuppressAddress(channel, address) {
7982
+ await this.del("/suppressions", { channel, address });
7945
7983
  }
7946
- async listSuppressions() {
7947
- return unavailable("listSuppressions");
7984
+ async listSuppressions(opts = {}) {
7985
+ return pick(await this.g("/suppressions", stripUndefined(opts)), "suppressions") ?? [];
7948
7986
  }
7949
7987
  async syncSuppressions() {
7950
7988
  return unavailable("syncSuppressions");
7951
7989
  }
7952
- async generateBrief() {
7953
- return unavailable("generateBrief");
7990
+ async generateBrief(contactId) {
7991
+ const r = await this.g(`/contacts/${this.enc(contactId)}/brief-text`);
7992
+ return String(r?.text ?? "");
7954
7993
  }
7955
- async getContactCard() {
7956
- return unavailable("getContactCard");
7994
+ async getContactCard(contactId) {
7995
+ return pick(await this.g(`/contacts/${this.enc(contactId)}/card`), "card");
7957
7996
  }
7958
- async getContactBrief() {
7959
- return unavailable("getContactBrief");
7997
+ async getContactBrief(contactId, taskContext) {
7998
+ return pick(await this.g(`/contacts/${this.enc(contactId)}/brief`, taskContext ? { context: taskContext } : undefined), "brief");
7960
7999
  }
7961
- async assembleContext() {
7962
- return unavailable("assembleContext");
8000
+ async assembleContext(contactIds, format) {
8001
+ return pick(await this.post("/assemble-context", { contact_ids: contactIds, format }), "context");
7963
8002
  }
7964
- async getUpcomingItems() {
7965
- return unavailable("getUpcomingItems");
8003
+ async getUpcomingItems(days) {
8004
+ return pick(await this.g("/upcoming", { days }), "items") ?? [];
7966
8005
  }
7967
8006
  async getNetworkStats() {
7968
- return unavailable("getNetworkStats");
8007
+ return pick(await this.g("/network-stats"), "stats");
7969
8008
  }
7970
8009
  async listContactAudit() {
7971
- return unavailable("listContactAudit");
8010
+ return pick(await this.g("/contact-audit"), "audit") ?? [];
7972
8011
  }
7973
- async getContactTimeline() {
7974
- return unavailable("getContactTimeline");
8012
+ async getContactTimeline(contactId, limit) {
8013
+ return pick(await this.g(`/contacts/${this.enc(contactId)}/timeline`, { limit }), "timeline") ?? [];
7975
8014
  }
7976
8015
  async ingestMeetingParticipants() {
7977
8016
  return unavailable("ingestMeetingParticipants");
@@ -8001,13 +8040,13 @@ class ApiStore {
8001
8040
  return unavailable("lockVault");
8002
8041
  }
8003
8042
  async isVaultInitialized() {
8004
- return unavailable("isVaultInitialized");
8043
+ return false;
8005
8044
  }
8006
8045
  async isVaultUnlocked() {
8007
- return unavailable("isVaultUnlocked");
8046
+ return false;
8008
8047
  }
8009
8048
  async vaultStatus() {
8010
- return unavailable("vaultStatus");
8049
+ return pick(await this.g("/vault-status"), "vault") ?? { initialized: false, unlocked: false, document_count: 0 };
8011
8050
  }
8012
8051
  async saveFeedback() {
8013
8052
  return unavailable("saveFeedback");
@@ -8016,7 +8055,7 @@ class ApiStore {
8016
8055
  return null;
8017
8056
  }
8018
8057
  async listActiveWebhooks() {
8019
- return unavailable("listActiveWebhooks");
8058
+ return [];
8020
8059
  }
8021
8060
  }
8022
8061
  function getStore(env = process.env) {
@@ -17305,6 +17344,32 @@ function iso(value) {
17305
17344
  return value.toISOString();
17306
17345
  return typeof value === "string" ? value : String(value ?? "");
17307
17346
  }
17347
+ function isoOrNull(value) {
17348
+ if (value === null || value === undefined)
17349
+ return null;
17350
+ return iso(value);
17351
+ }
17352
+ function pj(value, fallback) {
17353
+ if (value === null || value === undefined)
17354
+ return fallback;
17355
+ if (typeof value === "string") {
17356
+ if (value === "")
17357
+ return fallback;
17358
+ try {
17359
+ const parsed = JSON.parse(value);
17360
+ return parsed ?? fallback;
17361
+ } catch {
17362
+ return fallback;
17363
+ }
17364
+ }
17365
+ return value;
17366
+ }
17367
+ function newUuid() {
17368
+ return crypto.randomUUID();
17369
+ }
17370
+ function nowIso() {
17371
+ return new Date().toISOString();
17372
+ }
17308
17373
  function parseJson(value) {
17309
17374
  if (!value)
17310
17375
  return {};
@@ -17602,115 +17667,1484 @@ class ContactsPgStore {
17602
17667
  tags: Number(row?.tags ?? 0)
17603
17668
  };
17604
17669
  }
17605
- }
17606
- function getContactsPgStore(client) {
17607
- if (!cachedStore2)
17608
- cachedStore2 = new ContactsPgStore(client);
17609
- return cachedStore2;
17610
- }
17611
- var cachedStore2 = null;
17612
-
17613
- // src/server/v1.ts
17614
- function json5(body, status = 200) {
17615
- return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS });
17616
- }
17617
- function error(status, message, extra) {
17618
- return json5({ error: message, ...extra ?? {} }, status);
17619
- }
17620
- async function readJson(req) {
17621
- try {
17622
- const text = await req.text();
17623
- if (!text)
17624
- return {};
17625
- return JSON.parse(text);
17626
- } catch {
17627
- return null;
17670
+ async loadDetails(contact) {
17671
+ const [emails, phones, tags, company] = await Promise.all([
17672
+ this.client.many(`SELECT * FROM emails WHERE contact_id = $1`, [contact.id]),
17673
+ this.client.many(`SELECT * FROM phones WHERE contact_id = $1`, [contact.id]),
17674
+ this.client.many(`SELECT t.* FROM tags t JOIN contact_tags ct ON ct.tag_id = t.id WHERE ct.contact_id = $1`, [contact.id]),
17675
+ contact.company_id ? this.client.get(`SELECT * FROM companies WHERE id = $1`, [contact.company_id]) : Promise.resolve(null)
17676
+ ]);
17677
+ return {
17678
+ ...contact,
17679
+ emails: emails.map((e) => ({ ...e, is_primary: Boolean(e.is_primary), created_at: isoOrNull(e.created_at) })),
17680
+ phones: phones.map((p) => ({ ...p, is_primary: Boolean(p.is_primary), created_at: isoOrNull(p.created_at) })),
17681
+ addresses: [],
17682
+ social_profiles: [],
17683
+ tags: tags.map((t) => mapTag(t)),
17684
+ company: company ? mapCompany(company) : null
17685
+ };
17628
17686
  }
17629
- }
17630
- async function handleV1Request(req, url) {
17631
- const path = url.pathname;
17632
- if (path !== "/v1" && !path.startsWith("/v1/"))
17633
- return null;
17634
- const method = req.method.toUpperCase();
17635
- const isWrite = method !== "GET" && method !== "HEAD";
17636
- const requiredScopes = [isWrite ? `${CONTACTS_APP_SLUG}:write` : `${CONTACTS_APP_SLUG}:read`];
17637
- let verifier;
17638
- try {
17639
- verifier = getCloudVerifier();
17640
- } catch (e) {
17641
- return error(503, e.message);
17687
+ async searchContacts(q) {
17688
+ const rows = await this.client.many(`SELECT * FROM contacts WHERE search_vector @@ plainto_tsquery('simple', $1) OR display_name ILIKE $2 ORDER BY display_name ASC LIMIT 50`, [q, `%${q}%`]);
17689
+ return Promise.all(rows.map((r) => this.loadDetails(mapContact(r))));
17642
17690
  }
17643
- const decision = await verifier.authenticate(req.headers, { method, path, requiredScopes });
17644
- if (!decision.ok) {
17645
- return error(decision.status, decision.message, { reason: decision.reason });
17691
+ async listColdContacts(days) {
17692
+ const cutoff = new Date(Date.now() - days * 86400000).toISOString();
17693
+ const rows = await this.client.many(`SELECT * FROM contacts
17694
+ WHERE archived = false AND do_not_contact = false
17695
+ AND (last_contacted_at IS NULL OR last_contacted_at < $1)
17696
+ ORDER BY last_contacted_at ASC NULLS FIRST LIMIT 100`, [cutoff]);
17697
+ return Promise.all(rows.map((r) => this.loadDetails(mapContact(r))));
17646
17698
  }
17647
- await ensureCloudSchemaBestEffort();
17648
- const store = getContactsPgStore(getCloudClient());
17649
- const segments = path.split("/").filter(Boolean);
17650
- const resource = segments[1];
17651
- const id = segments[2];
17652
- try {
17653
- if (resource === "contacts") {
17654
- if (!id) {
17655
- if (method === "GET") {
17656
- const result = await store.listContacts({
17657
- ...url.searchParams.get("company_id") ? { company_id: url.searchParams.get("company_id") } : {},
17658
- ...url.searchParams.get("status") ? { status: url.searchParams.get("status") } : {},
17659
- ...url.searchParams.get("q") ? { q: url.searchParams.get("q") } : {},
17660
- ...url.searchParams.get("limit") ? { limit: Number(url.searchParams.get("limit")) } : {},
17661
- ...url.searchParams.get("offset") ? { offset: Number(url.searchParams.get("offset")) } : {}
17662
- });
17663
- return json5(result);
17664
- }
17665
- if (method === "POST") {
17666
- const body = await readJson(req);
17667
- if (!body)
17668
- return error(400, "invalid JSON body");
17669
- const contact = await store.createContact(body);
17670
- return json5({ contact }, 201);
17671
- }
17672
- return error(405, `method ${method} not allowed on /v1/contacts`);
17673
- }
17674
- if (method === "GET") {
17675
- const contact = await store.getContact(id);
17676
- return contact ? json5({ contact }) : error(404, "contact not found");
17677
- }
17678
- if (method === "PATCH" || method === "PUT") {
17679
- const body = await readJson(req);
17680
- if (!body)
17681
- return error(400, "invalid JSON body");
17682
- const contact = await store.updateContact(id, body);
17683
- return contact ? json5({ contact }) : error(404, "contact not found");
17684
- }
17685
- if (method === "DELETE") {
17686
- const deleted = await store.deleteContact(id);
17687
- return deleted ? json5({ deleted: true, id }) : error(404, "contact not found");
17699
+ async listContactsNotContactedSince(days, limit) {
17700
+ const cutoff = new Date(Date.now() - days * 86400000).toISOString();
17701
+ return this.client.many(`SELECT id, display_name, last_contacted_at FROM contacts
17702
+ WHERE (last_contacted_at IS NULL OR last_contacted_at < $1) AND archived = false LIMIT $2`, [cutoff, Math.max(1, limit)]);
17703
+ }
17704
+ async listFollowupDueContacts(onOrBefore) {
17705
+ return this.client.many(`SELECT id, display_name, follow_up_at FROM contacts
17706
+ WHERE follow_up_at IS NOT NULL AND follow_up_at <= $1 AND archived = false`, [onOrBefore]);
17707
+ }
17708
+ async findContactsForContext(topic, limit) {
17709
+ const like = `%${topic}%`;
17710
+ const [byTitle, byNotes, byCompany, bySpec] = await Promise.all([
17711
+ this.client.many(`SELECT c.id, c.display_name, c.job_title, 'job_title' AS reason FROM contacts c WHERE c.job_title ILIKE $1 AND c.archived = false LIMIT 20`, [like]),
17712
+ this.client.many(`SELECT c.id, c.display_name, c.job_title, 'notes' AS reason FROM contacts c WHERE c.notes ILIKE $1 AND c.archived = false LIMIT 10`, [like]),
17713
+ this.client.many(`SELECT c.id, c.display_name, c.job_title, 'company' AS reason FROM contacts c JOIN companies co ON c.company_id = co.id WHERE (co.name ILIKE $1 OR co.industry ILIKE $1) AND c.archived = false LIMIT 10`, [like]),
17714
+ this.client.many(`SELECT c.id, c.display_name, c.job_title, om.specialization AS reason FROM contacts c JOIN org_members om ON c.id = om.contact_id WHERE om.specialization ILIKE $1 LIMIT 10`, [like])
17715
+ ]);
17716
+ const seen = new Set;
17717
+ return [...byTitle, ...bySpec, ...byCompany, ...byNotes].filter((r) => {
17718
+ if (seen.has(r.id))
17719
+ return false;
17720
+ seen.add(r.id);
17721
+ return true;
17722
+ }).slice(0, limit);
17723
+ }
17724
+ async findEmailDuplicates() {
17725
+ const rows = await this.client.many(`SELECT MIN(e.address) AS email, string_agg(e.contact_id, ',') AS ids
17726
+ FROM emails e WHERE e.contact_id IS NOT NULL
17727
+ GROUP BY LOWER(e.address) HAVING COUNT(*) > 1`);
17728
+ return rows.map((r) => ({ email: r.email, contact_ids: (r.ids ?? "").split(",").filter(Boolean) }));
17729
+ }
17730
+ async findNameDuplicates() {
17731
+ const contacts = await this.client.many(`SELECT id, display_name FROM contacts`);
17732
+ const lev = (a, b) => {
17733
+ const m = a.length, n = b.length;
17734
+ const dp = Array.from({ length: m + 1 }, (_, i) => Array.from({ length: n + 1 }, (_2, j) => i === 0 ? j : j === 0 ? i : 0));
17735
+ for (let i = 1;i <= m; i++)
17736
+ for (let j = 1;j <= n; j++)
17737
+ dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
17738
+ return dp[m][n];
17739
+ };
17740
+ const pairs = [];
17741
+ for (let i = 0;i < contacts.length; i++) {
17742
+ for (let j = i + 1;j < contacts.length; j++) {
17743
+ const dist = lev(contacts[i].display_name.toLowerCase(), contacts[j].display_name.toLowerCase());
17744
+ if (dist <= 2 && dist > 0)
17745
+ pairs.push({ contact_ids: [contacts[i].id, contacts[j].id], similarity: dist });
17688
17746
  }
17689
- return error(405, `method ${method} not allowed on /v1/contacts/:id`);
17690
17747
  }
17691
- if (resource === "companies") {
17692
- if (!id) {
17693
- if (method === "GET") {
17694
- const result = await store.listCompanies({
17695
- ...url.searchParams.get("industry") ? { industry: url.searchParams.get("industry") } : {},
17696
- ...url.searchParams.get("limit") ? { limit: Number(url.searchParams.get("limit")) } : {},
17697
- ...url.searchParams.get("offset") ? { offset: Number(url.searchParams.get("offset")) } : {}
17698
- });
17699
- return json5(result);
17700
- }
17701
- if (method === "POST") {
17702
- const body = await readJson(req);
17703
- if (!body || typeof body.name !== "string" || !body.name.trim()) {
17704
- return error(400, "name is required");
17705
- }
17706
- const company = await store.createCompany(body);
17707
- return json5({ company }, 201);
17708
- }
17709
- return error(405, `method ${method} not allowed on /v1/companies`);
17710
- }
17711
- if (method === "GET") {
17712
- const company = await store.getCompany(id);
17713
- return company ? json5({ company }) : error(404, "company not found");
17748
+ return pairs;
17749
+ }
17750
+ async getRecentContactEvents(since, eventTypes) {
17751
+ const params = [];
17752
+ let sql = `SELECT * FROM activity_log WHERE 1=1`;
17753
+ if (since) {
17754
+ params.push(since);
17755
+ sql += ` AND created_at >= $${params.length}`;
17756
+ }
17757
+ if (eventTypes?.length) {
17758
+ const placeholders = eventTypes.map((_, i) => `$${params.length + i + 1}`);
17759
+ params.push(...eventTypes);
17760
+ sql += ` AND action IN (${placeholders.join(",")})`;
17761
+ }
17762
+ sql += ` ORDER BY created_at DESC LIMIT 100`;
17763
+ const rows = await this.client.many(sql, params);
17764
+ return rows.map((r) => ({ ...r, created_at: isoOrNull(r.created_at) }));
17765
+ }
17766
+ mapDeal(r) {
17767
+ return { id: r.id, title: r.title, contact_id: r.contact_id, company_id: r.company_id, stage: r.stage, value_usd: r.value_usd, currency: r.currency, close_date: r.close_date, notes: r.notes, created_at: iso(r.created_at), updated_at: iso(r.updated_at) };
17768
+ }
17769
+ async createDeal(input) {
17770
+ const id = newUuid();
17771
+ const row = await this.client.get(`INSERT INTO deals (id, title, contact_id, company_id, stage, value_usd, currency, close_date, notes)
17772
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9) RETURNING *`, [id, input.title, input.contact_id ?? null, input.company_id ?? null, input.stage ?? "lead", input.value_usd ?? null, input.currency ?? "USD", input.close_date ?? null, input.notes ?? null]);
17773
+ return this.mapDeal(row);
17774
+ }
17775
+ async getDeal(id) {
17776
+ const row = await this.client.get(`SELECT * FROM deals WHERE id = $1`, [id]);
17777
+ return row ? this.mapDeal(row) : null;
17778
+ }
17779
+ async listDeals(opts = {}) {
17780
+ const where = [];
17781
+ const params = [];
17782
+ if (opts.stage) {
17783
+ params.push(opts.stage);
17784
+ where.push(`stage = $${params.length}`);
17785
+ }
17786
+ if (opts.contact_id) {
17787
+ params.push(opts.contact_id);
17788
+ where.push(`contact_id = $${params.length}`);
17789
+ }
17790
+ if (opts.company_id) {
17791
+ params.push(opts.company_id);
17792
+ where.push(`company_id = $${params.length}`);
17793
+ }
17794
+ const sql = `SELECT * FROM deals ${where.length ? `WHERE ${where.join(" AND ")}` : ""} ORDER BY created_at DESC`;
17795
+ return (await this.client.many(sql, params)).map((r) => this.mapDeal(r));
17796
+ }
17797
+ async updateDeal(id, input) {
17798
+ const existing = await this.getDeal(id);
17799
+ if (!existing)
17800
+ return null;
17801
+ const cols = ["title", "contact_id", "company_id", "stage", "value_usd", "currency", "close_date", "notes"];
17802
+ const sets = [];
17803
+ const params = [id];
17804
+ for (const c of cols)
17805
+ if (c in input) {
17806
+ params.push(input[c] ?? null);
17807
+ sets.push(`${c} = $${params.length}`);
17808
+ }
17809
+ sets.push(`updated_at = NOW()`);
17810
+ const row = await this.client.get(`UPDATE deals SET ${sets.join(", ")} WHERE id = $1 RETURNING *`, params);
17811
+ return row ? this.mapDeal(row) : null;
17812
+ }
17813
+ async deleteDeal(id) {
17814
+ return (await this.client.query(`DELETE FROM deals WHERE id = $1`, [id])).rowCount > 0;
17815
+ }
17816
+ mapEvent(r) {
17817
+ return { id: r.id, title: r.title, type: r.type, event_date: r.event_date, duration_min: r.duration_min, contact_ids: pj(r.contact_ids, []), company_id: r.company_id, notes: r.notes, outcome: r.outcome, deal_id: r.deal_id, created_at: iso(r.created_at) };
17818
+ }
17819
+ async logEvent(input) {
17820
+ const id = newUuid();
17821
+ const row = await this.client.get(`INSERT INTO events (id, title, type, event_date, duration_min, contact_ids, company_id, notes, outcome, deal_id)
17822
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) RETURNING *`, [id, input.title, input.type ?? "meeting", input.event_date, input.duration_min ?? null, JSON.stringify(input.contact_ids ?? []), input.company_id ?? null, input.notes ?? null, input.outcome ?? null, input.deal_id ?? null]);
17823
+ return this.mapEvent(row);
17824
+ }
17825
+ async listEvents(opts = {}) {
17826
+ const where = [];
17827
+ const params = [];
17828
+ if (opts.contact_id) {
17829
+ params.push(`%${opts.contact_id}%`);
17830
+ where.push(`contact_ids LIKE $${params.length}`);
17831
+ }
17832
+ if (opts.company_id) {
17833
+ params.push(opts.company_id);
17834
+ where.push(`company_id = $${params.length}`);
17835
+ }
17836
+ if (opts.type) {
17837
+ params.push(opts.type);
17838
+ where.push(`type = $${params.length}`);
17839
+ }
17840
+ if (opts.date_from) {
17841
+ params.push(opts.date_from);
17842
+ where.push(`event_date >= $${params.length}`);
17843
+ }
17844
+ if (opts.date_to) {
17845
+ params.push(opts.date_to);
17846
+ where.push(`event_date <= $${params.length}`);
17847
+ }
17848
+ const sql = `SELECT * FROM events ${where.length ? `WHERE ${where.join(" AND ")}` : ""} ORDER BY event_date DESC`;
17849
+ return (await this.client.many(sql, params)).map((r) => this.mapEvent(r));
17850
+ }
17851
+ async deleteEvent(id) {
17852
+ return (await this.client.query(`DELETE FROM events WHERE id = $1`, [id])).rowCount > 0;
17853
+ }
17854
+ mapTask(r) {
17855
+ return { id: r.id, title: r.title, description: r.description, contact_id: r.contact_id, assigned_by: r.assigned_by, deadline: r.deadline, status: r.status, priority: r.priority, entity_id: r.entity_id, linked_todos_task_id: r.linked_todos_task_id, escalation_rules: pj(r.escalation_rules, []), created_at: iso(r.created_at), updated_at: iso(r.updated_at) };
17856
+ }
17857
+ async createContactTask(input) {
17858
+ const id = newUuid();
17859
+ const row = await this.client.get(`INSERT INTO contact_tasks (id, title, description, contact_id, assigned_by, deadline, status, priority, entity_id, linked_todos_task_id, escalation_rules)
17860
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11) RETURNING *`, [id, input.title, input.description ?? null, input.contact_id, input.assigned_by ?? null, input.deadline ?? null, input.status ?? "pending", input.priority ?? "medium", input.entity_id ?? null, input.linked_todos_task_id ?? null, JSON.stringify(input.escalation_rules ?? [])]);
17861
+ return this.mapTask(row);
17862
+ }
17863
+ async listContactTasks(opts = {}) {
17864
+ const where = [];
17865
+ const params = [];
17866
+ if (opts.contact_id) {
17867
+ params.push(opts.contact_id);
17868
+ where.push(`contact_id = $${params.length}`);
17869
+ }
17870
+ if (opts.entity_id) {
17871
+ params.push(opts.entity_id);
17872
+ where.push(`entity_id = $${params.length}`);
17873
+ }
17874
+ if (opts.status) {
17875
+ params.push(opts.status);
17876
+ where.push(`status = $${params.length}`);
17877
+ }
17878
+ if (opts.priority) {
17879
+ params.push(opts.priority);
17880
+ where.push(`priority = $${params.length}`);
17881
+ }
17882
+ const sql = `SELECT * FROM contact_tasks ${where.length ? `WHERE ${where.join(" AND ")}` : ""} ORDER BY deadline ASC NULLS LAST, priority DESC, created_at ASC`;
17883
+ return (await this.client.many(sql, params)).map((r) => this.mapTask(r));
17884
+ }
17885
+ async updateContactTask(id, input) {
17886
+ const cols = ["title", "description", "assigned_by", "deadline", "status", "priority", "entity_id", "linked_todos_task_id"];
17887
+ const sets = [];
17888
+ const params = [id];
17889
+ for (const c of cols)
17890
+ if (c in input) {
17891
+ params.push(input[c] ?? null);
17892
+ sets.push(`${c} = $${params.length}`);
17893
+ }
17894
+ if ("escalation_rules" in input) {
17895
+ params.push(JSON.stringify(input.escalation_rules));
17896
+ sets.push(`escalation_rules = $${params.length}`);
17897
+ }
17898
+ sets.push(`updated_at = NOW()`);
17899
+ const row = await this.client.get(`UPDATE contact_tasks SET ${sets.join(", ")} WHERE id = $1 RETURNING *`, params);
17900
+ return row ? this.mapTask(row) : null;
17901
+ }
17902
+ async deleteContactTask(id) {
17903
+ return (await this.client.query(`DELETE FROM contact_tasks WHERE id = $1`, [id])).rowCount > 0;
17904
+ }
17905
+ async listOverdueTasks() {
17906
+ const rows = await this.client.many(`SELECT * FROM contact_tasks WHERE deadline < $1 AND status NOT IN ('completed','cancelled') ORDER BY deadline ASC`, [nowIso()]);
17907
+ return rows.map((r) => this.mapTask(r));
17908
+ }
17909
+ async checkEscalations() {
17910
+ const overdue = await this.listOverdueTasks();
17911
+ const nowMs = Date.now();
17912
+ const results = [];
17913
+ for (const task of overdue) {
17914
+ const rules = task.escalation_rules ?? [];
17915
+ if (!task.deadline || rules.length === 0)
17916
+ continue;
17917
+ const days = (nowMs - new Date(task.deadline).getTime()) / 86400000;
17918
+ const triggered = rules.filter((r) => days >= r.after_days);
17919
+ if (triggered.length)
17920
+ results.push({ task, rules_triggered: triggered });
17921
+ }
17922
+ return results;
17923
+ }
17924
+ mapApplication(r) {
17925
+ return { id: r.id, program_name: r.program_name, provider_company_id: r.provider_company_id, type: r.type, value_usd: r.value_usd, applicant_contact_id: r.applicant_contact_id, primary_contact_id: r.primary_contact_id, status: r.status, submitted_date: r.submitted_date, decision_date: r.decision_date, follow_up_date: r.follow_up_date, notes: r.notes, method: r.method ?? null, form_url: r.form_url, metadata: pj(r.metadata, {}), created_at: iso(r.created_at), updated_at: iso(r.updated_at) };
17926
+ }
17927
+ async createApplication(input) {
17928
+ const id = newUuid();
17929
+ const row = await this.client.get(`INSERT INTO applications (id, program_name, provider_company_id, type, value_usd, applicant_contact_id, primary_contact_id, status, submitted_date, decision_date, follow_up_date, notes, method, form_url, metadata)
17930
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15) RETURNING *`, [id, input.program_name, input.provider_company_id ?? null, input.type ?? "other", input.value_usd ?? null, input.applicant_contact_id ?? null, input.primary_contact_id ?? null, input.status ?? "draft", input.submitted_date ?? null, input.decision_date ?? null, input.follow_up_date ?? null, input.notes ?? null, input.method ?? null, input.form_url ?? null, JSON.stringify(input.metadata ?? {})]);
17931
+ return this.mapApplication(row);
17932
+ }
17933
+ async listApplications(opts = {}) {
17934
+ const where = [];
17935
+ const params = [];
17936
+ if (opts.type) {
17937
+ params.push(opts.type);
17938
+ where.push(`type = $${params.length}`);
17939
+ }
17940
+ if (opts.status) {
17941
+ params.push(opts.status);
17942
+ where.push(`status = $${params.length}`);
17943
+ }
17944
+ if (opts.provider_company_id) {
17945
+ params.push(opts.provider_company_id);
17946
+ where.push(`provider_company_id = $${params.length}`);
17947
+ }
17948
+ if (opts.applicant_contact_id) {
17949
+ params.push(opts.applicant_contact_id);
17950
+ where.push(`applicant_contact_id = $${params.length}`);
17951
+ }
17952
+ const sql = `SELECT * FROM applications ${where.length ? `WHERE ${where.join(" AND ")}` : ""} ORDER BY created_at DESC`;
17953
+ return (await this.client.many(sql, params)).map((r) => this.mapApplication(r));
17954
+ }
17955
+ async updateApplication(id, input) {
17956
+ const cols = ["program_name", "provider_company_id", "type", "value_usd", "applicant_contact_id", "primary_contact_id", "status", "submitted_date", "decision_date", "follow_up_date", "notes", "method", "form_url"];
17957
+ const sets = [];
17958
+ const params = [id];
17959
+ for (const c of cols)
17960
+ if (c in input) {
17961
+ params.push(input[c] ?? null);
17962
+ sets.push(`${c} = $${params.length}`);
17963
+ }
17964
+ if ("metadata" in input) {
17965
+ params.push(JSON.stringify(input.metadata));
17966
+ sets.push(`metadata = $${params.length}`);
17967
+ }
17968
+ sets.push(`updated_at = NOW()`);
17969
+ const row = await this.client.get(`UPDATE applications SET ${sets.join(", ")} WHERE id = $1 RETURNING *`, params);
17970
+ return row ? this.mapApplication(row) : null;
17971
+ }
17972
+ async listFollowUpDueApplications() {
17973
+ const today = new Date().toISOString().slice(0, 10);
17974
+ const rows = await this.client.many(`SELECT * FROM applications WHERE follow_up_date <= $1 AND status = 'follow_up_needed' ORDER BY follow_up_date ASC`, [today]);
17975
+ return rows.map((r) => this.mapApplication(r));
17976
+ }
17977
+ mapGroup(r) {
17978
+ return { ...r, created_at: isoOrNull(r.created_at), updated_at: isoOrNull(r.updated_at), member_count: r.member_count != null ? Number(r.member_count) : undefined, company_count: r.company_count != null ? Number(r.company_count) : undefined };
17979
+ }
17980
+ async createGroup(input) {
17981
+ const id = newUuid();
17982
+ await this.client.execute(`INSERT INTO groups (id, name, description, project_id) VALUES ($1,$2,$3,$4)`, [id, input.name, input.description ?? null, input.project_id ?? null]);
17983
+ return this.getGroup(id);
17984
+ }
17985
+ async getGroup(id) {
17986
+ const row = await this.client.get(`SELECT * FROM groups WHERE id = $1`, [id]);
17987
+ return row ? this.mapGroup(row) : null;
17988
+ }
17989
+ async listGroups(projectId) {
17990
+ const params = [];
17991
+ let where = "";
17992
+ if (projectId) {
17993
+ params.push(projectId);
17994
+ where = `WHERE g.project_id = $1`;
17995
+ }
17996
+ const rows = await this.client.many(`SELECT g.*, (SELECT COUNT(*) FROM contact_groups cg WHERE cg.group_id = g.id) AS member_count,
17997
+ (SELECT COUNT(*) FROM company_groups cog WHERE cog.group_id = g.id) AS company_count
17998
+ FROM groups g ${where} ORDER BY g.name`, params);
17999
+ return rows.map((r) => this.mapGroup(r));
18000
+ }
18001
+ async updateGroup(id, input) {
18002
+ const sets = [];
18003
+ const params = [id];
18004
+ for (const c of ["name", "description", "project_id"])
18005
+ if (c in input) {
18006
+ params.push(input[c] ?? null);
18007
+ sets.push(`${c} = $${params.length}`);
18008
+ }
18009
+ sets.push(`updated_at = NOW()`);
18010
+ await this.client.execute(`UPDATE groups SET ${sets.join(", ")} WHERE id = $1`, params);
18011
+ return this.getGroup(id);
18012
+ }
18013
+ async deleteGroup(id) {
18014
+ return (await this.client.query(`DELETE FROM groups WHERE id = $1`, [id])).rowCount > 0;
18015
+ }
18016
+ async addContactToGroup(contactId, groupId) {
18017
+ const existing = await this.client.get(`SELECT 1 FROM contact_groups WHERE contact_id = $1 AND group_id = $2`, [contactId, groupId]);
18018
+ if (existing)
18019
+ return { added: false, already_member: true };
18020
+ await this.client.execute(`INSERT INTO contact_groups (contact_id, group_id) VALUES ($1,$2)`, [contactId, groupId]);
18021
+ return { added: true, already_member: false };
18022
+ }
18023
+ async removeContactFromGroup(contactId, groupId) {
18024
+ await this.client.execute(`DELETE FROM contact_groups WHERE contact_id = $1 AND group_id = $2`, [contactId, groupId]);
18025
+ }
18026
+ async listContactsInGroup(groupId) {
18027
+ return (await this.client.many(`SELECT contact_id FROM contact_groups WHERE group_id = $1`, [groupId])).map((r) => r.contact_id);
18028
+ }
18029
+ async listGroupsForContact(contactId) {
18030
+ const rows = await this.client.many(`SELECT g.* FROM groups g JOIN contact_groups cg ON g.id = cg.group_id WHERE cg.contact_id = $1 ORDER BY g.name`, [contactId]);
18031
+ return rows.map((r) => this.mapGroup(r));
18032
+ }
18033
+ async addCompanyToGroup(companyId, groupId) {
18034
+ const existing = await this.client.get(`SELECT 1 FROM company_groups WHERE company_id = $1 AND group_id = $2`, [companyId, groupId]);
18035
+ if (existing)
18036
+ return { added: false, already_member: true };
18037
+ await this.client.execute(`INSERT INTO company_groups (company_id, group_id) VALUES ($1,$2)`, [companyId, groupId]);
18038
+ return { added: true, already_member: false };
18039
+ }
18040
+ async removeCompanyFromGroup(companyId, groupId) {
18041
+ await this.client.execute(`DELETE FROM company_groups WHERE company_id = $1 AND group_id = $2`, [companyId, groupId]);
18042
+ }
18043
+ async listCompaniesInGroup(groupId) {
18044
+ return (await this.client.many(`SELECT company_id FROM company_groups WHERE group_id = $1`, [groupId])).map((r) => r.company_id);
18045
+ }
18046
+ async listGroupsForCompany(companyId) {
18047
+ const rows = await this.client.many(`SELECT g.* FROM groups g JOIN company_groups cog ON g.id = cog.group_id WHERE cog.company_id = $1 ORDER BY g.name`, [companyId]);
18048
+ return rows.map((r) => this.mapGroup(r));
18049
+ }
18050
+ mapVendorComm(r) {
18051
+ return { id: r.id, company_id: r.company_id, contact_id: r.contact_id, comm_date: r.comm_date, type: r.type, direction: r.direction, subject: r.subject, body: r.body, status: r.status, invoice_amount: r.invoice_amount, invoice_currency: r.invoice_currency, invoice_ref: r.invoice_ref, follow_up_date: r.follow_up_date, follow_up_done: Boolean(r.follow_up_done), created_at: iso(r.created_at) };
18052
+ }
18053
+ async logVendorCommunication(input) {
18054
+ const id = newUuid();
18055
+ const row = await this.client.get(`INSERT INTO vendor_communications (id, company_id, contact_id, comm_date, type, direction, subject, body, status, invoice_amount, invoice_currency, invoice_ref, follow_up_date, follow_up_done)
18056
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14) RETURNING *`, [id, input.company_id, input.contact_id ?? null, input.comm_date, input.type ?? "email", input.direction ?? "outbound", input.subject ?? null, input.body ?? null, input.status ?? "sent", input.invoice_amount ?? null, input.invoice_currency ?? null, input.invoice_ref ?? null, input.follow_up_date ?? null, Boolean(input.follow_up_done)]);
18057
+ return this.mapVendorComm(row);
18058
+ }
18059
+ async listVendorCommunications(companyId, opts = {}) {
18060
+ const where = ["company_id = $1"];
18061
+ const params = [companyId];
18062
+ if (opts.type) {
18063
+ params.push(opts.type);
18064
+ where.push(`type = $${params.length}`);
18065
+ }
18066
+ if (opts.status) {
18067
+ params.push(opts.status);
18068
+ where.push(`status = $${params.length}`);
18069
+ }
18070
+ if (opts.direction) {
18071
+ params.push(opts.direction);
18072
+ where.push(`direction = $${params.length}`);
18073
+ }
18074
+ const rows = await this.client.many(`SELECT * FROM vendor_communications WHERE ${where.join(" AND ")} ORDER BY comm_date DESC`, params);
18075
+ return rows.map((r) => this.mapVendorComm(r));
18076
+ }
18077
+ async listMissingInvoices() {
18078
+ const rows = await this.client.many(`SELECT * FROM vendor_communications WHERE type = 'invoice_request' AND status IN ('awaiting_response','no_response') ORDER BY comm_date ASC`);
18079
+ return rows.map((r) => this.mapVendorComm(r));
18080
+ }
18081
+ async listPendingFollowUps() {
18082
+ const today = new Date().toISOString().slice(0, 10);
18083
+ const rows = await this.client.many(`SELECT * FROM vendor_communications WHERE follow_up_date <= $1 AND follow_up_done = false ORDER BY follow_up_date ASC`, [today]);
18084
+ return rows.map((r) => this.mapVendorComm(r));
18085
+ }
18086
+ async markFollowUpDone(id) {
18087
+ const row = await this.client.get(`UPDATE vendor_communications SET follow_up_done = true WHERE id = $1 RETURNING *`, [id]);
18088
+ return row ? this.mapVendorComm(row) : null;
18089
+ }
18090
+ mapOrgMember(r) {
18091
+ return { id: r.id, company_id: r.company_id, contact_id: r.contact_id, title: r.title, specialization: r.specialization, office_phone: r.office_phone, response_sla_hours: r.response_sla_hours, notes: r.notes, created_at: iso(r.created_at), updated_at: iso(r.updated_at) };
18092
+ }
18093
+ async addOrgMember(input) {
18094
+ const id = newUuid();
18095
+ const row = await this.client.get(`INSERT INTO org_members (id, company_id, contact_id, title, specialization, office_phone, response_sla_hours, notes)
18096
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8) RETURNING *`, [id, input.company_id, input.contact_id, input.title ?? null, input.specialization ?? null, input.office_phone ?? null, input.response_sla_hours ?? null, input.notes ?? null]);
18097
+ return this.mapOrgMember(row);
18098
+ }
18099
+ async listOrgMembers(companyId) {
18100
+ return (await this.client.many(`SELECT * FROM org_members WHERE company_id = $1 ORDER BY created_at ASC`, [companyId])).map((r) => this.mapOrgMember(r));
18101
+ }
18102
+ async updateOrgMember(id, input) {
18103
+ const sets = [];
18104
+ const params = [id];
18105
+ for (const c of ["title", "specialization", "office_phone", "response_sla_hours", "notes"])
18106
+ if (c in input) {
18107
+ params.push(input[c] ?? null);
18108
+ sets.push(`${c} = $${params.length}`);
18109
+ }
18110
+ sets.push(`updated_at = NOW()`);
18111
+ const row = await this.client.get(`UPDATE org_members SET ${sets.join(", ")} WHERE id = $1 RETURNING *`, params);
18112
+ return row ? this.mapOrgMember(row) : null;
18113
+ }
18114
+ async removeOrgMember(id) {
18115
+ return (await this.client.query(`DELETE FROM org_members WHERE id = $1`, [id])).rowCount > 0;
18116
+ }
18117
+ async listOrgMembersForContact(contactId) {
18118
+ return (await this.client.many(`SELECT * FROM org_members WHERE contact_id = $1 ORDER BY created_at ASC`, [contactId])).map((r) => this.mapOrgMember(r));
18119
+ }
18120
+ mapNote(r) {
18121
+ return { ...r, created_at: isoOrNull(r.created_at) };
18122
+ }
18123
+ async addNote(contactId, body, createdBy, companyId) {
18124
+ const contact = await this.client.get(`SELECT id FROM contacts WHERE id = $1`, [contactId]);
18125
+ if (!contact)
18126
+ throw new Error(`Contact not found: ${contactId}`);
18127
+ const id = newUuid();
18128
+ const row = await this.client.get(`INSERT INTO contact_notes (id, contact_id, body, created_by, company_id) VALUES ($1,$2,$3,$4,$5) RETURNING *`, [id, contactId, body, createdBy ?? null, companyId ?? null]);
18129
+ return this.mapNote(row);
18130
+ }
18131
+ async listNotes(contactId) {
18132
+ return (await this.client.many(`SELECT * FROM contact_notes WHERE contact_id = $1 ORDER BY created_at ASC`, [contactId])).map((r) => this.mapNote(r));
18133
+ }
18134
+ async listNotesForContactAtCompany(contactId, companyId) {
18135
+ return (await this.client.many(`SELECT * FROM contact_notes WHERE contact_id = $1 AND company_id = $2 ORDER BY created_at ASC`, [contactId, companyId])).map((r) => this.mapNote(r));
18136
+ }
18137
+ async deleteNote(noteId) {
18138
+ await this.client.execute(`DELETE FROM contact_notes WHERE id = $1`, [noteId]);
18139
+ }
18140
+ async createRelationship(input) {
18141
+ const id = newUuid();
18142
+ const row = await this.client.get(`INSERT INTO contact_relationships (id, contact_a_id, contact_b_id, relationship_type, notes)
18143
+ VALUES ($1,$2,$3,$4,$5) RETURNING *`, [id, input.contact_a_id, input.contact_b_id, input.relationship_type ?? "knows", input.notes ?? null]);
18144
+ return { ...row, created_at: isoOrNull(row?.created_at) };
18145
+ }
18146
+ async listRelationships(opts = {}) {
18147
+ if (opts.contact_id) {
18148
+ return this.client.many(`SELECT * FROM contact_relationships WHERE contact_a_id = $1 OR contact_b_id = $1`, [opts.contact_id]);
18149
+ }
18150
+ return this.client.many(`SELECT * FROM contact_relationships`);
18151
+ }
18152
+ async deleteRelationship(id) {
18153
+ await this.client.execute(`DELETE FROM contact_relationships WHERE id = $1`, [id]);
18154
+ }
18155
+ async createCompanyRelationship(input) {
18156
+ const id = newUuid();
18157
+ const row = await this.client.get(`INSERT INTO company_relationships (id, contact_id, company_id, relationship_type, notes, start_date, end_date, is_primary, status)
18158
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9) RETURNING *`, [id, input.contact_id, input.company_id, input.relationship_type, input.notes ?? null, input.start_date ?? null, input.end_date ?? null, Boolean(input.is_primary), input.status ?? "active"]);
18159
+ return { ...row, created_at: isoOrNull(row?.created_at), is_primary: Boolean(row?.is_primary) };
18160
+ }
18161
+ async listCompanyRelationships(opts = {}) {
18162
+ const where = [];
18163
+ const params = [];
18164
+ if (opts.contact_id) {
18165
+ params.push(opts.contact_id);
18166
+ where.push(`contact_id = $${params.length}`);
18167
+ }
18168
+ if (opts.company_id) {
18169
+ params.push(opts.company_id);
18170
+ where.push(`company_id = $${params.length}`);
18171
+ }
18172
+ const sql = `SELECT * FROM company_relationships ${where.length ? `WHERE ${where.join(" AND ")}` : ""} ORDER BY created_at DESC`;
18173
+ return (await this.client.many(sql, params)).map((r) => ({ ...r, created_at: isoOrNull(r.created_at), is_primary: Boolean(r.is_primary) }));
18174
+ }
18175
+ async deleteCompanyRelationship(id) {
18176
+ await this.client.execute(`DELETE FROM company_relationships WHERE id = $1`, [id]);
18177
+ }
18178
+ async getFieldHistory(contactId, fieldName) {
18179
+ const rows = fieldName ? await this.client.many(`SELECT * FROM contact_field_history WHERE contact_id = $1 AND field_name = $2 ORDER BY valid_from DESC`, [contactId, fieldName]) : await this.client.many(`SELECT * FROM contact_field_history WHERE contact_id = $1 ORDER BY valid_from DESC`, [contactId]);
18180
+ return rows.map((r) => ({ ...r, valid_from: isoOrNull(r.valid_from), created_at: isoOrNull(r.created_at) }));
18181
+ }
18182
+ async getContactAt(contactId, timestamp) {
18183
+ const rows = await this.client.many(`SELECT field_name, new_value FROM contact_field_history WHERE contact_id = $1 AND valid_from <= $2 ORDER BY valid_from ASC`, [contactId, timestamp]);
18184
+ const result = {};
18185
+ for (const r of rows)
18186
+ if (r.new_value != null)
18187
+ result[r.field_name] = r.new_value;
18188
+ return result;
18189
+ }
18190
+ mapJob(r) {
18191
+ return { ...r, is_current: Boolean(r.is_current), inferred: Boolean(r.inferred), created_at: isoOrNull(r.created_at) };
18192
+ }
18193
+ async addJobEntry(contactId, input) {
18194
+ if (input.is_current) {
18195
+ await this.client.execute(`UPDATE job_history SET is_current = false, end_date = COALESCE(end_date, $1) WHERE contact_id = $2 AND is_current = true`, [new Date().toISOString().slice(0, 10), contactId]);
18196
+ }
18197
+ const id = newUuid();
18198
+ const row = await this.client.get(`INSERT INTO job_history (id, contact_id, company_id, company_name, title, start_date, end_date, is_current, inferred, source)
18199
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) RETURNING *`, [id, contactId, input.company_id ?? null, input.company_name, input.title ?? null, input.start_date ?? null, input.end_date ?? null, Boolean(input.is_current), Boolean(input.inferred), input.source ?? null]);
18200
+ return this.mapJob(row);
18201
+ }
18202
+ async getJobHistory(contactId) {
18203
+ return (await this.client.many(`SELECT * FROM job_history WHERE contact_id = $1 ORDER BY is_current DESC, start_date DESC`, [contactId])).map((r) => this.mapJob(r));
18204
+ }
18205
+ mapLearning(r) {
18206
+ return { ...r, tags: pj(r.tags, []), created_at: isoOrNull(r.created_at), updated_at: isoOrNull(r.updated_at) };
18207
+ }
18208
+ async saveLearning(contactId, input) {
18209
+ const id = newUuid();
18210
+ const row = await this.client.get(`INSERT INTO contact_learnings (id, contact_id, content, type, confidence, importance, learned_by, session_id, visibility, tags)
18211
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) RETURNING *`, [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 ?? [])]);
18212
+ return this.mapLearning(row);
18213
+ }
18214
+ async getLearnings(contactId, opts = {}) {
18215
+ let sql = `SELECT * FROM contact_learnings WHERE contact_id = $1`;
18216
+ const params = [contactId];
18217
+ if (opts.type) {
18218
+ params.push(opts.type);
18219
+ sql += ` AND type = $${params.length}`;
18220
+ }
18221
+ if (opts.min_importance) {
18222
+ params.push(opts.min_importance);
18223
+ sql += ` AND importance >= $${params.length}`;
18224
+ }
18225
+ if (opts.visibility) {
18226
+ params.push(opts.visibility);
18227
+ sql += ` AND visibility = $${params.length}`;
18228
+ }
18229
+ sql += ` ORDER BY importance DESC, confidence DESC`;
18230
+ return (await this.client.many(sql, params)).map((r) => this.mapLearning(r));
18231
+ }
18232
+ async searchLearnings(query, opts = {}) {
18233
+ let sql = `SELECT * FROM contact_learnings WHERE content ILIKE $1`;
18234
+ const params = [`%${query}%`];
18235
+ if (opts.type) {
18236
+ params.push(opts.type);
18237
+ sql += ` AND type = $${params.length}`;
18238
+ }
18239
+ if (opts.contact_id) {
18240
+ params.push(opts.contact_id);
18241
+ sql += ` AND contact_id = $${params.length}`;
18242
+ }
18243
+ sql += ` ORDER BY importance DESC, confidence DESC LIMIT 50`;
18244
+ return (await this.client.many(sql, params)).map((r) => this.mapLearning(r));
18245
+ }
18246
+ async confirmLearning(learningId) {
18247
+ await this.client.execute(`UPDATE contact_learnings SET confirmed_count = confirmed_count + 1, confidence = LEAST(100, confidence + 10), updated_at = NOW() WHERE id = $1`, [learningId]);
18248
+ }
18249
+ async getStaleLearnings(daysOld, minConfidence) {
18250
+ const cutoff = new Date(Date.now() - daysOld * 86400000).toISOString();
18251
+ return (await this.client.many(`SELECT * FROM contact_learnings WHERE confirmed_count = 0 AND created_at < $1 AND confidence >= $2 ORDER BY confidence ASC LIMIT 50`, [cutoff, minConfidence])).map((r) => this.mapLearning(r));
18252
+ }
18253
+ async runLearningMaintenance() {
18254
+ const cutoff = new Date(Date.now() - 30 * 86400000).toISOString();
18255
+ const res = await this.client.query(`UPDATE contact_learnings SET confidence = GREATEST(10, confidence - 5), updated_at = NOW() WHERE confirmed_count = 0 AND created_at < $1 AND confidence > 10`, [cutoff]);
18256
+ const dups = await this.client.many(`SELECT contact_id, COUNT(*) AS cnt FROM contact_learnings GROUP BY contact_id, LOWER(SUBSTR(content,1,30)) HAVING COUNT(*) > 1`);
18257
+ return { decayed_count: res.rowCount, potential_contradictions: dups };
18258
+ }
18259
+ mapLock(r) {
18260
+ return { ...r, acquired_at: isoOrNull(r.acquired_at), expires_at: isoOrNull(r.expires_at) };
18261
+ }
18262
+ async acquireContactLock(contactId, agentName, ttlSeconds = 300, reason, sessionId) {
18263
+ await this.client.execute(`DELETE FROM contact_locks WHERE expires_at < NOW()`);
18264
+ const existing = await this.client.get(`SELECT * FROM contact_locks WHERE contact_id = $1`, [contactId]);
18265
+ if (existing)
18266
+ return { acquired: false, held_by: existing.agent_name, lock: this.mapLock(existing) };
18267
+ const id = newUuid();
18268
+ const expiresAt = new Date(Date.now() + ttlSeconds * 1000).toISOString();
18269
+ const row = await this.client.get(`INSERT INTO contact_locks (id, contact_id, agent_name, reason, expires_at, session_id) VALUES ($1,$2,$3,$4,$5,$6) RETURNING *`, [id, contactId, agentName, reason ?? null, expiresAt, sessionId ?? null]);
18270
+ return { acquired: true, lock: this.mapLock(row) };
18271
+ }
18272
+ async releaseContactLock(contactId, agentName) {
18273
+ return (await this.client.query(`DELETE FROM contact_locks WHERE contact_id = $1 AND agent_name = $2`, [contactId, agentName])).rowCount > 0;
18274
+ }
18275
+ async checkContactLock(contactId) {
18276
+ await this.client.execute(`DELETE FROM contact_locks WHERE expires_at < NOW()`);
18277
+ const row = await this.client.get(`SELECT * FROM contact_locks WHERE contact_id = $1`, [contactId]);
18278
+ return row ? this.mapLock(row) : null;
18279
+ }
18280
+ async logAgentActivity(contactId, agentName, action, details, sessionId) {
18281
+ await this.client.execute(`INSERT INTO contact_agent_activity (id, contact_id, agent_name, action, details, session_id) VALUES ($1,$2,$3,$4,$5,$6)`, [newUuid(), contactId, agentName, action, details ?? null, sessionId ?? null]);
18282
+ }
18283
+ async getAgentActivity(contactId, limit = 20) {
18284
+ return (await this.client.many(`SELECT * FROM contact_agent_activity WHERE contact_id = $1 ORDER BY created_at DESC LIMIT $2`, [contactId, limit])).map((r) => ({ ...r, created_at: isoOrNull(r.created_at) }));
18285
+ }
18286
+ async resolveContactIdentity(partial) {
18287
+ const matches = new Map;
18288
+ const add = (id, name, title, score, reason) => {
18289
+ const ex = matches.get(id);
18290
+ if (ex) {
18291
+ ex.confidence_score = Math.min(100, ex.confidence_score + score);
18292
+ ex.match_reasons.push(reason);
18293
+ } else
18294
+ matches.set(id, { contact: { id, display_name: name, job_title: title }, confidence_score: score, match_reasons: [reason] });
18295
+ };
18296
+ if (partial.email) {
18297
+ const rows = await this.client.many(`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($1)`, [partial.email]);
18298
+ rows.forEach((r) => add(r.id, r.display_name, r.job_title, 90, `email match: ${partial.email}`));
18299
+ }
18300
+ if (partial.linkedin_url) {
18301
+ const tail = partial.linkedin_url.split("/").pop();
18302
+ const rows = await this.client.many(`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 $1`, [`%${tail}%`]);
18303
+ rows.forEach((r) => add(r.id, r.display_name, r.job_title, 85, `linkedin match`));
18304
+ }
18305
+ if (partial.name) {
18306
+ const rows = await this.client.many(`SELECT id, display_name, job_title FROM contacts WHERE display_name ILIKE $1 AND archived = false LIMIT 10`, [`%${partial.name}%`]);
18307
+ rows.forEach((r) => add(r.id, r.display_name, r.job_title, 40, `name match: ${partial.name}`));
18308
+ }
18309
+ return Array.from(matches.values()).sort((a, b) => b.confidence_score - a.confidence_score);
18310
+ }
18311
+ async addContactIdentity(contactId, system, externalId, externalUrl, confidence = "inferred") {
18312
+ const id = newUuid();
18313
+ const row = await this.client.get(`INSERT INTO contact_identities (id, contact_id, system, external_id, external_url, confidence) VALUES ($1,$2,$3,$4,$5,$6)
18314
+ ON CONFLICT (system, external_id) DO UPDATE SET contact_id = excluded.contact_id, external_url = excluded.external_url, confidence = excluded.confidence RETURNING *`, [id, contactId, system, externalId, externalUrl ?? null, confidence]);
18315
+ return { ...row, created_at: isoOrNull(row?.created_at) };
18316
+ }
18317
+ async getContactIdentities(contactId) {
18318
+ return (await this.client.many(`SELECT * FROM contact_identities WHERE contact_id = $1 ORDER BY created_at DESC`, [contactId])).map((r) => ({ ...r, created_at: isoOrNull(r.created_at) }));
18319
+ }
18320
+ signalRow(r) {
18321
+ const last = r.last_contacted_at;
18322
+ return {
18323
+ contact_id: r.contact_id,
18324
+ display_name: r.display_name,
18325
+ last_contacted_at: last,
18326
+ interaction_count_30d: Number(r.interaction_count_30d ?? 0),
18327
+ engagement_status: r.engagement_status ?? null,
18328
+ relationship_health: r.relationship_health ?? null,
18329
+ days_since_contact: last ? Math.floor((Date.now() - new Date(last).getTime()) / 86400000) : null
18330
+ };
18331
+ }
18332
+ async getRelationshipSignals(contactId) {
18333
+ const row = await this.client.get(`SELECT id AS contact_id, display_name, last_contacted_at, interaction_count_30d, engagement_status, relationship_health FROM contacts WHERE id = $1`, [contactId]);
18334
+ if (!row)
18335
+ return [];
18336
+ const base = this.signalRow(row);
18337
+ const cnt = base.interaction_count_30d;
18338
+ const health = base.relationship_health ?? 50;
18339
+ const daysSince = base.days_since_contact;
18340
+ let signal_type = "healthy";
18341
+ let reason = `Last contact ${daysSince}d ago, ${cnt} interactions in 30d`;
18342
+ if (daysSince === null || daysSince > 180) {
18343
+ signal_type = "ghost";
18344
+ reason = "No contact in 180+ days or never contacted";
18345
+ } else if (daysSince > 60 && cnt === 0) {
18346
+ signal_type = "cooling";
18347
+ reason = `No contact in ${daysSince} days, no recent interactions`;
18348
+ } else if (cnt > 3 && health > 70) {
18349
+ signal_type = "warming";
18350
+ reason = `${cnt} interactions in last 30 days, health score ${health}`;
18351
+ }
18352
+ return [{ ...base, signal_type, reason }];
18353
+ }
18354
+ async getGhostContacts() {
18355
+ const cutoff = new Date(Date.now() - 180 * 86400000).toISOString();
18356
+ const rows = await this.client.many(`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 last_contacted_at < $1) AND archived = false ORDER BY last_contacted_at ASC NULLS FIRST LIMIT 50`, [cutoff]);
18357
+ return rows.map((r) => ({ ...this.signalRow(r), signal_type: "ghost", reason: "No contact in 180+ days or never contacted" }));
18358
+ }
18359
+ async getWarmingContacts() {
18360
+ const rows = await this.client.many(`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 = false ORDER BY relationship_health DESC LIMIT 50`);
18361
+ return rows.map((r) => ({ ...this.signalRow(r), signal_type: "warming", reason: `${Number(r.interaction_count_30d ?? 0)} interactions in last 30 days` }));
18362
+ }
18363
+ async recomputeSignals() {
18364
+ const res = await this.client.query(`UPDATE contacts SET engagement_status = CASE
18365
+ WHEN interaction_count_30d > 3 THEN 'warming'
18366
+ WHEN last_contacted_at IS NULL OR EXTRACT(EPOCH FROM (NOW() - last_contacted_at::timestamptz)) / 86400 > 180 THEN 'ghost'
18367
+ WHEN EXTRACT(EPOCH FROM (NOW() - last_contacted_at::timestamptz)) / 86400 > 60 THEN 'cooling'
18368
+ ELSE 'stable' END,
18369
+ updated_at = NOW() WHERE archived = false`);
18370
+ return { updated: res.rowCount };
18371
+ }
18372
+ async getFreshnessScore(contactId) {
18373
+ const contact = await this.client.get(`SELECT * FROM contacts WHERE id = $1`, [contactId]);
18374
+ if (!contact)
18375
+ throw new Error(`Contact not found: ${contactId}`);
18376
+ const historyRows = await this.client.many(`SELECT field_name, new_value, source, created_at FROM contact_field_history WHERE contact_id = $1 ORDER BY created_at DESC`, [contactId]);
18377
+ const verifiedRows = await this.client.many(`SELECT field_name, last_verified_at, source FROM contact_field_confidence WHERE contact_id = $1 AND confidence = 'verified'`, [contactId]).catch(() => []);
18378
+ const scored = ["display_name", "job_title", "company_id", "emails", "phones", "last_contacted_at"];
18379
+ const verifiedMap = new Map(verifiedRows.map((r) => [r.field_name, r]));
18380
+ const historyMap = new Map;
18381
+ for (const r of historyRows)
18382
+ if (!historyMap.has(r.field_name))
18383
+ historyMap.set(r.field_name, r);
18384
+ const fields = [];
18385
+ for (const field of scored) {
18386
+ let value = null;
18387
+ if (field === "emails") {
18388
+ const e = await this.client.get(`SELECT address FROM emails WHERE contact_id = $1 LIMIT 1`, [contactId]);
18389
+ value = e?.address ?? null;
18390
+ } else if (field === "phones") {
18391
+ const p = await this.client.get(`SELECT number FROM phones WHERE contact_id = $1 LIMIT 1`, [contactId]);
18392
+ value = p?.number ?? null;
18393
+ } else
18394
+ value = contact[field] != null ? String(contact[field]) : null;
18395
+ const verified = verifiedMap.get(field);
18396
+ const history = historyMap.get(field);
18397
+ let confidence = "unknown";
18398
+ let days_old = null;
18399
+ let last_verified_at = null;
18400
+ let source = null;
18401
+ if (verified) {
18402
+ confidence = "verified";
18403
+ last_verified_at = isoOrNull(verified.last_verified_at);
18404
+ source = verified.source;
18405
+ days_old = last_verified_at ? Math.floor((Date.now() - new Date(last_verified_at).getTime()) / 86400000) : null;
18406
+ } else if (history) {
18407
+ confidence = history.source === "import" ? "imported" : "inferred";
18408
+ last_verified_at = isoOrNull(history.created_at);
18409
+ source = history.source;
18410
+ days_old = last_verified_at ? Math.floor((Date.now() - new Date(last_verified_at).getTime()) / 86400000) : null;
18411
+ if (days_old != null && days_old > 365)
18412
+ confidence = "stale";
18413
+ } else if (value)
18414
+ confidence = "inferred";
18415
+ fields.push({ field_name: field, value, last_verified_at, source, confidence, days_old });
18416
+ }
18417
+ const fieldScore = fields.reduce((acc, f) => {
18418
+ if (!f.value)
18419
+ return acc;
18420
+ if (f.confidence === "verified")
18421
+ return acc + 20;
18422
+ if (f.confidence === "imported" || f.confidence === "inferred")
18423
+ return acc + 10;
18424
+ return acc + 5;
18425
+ }, 0);
18426
+ return { contact_id: contactId, overall_score: Math.min(100, fieldScore), fields, stale_fields: fields.filter((f) => f.confidence === "stale" || !f.value && f.field_name !== "phones").map((f) => f.field_name), verified_fields: fields.filter((f) => f.confidence === "verified").map((f) => f.field_name) };
18427
+ }
18428
+ async getStaleContacts(threshold = 40) {
18429
+ return this.client.many(`SELECT * FROM (
18430
+ SELECT c.id AS contact_id, c.display_name,
18431
+ ((CASE WHEN c.job_title IS NOT NULL THEN 15 ELSE 0 END) +
18432
+ (CASE WHEN c.company_id IS NOT NULL THEN 15 ELSE 0 END) +
18433
+ (CASE WHEN c.last_contacted_at IS NOT NULL THEN 20 ELSE 0 END) +
18434
+ (CASE WHEN EXISTS(SELECT 1 FROM emails WHERE contact_id = c.id) THEN 20 ELSE 0 END) +
18435
+ (CASE WHEN EXISTS(SELECT 1 FROM phones WHERE contact_id = c.id) THEN 15 ELSE 0 END) +
18436
+ (CASE WHEN c.notes IS NOT NULL THEN 10 ELSE 0 END) +
18437
+ (CASE WHEN EXISTS(SELECT 1 FROM contact_tags WHERE contact_id = c.id) THEN 5 ELSE 0 END)) AS score
18438
+ FROM contacts c WHERE c.archived = false
18439
+ ) sub WHERE score < $1 ORDER BY score ASC LIMIT 100`, [threshold]);
18440
+ }
18441
+ async markFieldVerified(contactId, fieldName, source) {
18442
+ await this.client.execute(`INSERT INTO contact_field_confidence (id, contact_id, field_name, confidence, source, last_verified_at) VALUES ($1,$2,$3,'verified',$4,NOW())
18443
+ ON CONFLICT (contact_id, field_name) DO UPDATE SET confidence = 'verified', source = excluded.source, last_verified_at = NOW()`, [newUuid(), contactId, fieldName, source ?? null]);
18444
+ }
18445
+ async computeRelationshipStrength(contactId) {
18446
+ const c = await this.client.get(`SELECT last_contacted_at, interaction_count_30d FROM contacts WHERE id = $1`, [contactId]);
18447
+ if (!c)
18448
+ return 0;
18449
+ let score = 50;
18450
+ if (c.last_contacted_at) {
18451
+ const days = Math.floor((Date.now() - new Date(c.last_contacted_at).getTime()) / 86400000);
18452
+ score += days < 7 ? 30 : days < 30 ? 20 : days < 90 ? 5 : -20;
18453
+ } else
18454
+ score -= 20;
18455
+ score += Math.min(20, (c.interaction_count_30d || 0) * 4);
18456
+ return Math.max(0, Math.min(100, score));
18457
+ }
18458
+ async findWarmPath(fromContactId, toContactId) {
18459
+ const visited = new Set([fromContactId]);
18460
+ const queue = [{ id: fromContactId, path: [] }];
18461
+ while (queue.length) {
18462
+ const { id, path } = queue.shift();
18463
+ if (id === toContactId)
18464
+ return path;
18465
+ if (path.length >= 4)
18466
+ continue;
18467
+ const neighbors = await this.client.many(`SELECT cr.contact_a_id, cr.contact_b_id, cr.strength_score, c.display_name FROM contact_relationships cr JOIN contacts c ON (CASE WHEN cr.contact_a_id = $1 THEN cr.contact_b_id ELSE cr.contact_a_id END) = c.id WHERE cr.contact_a_id = $1 OR cr.contact_b_id = $1 LIMIT 20`, [id]);
18468
+ for (const n of neighbors) {
18469
+ const nextId = n.contact_a_id === id ? n.contact_b_id : n.contact_a_id;
18470
+ if (visited.has(nextId))
18471
+ continue;
18472
+ visited.add(nextId);
18473
+ queue.push({ id: nextId, path: [...path, { contact_id: nextId, display_name: n.display_name, strength: n.strength_score || 50 }] });
18474
+ }
18475
+ }
18476
+ return [];
18477
+ }
18478
+ async findConnectionsAtCompany(companyId) {
18479
+ return this.client.many(`SELECT c.id AS contact_id, c.display_name, c.job_title, c.relationship_health AS strength FROM contacts c WHERE c.company_id = $1 AND c.archived = false ORDER BY c.relationship_health DESC`, [companyId]);
18480
+ }
18481
+ async detectCoolingRelationships() {
18482
+ const cutoff = new Date(Date.now() - 45 * 86400000).toISOString();
18483
+ const rows = await this.client.many(`SELECT id AS contact_id, display_name, last_contacted_at FROM contacts WHERE last_contacted_at IS NOT NULL AND last_contacted_at < $1 AND engagement_status != 'ghost' AND archived = false ORDER BY last_contacted_at ASC LIMIT 50`, [cutoff]);
18484
+ return rows.map((r) => ({ contact_id: r.contact_id, display_name: r.display_name, days_since: Math.floor((Date.now() - new Date(r.last_contacted_at).getTime()) / 86400000) }));
18485
+ }
18486
+ async addOrgChartEdge(companyId, contactAId, contactBId, edgeType, inferred = false) {
18487
+ const id = newUuid();
18488
+ const row = await this.client.get(`INSERT INTO org_chart_edges (id, company_id, contact_a_id, contact_b_id, edge_type, inferred) VALUES ($1,$2,$3,$4,$5,$6)
18489
+ ON CONFLICT (company_id, contact_a_id, contact_b_id, edge_type) DO UPDATE SET inferred = excluded.inferred RETURNING *`, [id, companyId, contactAId, contactBId, edgeType, inferred]);
18490
+ return { ...row, inferred: Boolean(row?.inferred), created_at: isoOrNull(row?.created_at) };
18491
+ }
18492
+ async listOrgChart(companyId) {
18493
+ return this.client.many(`SELECT ca.display_name AS contact_a_name, cb.display_name AS contact_b_name, e.edge_type
18494
+ FROM org_chart_edges e JOIN contacts ca ON e.contact_a_id = ca.id JOIN contacts cb ON e.contact_b_id = cb.id WHERE e.company_id = $1`, [companyId]);
18495
+ }
18496
+ async setDealContactRole(dealId, contactId, accountRole) {
18497
+ const id = newUuid();
18498
+ const row = await this.client.get(`INSERT INTO deal_contact_roles (id, deal_id, contact_id, account_role) VALUES ($1,$2,$3,$4)
18499
+ ON CONFLICT (deal_id, contact_id) DO UPDATE SET account_role = excluded.account_role RETURNING *`, [id, dealId, contactId, accountRole]);
18500
+ return { ...row, created_at: isoOrNull(row?.created_at) };
18501
+ }
18502
+ async getDealTeam(dealId) {
18503
+ return this.client.many(`SELECT c.display_name, r.account_role, c.job_title FROM deal_contact_roles r JOIN contacts c ON r.contact_id = c.id WHERE r.deal_id = $1`, [dealId]);
18504
+ }
18505
+ async getCoverageGaps(companyId) {
18506
+ const team = await this.client.many(`SELECT DISTINCT r.account_role FROM deal_contact_roles r JOIN deals d ON r.deal_id = d.id WHERE d.company_id = $1`, [companyId]);
18507
+ const covered = new Set(team.map((t) => t.account_role));
18508
+ const key = ["economic_buyer", "technical_evaluator", "champion"];
18509
+ return { covered: Array.from(covered), missing_key_roles: key.filter((k) => !covered.has(k)) };
18510
+ }
18511
+ mapAudience(r) {
18512
+ return { id: r.id, audience_id: r.audience_id, name: r.name, match: r.match, predicates: pj(r.predicates, []), consent_policy: r.consent_policy, suppression_synced_at: isoOrNull(r.suppression_synced_at), created_at: isoOrNull(r.created_at), updated_at: isoOrNull(r.updated_at) };
18513
+ }
18514
+ async createAudience(input) {
18515
+ const audienceId = String(input.audience_id ?? "");
18516
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(audienceId))
18517
+ throw new Error(`audience_id must be a lowercase dashed slug: ${audienceId}`);
18518
+ const predicates = input.predicates ?? [];
18519
+ if (!Array.isArray(predicates) || predicates.length === 0)
18520
+ throw new Error("at least one predicate is required");
18521
+ const dupe = await this.client.get(`SELECT id FROM audiences WHERE audience_id = $1`, [audienceId]);
18522
+ if (dupe)
18523
+ throw new Error(`duplicate audience_id: ${audienceId}`);
18524
+ const id = newUuid();
18525
+ const row = await this.client.get(`INSERT INTO audiences (id, audience_id, name, match, predicates, consent_policy) VALUES ($1,$2,$3,$4,$5,$6) RETURNING *`, [id, audienceId, input.name, input.match ?? "all", JSON.stringify(predicates), input.consent_policy ?? "opt_in"]);
18526
+ const mapped = this.mapAudience(row);
18527
+ return { ...mapped, id: mapped.id, audience_id: mapped.audience_id };
18528
+ }
18529
+ async getAudience(idOrSlug) {
18530
+ const row = await this.client.get(`SELECT * FROM audiences WHERE id = $1 OR audience_id = $1`, [idOrSlug]);
18531
+ if (!row)
18532
+ throw new Error(`audience not found: ${idOrSlug}`);
18533
+ return this.mapAudience(row);
18534
+ }
18535
+ async listAudiences() {
18536
+ return (await this.client.many(`SELECT * FROM audiences ORDER BY audience_id ASC`)).map((r) => this.mapAudience(r));
18537
+ }
18538
+ async updateAudience(idOrSlug, input) {
18539
+ const audience = await this.getAudience(idOrSlug);
18540
+ const sets = [];
18541
+ const params = [audience.id];
18542
+ if ("name" in input) {
18543
+ params.push(input.name);
18544
+ sets.push(`name = $${params.length}`);
18545
+ }
18546
+ if ("match" in input) {
18547
+ params.push(input.match);
18548
+ sets.push(`match = $${params.length}`);
18549
+ }
18550
+ if ("predicates" in input) {
18551
+ params.push(JSON.stringify(input.predicates));
18552
+ sets.push(`predicates = $${params.length}`);
18553
+ }
18554
+ if ("consent_policy" in input) {
18555
+ params.push(input.consent_policy);
18556
+ sets.push(`consent_policy = $${params.length}`);
18557
+ }
18558
+ if (sets.length) {
18559
+ sets.push(`updated_at = NOW()`);
18560
+ await this.client.execute(`UPDATE audiences SET ${sets.join(", ")} WHERE id = $1`, params);
18561
+ }
18562
+ return this.getAudience(String(audience.id));
18563
+ }
18564
+ async deleteAudience(idOrSlug) {
18565
+ const audience = await this.getAudience(idOrSlug);
18566
+ await this.client.execute(`DELETE FROM audiences WHERE id = $1`, [String(audience.id)]);
18567
+ }
18568
+ async setContactConsent(contactId, channel, status, source) {
18569
+ const contact = await this.client.get(`SELECT id FROM contacts WHERE id = $1`, [contactId]);
18570
+ if (!contact)
18571
+ throw new Error(`Contact not found: ${contactId}`);
18572
+ const row = await this.client.get(`INSERT INTO contact_consent (contact_id, channel, status, source, updated_at) VALUES ($1,$2,$3,$4,NOW())
18573
+ ON CONFLICT (contact_id, channel) DO UPDATE SET status = excluded.status, source = excluded.source, updated_at = excluded.updated_at RETURNING *`, [contactId, channel, status, source ?? null]);
18574
+ return { ...row, updated_at: isoOrNull(row?.updated_at) };
18575
+ }
18576
+ async listContactConsent(contactId) {
18577
+ return (await this.client.many(`SELECT * FROM contact_consent WHERE contact_id = $1 ORDER BY channel ASC`, [contactId])).map((r) => ({ ...r, updated_at: isoOrNull(r.updated_at) }));
18578
+ }
18579
+ async suppressAddress(input) {
18580
+ const id = newUuid();
18581
+ const row = await this.client.get(`INSERT INTO contact_suppressions (id, contact_id, channel, address, reason) VALUES ($1,$2,$3,$4,$5)
18582
+ ON CONFLICT (channel, address) DO UPDATE SET reason = excluded.reason, contact_id = COALESCE(excluded.contact_id, contact_suppressions.contact_id), synced_at = NULL RETURNING *`, [id, input.contact_id ?? null, input.channel, input.address, input.reason ?? null]);
18583
+ if (input.contact_id) {
18584
+ const c = await this.client.get(`SELECT id FROM contacts WHERE id = $1`, [input.contact_id]);
18585
+ if (c)
18586
+ await this.setContactConsent(input.contact_id, input.channel, "opt_out", input.reason ?? "suppressed");
18587
+ }
18588
+ return { ...row, created_at: isoOrNull(row?.created_at), synced_at: isoOrNull(row?.synced_at) };
18589
+ }
18590
+ async unsuppressAddress(channel, address) {
18591
+ await this.client.execute(`DELETE FROM contact_suppressions WHERE channel = $1 AND address = $2`, [channel, address]);
18592
+ }
18593
+ async listSuppressions(opts = {}) {
18594
+ const where = [];
18595
+ const params = [];
18596
+ if (opts.channel) {
18597
+ params.push(opts.channel);
18598
+ where.push(`channel = $${params.length}`);
18599
+ }
18600
+ if (opts.unsyncedOnly)
18601
+ where.push(`synced_at IS NULL`);
18602
+ const sql = `SELECT * FROM contact_suppressions ${where.length ? `WHERE ${where.join(" AND ")}` : ""} ORDER BY created_at ASC`;
18603
+ return (await this.client.many(sql, params)).map((r) => ({ ...r, created_at: isoOrNull(r.created_at), synced_at: isoOrNull(r.synced_at) }));
18604
+ }
18605
+ async resolveAudience(idOrSlug, channel) {
18606
+ const audience = await this.getAudience(idOrSlug);
18607
+ const candidates = await this.client.many(`SELECT * FROM contacts WHERE archived = false`);
18608
+ const predicates = audience.predicates;
18609
+ const norm = (v) => v === null || v === undefined ? null : typeof v === "boolean" ? v ? "true" : "false" : String(v);
18610
+ const compare2 = (actual, p) => {
18611
+ const op = p.op ?? "eq";
18612
+ const a = norm(actual);
18613
+ switch (op) {
18614
+ case "exists":
18615
+ return a !== null && a !== "";
18616
+ case "not_exists":
18617
+ return a === null || a === "";
18618
+ case "eq":
18619
+ return a !== null && a === norm(p.value);
18620
+ case "neq":
18621
+ return a === null || a !== norm(p.value);
18622
+ case "in":
18623
+ return a !== null && (p.values ?? []).some((v) => norm(v) === a);
18624
+ case "not_in":
18625
+ return a === null || !(p.values ?? []).some((v) => norm(v) === a);
18626
+ default:
18627
+ return false;
18628
+ }
18629
+ };
18630
+ const membership = (names, p) => {
18631
+ const op = p.op ?? "eq";
18632
+ const set = new Set(names.map((n) => n.toLowerCase()));
18633
+ const has = (v) => {
18634
+ const n = norm(v);
18635
+ return n !== null && set.has(n.toLowerCase());
18636
+ };
18637
+ switch (op) {
18638
+ case "exists":
18639
+ return set.size > 0;
18640
+ case "not_exists":
18641
+ return set.size === 0;
18642
+ case "eq":
18643
+ return has(p.value);
18644
+ case "neq":
18645
+ return !has(p.value);
18646
+ case "in":
18647
+ return (p.values ?? []).some(has);
18648
+ case "not_in":
18649
+ return !(p.values ?? []).some(has);
18650
+ default:
18651
+ return false;
18652
+ }
18653
+ };
18654
+ const matched = [];
18655
+ for (const row of candidates) {
18656
+ const results = [];
18657
+ for (const p of predicates) {
18658
+ if (p.kind === "tag") {
18659
+ const names = (await this.client.many(`SELECT t.name FROM tags t JOIN contact_tags ct ON ct.tag_id = t.id WHERE ct.contact_id = $1`, [row.id])).map((r) => r.name);
18660
+ results.push(membership(names, p));
18661
+ } else if (p.kind === "group") {
18662
+ const rows = await this.client.many(`SELECT g.name, g.id FROM groups g JOIN contact_groups cg ON cg.group_id = g.id WHERE cg.contact_id = $1`, [row.id]);
18663
+ results.push(membership(rows.flatMap((r) => [r.name, r.id]), p));
18664
+ } else if (p.kind === "attribute") {
18665
+ const key = p.key ?? "";
18666
+ const val = key in row && key !== "custom_fields" ? row[key] : pj(row.custom_fields, {})[key];
18667
+ results.push(compare2(val, p));
18668
+ } else
18669
+ results.push(false);
18670
+ }
18671
+ if (audience.match === "any" ? results.some(Boolean) : results.every(Boolean))
18672
+ matched.push(row);
18673
+ }
18674
+ const suppressed = new Set((await this.client.many(`SELECT address FROM contact_suppressions WHERE channel = $1`, [channel])).map((r) => r.address.toLowerCase()));
18675
+ const recipients = [];
18676
+ const excluded = [];
18677
+ const consentAllows2 = (policy, status) => policy === "opt_in" ? status === "opt_in" : policy === "none" ? true : status !== "opt_out";
18678
+ for (const row of matched) {
18679
+ const cid = row.id;
18680
+ if (row.do_not_contact) {
18681
+ excluded.push({ contact_id: cid, reason: "do_not_contact" });
18682
+ continue;
18683
+ }
18684
+ let address = null;
18685
+ if (channel === "email") {
18686
+ const e = await this.client.get(`SELECT address FROM emails WHERE contact_id = $1 ORDER BY is_primary DESC, created_at ASC LIMIT 1`, [cid]);
18687
+ address = e?.address ?? null;
18688
+ } else if (channel === "sms") {
18689
+ const p = await this.client.get(`SELECT number FROM phones WHERE contact_id = $1 ORDER BY is_primary DESC, created_at ASC LIMIT 1`, [cid]);
18690
+ address = p?.number ?? null;
18691
+ } else {
18692
+ const s = await this.client.get(`SELECT handle, url FROM social_profiles WHERE contact_id = $1 AND platform = 'telegram' ORDER BY is_primary DESC, created_at ASC LIMIT 1`, [cid]);
18693
+ address = s?.handle ?? s?.url ?? null;
18694
+ }
18695
+ if (!address) {
18696
+ excluded.push({ contact_id: cid, reason: "no_address" });
18697
+ continue;
18698
+ }
18699
+ if (suppressed.has(address.toLowerCase())) {
18700
+ excluded.push({ contact_id: cid, reason: "suppressed" });
18701
+ continue;
18702
+ }
18703
+ const consent = await this.client.get(`SELECT status FROM contact_consent WHERE contact_id = $1 AND channel = $2`, [cid, channel]);
18704
+ const status = consent?.status ?? "unknown";
18705
+ if (!consentAllows2(String(audience.consent_policy), status)) {
18706
+ excluded.push({ contact_id: cid, reason: "consent" });
18707
+ continue;
18708
+ }
18709
+ recipients.push({ contact_id: cid, display_name: row.display_name, address, consent_status: status });
18710
+ }
18711
+ return { audience_id: audience.audience_id, channel, consent_policy: audience.consent_policy, matched: matched.length, recipients, excluded };
18712
+ }
18713
+ async getUpcomingItems(days = 7) {
18714
+ const now4 = new Date;
18715
+ const future = new Date(now4.getTime() + days * 86400000);
18716
+ const todayStr = now4.toISOString().slice(0, 10);
18717
+ const futureStr = future.toISOString().slice(0, 10);
18718
+ const urgency = (d) => d < todayStr ? "overdue" : d === todayStr ? "today" : "upcoming";
18719
+ const items = [];
18720
+ for (const r of await this.client.many(`SELECT id, display_name, follow_up_at FROM contacts WHERE follow_up_at IS NOT NULL AND follow_up_at <= $1 AND do_not_contact = false`, [futureStr]))
18721
+ items.push({ date: r.follow_up_at, type: "follow_up", contact_id: r.id, contact_name: r.display_name, title: `Follow up with ${r.display_name}`, urgency: urgency(r.follow_up_at) });
18722
+ for (const t of await this.client.many(`SELECT ct.id, ct.contact_id, ct.title, ct.deadline, c.display_name FROM contact_tasks ct JOIN contacts c ON ct.contact_id = c.id WHERE ct.deadline IS NOT NULL AND ct.deadline <= $1 AND ct.status NOT IN ('completed','cancelled')`, [futureStr]))
18723
+ items.push({ date: t.deadline, type: "task_deadline", contact_id: t.contact_id, contact_name: t.display_name, title: t.title, urgency: urgency(t.deadline) });
18724
+ for (const a of await this.client.many(`SELECT a.follow_up_date, a.program_name, c.display_name AS contact_name FROM applications a LEFT JOIN contacts c ON a.primary_contact_id = c.id WHERE a.follow_up_date IS NOT NULL AND a.follow_up_date <= $1`, [futureStr]))
18725
+ items.push({ date: a.follow_up_date, type: "application_followup", contact_name: a.contact_name ?? undefined, title: `Follow up: ${a.program_name}`, urgency: urgency(a.follow_up_date) });
18726
+ for (const v of await this.client.many(`SELECT vc.follow_up_date, vc.company_id, co.name AS company_name, vc.subject, vc.type FROM vendor_communications vc JOIN companies co ON vc.company_id = co.id WHERE vc.follow_up_date IS NOT NULL AND vc.follow_up_date <= $1 AND vc.follow_up_done = false`, [futureStr]))
18727
+ items.push({ date: v.follow_up_date, type: "vendor_followup", company_id: v.company_id, company_name: v.company_name, title: `Follow up with ${v.company_name}: ${v.subject || v.type}`, urgency: urgency(v.follow_up_date) });
18728
+ for (const c of await this.client.many(`SELECT id, display_name, birthday FROM contacts WHERE birthday IS NOT NULL AND do_not_contact = false`)) {
18729
+ const bday = new Date(c.birthday);
18730
+ const thisYear = new Date(now4.getFullYear(), bday.getMonth(), bday.getDate());
18731
+ const nextBday = thisYear >= now4 ? thisYear : new Date(now4.getFullYear() + 1, bday.getMonth(), bday.getDate());
18732
+ const nextStr = nextBday.toISOString().slice(0, 10);
18733
+ if (nextStr <= futureStr)
18734
+ items.push({ date: nextStr, type: "birthday", contact_id: c.id, contact_name: c.display_name, title: `Birthday: ${c.display_name}`, urgency: nextStr === todayStr ? "today" : "upcoming" });
18735
+ }
18736
+ return items.sort((a, b) => String(a.date).localeCompare(String(b.date)));
18737
+ }
18738
+ async listContactAudit() {
18739
+ const rows = await this.client.many(`SELECT * FROM contacts LIMIT 500`);
18740
+ const results = await Promise.all(rows.map(async (row) => {
18741
+ const details = await this.loadDetails(mapContact(row));
18742
+ const c = details;
18743
+ const missing = [];
18744
+ const suggestions = [];
18745
+ let score = 0;
18746
+ if (c.emails?.length)
18747
+ score += 20;
18748
+ else {
18749
+ missing.push("email");
18750
+ suggestions.push("Add an email address");
18751
+ }
18752
+ if (c.phones?.length)
18753
+ score += 15;
18754
+ else {
18755
+ missing.push("phone");
18756
+ suggestions.push("Add a phone number");
18757
+ }
18758
+ if (c.company_id)
18759
+ score += 15;
18760
+ else {
18761
+ missing.push("company");
18762
+ suggestions.push("Link to a company");
18763
+ }
18764
+ if (c.last_contacted_at)
18765
+ score += 20;
18766
+ else {
18767
+ missing.push("last_contacted_at");
18768
+ suggestions.push("Log a contact interaction");
18769
+ }
18770
+ if (c.tags?.length)
18771
+ score += 10;
18772
+ else {
18773
+ missing.push("tags");
18774
+ suggestions.push("Add at least one tag");
18775
+ }
18776
+ if (c.notes)
18777
+ score += 10;
18778
+ else {
18779
+ missing.push("notes");
18780
+ suggestions.push("Add notes");
18781
+ }
18782
+ if (c.job_title)
18783
+ score += 10;
18784
+ else {
18785
+ missing.push("job_title");
18786
+ suggestions.push("Add a job title");
18787
+ }
18788
+ return { contact_id: c.id, display_name: c.display_name, score, missing, suggestions };
18789
+ }));
18790
+ return results.sort((a, b) => a.score - b.score);
18791
+ }
18792
+ async getContactTimeline(contactId, limit = 50) {
18793
+ const items = [];
18794
+ for (const n of await this.client.many(`SELECT created_at, body FROM contact_notes WHERE contact_id = $1 ORDER BY created_at DESC LIMIT 50`, [contactId]))
18795
+ items.push({ date: iso(n.created_at), type: "note", title: "Note", body: n.body });
18796
+ for (const e of await this.client.many(`SELECT event_date, type, title, notes, outcome, duration_min FROM events WHERE contact_ids LIKE $1 ORDER BY event_date DESC LIMIT 50`, [`%${contactId}%`]))
18797
+ items.push({ date: e.event_date, type: "event", title: `${e.type}: ${e.title}`, body: e.notes ?? undefined, metadata: { outcome: e.outcome, duration_min: e.duration_min } });
18798
+ for (const t of await this.client.many(`SELECT title, created_at, updated_at, status, deadline, priority FROM contact_tasks WHERE contact_id = $1 ORDER BY created_at DESC LIMIT 30`, [contactId])) {
18799
+ items.push({ date: iso(t.created_at), type: "task_created", title: `Task created: ${t.title}`, metadata: { deadline: t.deadline, priority: t.priority } });
18800
+ if (t.status === "completed")
18801
+ items.push({ date: iso(t.updated_at), type: "task_completed", title: `Task completed: ${t.title}` });
18802
+ }
18803
+ for (const c of await this.client.many(`SELECT vc.comm_date, vc.type, co.name AS company_name, vc.subject FROM vendor_communications vc JOIN companies co ON vc.company_id = co.id WHERE vc.contact_id = $1 ORDER BY vc.comm_date DESC LIMIT 20`, [contactId]))
18804
+ items.push({ date: c.comm_date, type: "vendor_comm", title: `${c.type} \u2014 ${c.company_name}`, body: c.subject ?? undefined });
18805
+ for (const a of await this.client.many(`SELECT created_at, action, details FROM activity_log WHERE contact_id = $1 ORDER BY created_at DESC LIMIT 30`, [contactId]))
18806
+ items.push({ date: iso(a.created_at), type: "interaction", title: a.action, body: a.details ?? undefined });
18807
+ return items.sort((a, b) => b.date.localeCompare(a.date)).slice(0, limit);
18808
+ }
18809
+ async getNetworkStats() {
18810
+ const today = new Date().toISOString().slice(0, 10);
18811
+ const d30 = new Date(Date.now() - 30 * 86400000).toISOString().slice(0, 10);
18812
+ const d60 = new Date(Date.now() - 60 * 86400000).toISOString().slice(0, 10);
18813
+ const d7 = new Date(Date.now() + 7 * 86400000).toISOString().slice(0, 10);
18814
+ const n = async (sql, params = []) => Number((await this.client.get(sql, params))?.c ?? 0);
18815
+ return {
18816
+ total_contacts: await n(`SELECT COUNT(*) c FROM contacts WHERE archived = false`),
18817
+ total_companies: await n(`SELECT COUNT(*) c FROM companies WHERE archived = false`),
18818
+ owned_entities: await n(`SELECT COUNT(*) c FROM companies WHERE is_owned_entity = true`),
18819
+ total_tags: await n(`SELECT COUNT(*) c FROM tags`),
18820
+ total_groups: await n(`SELECT COUNT(*) c FROM groups`),
18821
+ total_deals: await n(`SELECT COUNT(*) c FROM deals WHERE stage NOT IN ('won','lost','cancelled')`),
18822
+ total_events: await n(`SELECT COUNT(*) c FROM events`),
18823
+ cold_30d: await n(`SELECT COUNT(*) c FROM contacts WHERE archived = false AND do_not_contact = false AND (last_contacted_at IS NULL OR last_contacted_at < $1)`, [d30]),
18824
+ cold_60d: await n(`SELECT COUNT(*) c FROM contacts WHERE archived = false AND do_not_contact = false AND (last_contacted_at IS NULL OR last_contacted_at < $1)`, [d60]),
18825
+ cold_never: await n(`SELECT COUNT(*) c FROM contacts WHERE archived = false AND do_not_contact = false AND last_contacted_at IS NULL`),
18826
+ contacts_with_email: await n(`SELECT COUNT(DISTINCT contact_id) c FROM emails WHERE contact_id IS NOT NULL`),
18827
+ contacts_with_phone: await n(`SELECT COUNT(DISTINCT contact_id) c FROM phones WHERE contact_id IS NOT NULL`),
18828
+ contacts_no_company: await n(`SELECT COUNT(*) c FROM contacts WHERE archived = false AND company_id IS NULL`),
18829
+ overdue_tasks: await n(`SELECT COUNT(*) c FROM contact_tasks WHERE deadline < $1 AND status NOT IN ('completed','cancelled')`, [today]),
18830
+ pending_applications: await n(`SELECT COUNT(*) c FROM applications WHERE status IN ('submitted','pending','follow_up_needed')`),
18831
+ missing_invoices: await n(`SELECT COUNT(*) c FROM vendor_communications WHERE type = 'invoice_request' AND status IN ('awaiting_response','no_response')`),
18832
+ upcoming_7d: await n(`SELECT COUNT(*) c FROM contacts WHERE follow_up_at BETWEEN $1 AND $2`, [today, d7]),
18833
+ notes_count: await n(`SELECT COUNT(*) c FROM contact_notes`),
18834
+ active_deals_value: await n(`SELECT COALESCE(SUM(value_usd),0) c FROM deals WHERE stage NOT IN ('won','lost','cancelled') AND currency = 'USD'`)
18835
+ };
18836
+ }
18837
+ async getContactCard(contactId) {
18838
+ const contact = await this.getContact(contactId);
18839
+ if (!contact)
18840
+ throw new Error(`Contact not found: ${contactId}`);
18841
+ const details = await this.loadDetails(contact);
18842
+ return {
18843
+ id: details.id,
18844
+ display_name: details.display_name,
18845
+ job_title: details.job_title,
18846
+ company: details.company?.name,
18847
+ primary_email: details.emails?.find((e) => e.is_primary)?.address || details.emails?.[0]?.address,
18848
+ primary_phone: details.phones?.find((p) => p.is_primary)?.number || details.phones?.[0]?.number
18849
+ };
18850
+ }
18851
+ async getContactBrief(contactId, taskContext) {
18852
+ const contact = await this.getContact(contactId);
18853
+ if (!contact)
18854
+ throw new Error(`Contact not found: ${contactId}`);
18855
+ const details = await this.loadDetails(contact);
18856
+ const notes = (await this.listNotes(contactId)).slice(0, 3);
18857
+ const learnings = (await this.getLearnings(contactId, { min_importance: 7 })).slice(0, 5);
18858
+ const ctx = (taskContext ?? "").toLowerCase();
18859
+ const last = contact.last_contacted_at;
18860
+ const daysSince = last ? Math.floor((Date.now() - new Date(last).getTime()) / 86400000) : null;
18861
+ const brief = {
18862
+ id: contact.id,
18863
+ display_name: contact.display_name,
18864
+ job_title: contact.job_title,
18865
+ company: details.company?.name,
18866
+ status: contact.status,
18867
+ last_contacted: daysSince !== null ? `${daysSince}d ago` : "never",
18868
+ relationship_health: contact.relationship_health,
18869
+ engagement_status: contact.engagement_status,
18870
+ preferred_contact: contact.preferred_contact_method
18871
+ };
18872
+ if (ctx.includes("meeting") || ctx.includes("call") || ctx.includes("prep")) {
18873
+ brief.recent_notes = notes.map((nt) => ({ date: String(nt.created_at ?? "").slice(0, 10), content: nt.body }));
18874
+ brief.key_learnings = learnings.map((l) => l.content);
18875
+ }
18876
+ if (ctx.includes("outreach") || ctx.includes("email")) {
18877
+ brief.preferred_channel = contact.preferred_channel;
18878
+ brief.follow_up_at = contact.follow_up_at;
18879
+ }
18880
+ if (ctx.includes("deal"))
18881
+ brief.company_details = details.company ? { name: details.company.name, domain: details.company.domain } : null;
18882
+ if (learnings.length)
18883
+ brief.top_learnings = learnings.map((l) => l.content);
18884
+ return brief;
18885
+ }
18886
+ async assembleContext(contactIds, format) {
18887
+ const briefs = await Promise.all(contactIds.map(async (id) => {
18888
+ try {
18889
+ return await this.getContactBrief(id, format);
18890
+ } catch {
18891
+ return { id, error: "not found" };
18892
+ }
18893
+ }));
18894
+ return { format, contact_count: contactIds.length, assembled_at: new Date().toISOString(), contacts: briefs };
18895
+ }
18896
+ async generateBrief(contactId) {
18897
+ const contact = await this.getContact(contactId);
18898
+ if (!contact)
18899
+ throw new Error(`Contact not found: ${contactId}`);
18900
+ const details = await this.loadDetails(contact);
18901
+ const notes = await this.listNotes(contactId);
18902
+ const allTasks = await this.listContactTasks({ contact_id: contactId });
18903
+ const tasks = allTasks.filter((t) => !["completed", "cancelled"].includes(String(t.status)));
18904
+ const nowIsoStr = new Date().toISOString();
18905
+ const overdueTasks = allTasks.filter((t) => t.deadline && String(t.deadline) < nowIsoStr && !["completed", "cancelled"].includes(String(t.status)));
18906
+ const companyRels = await this.listCompanyRelationships({ contact_id: contactId });
18907
+ const recentTimeline = await this.getContactTimeline(contactId, 5);
18908
+ const last = contact.last_contacted_at;
18909
+ const daysSince = last ? Math.floor((Date.now() - new Date(last).getTime()) / 86400000) : null;
18910
+ const lines = [];
18911
+ lines.push(`# ${contact.display_name}`);
18912
+ if (contact.job_title)
18913
+ lines.push(`**Role:** ${contact.job_title}${contact.company_id ? ` (linked to company)` : ""}`);
18914
+ const emails = details.emails ?? [];
18915
+ const phones = details.phones ?? [];
18916
+ const pe = emails.find((e) => e.is_primary) || emails[0];
18917
+ if (pe)
18918
+ lines.push(`**Email:** ${pe.address}`);
18919
+ const pp = phones.find((p) => p.is_primary) || phones[0];
18920
+ if (pp)
18921
+ lines.push(`**Phone:** ${pp.number}`);
18922
+ if (contact.preferred_contact_method)
18923
+ lines.push(`**Preferred contact:** ${contact.preferred_contact_method}`);
18924
+ lines.push("");
18925
+ lines.push(`## Status`);
18926
+ lines.push(`- Last contacted: ${daysSince !== null ? `${daysSince} days ago` : "never"}`);
18927
+ lines.push(`- Status: ${contact.status || "active"}`);
18928
+ if (contact.follow_up_at)
18929
+ lines.push(`- Follow-up scheduled: ${contact.follow_up_at}`);
18930
+ if (overdueTasks.length)
18931
+ lines.push(`- OVERDUE TASKS: ${overdueTasks.length}`);
18932
+ if (companyRels.length) {
18933
+ lines.push("");
18934
+ lines.push(`## Entity Relationships`);
18935
+ for (const r of companyRels)
18936
+ lines.push(`- ${r.relationship_type} \u2014 ${r.notes || ""}`);
18937
+ }
18938
+ if (tasks.length) {
18939
+ lines.push("");
18940
+ lines.push(`## Open Tasks`);
18941
+ for (const t of tasks)
18942
+ lines.push(`- [${t.priority}] ${t.title}${t.deadline ? ` (due ${t.deadline})` : ""}`);
18943
+ }
18944
+ if (notes.length) {
18945
+ lines.push("");
18946
+ lines.push(`## Recent Notes`);
18947
+ for (const nt of notes.slice(0, 3))
18948
+ lines.push(`**${String(nt.created_at ?? "").slice(0, 10)}:** ${nt.body}`);
18949
+ }
18950
+ if (recentTimeline.length) {
18951
+ lines.push("");
18952
+ lines.push(`## Recent Activity`);
18953
+ for (const item of recentTimeline)
18954
+ lines.push(`- ${item.date.slice(0, 10)} ${item.title}`);
18955
+ }
18956
+ if (contact.notes) {
18957
+ lines.push("");
18958
+ lines.push(`## Background Notes`);
18959
+ lines.push(String(contact.notes));
18960
+ }
18961
+ return lines.join(`
18962
+ `);
18963
+ }
18964
+ async vaultStatus() {
18965
+ let document_count = 0;
18966
+ try {
18967
+ document_count = Number((await this.client.get(`SELECT COUNT(*) n FROM contact_documents`))?.n ?? 0);
18968
+ } catch {}
18969
+ return { initialized: false, unlocked: false, document_count };
18970
+ }
18971
+ }
18972
+ function getContactsPgStore(client) {
18973
+ if (!cachedStore2)
18974
+ cachedStore2 = new ContactsPgStore(client);
18975
+ return cachedStore2;
18976
+ }
18977
+ var cachedStore2 = null;
18978
+
18979
+ // src/server/v1.ts
18980
+ function json5(body, status = 200) {
18981
+ return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS });
18982
+ }
18983
+ function error(status, message, extra) {
18984
+ return json5({ error: message, ...extra ?? {} }, status);
18985
+ }
18986
+ async function readJson(req) {
18987
+ try {
18988
+ const text = await req.text();
18989
+ if (!text)
18990
+ return {};
18991
+ return JSON.parse(text);
18992
+ } catch {
18993
+ return null;
18994
+ }
18995
+ }
18996
+ async function handleV1Request(req, url) {
18997
+ const path = url.pathname;
18998
+ if (path !== "/v1" && !path.startsWith("/v1/"))
18999
+ return null;
19000
+ const method = req.method.toUpperCase();
19001
+ const isWrite = method !== "GET" && method !== "HEAD";
19002
+ const requiredScopes = [isWrite ? `${CONTACTS_APP_SLUG}:write` : `${CONTACTS_APP_SLUG}:read`];
19003
+ let verifier;
19004
+ try {
19005
+ verifier = getCloudVerifier();
19006
+ } catch (e) {
19007
+ return error(503, e.message);
19008
+ }
19009
+ const decision = await verifier.authenticate(req.headers, { method, path, requiredScopes });
19010
+ if (!decision.ok) {
19011
+ return error(decision.status, decision.message, { reason: decision.reason });
19012
+ }
19013
+ await ensureCloudSchemaBestEffort();
19014
+ const store = getContactsPgStore(getCloudClient());
19015
+ const segments = path.split("/").filter(Boolean);
19016
+ const resource = segments[1];
19017
+ const id = segments[2];
19018
+ const sub = segments[3];
19019
+ const qp = (name) => url.searchParams.get(name) ?? undefined;
19020
+ const qn = (name) => {
19021
+ const v = url.searchParams.get(name);
19022
+ return v === null ? undefined : Number(v);
19023
+ };
19024
+ try {
19025
+ if (resource === "contacts" && id && sub) {
19026
+ if (method === "GET" && sub === "timeline")
19027
+ return json5({ timeline: await store.getContactTimeline(id, qn("limit") ?? 50) });
19028
+ if (method === "GET" && sub === "brief")
19029
+ return json5({ brief: await store.getContactBrief(id, qp("context")) });
19030
+ if (method === "GET" && sub === "brief-text")
19031
+ return json5({ text: await store.generateBrief(id) });
19032
+ if (method === "GET" && sub === "card")
19033
+ return json5({ card: await store.getContactCard(id) });
19034
+ if (method === "GET" && sub === "freshness")
19035
+ return json5({ freshness: await store.getFreshnessScore(id) });
19036
+ if (method === "GET" && sub === "signals")
19037
+ return json5({ signals: await store.getRelationshipSignals(id) });
19038
+ if (method === "GET" && sub === "notes")
19039
+ return json5({ notes: qp("company_id") ? await store.listNotesForContactAtCompany(id, qp("company_id")) : await store.listNotes(id) });
19040
+ if (method === "GET" && sub === "consent")
19041
+ return json5({ consent: await store.listContactConsent(id) });
19042
+ if (method === "GET" && sub === "identities")
19043
+ return json5({ identities: await store.getContactIdentities(id) });
19044
+ if (method === "GET" && sub === "groups")
19045
+ return json5({ groups: await store.listGroupsForContact(id) });
19046
+ if (method === "GET" && sub === "org-memberships")
19047
+ return json5({ org_members: await store.listOrgMembersForContact(id) });
19048
+ if (method === "GET" && sub === "relationships")
19049
+ return json5({ relationships: await store.listRelationships({ contact_id: id }) });
19050
+ if (method === "GET" && sub === "company-relationships")
19051
+ return json5({ relationships: await store.listCompanyRelationships({ contact_id: id }) });
19052
+ if (method === "GET" && sub === "field-history")
19053
+ return json5({ history: await store.getFieldHistory(id, qp("field_name")) });
19054
+ if (method === "GET" && sub === "field-at")
19055
+ return json5({ fields: await store.getContactAt(id, qp("timestamp") ?? new Date().toISOString()) });
19056
+ if (sub === "job-history") {
19057
+ if (method === "GET")
19058
+ return json5({ job_history: await store.getJobHistory(id) });
19059
+ if (method === "POST") {
19060
+ const body = await readJson(req);
19061
+ return json5({ job: await store.addJobEntry(id, body ?? {}) }, 201);
19062
+ }
19063
+ }
19064
+ if (sub === "learnings") {
19065
+ if (method === "GET")
19066
+ return json5({ learnings: await store.getLearnings(id, { type: qp("type"), min_importance: qn("min_importance"), visibility: qp("visibility") }) });
19067
+ if (method === "POST") {
19068
+ const body = await readJson(req);
19069
+ return json5({ learning: await store.saveLearning(id, body ?? {}) }, 201);
19070
+ }
19071
+ }
19072
+ if (sub === "consent" && method === "POST") {
19073
+ const body = await readJson(req);
19074
+ if (!body)
19075
+ return error(400, "invalid JSON body");
19076
+ return json5({ consent: await store.setContactConsent(id, body.channel, body.status, body.source) });
19077
+ }
19078
+ if (sub === "field-verify" && method === "POST") {
19079
+ const body = await readJson(req);
19080
+ if (!body)
19081
+ return error(400, "invalid JSON body");
19082
+ await store.markFieldVerified(id, body.field_name, body.source);
19083
+ return json5({ ok: true });
19084
+ }
19085
+ return error(404, `unknown /v1/contacts/:id/${sub}`);
19086
+ }
19087
+ if (resource === "contacts") {
19088
+ if (!id) {
19089
+ if (method === "GET") {
19090
+ const result = await store.listContacts({
19091
+ ...url.searchParams.get("company_id") ? { company_id: url.searchParams.get("company_id") } : {},
19092
+ ...url.searchParams.get("status") ? { status: url.searchParams.get("status") } : {},
19093
+ ...url.searchParams.get("q") ? { q: url.searchParams.get("q") } : {},
19094
+ ...url.searchParams.get("limit") ? { limit: Number(url.searchParams.get("limit")) } : {},
19095
+ ...url.searchParams.get("offset") ? { offset: Number(url.searchParams.get("offset")) } : {}
19096
+ });
19097
+ return json5(result);
19098
+ }
19099
+ if (method === "POST") {
19100
+ const body = await readJson(req);
19101
+ if (!body)
19102
+ return error(400, "invalid JSON body");
19103
+ const contact = await store.createContact(body);
19104
+ return json5({ contact }, 201);
19105
+ }
19106
+ return error(405, `method ${method} not allowed on /v1/contacts`);
19107
+ }
19108
+ if (method === "GET") {
19109
+ const contact = await store.getContact(id);
19110
+ return contact ? json5({ contact }) : error(404, "contact not found");
19111
+ }
19112
+ if (method === "PATCH" || method === "PUT") {
19113
+ const body = await readJson(req);
19114
+ if (!body)
19115
+ return error(400, "invalid JSON body");
19116
+ const contact = await store.updateContact(id, body);
19117
+ return contact ? json5({ contact }) : error(404, "contact not found");
19118
+ }
19119
+ if (method === "DELETE") {
19120
+ const deleted = await store.deleteContact(id);
19121
+ return deleted ? json5({ deleted: true, id }) : error(404, "contact not found");
19122
+ }
19123
+ return error(405, `method ${method} not allowed on /v1/contacts/:id`);
19124
+ }
19125
+ if (resource === "companies") {
19126
+ if (!id) {
19127
+ if (method === "GET") {
19128
+ const result = await store.listCompanies({
19129
+ ...url.searchParams.get("industry") ? { industry: url.searchParams.get("industry") } : {},
19130
+ ...url.searchParams.get("limit") ? { limit: Number(url.searchParams.get("limit")) } : {},
19131
+ ...url.searchParams.get("offset") ? { offset: Number(url.searchParams.get("offset")) } : {}
19132
+ });
19133
+ return json5(result);
19134
+ }
19135
+ if (method === "POST") {
19136
+ const body = await readJson(req);
19137
+ if (!body || typeof body.name !== "string" || !body.name.trim()) {
19138
+ return error(400, "name is required");
19139
+ }
19140
+ const company = await store.createCompany(body);
19141
+ return json5({ company }, 201);
19142
+ }
19143
+ return error(405, `method ${method} not allowed on /v1/companies`);
19144
+ }
19145
+ if (method === "GET") {
19146
+ const company = await store.getCompany(id);
19147
+ return company ? json5({ company }) : error(404, "company not found");
17714
19148
  }
17715
19149
  if (method === "PATCH" || method === "PUT") {
17716
19150
  const body = await readJson(req);
@@ -17761,6 +19195,521 @@ async function handleV1Request(req, url) {
17761
19195
  if (resource === "stats" && method === "GET") {
17762
19196
  return json5(await store.stats());
17763
19197
  }
19198
+ if (resource === "deals") {
19199
+ if (id && sub === "team" && method === "GET")
19200
+ return json5({ team: await store.getDealTeam(id) });
19201
+ if (id && sub === "roles" && method === "POST") {
19202
+ const b = await readJson(req);
19203
+ if (!b)
19204
+ return error(400, "invalid JSON body");
19205
+ return json5({ role: await store.setDealContactRole(id, b.contact_id, b.account_role) }, 201);
19206
+ }
19207
+ if (!id) {
19208
+ if (method === "GET")
19209
+ return json5({ deals: await store.listDeals({ stage: qp("stage"), contact_id: qp("contact_id"), company_id: qp("company_id") }) });
19210
+ if (method === "POST") {
19211
+ const b = await readJson(req);
19212
+ if (!b)
19213
+ return error(400, "invalid JSON body");
19214
+ return json5({ deal: await store.createDeal(b) }, 201);
19215
+ }
19216
+ return error(405, `method ${method} not allowed on /v1/deals`);
19217
+ }
19218
+ if (method === "GET") {
19219
+ const d = await store.getDeal(id);
19220
+ return d ? json5({ deal: d }) : error(404, "deal not found");
19221
+ }
19222
+ if (method === "PATCH" || method === "PUT") {
19223
+ const b = await readJson(req);
19224
+ if (!b)
19225
+ return error(400, "invalid JSON body");
19226
+ const d = await store.updateDeal(id, b);
19227
+ return d ? json5({ deal: d }) : error(404, "deal not found");
19228
+ }
19229
+ if (method === "DELETE")
19230
+ return await store.deleteDeal(id) ? json5({ deleted: true, id }) : error(404, "deal not found");
19231
+ return error(405, "method not allowed");
19232
+ }
19233
+ if (resource === "events") {
19234
+ if (!id) {
19235
+ if (method === "GET")
19236
+ return json5({ events: await store.listEvents({ contact_id: qp("contact_id"), company_id: qp("company_id"), type: qp("type"), date_from: qp("date_from"), date_to: qp("date_to") }) });
19237
+ if (method === "POST") {
19238
+ const b = await readJson(req);
19239
+ if (!b)
19240
+ return error(400, "invalid JSON body");
19241
+ return json5({ event: await store.logEvent(b) }, 201);
19242
+ }
19243
+ return error(405, "method not allowed");
19244
+ }
19245
+ if (method === "DELETE")
19246
+ return await store.deleteEvent(id) ? json5({ deleted: true, id }) : error(404, "event not found");
19247
+ return error(405, "method not allowed");
19248
+ }
19249
+ if (resource === "tasks") {
19250
+ if (id === "overdue" && method === "GET")
19251
+ return json5({ tasks: await store.listOverdueTasks() });
19252
+ if (id === "escalations" && method === "GET")
19253
+ return json5({ escalations: await store.checkEscalations() });
19254
+ if (!id) {
19255
+ if (method === "GET")
19256
+ return json5({ tasks: await store.listContactTasks({ contact_id: qp("contact_id"), entity_id: qp("entity_id"), status: qp("status"), priority: qp("priority") }) });
19257
+ if (method === "POST") {
19258
+ const b = await readJson(req);
19259
+ if (!b)
19260
+ return error(400, "invalid JSON body");
19261
+ return json5({ task: await store.createContactTask(b) }, 201);
19262
+ }
19263
+ return error(405, "method not allowed");
19264
+ }
19265
+ if (method === "PATCH" || method === "PUT") {
19266
+ const b = await readJson(req);
19267
+ if (!b)
19268
+ return error(400, "invalid JSON body");
19269
+ const t = await store.updateContactTask(id, b);
19270
+ return t ? json5({ task: t }) : error(404, "task not found");
19271
+ }
19272
+ if (method === "DELETE")
19273
+ return await store.deleteContactTask(id) ? json5({ deleted: true, id }) : error(404, "task not found");
19274
+ return error(405, "method not allowed");
19275
+ }
19276
+ if (resource === "applications") {
19277
+ if (id === "follow-up-due" && method === "GET")
19278
+ return json5({ applications: await store.listFollowUpDueApplications() });
19279
+ if (!id) {
19280
+ if (method === "GET")
19281
+ return json5({ applications: await store.listApplications({ type: qp("type"), status: qp("status"), provider_company_id: qp("provider_company_id"), applicant_contact_id: qp("applicant_contact_id") }) });
19282
+ if (method === "POST") {
19283
+ const b = await readJson(req);
19284
+ if (!b)
19285
+ return error(400, "invalid JSON body");
19286
+ return json5({ application: await store.createApplication(b) }, 201);
19287
+ }
19288
+ return error(405, "method not allowed");
19289
+ }
19290
+ if (method === "PATCH" || method === "PUT") {
19291
+ const b = await readJson(req);
19292
+ if (!b)
19293
+ return error(400, "invalid JSON body");
19294
+ const a = await store.updateApplication(id, b);
19295
+ return a ? json5({ application: a }) : error(404, "application not found");
19296
+ }
19297
+ return error(405, "method not allowed");
19298
+ }
19299
+ if (resource === "groups") {
19300
+ if (id === "for-contact" && sub && method === "GET")
19301
+ return json5({ groups: await store.listGroupsForContact(sub) });
19302
+ if (id === "for-company" && sub && method === "GET")
19303
+ return json5({ groups: await store.listGroupsForCompany(sub) });
19304
+ if (id && sub === "contacts") {
19305
+ if (method === "GET")
19306
+ return json5({ contact_ids: await store.listContactsInGroup(id) });
19307
+ if (method === "POST") {
19308
+ const b = await readJson(req);
19309
+ if (!b?.contact_id)
19310
+ return error(400, "contact_id required");
19311
+ return json5(await store.addContactToGroup(b.contact_id, id));
19312
+ }
19313
+ if (method === "DELETE") {
19314
+ const cid = segments[4];
19315
+ if (!cid)
19316
+ return error(400, "contact id required");
19317
+ await store.removeContactFromGroup(cid, id);
19318
+ return json5({ ok: true });
19319
+ }
19320
+ }
19321
+ if (id && sub === "companies") {
19322
+ if (method === "GET")
19323
+ return json5({ company_ids: await store.listCompaniesInGroup(id) });
19324
+ if (method === "POST") {
19325
+ const b = await readJson(req);
19326
+ if (!b?.company_id)
19327
+ return error(400, "company_id required");
19328
+ return json5(await store.addCompanyToGroup(b.company_id, id));
19329
+ }
19330
+ if (method === "DELETE") {
19331
+ const coid = segments[4];
19332
+ if (!coid)
19333
+ return error(400, "company id required");
19334
+ await store.removeCompanyFromGroup(coid, id);
19335
+ return json5({ ok: true });
19336
+ }
19337
+ }
19338
+ if (!id) {
19339
+ if (method === "GET")
19340
+ return json5({ groups: await store.listGroups(qp("project_id")) });
19341
+ if (method === "POST") {
19342
+ const b = await readJson(req);
19343
+ if (!b)
19344
+ return error(400, "invalid JSON body");
19345
+ return json5({ group: await store.createGroup(b) }, 201);
19346
+ }
19347
+ return error(405, "method not allowed");
19348
+ }
19349
+ if (method === "GET") {
19350
+ const g = await store.getGroup(id);
19351
+ return g ? json5({ group: g }) : error(404, "group not found");
19352
+ }
19353
+ if (method === "PATCH" || method === "PUT") {
19354
+ const b = await readJson(req);
19355
+ if (!b)
19356
+ return error(400, "invalid JSON body");
19357
+ const g = await store.updateGroup(id, b);
19358
+ return g ? json5({ group: g }) : error(404, "group not found");
19359
+ }
19360
+ if (method === "DELETE")
19361
+ return await store.deleteGroup(id) ? json5({ deleted: true, id }) : error(404, "group not found");
19362
+ return error(405, "method not allowed");
19363
+ }
19364
+ if (resource === "vendor-comms") {
19365
+ if (id === "missing-invoices" && method === "GET")
19366
+ return json5({ communications: await store.listMissingInvoices() });
19367
+ if (id === "pending-follow-ups" && method === "GET")
19368
+ return json5({ communications: await store.listPendingFollowUps() });
19369
+ if (id && sub === "mark-done" && method === "POST") {
19370
+ const c = await store.markFollowUpDone(id);
19371
+ return c ? json5({ communication: c }) : error(404, "not found");
19372
+ }
19373
+ if (!id) {
19374
+ if (method === "GET") {
19375
+ const companyId = qp("company_id");
19376
+ if (!companyId)
19377
+ return error(400, "company_id required");
19378
+ return json5({ communications: await store.listVendorCommunications(companyId, { type: qp("type"), status: qp("status"), direction: qp("direction") }) });
19379
+ }
19380
+ if (method === "POST") {
19381
+ const b = await readJson(req);
19382
+ if (!b)
19383
+ return error(400, "invalid JSON body");
19384
+ return json5({ communication: await store.logVendorCommunication(b) }, 201);
19385
+ }
19386
+ }
19387
+ return error(405, "method not allowed");
19388
+ }
19389
+ if (resource === "org-members") {
19390
+ if (!id) {
19391
+ if (method === "GET") {
19392
+ if (qp("contact_id"))
19393
+ return json5({ org_members: await store.listOrgMembersForContact(qp("contact_id")) });
19394
+ if (qp("company_id"))
19395
+ return json5({ org_members: await store.listOrgMembers(qp("company_id")) });
19396
+ return error(400, "company_id or contact_id required");
19397
+ }
19398
+ if (method === "POST") {
19399
+ const b = await readJson(req);
19400
+ if (!b)
19401
+ return error(400, "invalid JSON body");
19402
+ return json5({ org_member: await store.addOrgMember(b) }, 201);
19403
+ }
19404
+ return error(405, "method not allowed");
19405
+ }
19406
+ if (method === "PATCH" || method === "PUT") {
19407
+ const b = await readJson(req);
19408
+ if (!b)
19409
+ return error(400, "invalid JSON body");
19410
+ const m = await store.updateOrgMember(id, b);
19411
+ return m ? json5({ org_member: m }) : error(404, "not found");
19412
+ }
19413
+ if (method === "DELETE")
19414
+ return await store.removeOrgMember(id) ? json5({ deleted: true, id }) : error(404, "not found");
19415
+ return error(405, "method not allowed");
19416
+ }
19417
+ if (resource === "notes") {
19418
+ if (!id) {
19419
+ if (method === "GET") {
19420
+ const cid = qp("contact_id");
19421
+ if (!cid)
19422
+ return error(400, "contact_id required");
19423
+ return json5({ notes: qp("company_id") ? await store.listNotesForContactAtCompany(cid, qp("company_id")) : await store.listNotes(cid) });
19424
+ }
19425
+ if (method === "POST") {
19426
+ const b = await readJson(req);
19427
+ if (!b?.contact_id || !b?.body)
19428
+ return error(400, "contact_id and body required");
19429
+ return json5({ note: await store.addNote(b.contact_id, b.body, b.created_by, b.company_id) }, 201);
19430
+ }
19431
+ return error(405, "method not allowed");
19432
+ }
19433
+ if (method === "DELETE") {
19434
+ await store.deleteNote(id);
19435
+ return json5({ ok: true });
19436
+ }
19437
+ return error(405, "method not allowed");
19438
+ }
19439
+ if (resource === "relationships") {
19440
+ if (!id) {
19441
+ if (method === "GET")
19442
+ return json5({ relationships: await store.listRelationships({ contact_id: qp("contact_id") }) });
19443
+ if (method === "POST") {
19444
+ const b = await readJson(req);
19445
+ if (!b)
19446
+ return error(400, "invalid JSON body");
19447
+ return json5({ relationship: await store.createRelationship(b) }, 201);
19448
+ }
19449
+ }
19450
+ if (method === "DELETE" && id) {
19451
+ await store.deleteRelationship(id);
19452
+ return json5({ ok: true });
19453
+ }
19454
+ return error(405, "method not allowed");
19455
+ }
19456
+ if (resource === "company-relationships") {
19457
+ if (!id) {
19458
+ if (method === "GET")
19459
+ return json5({ relationships: await store.listCompanyRelationships({ contact_id: qp("contact_id"), company_id: qp("company_id") }) });
19460
+ if (method === "POST") {
19461
+ const b = await readJson(req);
19462
+ if (!b)
19463
+ return error(400, "invalid JSON body");
19464
+ return json5({ relationship: await store.createCompanyRelationship(b) }, 201);
19465
+ }
19466
+ }
19467
+ if (method === "DELETE" && id) {
19468
+ await store.deleteCompanyRelationship(id);
19469
+ return json5({ ok: true });
19470
+ }
19471
+ return error(405, "method not allowed");
19472
+ }
19473
+ if (resource === "learnings") {
19474
+ if (id === "search" && method === "GET")
19475
+ return json5({ learnings: await store.searchLearnings(qp("q") ?? "", { type: qp("type"), contact_id: qp("contact_id") }) });
19476
+ if (id === "stale" && method === "GET")
19477
+ return json5({ learnings: await store.getStaleLearnings(qn("days_old") ?? 30, qn("min_confidence") ?? 0) });
19478
+ if (id === "maintenance" && method === "POST")
19479
+ return json5(await store.runLearningMaintenance());
19480
+ if (id && sub === "confirm" && method === "POST") {
19481
+ await store.confirmLearning(id);
19482
+ return json5({ ok: true });
19483
+ }
19484
+ return error(404, "unknown /v1/learnings route");
19485
+ }
19486
+ if (resource === "locks") {
19487
+ if (id && method === "GET") {
19488
+ const l = await store.checkContactLock(id);
19489
+ return json5({ lock: l });
19490
+ }
19491
+ if (!id && method === "POST") {
19492
+ const b = await readJson(req);
19493
+ if (!b)
19494
+ return error(400, "invalid JSON body");
19495
+ return json5(await store.acquireContactLock(b.contact_id, b.agent_name, b.ttl_seconds, b.reason, b.session_id));
19496
+ }
19497
+ if (id && method === "DELETE") {
19498
+ const released = await store.releaseContactLock(id, qp("agent_name") ?? "");
19499
+ return json5({ released });
19500
+ }
19501
+ return error(405, "method not allowed");
19502
+ }
19503
+ if (resource === "activity") {
19504
+ if (method === "GET") {
19505
+ const cid = qp("contact_id");
19506
+ if (!cid)
19507
+ return error(400, "contact_id required");
19508
+ return json5({ activity: await store.getAgentActivity(cid, qn("limit") ?? 20) });
19509
+ }
19510
+ if (method === "POST") {
19511
+ const b = await readJson(req);
19512
+ if (!b)
19513
+ return error(400, "invalid JSON body");
19514
+ await store.logAgentActivity(b.contact_id, b.agent_name, b.action, b.details, b.session_id);
19515
+ return json5({ ok: true }, 201);
19516
+ }
19517
+ return error(405, "method not allowed");
19518
+ }
19519
+ if (resource === "identity") {
19520
+ if (id === "resolve" && method === "POST") {
19521
+ const b = await readJson(req);
19522
+ if (!b)
19523
+ return error(400, "invalid JSON body");
19524
+ return json5({ matches: await store.resolveContactIdentity(b) });
19525
+ }
19526
+ if (!id) {
19527
+ if (method === "GET") {
19528
+ const cid = qp("contact_id");
19529
+ if (!cid)
19530
+ return error(400, "contact_id required");
19531
+ return json5({ identities: await store.getContactIdentities(cid) });
19532
+ }
19533
+ if (method === "POST") {
19534
+ const b = await readJson(req);
19535
+ if (!b)
19536
+ return error(400, "invalid JSON body");
19537
+ return json5({ identity: await store.addContactIdentity(b.contact_id, b.system, b.external_id, b.external_url, b.confidence) }, 201);
19538
+ }
19539
+ }
19540
+ return error(405, "method not allowed");
19541
+ }
19542
+ if (resource === "signals") {
19543
+ if (id === "ghost" && method === "GET")
19544
+ return json5({ signals: await store.getGhostContacts() });
19545
+ if (id === "warming" && method === "GET")
19546
+ return json5({ signals: await store.getWarmingContacts() });
19547
+ if (id === "recompute" && method === "POST")
19548
+ return json5(await store.recomputeSignals());
19549
+ if (!id && method === "GET") {
19550
+ const cid = qp("contact_id");
19551
+ if (!cid)
19552
+ return error(400, "contact_id required");
19553
+ return json5({ signals: await store.getRelationshipSignals(cid) });
19554
+ }
19555
+ return error(405, "method not allowed");
19556
+ }
19557
+ if (resource === "freshness") {
19558
+ if (id === "stale" && method === "GET")
19559
+ return json5({ contacts: await store.getStaleContacts(qn("threshold") ?? 40) });
19560
+ if (id === "verify" && method === "POST") {
19561
+ const b = await readJson(req);
19562
+ if (!b)
19563
+ return error(400, "invalid JSON body");
19564
+ await store.markFieldVerified(b.contact_id, b.field_name, b.source);
19565
+ return json5({ ok: true });
19566
+ }
19567
+ if (id && method === "GET")
19568
+ return json5({ freshness: await store.getFreshnessScore(id) });
19569
+ return error(405, "method not allowed");
19570
+ }
19571
+ if (resource === "graph") {
19572
+ if (id === "strength" && sub && method === "GET")
19573
+ return json5({ strength: await store.computeRelationshipStrength(sub) });
19574
+ if (id === "warm-path" && method === "GET")
19575
+ return json5({ path: await store.findWarmPath(qp("from") ?? "", qp("to") ?? "") });
19576
+ if (id === "company" && sub && method === "GET")
19577
+ return json5({ connections: await store.findConnectionsAtCompany(sub) });
19578
+ if (id === "cooling" && method === "GET")
19579
+ return json5({ cooling: await store.detectCoolingRelationships() });
19580
+ return error(404, "unknown /v1/graph route");
19581
+ }
19582
+ if (resource === "org-chart") {
19583
+ if (id === "coverage" && sub && method === "GET")
19584
+ return json5({ coverage: await store.getCoverageGaps(sub) });
19585
+ if (!id) {
19586
+ if (method === "GET") {
19587
+ const cid = qp("company_id");
19588
+ if (!cid)
19589
+ return error(400, "company_id required");
19590
+ return json5({ edges: await store.listOrgChart(cid) });
19591
+ }
19592
+ if (method === "POST") {
19593
+ const b = await readJson(req);
19594
+ if (!b)
19595
+ return error(400, "invalid JSON body");
19596
+ return json5({ edge: await store.addOrgChartEdge(b.company_id, b.contact_a_id, b.contact_b_id, b.edge_type, b.inferred) }, 201);
19597
+ }
19598
+ }
19599
+ return error(405, "method not allowed");
19600
+ }
19601
+ if (resource === "audiences") {
19602
+ if (id && sub === "resolve" && method === "GET")
19603
+ return json5({ resolution: await store.resolveAudience(id, qp("channel") ?? "email") });
19604
+ if (!id) {
19605
+ if (method === "GET")
19606
+ return json5({ audiences: await store.listAudiences() });
19607
+ if (method === "POST") {
19608
+ const b = await readJson(req);
19609
+ if (!b)
19610
+ return error(400, "invalid JSON body");
19611
+ return json5({ audience: await store.createAudience(b) }, 201);
19612
+ }
19613
+ return error(405, "method not allowed");
19614
+ }
19615
+ if (method === "GET")
19616
+ return json5({ audience: await store.getAudience(id) });
19617
+ if (method === "PATCH" || method === "PUT") {
19618
+ const b = await readJson(req);
19619
+ if (!b)
19620
+ return error(400, "invalid JSON body");
19621
+ return json5({ audience: await store.updateAudience(id, b) });
19622
+ }
19623
+ if (method === "DELETE") {
19624
+ await store.deleteAudience(id);
19625
+ return json5({ deleted: true, id });
19626
+ }
19627
+ return error(405, "method not allowed");
19628
+ }
19629
+ if (resource === "consent") {
19630
+ if (method === "GET") {
19631
+ const cid = qp("contact_id");
19632
+ if (!cid)
19633
+ return error(400, "contact_id required");
19634
+ return json5({ consent: await store.listContactConsent(cid) });
19635
+ }
19636
+ if (method === "POST") {
19637
+ const b = await readJson(req);
19638
+ if (!b)
19639
+ return error(400, "invalid JSON body");
19640
+ return json5({ consent: await store.setContactConsent(b.contact_id, b.channel, b.status, b.source) });
19641
+ }
19642
+ return error(405, "method not allowed");
19643
+ }
19644
+ if (resource === "suppressions") {
19645
+ if (method === "GET")
19646
+ return json5({ suppressions: await store.listSuppressions({ channel: qp("channel"), unsyncedOnly: qp("unsynced") === "1" || qp("unsynced") === "true" }) });
19647
+ if (method === "POST") {
19648
+ const b = await readJson(req);
19649
+ if (!b)
19650
+ return error(400, "invalid JSON body");
19651
+ return json5({ suppression: await store.suppressAddress(b) }, 201);
19652
+ }
19653
+ if (method === "DELETE") {
19654
+ const channel = qp("channel");
19655
+ const address = qp("address");
19656
+ if (!channel || !address)
19657
+ return error(400, "channel and address required");
19658
+ await store.unsuppressAddress(channel, address);
19659
+ return json5({ ok: true });
19660
+ }
19661
+ return error(405, "method not allowed");
19662
+ }
19663
+ if (resource === "field-history" && method === "GET") {
19664
+ const cid = qp("contact_id");
19665
+ if (!cid)
19666
+ return error(400, "contact_id required");
19667
+ if (id === "at")
19668
+ return json5({ fields: await store.getContactAt(cid, qp("timestamp") ?? new Date().toISOString()) });
19669
+ return json5({ history: await store.getFieldHistory(cid, qp("field_name")) });
19670
+ }
19671
+ if (resource === "job-history") {
19672
+ if (method === "GET") {
19673
+ const cid = qp("contact_id");
19674
+ if (!cid)
19675
+ return error(400, "contact_id required");
19676
+ return json5({ job_history: await store.getJobHistory(cid) });
19677
+ }
19678
+ if (method === "POST") {
19679
+ const b = await readJson(req);
19680
+ if (!b?.contact_id)
19681
+ return error(400, "contact_id required");
19682
+ return json5({ job: await store.addJobEntry(b.contact_id, b) }, 201);
19683
+ }
19684
+ }
19685
+ if (resource === "cold-contacts" && method === "GET")
19686
+ return json5({ contacts: await store.listColdContacts(qn("days") ?? 30) });
19687
+ if (resource === "not-contacted" && method === "GET")
19688
+ return json5({ contacts: await store.listContactsNotContactedSince(qn("days") ?? 90, qn("limit") ?? 50) });
19689
+ if (resource === "followup-due-contacts" && method === "GET")
19690
+ return json5({ contacts: await store.listFollowupDueContacts(qp("on_or_before") ?? new Date().toISOString()) });
19691
+ if (resource === "contacts-for-context" && method === "GET")
19692
+ return json5({ contacts: await store.findContactsForContext(qp("topic") ?? "", qn("limit") ?? 20) });
19693
+ if (resource === "email-duplicates" && method === "GET")
19694
+ return json5({ duplicates: await store.findEmailDuplicates() });
19695
+ if (resource === "name-duplicates" && method === "GET")
19696
+ return json5({ duplicates: await store.findNameDuplicates() });
19697
+ if (resource === "contact-audit" && method === "GET")
19698
+ return json5({ audit: await store.listContactAudit() });
19699
+ if (resource === "upcoming" && method === "GET")
19700
+ return json5({ items: await store.getUpcomingItems(qn("days") ?? 7) });
19701
+ if (resource === "network-stats" && method === "GET")
19702
+ return json5({ stats: await store.getNetworkStats() });
19703
+ if (resource === "recent-events" && method === "GET")
19704
+ return json5({ events: await store.getRecentContactEvents(qp("since"), qp("types") ? qp("types").split(",") : undefined) });
19705
+ if (resource === "vault-status" && method === "GET")
19706
+ return json5({ vault: await store.vaultStatus() });
19707
+ if (resource === "assemble-context" && method === "POST") {
19708
+ const b = await readJson(req);
19709
+ if (!b)
19710
+ return error(400, "invalid JSON body");
19711
+ return json5({ context: await store.assembleContext(b.contact_ids ?? [], b.format ?? "meeting_prep") });
19712
+ }
17764
19713
  return error(404, `unknown /v1 resource: ${resource ?? "(root)"}`);
17765
19714
  } catch (e) {
17766
19715
  const msg = e.message || "internal error";