conduyt 1.6.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -39,6 +39,10 @@ conduyt search "acme corp" # search across the CRM
39
39
  conduyt insights summary # run an AI insight query
40
40
  conduyt privacy export <id> # GDPR data-portability export (owner/admin)
41
41
  conduyt privacy forget <id> --confirm FORGET # GDPR erasure — IRREVERSIBLE, owner only
42
+ conduyt reply-capture status # inbound reply capture: state, DNS records, provider health
43
+ conduyt reply-capture setup # create the receiving subdomain (--domain optional)
44
+ conduyt reply-capture verify # promote it to live once DNS is confirmed
45
+ conduyt reply-capture remove --confirm REMOVE # remove capture — replies to already-sent email are lost
42
46
  conduyt api GET /api/v1/companies # raw authenticated request (escape hatch)
43
47
  conduyt config show # show resolved config (key masked)
44
48
  ```
package/dist/index.js CHANGED
@@ -1666,6 +1666,54 @@ dialer
1666
1666
  .command("sync-local-presence")
1667
1667
  .description("Reconcile the Local Presence pool from Twilio. Only adopts numbers whose Twilio FriendlyName contains a standalone 'LP' (or 'LocalPres'), that are voice-capable, and that are US +1 E.164 — first 50 sorted matches; pool entries that no longer match are REMOVED. Name the number in Twilio first, then sync. Requires settings:edit")
1668
1668
  .action(run(async (client) => client.post("/api/v1/dialer/local-presence/sync", {})));
1669
+ // ---- reply capture (inbound email) ----
1670
+ // The receiving subdomain that makes replies to CRM email record and thread
1671
+ // against the contact. Same lifecycle as the sending domain: setup -> DNS ->
1672
+ // verify -> live, and the name MUST be a strict subdomain of the account's own
1673
+ // VERIFIED sending domain (that rule is the cross-tenant fence, not
1674
+ // formatting). Several non-2xx outcomes here are NORMAL, not retry loops:
1675
+ // 409 "already configured" (remove first), 409 "contact support to reconcile"
1676
+ // (an operator-repair state reachable only through a super-admin session
1677
+ // endpoint — an API key deliberately cannot fix it), 429 (per-account budget:
1678
+ // DNS takes minutes), and 503 (the shared provider allowance is busy with
1679
+ // customer sends — says nothing about the domain).
1680
+ const replyCapture = program
1681
+ .command("reply-capture")
1682
+ .description("Inbound reply capture: the receiving subdomain that threads email replies to contacts");
1683
+ replyCapture
1684
+ .command("status")
1685
+ .description("Show reply-capture state: configured/live, the capture domain and reply address, pending DNS records, and provider health. health='checking' means the provider is re-checking DNS and capture stays active; degraded=true means it can no longer receive and must be removed and set up again; needsSupport=true means an operator must reconcile it; cleanupPending=true means a previous removal did not finish at the provider — re-run `remove` to complete it. After a removal, draining=true (with drainEndsAt) means the drain window is open — a domain that was live at removal keeps receiving replies until it closes (one that was degraded has no such guarantee); parked=true means it is retired and inbound-inert. Either way that name is permanently unavailable — use suggestedDomain when setting up again")
1686
+ .action(run(async (client) => client.get("/api/v1/email-domains/reply-capture")));
1687
+ replyCapture
1688
+ .command("setup")
1689
+ .description("Create the receiving subdomain at the email provider and return the DNS records to add. Requires a verified sending domain; the capture name must be a subdomain of it. Omit --domain to use the suggested reply.<sending-domain>. Add the records, then run `reply-capture verify`. A 409 can also mean the requested name belongs to a retired (drained/parked) domain — retired names are permanently unavailable, so use suggestedDomain from `reply-capture status` (e.g. reply2.<domain>) instead of retrying the old name")
1690
+ .option("--domain <domain>", "capture subdomain, e.g. reply.acme.com (must be a subdomain of your verified sending domain)")
1691
+ .action(run(async (client, opts) => {
1692
+ const domain = opts.domain?.trim();
1693
+ if (opts.domain !== undefined && !domain) {
1694
+ fail("--domain must not be blank. Omit it entirely to use the suggested reply.<sending-domain>.");
1695
+ }
1696
+ // Sent only when supplied — an absent domain is how the API is told to
1697
+ // use its suggestion.
1698
+ return client.post("/api/v1/email-domains/reply-capture", domain ? { domain } : {});
1699
+ }));
1700
+ replyCapture
1701
+ .command("verify")
1702
+ .description("Check the pending capture domain's DNS and promote it to live once the provider confirms inbound receiving. verified=false with DNS record states is the expected answer while records propagate — poll sparingly, this is budgeted per account")
1703
+ .action(run(async (client) => client.post("/api/v1/email-domains/reply-capture/verify", {})));
1704
+ replyCapture
1705
+ .command("remove")
1706
+ .description("Remove reply capture. Outcome depends on the domain's state. LIVE with a provider-backed setup: it DRAINS — the capture Reply-To stops on new email immediately, but the address keeps receiving for 30 days (drainEndsAt in the response) so late replies to already-sent emails still reach the contact's timeline; after that it parks, and the name is permanently retired (no account can ever set it up again — resuming capture means a NEW subdomain, see suggestedDomain on `reply-capture status`). LIVE legacy row with no stored provider id: removed immediately with NO drain — reply threading stops at once and the response says providerCleanup='manual' (an operator finishes the provider side); the name stays reusable. DEGRADED: drain lifecycle and permanent retirement, but receiving was already broken, so no 30-day delivery guarantee. PENDING setup, a domain the provider has already lost, or an interrupted-removal retry (cleanupPending): torn down immediately, the row is deleted, and the name stays reusable. Sending-domain removal stays blocked by every capture row except a released tombstone — pending/cleanup rows clear right here, but a draining/parked row keeps blocking until support releases it after the drain. Requires --confirm REMOVE")
1707
+ // A VALUE, not a boolean flag: `--confirm false` on a boolean would read as
1708
+ // true and delete anyway (same footgun the GDPR forget command guards).
1709
+ .allowExcessArguments(false)
1710
+ .option("--confirm <word>", "required — pass exactly REMOVE to authorize the removal (a provider-backed live/degraded name is permanently retired)")
1711
+ .action(run(async (client, opts) => {
1712
+ if (opts.confirm !== "REMOVE") {
1713
+ fail("Refusing to remove: pass --confirm REMOVE to authorize this. Sending stops immediately, and a provider-backed live or degraded domain's name is permanently retired — there is no undo on the retirement (a legacy id-less live row instead stops threading at once).");
1714
+ }
1715
+ return client.del("/api/v1/email-domains/reply-capture");
1716
+ }));
1669
1717
  // ---- lifecycle (master lead status + intake deals) ----
1670
1718
  const lifecycle = program
1671
1719
  .command("lifecycle")
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "conduyt",
3
- "version": "1.6.0",
4
- "description": "Command-line interface for Conduyt CRM \u2014 manage contacts, deals, pipelines, and run insight queries from your terminal.",
3
+ "version": "1.8.0",
4
+ "description": "Command-line interface for Conduyt CRM manage contacts, deals, pipelines, and run insight queries from your terminal.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "bin": {
@@ -48,4 +48,4 @@
48
48
  "tsx": "^4.19.0",
49
49
  "@types/node": "^22.0.0"
50
50
  }
51
- }
51
+ }