conduyt 1.36.0 → 1.38.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
@@ -40,7 +40,13 @@ conduyt insights summary # run an AI insight query
40
40
  conduyt insights saved_report --report "Lead Activity" --period last_7_days # run any saved report as a tool
41
41
  conduyt insights dashboard --dashboard Floor --period today # run every tile of a board
42
42
  conduyt insights report_catalog # what this key can run
43
- conduyt messages transports # what a workflow Send SMS step can speak through today (Twilio numbers, Project Blue lines)
43
+ conduyt messages transports # what a workflow Send SMS step can speak through today (Twilio numbers, Project Blue lines, the WhatsApp sender)
44
+ conduyt whatsapp status # WhatsApp via your Twilio: connected? from which sender
45
+ conduyt whatsapp senders # the senders Twilio lists for the account (ONLINE = connectable)
46
+ conduyt whatsapp connect <senderSid> # connect a sender (verified with Twilio first)
47
+ conduyt whatsapp templates # approved / pending / rejected templates with their {{n}} placeholders
48
+ conduyt dialer voice-drop --contact <id> [--recording "Missed you"] [--if-human play] # a call that plays a Voicemail Drop recording to a machine
49
+ conduyt messages send-sms --contact <id> --transport whatsapp --whatsapp-template HX… --whatsapp-variables '{"1":"Alex"}' --body "…"
44
50
  conduyt privacy export <id> # GDPR data-portability export (owner/admin)
45
51
  conduyt privacy forget <id> --confirm FORGET # GDPR erasure — IRREVERSIBLE, owner only
46
52
  conduyt reply-capture status # inbound reply capture: state, DNS records, provider health
package/dist/index.js CHANGED
@@ -804,8 +804,34 @@ messages
804
804
  }));
805
805
  messages
806
806
  .command("transports")
807
- .description("What a workflow Send SMS step can speak through today (GET /sms/transports): Twilio {connected, numbers = account number + active reps' lines, valid send_sms fromNumber values}, Project Blue {connected, lines held by active reps, valid projectBlueLineId values for smsMode project_blue}, webhook provider count, FromK.ai coming soon (automations scope)")
807
+ .description("What a workflow Send SMS step can speak through today (GET /sms/transports): Twilio {connected, numbers = account number + active reps' lines, valid send_sms fromNumber values}, Project Blue {connected, lines held by active reps, valid projectBlueLineId values for smsMode project_blue}, WhatsApp {connected, sender for smsMode whatsapp}, Voice drop {connected, recordings = the Voicemail Drop catalog for smsMode voice_drop}, webhook provider count, FromK.ai coming soon (automations scope)")
808
808
  .action(run(async (client) => client.get("/api/v1/sms/transports")));
809
+ // ---- WhatsApp via Twilio (2026-09-11) ----
810
+ const whatsapp = program.command("whatsapp").description("WhatsApp via your own Twilio account: a Meta-approved sender, approved templates for outreach, free-form replies within a contact's 24-hour window");
811
+ whatsapp
812
+ .command("status")
813
+ .description("Connection status (GET /settings/whatsapp): connected | not_configured | twilio_required, the sender {phoneNumber, sid} (settings scope)")
814
+ .action(run(async (client) => client.get("/api/v1/settings/whatsapp")));
815
+ whatsapp
816
+ .command("senders")
817
+ .description("The WhatsApp senders Twilio lists under the account's credentials, with status (only ONLINE can be connected) and which one is connected (settings scope)")
818
+ .action(run(async (client) => client.get("/api/v1/settings/whatsapp/senders")));
819
+ whatsapp
820
+ .command("connect <senderSid>")
821
+ .description("Connect the account's WhatsApp sender by its Twilio sid (XE…, see `whatsapp senders`) — verified with Twilio first: on this account and ONLINE; one sender belongs to one Conduyt account (settings scope)")
822
+ .action(run(async (client, senderSid) => {
823
+ if (!/^XE[0-9a-fA-F]{32}$/.test(senderSid.trim()))
824
+ throw new Error("senderSid must be a Twilio WhatsApp sender SID (XE followed by 32 hex characters).");
825
+ return client.put("/api/v1/settings/whatsapp", { senderSid: senderSid.trim() });
826
+ }));
827
+ whatsapp
828
+ .command("disconnect")
829
+ .description("Forget the WhatsApp sender — workflow steps on the WhatsApp channel stop sending (settings scope)")
830
+ .action(run(async (client) => client.del("/api/v1/settings/whatsapp")));
831
+ whatsapp
832
+ .command("templates")
833
+ .description("The account's WhatsApp templates (Twilio Content) with Meta's approval verdict: sid (a send-sms --whatsapp-template value), text with {{n}} placeholders, variables, approval (automations scope)")
834
+ .action(run(async (client) => client.get("/api/v1/sms/whatsapp/templates")));
809
835
  messages
810
836
  .command("send-sms")
811
837
  .description("Send an outbound SMS (POST /messages/sms/send) — via Twilio, or from your own Project Blue iPhone line with --transport project_blue")
@@ -813,7 +839,9 @@ messages
813
839
  .option("--body <text>", "SMS body, 1-1600 chars (required)")
814
840
  .option("--from <number>", "from number — must be an account-owned Twilio number/agent DID")
815
841
  .option("--provider <id>", "smsProviderId UUID")
816
- .option("--transport <transport>", "twilio (default) | project_blue — send from your assigned Project Blue iPhone line (iMessage when the lead is on Apple, SMS otherwise)")
842
+ .option("--transport <transport>", "twilio (default) | project_blue — your assigned Project Blue iPhone line (iMessage when the lead is on Apple, SMS otherwise) | whatsapp — the account's WhatsApp sender (free-form only within 24 hours of the lead's last WhatsApp message, else an approved template via --whatsapp-template)")
843
+ .option("--whatsapp-template <sid>", "transport whatsapp: an APPROVED template (Twilio Content SID HX…, see `whatsapp templates`); Conduyt reads it from Twilio at send time and records its text with the variables in place")
844
+ .option("--whatsapp-variables <json>", 'transport whatsapp with a template: placeholder number → value as JSON, e.g. {"1":"Alex"}')
817
845
  .option("--idempotency-key <key>", "operation key (8-200 chars) for at-most-once sends: reuse the SAME key when retrying a send whose outcome you did not see")
818
846
  .option("--json <json>", "full JSON body, merged over the flags")
819
847
  .action(run(async (client, opts) => {
@@ -828,6 +856,10 @@ messages
828
856
  body.smsProviderId = opts.provider;
829
857
  if (opts.transport !== undefined)
830
858
  body.transport = opts.transport;
859
+ if (opts.whatsappTemplate !== undefined)
860
+ body.whatsappContentSid = opts.whatsappTemplate;
861
+ if (opts.whatsappVariables !== undefined)
862
+ body.whatsappContentVariables = jsonArg(opts.whatsappVariables, "--whatsapp-variables");
831
863
  if (opts.idempotencyKey !== undefined)
832
864
  body.idempotencyKey = opts.idempotencyKey;
833
865
  if (opts.json !== undefined)
@@ -1805,6 +1837,23 @@ program
1805
1837
  }));
1806
1838
  // ---- dialer ----
1807
1839
  const dialer = program.command("dialer").description("Dialer operations");
1840
+ dialer
1841
+ .command("voice-drop")
1842
+ .description("Place a voice drop to a contact (POST /calls/voice-drop): an outbound Twilio call with answering-machine detection that plays one of the account's Voicemail Drop recordings (Settings → Twilio → Voicemail Drop; names from `messages transports` voiceDrop.recordings) when a machine answers, and hangs up quietly when a person answers unless --if-human play. Governed like a call: calls-tier consent (a texting STOP does not block it, a DNC or litigator row does), calling hours, one live call per lead. Placed from your own Twilio line (else the account number) unless --from names a number the account owns. Returns 202 with the Call row; the outcome lands on it through the voice webhooks (disposition voicemail_drop = played to a machine, voice_drop_human = a person answered, voice_drop_live = played to a person). Refusals carry a code (dnc, litigator, calling_window, active_call, voice_drop_recording_missing, voice_drop_not_configured); 429 and 502 are safe to retry (dialer scope)")
1843
+ .requiredOption("--contact <id>", "contact UUID (the drop calls the contact's primary number)")
1844
+ .option("--recording <name>", "a Voicemail Drop template name; omit for the default recording")
1845
+ .option("--from <number>", "caller ID the account owns (E.164); omit for your own line")
1846
+ .option("--if-human <rule>", "hang_up (default) | play — what the call does when a person answers")
1847
+ .action(run(async (client, opts) => {
1848
+ const body = { contactId: opts.contact };
1849
+ if (opts.recording !== undefined)
1850
+ body.recording = opts.recording;
1851
+ if (opts.from !== undefined)
1852
+ body.fromNumber = opts.from;
1853
+ if (opts.ifHuman !== undefined)
1854
+ body.onHuman = opts.ifHuman;
1855
+ return client.post("/api/v1/calls/voice-drop", body);
1856
+ }));
1808
1857
  dialer
1809
1858
  .command("sync-local-presence")
1810
1859
  .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")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "conduyt",
3
- "version": "1.36.0",
3
+ "version": "1.38.0",
4
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",