@hasna/contacts 0.6.34 → 0.6.35

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.
Files changed (212) hide show
  1. package/dashboard/dist/assets/index-0l6aQb1t.css +1 -0
  2. package/dashboard/dist/assets/index-opnZdkVD.js +229 -0
  3. package/dashboard/dist/index.html +13 -0
  4. package/dist/cli/commands/advanced.d.ts +3 -0
  5. package/dist/cli/commands/advanced.d.ts.map +1 -0
  6. package/dist/cli/commands/audience.d.ts +18 -0
  7. package/dist/cli/commands/audience.d.ts.map +1 -0
  8. package/dist/cli/commands/core.d.ts +3 -0
  9. package/dist/cli/commands/core.d.ts.map +1 -0
  10. package/dist/cli/commands/crm.d.ts +3 -0
  11. package/dist/cli/commands/crm.d.ts.map +1 -0
  12. package/dist/cli/index.d.ts +3 -0
  13. package/dist/cli/index.d.ts.map +1 -0
  14. package/dist/cli/index.js +1060 -46
  15. package/dist/cli/storage.d.ts +3 -0
  16. package/dist/cli/storage.d.ts.map +1 -0
  17. package/dist/cli/utils.d.ts +10 -0
  18. package/dist/cli/utils.d.ts.map +1 -0
  19. package/dist/cloud/http-storage.d.ts +88 -0
  20. package/dist/cloud/http-storage.d.ts.map +1 -0
  21. package/dist/db/activity.d.ts +15 -0
  22. package/dist/db/activity.d.ts.map +1 -0
  23. package/dist/db/applications.d.ts +10 -0
  24. package/dist/db/applications.d.ts.map +1 -0
  25. package/dist/db/audiences.d.ts +51 -0
  26. package/dist/db/audiences.d.ts.map +1 -0
  27. package/dist/db/companies.d.ts +19 -0
  28. package/dist/db/companies.d.ts.map +1 -0
  29. package/dist/db/contact-tasks.d.ts +19 -0
  30. package/dist/db/contact-tasks.d.ts.map +1 -0
  31. package/dist/db/contacts.d.ts +30 -0
  32. package/dist/db/contacts.d.ts.map +1 -0
  33. package/dist/db/coordination.d.ts +30 -0
  34. package/dist/db/coordination.d.ts.map +1 -0
  35. package/dist/db/database.d.ts +8 -0
  36. package/dist/db/database.d.ts.map +1 -0
  37. package/dist/db/deals.d.ts +14 -0
  38. package/dist/db/deals.d.ts.map +1 -0
  39. package/dist/db/documents.d.ts +38 -0
  40. package/dist/db/documents.d.ts.map +1 -0
  41. package/dist/db/events.d.ts +14 -0
  42. package/dist/db/events.d.ts.map +1 -0
  43. package/dist/db/field-history.d.ts +17 -0
  44. package/dist/db/field-history.d.ts.map +1 -0
  45. package/dist/db/freshness.d.ts +24 -0
  46. package/dist/db/freshness.d.ts.map +1 -0
  47. package/dist/db/graph.d.ts +21 -0
  48. package/dist/db/graph.d.ts.map +1 -0
  49. package/dist/db/groups.d.ts +22 -0
  50. package/dist/db/groups.d.ts.map +1 -0
  51. package/dist/db/health.d.ts +40 -0
  52. package/dist/db/health.d.ts.map +1 -0
  53. package/dist/db/identity.d.ts +32 -0
  54. package/dist/db/identity.d.ts.map +1 -0
  55. package/dist/db/job-history.d.ts +29 -0
  56. package/dist/db/job-history.d.ts.map +1 -0
  57. package/dist/db/learnings.d.ts +43 -0
  58. package/dist/db/learnings.d.ts.map +1 -0
  59. package/dist/db/notes.d.ts +8 -0
  60. package/dist/db/notes.d.ts.map +1 -0
  61. package/dist/db/org-chart.d.ts +37 -0
  62. package/dist/db/org-chart.d.ts.map +1 -0
  63. package/dist/db/org-members.d.ts +9 -0
  64. package/dist/db/org-members.d.ts.map +1 -0
  65. package/dist/db/paths.d.ts +3 -0
  66. package/dist/db/paths.d.ts.map +1 -0
  67. package/dist/db/pg-migrations.d.ts +8 -0
  68. package/dist/db/pg-migrations.d.ts.map +1 -0
  69. package/dist/db/project-memberships.d.ts +8 -0
  70. package/dist/db/project-memberships.d.ts.map +1 -0
  71. package/dist/db/relationships.d.ts +30 -0
  72. package/dist/db/relationships.d.ts.map +1 -0
  73. package/dist/db/signals.d.ts +18 -0
  74. package/dist/db/signals.d.ts.map +1 -0
  75. package/dist/db/sqlite-adapter.d.ts +15 -0
  76. package/dist/db/sqlite-adapter.d.ts.map +1 -0
  77. package/dist/db/storage.d.ts +15 -0
  78. package/dist/db/storage.d.ts.map +1 -0
  79. package/dist/db/tags.d.ts +14 -0
  80. package/dist/db/tags.d.ts.map +1 -0
  81. package/dist/db/tombstones.d.ts +19 -0
  82. package/dist/db/tombstones.d.ts.map +1 -0
  83. package/dist/db/vendor-comms.d.ts +15 -0
  84. package/dist/db/vendor-comms.d.ts.map +1 -0
  85. package/dist/generated/storage-kit/health.d.ts +20 -0
  86. package/dist/generated/storage-kit/health.d.ts.map +1 -0
  87. package/dist/generated/storage-kit/index.d.ts +8 -0
  88. package/dist/generated/storage-kit/index.d.ts.map +1 -0
  89. package/dist/generated/storage-kit/migrations.d.ts +48 -0
  90. package/dist/generated/storage-kit/migrations.d.ts.map +1 -0
  91. package/dist/generated/storage-kit/mode.d.ts +48 -0
  92. package/dist/generated/storage-kit/mode.d.ts.map +1 -0
  93. package/dist/generated/storage-kit/pool.d.ts +34 -0
  94. package/dist/generated/storage-kit/pool.d.ts.map +1 -0
  95. package/dist/generated/storage-kit/query.d.ts +36 -0
  96. package/dist/generated/storage-kit/query.d.ts.map +1 -0
  97. package/dist/generated/storage-kit/tls.d.ts +26 -0
  98. package/dist/generated/storage-kit/tls.d.ts.map +1 -0
  99. package/dist/index.d.ts +9 -0
  100. package/dist/index.d.ts.map +1 -0
  101. package/dist/index.js +613 -23
  102. package/dist/lib/apple-contacts.d.ts +3 -0
  103. package/dist/lib/apple-contacts.d.ts.map +1 -0
  104. package/dist/lib/audience-contract.d.ts +215 -0
  105. package/dist/lib/audience-contract.d.ts.map +1 -0
  106. package/dist/lib/audit.d.ts +12 -0
  107. package/dist/lib/audit.d.ts.map +1 -0
  108. package/dist/lib/brief.d.ts +3 -0
  109. package/dist/lib/brief.d.ts.map +1 -0
  110. package/dist/lib/config.d.ts +7 -0
  111. package/dist/lib/config.d.ts.map +1 -0
  112. package/dist/lib/connector.d.ts +44 -0
  113. package/dist/lib/connector.d.ts.map +1 -0
  114. package/dist/lib/context.d.ts +5 -0
  115. package/dist/lib/context.d.ts.map +1 -0
  116. package/dist/lib/dedup.d.ts +10 -0
  117. package/dist/lib/dedup.d.ts.map +1 -0
  118. package/dist/lib/document-scanner.d.ts +8 -0
  119. package/dist/lib/document-scanner.d.ts.map +1 -0
  120. package/dist/lib/embeddings.d.ts +9 -0
  121. package/dist/lib/embeddings.d.ts.map +1 -0
  122. package/dist/lib/export.d.ts +8 -0
  123. package/dist/lib/export.d.ts.map +1 -0
  124. package/dist/lib/freshness.d.ts +19 -0
  125. package/dist/lib/freshness.d.ts.map +1 -0
  126. package/dist/lib/gmail-import.d.ts +51 -0
  127. package/dist/lib/gmail-import.d.ts.map +1 -0
  128. package/dist/lib/google-contacts.d.ts +115 -0
  129. package/dist/lib/google-contacts.d.ts.map +1 -0
  130. package/dist/lib/images.d.ts +30 -0
  131. package/dist/lib/images.d.ts.map +1 -0
  132. package/dist/lib/import.d.ts +10 -0
  133. package/dist/lib/import.d.ts.map +1 -0
  134. package/dist/lib/learning-maintenance.d.ts +8 -0
  135. package/dist/lib/learning-maintenance.d.ts.map +1 -0
  136. package/dist/lib/mailery-sync.d.ts +46 -0
  137. package/dist/lib/mailery-sync.d.ts.map +1 -0
  138. package/dist/lib/meeting-capture.d.ts +15 -0
  139. package/dist/lib/meeting-capture.d.ts.map +1 -0
  140. package/dist/lib/package-version.d.ts +6 -0
  141. package/dist/lib/package-version.d.ts.map +1 -0
  142. package/dist/lib/signals.d.ts +9 -0
  143. package/dist/lib/signals.d.ts.map +1 -0
  144. package/dist/lib/signature-parser.d.ts +36 -0
  145. package/dist/lib/signature-parser.d.ts.map +1 -0
  146. package/dist/lib/stats.d.ts +24 -0
  147. package/dist/lib/stats.d.ts.map +1 -0
  148. package/dist/lib/timeline.d.ts +11 -0
  149. package/dist/lib/timeline.d.ts.map +1 -0
  150. package/dist/lib/upcoming.d.ts +14 -0
  151. package/dist/lib/upcoming.d.ts.map +1 -0
  152. package/dist/lib/vault.d.ts +24 -0
  153. package/dist/lib/vault.d.ts.map +1 -0
  154. package/dist/mcp/handlers/advanced.d.ts +14 -0
  155. package/dist/mcp/handlers/advanced.d.ts.map +1 -0
  156. package/dist/mcp/handlers/audience.d.ts +9 -0
  157. package/dist/mcp/handlers/audience.d.ts.map +1 -0
  158. package/dist/mcp/handlers/core.d.ts +10 -0
  159. package/dist/mcp/handlers/core.d.ts.map +1 -0
  160. package/dist/mcp/handlers/crm.d.ts +14 -0
  161. package/dist/mcp/handlers/crm.d.ts.map +1 -0
  162. package/dist/mcp/handlers/index.d.ts +4 -0
  163. package/dist/mcp/handlers/index.d.ts.map +1 -0
  164. package/dist/mcp/handlers/types.d.ts +10 -0
  165. package/dist/mcp/handlers/types.d.ts.map +1 -0
  166. package/dist/mcp/http.d.ts +17 -0
  167. package/dist/mcp/http.d.ts.map +1 -0
  168. package/dist/mcp/index.d.ts +5 -0
  169. package/dist/mcp/index.d.ts.map +1 -0
  170. package/dist/mcp/index.js +339 -27
  171. package/dist/mcp/register-tools.d.ts +3 -0
  172. package/dist/mcp/register-tools.d.ts.map +1 -0
  173. package/dist/mcp/schema.d.ts +3 -0
  174. package/dist/mcp/schema.d.ts.map +1 -0
  175. package/dist/mcp/storage-tools.d.ts +3 -0
  176. package/dist/mcp/storage-tools.d.ts.map +1 -0
  177. package/dist/mcp/tools.d.ts +3799 -0
  178. package/dist/mcp/tools.d.ts.map +1 -0
  179. package/dist/sdk/index.d.ts +16 -0
  180. package/dist/sdk/index.d.ts.map +1 -0
  181. package/dist/sdk/index.js +63 -0
  182. package/dist/sdk/v1.generated.d.ts +270 -0
  183. package/dist/sdk/v1.generated.d.ts.map +1 -0
  184. package/dist/server/cloud-index.d.ts +3 -0
  185. package/dist/server/cloud-index.d.ts.map +1 -0
  186. package/dist/server/cloud-serve.d.ts +3 -0
  187. package/dist/server/cloud-serve.d.ts.map +1 -0
  188. package/dist/server/cloud.d.ts +53 -0
  189. package/dist/server/cloud.d.ts.map +1 -0
  190. package/dist/server/index.d.ts +3 -0
  191. package/dist/server/index.d.ts.map +1 -0
  192. package/dist/server/index.js +1022 -46
  193. package/dist/server/openapi.d.ts +1134 -0
  194. package/dist/server/openapi.d.ts.map +1 -0
  195. package/dist/server/pg-store.d.ts +893 -0
  196. package/dist/server/pg-store.d.ts.map +1 -0
  197. package/dist/server/security.d.ts +24 -0
  198. package/dist/server/security.d.ts.map +1 -0
  199. package/dist/server/serve.d.ts +9 -0
  200. package/dist/server/serve.d.ts.map +1 -0
  201. package/dist/server/v1.d.ts +28 -0
  202. package/dist/server/v1.d.ts.map +1 -0
  203. package/dist/store/index.d.ts +387 -0
  204. package/dist/store/index.d.ts.map +1 -0
  205. package/dist/types/index.d.ts +856 -0
  206. package/dist/types/index.d.ts.map +1 -0
  207. package/dist/types/project-memberships.d.ts +32 -0
  208. package/dist/types/project-memberships.d.ts.map +1 -0
  209. package/migrations/0015_contact_project_membership_authority.sql +88 -0
  210. package/package.json +1 -1
  211. package/vendor/fast-uri/README.md +12 -0
  212. package/dist/server/cloud-index.js +0 -9187
package/dist/mcp/index.js CHANGED
@@ -6741,7 +6741,10 @@ function uuid2() {
6741
6741
  return crypto.randomUUID();
6742
6742
  }
6743
6743
  function now() {
6744
- return new Date().toISOString();
6744
+ const currentMs = Date.now();
6745
+ const nextMs = currentMs > lastNowMs ? currentMs : lastNowMs + 1;
6746
+ lastNowMs = nextMs;
6747
+ return new Date(nextMs).toISOString();
6745
6748
  }
6746
6749
  function quoteIdentifier(identifier) {
6747
6750
  return `"${identifier.replaceAll('"', '""')}"`;
@@ -6779,7 +6782,7 @@ function runMigrations(db) {
6779
6782
  db.exec(`INSERT OR REPLACE INTO _migrations(version) VALUES(${i})`);
6780
6783
  }
6781
6784
  }
6782
- var MIGRATIONS, _db = null;
6785
+ var MIGRATIONS, _db = null, lastNowMs = 0;
6783
6786
  var init_database = __esm(() => {
6784
6787
  init_sqlite_adapter();
6785
6788
  init_paths();
@@ -7351,6 +7354,81 @@ var init_database = __esm(() => {
7351
7354
  );
7352
7355
 
7353
7356
  CREATE INDEX IF NOT EXISTS idx_contacts_tombstones_deleted_at ON _contacts_tombstones(deleted_at);
7357
+ `,
7358
+ `
7359
+ CREATE TABLE IF NOT EXISTS contact_project_membership_states (
7360
+ contact_id TEXT NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
7361
+ project_id TEXT NOT NULL,
7362
+ linked INTEGER NOT NULL CHECK(linked IN (0, 1)),
7363
+ revision INTEGER NOT NULL DEFAULT 0 CHECK(revision >= 0),
7364
+ updated_at TEXT NOT NULL,
7365
+ PRIMARY KEY (contact_id, project_id)
7366
+ );
7367
+
7368
+ INSERT OR IGNORE INTO contact_project_membership_states
7369
+ (contact_id, project_id, linked, revision, updated_at)
7370
+ SELECT contact_id, project_id, 1, 0, datetime('now')
7371
+ FROM contact_projects;
7372
+
7373
+ CREATE TABLE IF NOT EXISTS contact_project_membership_receipts (
7374
+ receipt_id TEXT PRIMARY KEY,
7375
+ direction TEXT NOT NULL CHECK(direction IN ('attach', 'detach')),
7376
+ contact_id TEXT NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
7377
+ project_id TEXT NOT NULL,
7378
+ operation_id TEXT NOT NULL,
7379
+ step_id TEXT NOT NULL,
7380
+ expected_version TEXT NOT NULL,
7381
+ before_json TEXT NOT NULL,
7382
+ after_json TEXT NOT NULL,
7383
+ created_at TEXT NOT NULL,
7384
+ UNIQUE(operation_id, step_id)
7385
+ );
7386
+
7387
+ CREATE INDEX IF NOT EXISTS idx_contact_project_membership_states_project
7388
+ ON contact_project_membership_states(project_id, linked, contact_id);
7389
+ CREATE INDEX IF NOT EXISTS idx_contact_project_membership_receipts_target
7390
+ ON contact_project_membership_receipts(contact_id, project_id, created_at);
7391
+
7392
+ CREATE TRIGGER IF NOT EXISTS sync_contact_project_membership_state_after_insert
7393
+ AFTER INSERT ON contact_projects
7394
+ BEGIN
7395
+ INSERT INTO contact_project_membership_states
7396
+ (contact_id, project_id, linked, revision, updated_at)
7397
+ VALUES (NEW.contact_id, NEW.project_id, 1, 1, datetime('now'))
7398
+ ON CONFLICT(contact_id, project_id) DO UPDATE SET
7399
+ linked = 1,
7400
+ revision = CASE
7401
+ WHEN contact_project_membership_states.linked = 1
7402
+ THEN contact_project_membership_states.revision
7403
+ ELSE contact_project_membership_states.revision + 1
7404
+ END,
7405
+ updated_at = CASE
7406
+ WHEN contact_project_membership_states.linked = 1
7407
+ THEN contact_project_membership_states.updated_at
7408
+ ELSE datetime('now')
7409
+ END;
7410
+ END;
7411
+
7412
+ CREATE TRIGGER IF NOT EXISTS sync_contact_project_membership_state_after_delete
7413
+ AFTER DELETE ON contact_projects
7414
+ WHEN EXISTS (SELECT 1 FROM contacts WHERE id = OLD.contact_id)
7415
+ BEGIN
7416
+ INSERT INTO contact_project_membership_states
7417
+ (contact_id, project_id, linked, revision, updated_at)
7418
+ VALUES (OLD.contact_id, OLD.project_id, 0, 1, datetime('now'))
7419
+ ON CONFLICT(contact_id, project_id) DO UPDATE SET
7420
+ linked = 0,
7421
+ revision = CASE
7422
+ WHEN contact_project_membership_states.linked = 0
7423
+ THEN contact_project_membership_states.revision
7424
+ ELSE contact_project_membership_states.revision + 1
7425
+ END,
7426
+ updated_at = CASE
7427
+ WHEN contact_project_membership_states.linked = 0
7428
+ THEN contact_project_membership_states.updated_at
7429
+ ELSE datetime('now')
7430
+ END;
7431
+ END;
7354
7432
  `
7355
7433
  ];
7356
7434
  });
@@ -7448,6 +7526,219 @@ var init_tombstones = __esm(() => {
7448
7526
  init_database();
7449
7527
  });
7450
7528
 
7529
+ // src/types/project-memberships.ts
7530
+ var ContactProjectMembershipConflictError;
7531
+ var init_project_memberships = __esm(() => {
7532
+ ContactProjectMembershipConflictError = class ContactProjectMembershipConflictError extends Error {
7533
+ constructor(message) {
7534
+ super(message);
7535
+ this.name = "ContactProjectMembershipConflictError";
7536
+ }
7537
+ };
7538
+ });
7539
+
7540
+ // src/db/project-memberships.ts
7541
+ import { createHash } from "crypto";
7542
+ function digest(value) {
7543
+ return createHash("sha256").update(value).digest("hex");
7544
+ }
7545
+ function membershipVersion(contactId, projectId, linked, revision) {
7546
+ return `cpmv_${digest(JSON.stringify([contactId, projectId, linked, revision])).slice(0, 32)}`;
7547
+ }
7548
+ function receiptId(input) {
7549
+ return `cpmr_${digest(JSON.stringify([
7550
+ input.operation_id,
7551
+ input.step_id,
7552
+ input.contact_id,
7553
+ input.project_id
7554
+ ])).slice(0, 32)}`;
7555
+ }
7556
+ function stateRow(contactId, projectId, db) {
7557
+ const persisted = db.query(`SELECT contact_id, project_id, linked, revision
7558
+ FROM contact_project_membership_states
7559
+ WHERE contact_id = ? AND project_id = ?`).get(contactId, projectId);
7560
+ if (persisted)
7561
+ return persisted;
7562
+ const linked = Boolean(db.query(`SELECT 1 AS present FROM contact_projects WHERE contact_id = ? AND project_id = ?`).get(contactId, projectId));
7563
+ return { contact_id: contactId, project_id: projectId, linked: linked ? 1 : 0, revision: 0 };
7564
+ }
7565
+ function snapshot(row) {
7566
+ const linked = Boolean(row.linked);
7567
+ return {
7568
+ contact_id: row.contact_id,
7569
+ project_id: row.project_id,
7570
+ linked,
7571
+ version: membershipVersion(row.contact_id, row.project_id, linked, row.revision)
7572
+ };
7573
+ }
7574
+ function required2(value, name) {
7575
+ const normalized = value.trim();
7576
+ if (!normalized)
7577
+ throw new Error(`${name} is required`);
7578
+ return normalized;
7579
+ }
7580
+ function normalizeInput(input) {
7581
+ return {
7582
+ contact_id: required2(input.contact_id, "contact_id"),
7583
+ project_id: required2(input.project_id, "project_id"),
7584
+ operation_id: required2(input.operation_id, "operation_id"),
7585
+ step_id: required2(input.step_id, "step_id"),
7586
+ expected_version: required2(input.expected_version, "expected_version")
7587
+ };
7588
+ }
7589
+ function parseSnapshot(value) {
7590
+ return JSON.parse(value);
7591
+ }
7592
+ function replay(row, direction, input) {
7593
+ if (row.direction !== direction || row.contact_id !== input.contact_id || row.project_id !== input.project_id || row.expected_version !== input.expected_version) {
7594
+ throw new ContactProjectMembershipConflictError(`operation_id/step_id already accepted for a different contact-project membership mutation`);
7595
+ }
7596
+ return {
7597
+ outcome: "duplicate_of_accepted",
7598
+ operation_id: row.operation_id,
7599
+ step_id: row.step_id,
7600
+ before: parseSnapshot(row.before_json),
7601
+ after: parseSnapshot(row.after_json),
7602
+ receipt_id: row.receipt_id
7603
+ };
7604
+ }
7605
+ function transitionWithoutReceipt(contactId, projectId, linked, db) {
7606
+ const beforeRow = stateRow(contactId, projectId, db);
7607
+ const changed = Boolean(beforeRow.linked) !== linked;
7608
+ const afterRow = {
7609
+ ...beforeRow,
7610
+ linked: linked ? 1 : 0,
7611
+ revision: changed ? beforeRow.revision + 1 : beforeRow.revision
7612
+ };
7613
+ const changedAt = now();
7614
+ db.run(`INSERT INTO contact_project_membership_states
7615
+ (contact_id, project_id, linked, revision, updated_at)
7616
+ VALUES (?, ?, ?, ?, ?)
7617
+ ON CONFLICT(contact_id, project_id) DO UPDATE SET
7618
+ linked = excluded.linked,
7619
+ revision = excluded.revision,
7620
+ updated_at = excluded.updated_at`, [contactId, projectId, afterRow.linked, afterRow.revision, changedAt]);
7621
+ if (linked) {
7622
+ db.run(`INSERT OR IGNORE INTO contact_projects (contact_id, project_id) VALUES (?, ?)`, [contactId, projectId]);
7623
+ } else {
7624
+ db.run(`DELETE FROM contact_projects WHERE contact_id = ? AND project_id = ?`, [contactId, projectId]);
7625
+ }
7626
+ return changed;
7627
+ }
7628
+ function setContactProjectMembershipWithoutReceipt(contactId, projectId, linked, db = getDatabase()) {
7629
+ const normalizedContactId = required2(contactId, "contact_id");
7630
+ const normalizedProjectId = required2(projectId, "project_id");
7631
+ return db.transaction(() => transitionWithoutReceipt(normalizedContactId, normalizedProjectId, linked, db));
7632
+ }
7633
+ function replaceContactProjectMembershipsWithoutReceipts(contactId, projectIds, db = getDatabase()) {
7634
+ const normalizedContactId = required2(contactId, "contact_id");
7635
+ const normalizedProjectIds = [...new Set(projectIds.map((projectId) => required2(projectId, "project_id")))];
7636
+ return db.transaction(() => {
7637
+ const currentRows = db.query(`SELECT project_id FROM contact_projects WHERE contact_id = ?`).all(normalizedContactId);
7638
+ const desired = new Set(normalizedProjectIds);
7639
+ const population = new Set([...currentRows.map((row) => row.project_id), ...normalizedProjectIds]);
7640
+ for (const projectId of population) {
7641
+ transitionWithoutReceipt(normalizedContactId, projectId, desired.has(projectId), db);
7642
+ }
7643
+ return normalizedProjectIds;
7644
+ });
7645
+ }
7646
+ function readContactProjectMembership(contactId, projectId, db = getDatabase()) {
7647
+ return snapshot(stateRow(required2(contactId, "contact_id"), required2(projectId, "project_id"), db));
7648
+ }
7649
+ function listContactProjectMemberships(projectId, maxItems, db = getDatabase()) {
7650
+ const normalizedProjectId = required2(projectId, "project_id");
7651
+ if (!Number.isInteger(maxItems) || maxItems < 1)
7652
+ throw new Error("max_items must be a positive integer");
7653
+ const rows = db.query(`SELECT cp.contact_id, cp.project_id,
7654
+ COALESCE(state.linked, 1) AS linked,
7655
+ COALESCE(state.revision, 0) AS revision
7656
+ FROM contact_projects cp
7657
+ LEFT JOIN contact_project_membership_states state
7658
+ ON state.contact_id = cp.contact_id AND state.project_id = cp.project_id
7659
+ WHERE cp.project_id = ?
7660
+ ORDER BY cp.contact_id ASC
7661
+ LIMIT ?`).all(normalizedProjectId, maxItems + 1);
7662
+ if (rows.length > maxItems) {
7663
+ throw new Error(`contact project membership collection exceeds max_items=${maxItems}`);
7664
+ }
7665
+ const contactIds = rows.map((row) => row.contact_id);
7666
+ return {
7667
+ project_id: normalizedProjectId,
7668
+ contact_ids: contactIds,
7669
+ complete: true,
7670
+ membership_revision: `cpml_${digest(JSON.stringify(rows.map((row) => snapshot(row)))).slice(0, 32)}`
7671
+ };
7672
+ }
7673
+ function mutateContactProjectMembership(direction, rawInput, db = getDatabase()) {
7674
+ const input = normalizeInput(rawInput);
7675
+ return db.transaction(() => {
7676
+ const existingReceipt = db.query(`SELECT direction, contact_id, project_id, operation_id, step_id, expected_version,
7677
+ before_json, after_json, receipt_id
7678
+ FROM contact_project_membership_receipts
7679
+ WHERE operation_id = ? AND step_id = ?`).get(input.operation_id, input.step_id);
7680
+ if (existingReceipt)
7681
+ return replay(existingReceipt, direction, input);
7682
+ const contact = db.query(`SELECT id FROM contacts WHERE id = ?`).get(input.contact_id);
7683
+ if (!contact)
7684
+ throw new Error(`contact not found: ${input.contact_id}`);
7685
+ const beforeRow = stateRow(input.contact_id, input.project_id, db);
7686
+ const before = snapshot(beforeRow);
7687
+ if (before.version !== input.expected_version) {
7688
+ throw new ContactProjectMembershipConflictError(`contact project membership expected_version conflict: expected ${input.expected_version}, current ${before.version}`);
7689
+ }
7690
+ const desiredLinked = direction === "attach";
7691
+ const changed = before.linked !== desiredLinked;
7692
+ const afterRow = {
7693
+ ...beforeRow,
7694
+ linked: desiredLinked ? 1 : 0,
7695
+ revision: changed ? beforeRow.revision + 1 : beforeRow.revision
7696
+ };
7697
+ const changedAt = now();
7698
+ db.run(`INSERT INTO contact_project_membership_states
7699
+ (contact_id, project_id, linked, revision, updated_at)
7700
+ VALUES (?, ?, ?, ?, ?)
7701
+ ON CONFLICT(contact_id, project_id) DO UPDATE SET
7702
+ linked = excluded.linked,
7703
+ revision = excluded.revision,
7704
+ updated_at = excluded.updated_at`, [input.contact_id, input.project_id, afterRow.linked, afterRow.revision, changedAt]);
7705
+ if (desiredLinked) {
7706
+ db.run(`INSERT OR IGNORE INTO contact_projects (contact_id, project_id) VALUES (?, ?)`, [input.contact_id, input.project_id]);
7707
+ } else {
7708
+ db.run(`DELETE FROM contact_projects WHERE contact_id = ? AND project_id = ?`, [input.contact_id, input.project_id]);
7709
+ }
7710
+ const after = snapshot(afterRow);
7711
+ const id = receiptId(input);
7712
+ db.run(`INSERT INTO contact_project_membership_receipts (
7713
+ receipt_id, direction, contact_id, project_id, operation_id, step_id,
7714
+ expected_version, before_json, after_json, created_at
7715
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
7716
+ id,
7717
+ direction,
7718
+ input.contact_id,
7719
+ input.project_id,
7720
+ input.operation_id,
7721
+ input.step_id,
7722
+ input.expected_version,
7723
+ JSON.stringify(before),
7724
+ JSON.stringify(after),
7725
+ changedAt
7726
+ ]);
7727
+ return {
7728
+ outcome: changed ? "accepted" : "duplicate_of_accepted",
7729
+ operation_id: input.operation_id,
7730
+ step_id: input.step_id,
7731
+ before,
7732
+ after,
7733
+ receipt_id: id
7734
+ };
7735
+ });
7736
+ }
7737
+ var init_project_memberships2 = __esm(() => {
7738
+ init_database();
7739
+ init_project_memberships();
7740
+ });
7741
+
7451
7742
  // src/db/contacts.ts
7452
7743
  var exports_contacts = {};
7453
7744
  __export(exports_contacts, {
@@ -8046,34 +8337,32 @@ function autoLinkContactToCompany(contactId, db) {
8046
8337
  }
8047
8338
  function linkContactToProject(contactId, projectId, db) {
8048
8339
  const d = db || getDatabase();
8049
- d.run(`INSERT OR IGNORE INTO contact_projects (contact_id, project_id) VALUES (?, ?)`, [contactId, projectId]);
8340
+ setContactProjectMembershipWithoutReceipt(contactId, projectId, true, d);
8050
8341
  }
8051
8342
  function unlinkContactFromProject(contactId, projectId, db) {
8052
8343
  const d = db || getDatabase();
8053
- d.run(`DELETE FROM contact_projects WHERE contact_id = ? AND project_id = ?`, [contactId, projectId]);
8344
+ setContactProjectMembershipWithoutReceipt(contactId, projectId, false, d);
8054
8345
  }
8055
8346
  function getContactProjectIds(contactId, db) {
8056
8347
  const d = db || getDatabase();
8057
- const rows = d.query(`SELECT project_id FROM contact_projects WHERE contact_id = ?`).all(contactId);
8348
+ const rows = d.query(`SELECT project_id FROM contact_projects WHERE contact_id = ? ORDER BY project_id ASC`).all(contactId);
8058
8349
  return rows.map((r) => r.project_id);
8059
8350
  }
8060
8351
  function listContactIdsByProject(projectId, db) {
8061
8352
  const d = db || getDatabase();
8062
- const rows = d.query(`SELECT contact_id FROM contact_projects WHERE project_id = ?`).all(projectId);
8353
+ const rows = d.query(`SELECT contact_id FROM contact_projects WHERE project_id = ? ORDER BY contact_id ASC`).all(projectId);
8063
8354
  return rows.map((r) => r.contact_id);
8064
8355
  }
8065
8356
  function setContactProjects(contactId, projectIds, db) {
8066
8357
  const d = db || getDatabase();
8067
- d.run(`DELETE FROM contact_projects WHERE contact_id = ?`, [contactId]);
8068
- for (const pid of projectIds) {
8069
- d.run(`INSERT OR IGNORE INTO contact_projects (contact_id, project_id) VALUES (?, ?)`, [contactId, pid]);
8070
- }
8358
+ replaceContactProjectMembershipsWithoutReceipts(contactId, projectIds, d);
8071
8359
  }
8072
8360
  var init_contacts = __esm(() => {
8073
8361
  init_types();
8074
8362
  init_database();
8075
8363
  init_activity();
8076
8364
  init_tombstones();
8365
+ init_project_memberships2();
8077
8366
  });
8078
8367
 
8079
8368
  // src/db/events.ts
@@ -21293,6 +21582,7 @@ function getStorageStatus(db = getDatabase()) {
21293
21582
 
21294
21583
  // src/store/index.ts
21295
21584
  init_contacts();
21585
+ init_project_memberships2();
21296
21586
 
21297
21587
  // src/db/companies.ts
21298
21588
  init_types();
@@ -22822,7 +23112,7 @@ init_database();
22822
23112
  init_database();
22823
23113
  import { existsSync as existsSync4, readFileSync as readFileSync2, writeFileSync, mkdirSync as mkdirSync3, unlinkSync } from "fs";
22824
23114
  import { join as join3 } from "path";
22825
- import { createCipheriv, createDecipheriv, randomBytes, pbkdf2Sync, createHash } from "crypto";
23115
+ import { createCipheriv, createDecipheriv, randomBytes, pbkdf2Sync, createHash as createHash2 } from "crypto";
22826
23116
  var VAULT_DIR = getDataDir();
22827
23117
  var VAULT_CONFIG = join3(VAULT_DIR, "vault.json");
22828
23118
  var VAULT_SESSION = join3(VAULT_DIR, ".vault-session");
@@ -22868,7 +23158,7 @@ function initVault(passphrase) {
22868
23158
  mkdirSync3(DOCUMENTS_DIR, { recursive: true });
22869
23159
  const salt = randomBytes(32);
22870
23160
  const key = deriveKey(passphrase, salt);
22871
- const keyHash = createHash("sha256").update(key).digest("hex");
23161
+ const keyHash = createHash2("sha256").update(key).digest("hex");
22872
23162
  const config2 = { salt: salt.toString("hex"), key_hash: keyHash, created_at: new Date().toISOString() };
22873
23163
  writeFileSync(VAULT_CONFIG, JSON.stringify(config2, null, 2));
22874
23164
  _derivedKey = key;
@@ -22883,7 +23173,7 @@ function unlockVault(passphrase) {
22883
23173
  const config2 = JSON.parse(readFileSync2(VAULT_CONFIG, "utf-8"));
22884
23174
  const salt = Buffer.from(config2.salt, "hex");
22885
23175
  const key = deriveKey(passphrase, salt);
22886
- const keyHash = createHash("sha256").update(key).digest("hex");
23176
+ const keyHash = createHash2("sha256").update(key).digest("hex");
22887
23177
  if (keyHash !== config2.key_hash)
22888
23178
  return false;
22889
23179
  _derivedKey = key;
@@ -24419,6 +24709,15 @@ class LocalStore {
24419
24709
  async listContactIdsByProject(projectId) {
24420
24710
  return listContactIdsByProject(projectId, this.db);
24421
24711
  }
24712
+ async readContactProjectMembership(contactId, projectId) {
24713
+ return readContactProjectMembership(contactId, projectId, this.db);
24714
+ }
24715
+ async listContactProjectMemberships(projectId, maxItems) {
24716
+ return listContactProjectMemberships(projectId, maxItems, this.db);
24717
+ }
24718
+ async mutateContactProjectMembership(direction, input) {
24719
+ return mutateContactProjectMembership(direction, input, this.db);
24720
+ }
24422
24721
  async createCompany(input) {
24423
24722
  return createCompany(input, this.db);
24424
24723
  }
@@ -25041,20 +25340,33 @@ class ApiStore {
25041
25340
  async findContactByEmailAddress(address) {
25042
25341
  return this.getContactByEmail(address);
25043
25342
  }
25044
- async linkContactToProject() {
25045
- return unavailable("linkContactToProject");
25343
+ async linkContactToProject(contactId, projectId) {
25344
+ await this.client.transport.put(`/contacts/${this.enc(contactId)}/projects/${this.enc(projectId)}`);
25046
25345
  }
25047
- async unlinkContactFromProject() {
25048
- return unavailable("unlinkContactFromProject");
25346
+ async unlinkContactFromProject(contactId, projectId) {
25347
+ await this.del(`/contacts/${this.enc(contactId)}/projects/${this.enc(projectId)}`);
25348
+ }
25349
+ async getContactProjectIds(contactId) {
25350
+ return pick2(await this.g(`/contacts/${this.enc(contactId)}/projects`), "project_ids") ?? [];
25351
+ }
25352
+ async setContactProjects(contactId, projectIds) {
25353
+ await this.client.transport.put(`/contacts/${this.enc(contactId)}/projects`, { project_ids: projectIds });
25354
+ }
25355
+ async listContactIdsByProject(projectId) {
25356
+ return pick2(await this.g(`/projects/${this.enc(projectId)}/contacts`), "contact_ids") ?? [];
25049
25357
  }
25050
- async getContactProjectIds() {
25051
- return unavailable("getContactProjectIds");
25358
+ async readContactProjectMembership(contactId, projectId) {
25359
+ return this.client.transport.get(`/projects/${this.enc(projectId)}/contact-memberships/${this.enc(contactId)}`);
25052
25360
  }
25053
- async setContactProjects() {
25054
- return unavailable("setContactProjects");
25361
+ async listContactProjectMemberships(projectId, maxItems) {
25362
+ return this.client.transport.get(`/projects/${this.enc(projectId)}/contact-memberships`, { query: { max_items: maxItems } });
25055
25363
  }
25056
- async listContactIdsByProject() {
25057
- return unavailable("listContactIdsByProject");
25364
+ async mutateContactProjectMembership(direction, input) {
25365
+ return this.client.transport.post(`/projects/${this.enc(input.project_id)}/contact-memberships/${this.enc(input.contact_id)}/${direction}`, {
25366
+ operation_id: input.operation_id,
25367
+ step_id: input.step_id,
25368
+ expected_version: input.expected_version
25369
+ });
25058
25370
  }
25059
25371
  async createCompany(input) {
25060
25372
  const res = await this.client.create("companies", stripUndefined(input));
@@ -27448,8 +27760,8 @@ var _contactsAgents = new Map;
27448
27760
  var advancedHandlers = {
27449
27761
  get_field_history: async (a) => json3({ history: await getStore().getFieldHistory(a.contact_id, a.field_name) }),
27450
27762
  get_contact_at: async (a) => {
27451
- const snapshot = await getStore().getContactAt(a.contact_id, a.timestamp);
27452
- return json3({ contact_id: a.contact_id, timestamp: a.timestamp, snapshot });
27763
+ const snapshot2 = await getStore().getContactAt(a.contact_id, a.timestamp);
27764
+ return json3({ contact_id: a.contact_id, timestamp: a.timestamp, snapshot: snapshot2 });
27453
27765
  },
27454
27766
  get_job_history: async (a) => json3({ history: await getStore().getJobHistory(a.contact_id) }),
27455
27767
  add_job_entry: async (a) => json3(await getStore().addJobEntry(a.contact_id, {
@@ -27925,11 +28237,11 @@ function jsonSchemaToZodType(schema) {
27925
28237
  }
27926
28238
  function jsonSchemaToZodObject(schema) {
27927
28239
  const objectSchema = schema && typeof schema === "object" ? schema : {};
27928
- const required2 = new Set(objectSchema.required ?? []);
28240
+ const required3 = new Set(objectSchema.required ?? []);
27929
28241
  const shape = {};
27930
28242
  for (const [key, value] of Object.entries(objectSchema.properties ?? {})) {
27931
28243
  const propertySchema = jsonSchemaToZodType(value);
27932
- shape[key] = required2.has(key) ? propertySchema : propertySchema.optional();
28244
+ shape[key] = required3.has(key) ? propertySchema : propertySchema.optional();
27933
28245
  }
27934
28246
  return describeSchema(exports_external.object(shape).passthrough(), objectSchema.description);
27935
28247
  }
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerContactsTools(server: McpServer): void;
3
+ //# sourceMappingURL=register-tools.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"register-tools.d.ts","sourceRoot":"","sources":["../../src/mcp/register-tools.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAczE,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,QAoBtD"}
@@ -0,0 +1,3 @@
1
+ import { z } from "zod";
2
+ export declare function jsonSchemaToZodObject(schema: unknown): z.ZodObject<Record<string, z.ZodTypeAny>, "passthrough", z.ZodTypeAny, z.objectOutputType<Record<string, z.ZodTypeAny>, z.ZodTypeAny, "passthrough">, z.objectInputType<Record<string, z.ZodTypeAny>, z.ZodTypeAny, "passthrough">>;
3
+ //# sourceMappingURL=schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/mcp/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAmB,MAAM,KAAK,CAAC;AAgEzC,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,OAAO,uOAWpD"}
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerContactsStorageTools(server: McpServer): void;
3
+ //# sourceMappingURL=storage-tools.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage-tools.d.ts","sourceRoot":"","sources":["../../src/mcp/storage-tools.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AA0CzE,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CA4CpE"}