conduyt 1.11.0 → 1.14.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/dist/client.js CHANGED
@@ -50,6 +50,9 @@ export class ConduytClient {
50
50
  patch(path, body) {
51
51
  return this.request("PATCH", path, body);
52
52
  }
53
+ put(path, body) {
54
+ return this.request("PUT", path, body);
55
+ }
53
56
  del(path) {
54
57
  return this.request("DELETE", path);
55
58
  }
package/dist/index.js CHANGED
@@ -81,6 +81,7 @@ contacts
81
81
  .option("--last <name>", "last name")
82
82
  .option("--email <email>", "email")
83
83
  .option("--phone <phone>", "phone")
84
+ .option("--timezone <iana>", "IANA timezone of the lead (e.g. America/Chicago); omit to let Conduyt derive it from ZIP → phone area code → state. On update pass '' to clear the override")
84
85
  .option("--company <company>", "company name (auto-creates/links a company)")
85
86
  .option("--source <source>", "lead source (first-write-wins acquisition truth)")
86
87
  .option("--master-status <status>", "lead lifecycle status: open|won|lost|abandoned|disqualified or an account custom (terminal statuses silence all outbound automation)")
@@ -95,6 +96,8 @@ contacts
95
96
  body.email = opts.email;
96
97
  if (opts.phone !== undefined)
97
98
  body.phone = opts.phone;
99
+ if (opts.timezone !== undefined)
100
+ body.timezone = opts.timezone === "" ? null : opts.timezone;
98
101
  if (opts.company !== undefined)
99
102
  body.company = opts.company;
100
103
  if (opts.source !== undefined) {
@@ -123,6 +126,7 @@ contacts
123
126
  .option("--last <name>", "last name")
124
127
  .option("--email <email>", "email")
125
128
  .option("--phone <phone>", "phone")
129
+ .option("--timezone <iana>", "IANA timezone of the lead (e.g. America/Chicago); omit to let Conduyt derive it from ZIP → phone area code → state. On update pass '' to clear the override")
126
130
  .option("--company <company>", "company name (auto-creates/links a company; pass '' to clear)")
127
131
  .option("--source <source>", "correct the lead's first-touch source (normally set once at creation — update only to fix bad attribution; pass 'null' to clear)")
128
132
  .option("--master-status <status>", "lead lifecycle status: open|won|lost|abandoned|disqualified or an account custom (terminal statuses silence all outbound automation)")
@@ -138,6 +142,8 @@ contacts
138
142
  body.email = opts.email;
139
143
  if (opts.phone !== undefined)
140
144
  body.phone = opts.phone;
145
+ if (opts.timezone !== undefined)
146
+ body.timezone = opts.timezone === "" ? null : opts.timezone;
141
147
  if (opts.company !== undefined)
142
148
  body.company = opts.company;
143
149
  if (opts.source !== undefined) {
@@ -769,11 +775,13 @@ messages
769
775
  }));
770
776
  messages
771
777
  .command("send-sms")
772
- .description("Send an outbound SMS via Twilio (POST /messages/sms/send)")
778
+ .description("Send an outbound SMS (POST /messages/sms/send) — via Twilio, or from your own Project Blue iPhone line with --transport project_blue")
773
779
  .option("--contact <id>", "contact UUID (required)")
774
780
  .option("--body <text>", "SMS body, 1-1600 chars (required)")
775
781
  .option("--from <number>", "from number — must be an account-owned Twilio number/agent DID")
776
782
  .option("--provider <id>", "smsProviderId UUID")
783
+ .option("--transport <transport>", "twilio (default) | project_blue — send from your assigned Project Blue iPhone line (iMessage when the lead is on Apple, SMS otherwise)")
784
+ .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")
777
785
  .option("--json <json>", "full JSON body, merged over the flags")
778
786
  .action(run(async (client, opts) => {
779
787
  const body = {};
@@ -785,6 +793,10 @@ messages
785
793
  body.fromNumber = opts.from;
786
794
  if (opts.provider !== undefined)
787
795
  body.smsProviderId = opts.provider;
796
+ if (opts.transport !== undefined)
797
+ body.transport = opts.transport;
798
+ if (opts.idempotencyKey !== undefined)
799
+ body.idempotencyKey = opts.idempotencyKey;
788
800
  if (opts.json !== undefined)
789
801
  Object.assign(body, jsonArg(opts.json, "--json"));
790
802
  return client.post("/api/v1/messages/sms/send", body);
@@ -972,6 +984,10 @@ automations
972
984
  .command("schema")
973
985
  .description("Get the node/field/operator schema catalog (node types, action rules, graph shape)")
974
986
  .action(run(async (client) => client.get("/api/v1/automations/schema")));
987
+ automations
988
+ .command("events")
989
+ .description("List trigger events with their labels, payload fields and documented scalar conditions (e.g. contact.untouched → untouchedDays: 1-365)")
990
+ .action(run(async (client) => client.get("/api/v1/automations/events")));
975
991
  automations
976
992
  .command("resolve")
977
993
  .description("Resolve human-readable names to UUIDs. Pass comma lists via flags and/or a full JSON body")
@@ -1827,6 +1843,34 @@ lifecycle
1827
1843
  throw new Error(`Invalid status name '${bad}' — 1-40 chars; letters, numbers, spaces, - and _ only.`);
1828
1844
  return client.patch("/api/v1/settings", { masterStatuses: clean });
1829
1845
  }));
1846
+ // ---- account default columns for the Contacts list (tri-surface parity, 2026-09-03) ----
1847
+ const CONTACT_COLUMN_IDS = ["name", "email", "phone", "company", "source", "tags", "score", "intent", "stage", "createdAt", "lastActivity", "assignedTo", "address", "city", "state", "zip"];
1848
+ lifecycle
1849
+ .command("get-contact-columns")
1850
+ .description("Show the account-level DEFAULT COLUMNS for the Contacts list (every user gets them merged into their own layout on their next visit; each version once). null = no account default.")
1851
+ .action(run(async (client) => {
1852
+ const result = (await client.get("/api/v1/settings"));
1853
+ const raw = result?.data?.contactTableDefaults;
1854
+ return { data: raw && typeof raw === "object" ? raw : null, columnIds: [...CONTACT_COLUMN_IDS, "custom:<fieldKey>"] };
1855
+ }));
1856
+ lifecycle
1857
+ .command("set-contact-columns [ids...]")
1858
+ .description("Set the account-level DEFAULT COLUMNS for the Contacts list (settings scope). Every user's saved layout gains the ids it lacks on their next visit (appended; nothing moves); the version bumps only when the set changes. Ids: the standard column ids or custom:<fieldKey>; up to 10. --clear removes the default.")
1859
+ .option("--clear", "remove the account default (users keep what they already show)")
1860
+ .action(run(async (client, ids, opts) => {
1861
+ ids = ids ?? [];
1862
+ if (ids.length === 0 && !opts.clear)
1863
+ throw new Error("Pass at least one column id, or --clear to remove the account default.");
1864
+ if (ids.length > 0 && opts.clear)
1865
+ throw new Error("--clear cannot be combined with column ids.");
1866
+ const clean = ids.map((s) => s.trim());
1867
+ if (clean.length > 8)
1868
+ throw new Error("At most 8 default columns (the table keeps its two anchors).");
1869
+ const bad = clean.find((id) => !CONTACT_COLUMN_IDS.includes(id) && !/^custom:[A-Za-z0-9_][A-Za-z0-9_.-]{0,99}$/.test(id));
1870
+ if (bad)
1871
+ throw new Error(`Unknown column id '${bad}' — one of ${CONTACT_COLUMN_IDS.join(", ")} or custom:<fieldKey>.`);
1872
+ return client.patch("/api/v1/settings", { contactTableDefaults: { columns: clean } });
1873
+ }));
1830
1874
  // ---- call flows + ring groups (#25 tri-surface parity, 2026-08-23) ----
1831
1875
  // IVR routing graphs and their companion ring sets. Settings-scoped at the
1832
1876
  // API-key boundary (same tier as the rest of the Twilio setup surface): a
@@ -1845,7 +1889,7 @@ callFlows
1845
1889
  .command("create <name>")
1846
1890
  .description("Create a call flow (starts as a DRAFT)")
1847
1891
  .option("--phone-number <e164>", "account number this flow answers")
1848
- .option("--graph <json>", "initial flow graph (node/edge JSON)")
1892
+ .option("--graph <json>", "initial flow graph (node/edge JSON) Kinds: entry, greeting, agent, group (queue.repeatRing = queue mode), menu (options[{digit,label,next}]), function (functionId, timeoutSeconds 2-8, next/failNext), set_attributes (assignments[{key,value}]), branch (attribute, op, value, next/elseNext), timing (hours, timezone, next/closedNext), voicemail, forward, hangup; every branch must reach an ending.")
1849
1893
  .action(run(async (client, name, opts) => {
1850
1894
  const body = { name };
1851
1895
  if (opts.phoneNumber !== undefined)
@@ -1859,7 +1903,7 @@ callFlows
1859
1903
  .description("Update the DRAFT (published behavior changes only on the next publish)")
1860
1904
  .option("--name <name>", "new name")
1861
1905
  .option("--phone-number <e164>", "new number")
1862
- .option("--graph <json>", "replacement draft graph (node/edge JSON) — REQUIRES --expected-draft-revision")
1906
+ .option("--graph <json>", "replacement draft graph (node/edge JSON) — REQUIRES --expected-draft-revision. Kinds: entry, greeting, agent, group (queue.repeatRing = queue mode), menu (options[{digit,label,next}]), function (functionId, timeoutSeconds 2-8, next/failNext), set_attributes (assignments[{key,value}]), branch (attribute, op, value, next/elseNext), timing (hours, timezone, next/closedNext), voicemail, forward, hangup; every branch must reach an ending")
1863
1907
  .option("--expected-draft-revision <n>", "draftRevision from your last `call-flows get` read (REQUIRED with --graph; the API 428s without it — it is graph-specific, so calls/publish never invalidate it)")
1864
1908
  .action(run(async (client, id, opts) => {
1865
1909
  assertUuid(id, "call flow id");
@@ -1916,6 +1960,46 @@ callFlows
1916
1960
  .command("simulate <id>")
1917
1961
  .description('"If someone calls right now, who picks up?" — DRAFT graph vs LIVE availability, no side effects')
1918
1962
  .action(run(async (client, id) => { assertUuid(id, "call flow id"); return client.post(`/api/v1/call-flows/${encodeURIComponent(id)}/simulate`, {}); }));
1963
+ // #32 Call Flows function endpoints (Amazon Connect "Invoke Lambda" parity): the
1964
+ // HTTPS endpoints a flow's "Call a function" step may invoke mid-call.
1965
+ const callFlowFunctions = callFlows.command("functions").description("Function endpoints a \"Call a function\" step can invoke mid-call (settings scope; admin to change)");
1966
+ callFlowFunctions
1967
+ .command("list")
1968
+ .description("List the account's function endpoints")
1969
+ .action(run(async (client) => client.get("/api/v1/call-flows/functions")));
1970
+ callFlowFunctions
1971
+ .command("create <name>")
1972
+ .description("Register a function endpoint. Conduyt POSTs a signed JSON call snapshot (X-Conduyt-Signature = sha256 HMAC of `${timestamp}.${body}`, X-Conduyt-Timestamp, Idempotency-Key); reply 200 with a JSON object — top-level strings/numbers/booleans become call attributes. https + public host only, no redirects, 2-8 s timeout")
1973
+ .requiredOption("--url <https-url>", "HTTPS endpoint (ports 443/8443)")
1974
+ .requiredOption("--secret <secret>", "HMAC signing secret (16-200 chars) the endpoint verifies with")
1975
+ .action(run(async (client, name, opts) => client.post("/api/v1/call-flows/functions", { name, url: opts.url, secret: opts.secret })));
1976
+ callFlowFunctions
1977
+ .command("update <id>")
1978
+ .description("Rename, change the URL, or rotate the secret")
1979
+ .option("--name <name>", "new name")
1980
+ .option("--url <https-url>", "new HTTPS endpoint")
1981
+ .option("--secret <secret>", "new signing secret (rotation)")
1982
+ .action(run(async (client, id, opts) => {
1983
+ assertUuid(id, "function id");
1984
+ const body = {};
1985
+ if (opts.name !== undefined)
1986
+ body.name = opts.name;
1987
+ if (opts.url !== undefined)
1988
+ body.url = opts.url;
1989
+ if (opts.secret !== undefined)
1990
+ body.secret = opts.secret;
1991
+ if (Object.keys(body).length === 0)
1992
+ throw new Error("Nothing to update. Pass at least one flag.");
1993
+ return client.patch(`/api/v1/call-flows/functions/${encodeURIComponent(id)}`, body);
1994
+ }));
1995
+ callFlowFunctions
1996
+ .command("delete <id>")
1997
+ .description("Remove a function endpoint (409 while a PUBLISHED flow still calls it)")
1998
+ .action(run(async (client, id) => { assertUuid(id, "function id"); return client.del(`/api/v1/call-flows/functions/${encodeURIComponent(id)}`); }));
1999
+ callFlowFunctions
2000
+ .command("test <id>")
2001
+ .description("Send a real signed test call (callSid TEST, sample caller) and report status, latency and the attributes it would save")
2002
+ .action(run(async (client, id) => { assertUuid(id, "function id"); return client.post(`/api/v1/call-flows/functions/${encodeURIComponent(id)}/test`, {}); }));
1919
2003
  callFlows
1920
2004
  .command("roster")
1921
2005
  .description("Routing roster: members with DIDs + live availability")
@@ -2026,6 +2110,104 @@ ringGroups
2026
2110
  }
2027
2111
  return client.post("/api/v1/ring-groups/release-number", body);
2028
2112
  }));
2113
+ // ---------------------------------------------------------------------------
2114
+ // Smart Views + Smart Dialing (#53). The dial-enabled Smart Views are the
2115
+ // Smart Dialing priorities: agents dial them one lead at a time, top to
2116
+ // bottom. Dialing settings + reorder are admin-only in the API.
2117
+ // ---------------------------------------------------------------------------
2118
+ const smartViews = program.command("smart-views").description("Smart Views and their Smart Dialing priorities");
2119
+ smartViews
2120
+ .command("list")
2121
+ .description("List Smart Views in sidebar order (= Smart Dialing priority) with lead counts and dialing settings")
2122
+ .option("--dialing", "only the dial-enabled views (the Smart Dialing priorities)")
2123
+ .action(run(async (client, opts) => {
2124
+ const views = (await client.get("/api/v1/smart-views"));
2125
+ const rows = Array.isArray(views) ? views : [];
2126
+ const sorted = [...rows].sort((a, b) => Number(a.sortPosition ?? 0) - Number(b.sortPosition ?? 0));
2127
+ return opts.dialing ? sorted.filter((v) => v.dialEnabled === true) : sorted;
2128
+ }));
2129
+ smartViews
2130
+ .command("dialing <id>")
2131
+ .description("Change one Smart View's Smart Dialing settings (PATCH — only the flags you pass change)")
2132
+ .option("--enable", "make this view a Smart Dialing priority")
2133
+ .option("--disable", "stop dialing this view")
2134
+ .option("--max-attempts <n>", "stop after N calls to a lead, ever (1-20)")
2135
+ .option("--per-day <n>", "most calls to one lead per account-local day (1-20); pass 'none' to remove the daily cap")
2136
+ .option("--cooldown <minutes>", "minutes to wait between calls to the same lead (0-10080)")
2137
+ .option("--schedule <json>", 'retry schedule JSON, e.g. \'[{"attemptNumber":1,"delayMinutes":0},{"attemptNumber":2,"delayMinutes":60}]\'')
2138
+ .action(run(async (client, id, opts) => {
2139
+ assertUuid(id, "smart view id");
2140
+ if (opts.enable && opts.disable)
2141
+ throw new Error("Pass --enable or --disable, not both.");
2142
+ const body = {};
2143
+ if (opts.enable)
2144
+ body.dialEnabled = true;
2145
+ if (opts.disable)
2146
+ body.dialEnabled = false;
2147
+ const intFlag = (raw, flag, min, max) => {
2148
+ if (raw === undefined)
2149
+ return undefined;
2150
+ const n = Number(raw);
2151
+ if (!Number.isInteger(n) || n < min || n > max)
2152
+ throw new Error(`${flag} must be an integer between ${min} and ${max}.`);
2153
+ return n;
2154
+ };
2155
+ const maxAttempts = intFlag(opts.maxAttempts, "--max-attempts", 1, 20);
2156
+ if (maxAttempts !== undefined)
2157
+ body.maxDialAttempts = maxAttempts;
2158
+ if (opts.perDay !== undefined) {
2159
+ body.maxDialAttemptsPerDay = opts.perDay.trim().toLowerCase() === "none" ? null : intFlag(opts.perDay, "--per-day", 1, 20);
2160
+ }
2161
+ const cooldown = intFlag(opts.cooldown, "--cooldown", 0, 10080);
2162
+ if (cooldown !== undefined)
2163
+ body.dialCooldownMinutes = cooldown;
2164
+ if (opts.schedule !== undefined) {
2165
+ let parsed;
2166
+ try {
2167
+ parsed = JSON.parse(opts.schedule);
2168
+ }
2169
+ catch {
2170
+ throw new Error("--schedule must be valid JSON.");
2171
+ }
2172
+ if (!Array.isArray(parsed) || parsed.length === 0 || parsed.length > 20)
2173
+ throw new Error("--schedule must be a JSON array of 1-20 steps.");
2174
+ for (const step of parsed) {
2175
+ if (!step || !Number.isInteger(step.attemptNumber) || Number(step.attemptNumber) < 1 || !Number.isInteger(step.delayMinutes) || Number(step.delayMinutes) < 0) {
2176
+ throw new Error("Each --schedule step needs attemptNumber (>= 1) and delayMinutes (>= 0).");
2177
+ }
2178
+ }
2179
+ body.dialSchedule = parsed;
2180
+ }
2181
+ if (Object.keys(body).length === 0)
2182
+ throw new Error("Nothing to change. Pass --enable/--disable, --max-attempts, --per-day, --cooldown or --schedule.");
2183
+ return client.patch(`/api/v1/smart-views/${encodeURIComponent(id)}`, body);
2184
+ }));
2185
+ smartViews
2186
+ .command("dial-order [ids...]")
2187
+ .description("No ids: show the Smart Dialing priorities, uncapped (every dial-enabled view in dial order + candidates). With ids: set the priority order — every dial-enabled view id, first = dialed first (the server keeps non-priority views in their slots)")
2188
+ .action(run(async (client, ids) => {
2189
+ if (!ids || ids.length === 0)
2190
+ return client.get("/api/v1/smart-views/dial-order");
2191
+ if (ids.length > 2000)
2192
+ throw new Error("Pass at most 2000 Smart View ids.");
2193
+ if (new Set(ids).size !== ids.length)
2194
+ throw new Error("Smart View ids must not repeat.");
2195
+ for (const id of ids)
2196
+ assertUuid(id, "smart view id");
2197
+ return client.patch("/api/v1/smart-views/dial-order", { order: ids });
2198
+ }));
2199
+ smartViews
2200
+ .command("reorder <ids...>")
2201
+ .description("Set the SIDEBAR order of Smart Views. Pass EVERY view id; views left out keep a stale position. For the dialing priority use dial-order")
2202
+ .action(run(async (client, ids) => {
2203
+ if (ids.length === 0 || ids.length > 200)
2204
+ throw new Error("Pass 1-200 Smart View ids.");
2205
+ if (new Set(ids).size !== ids.length)
2206
+ throw new Error("Smart View ids must not repeat.");
2207
+ for (const id of ids)
2208
+ assertUuid(id, "smart view id");
2209
+ return client.patch("/api/v1/smart-views/reorder", { order: ids });
2210
+ }));
2029
2211
  program.parseAsync(process.argv).catch(fail);
2030
2212
  // helpers
2031
2213
  // Parse a --json / --stages style argument, throwing a clear CLI error (caught
@@ -2064,6 +2246,44 @@ function assertFiniteValue(v) {
2064
2246
  // The path is also URL-encoded at the call site as defence in depth. The regex
2065
2247
  // is inlined (not a module-level const) so it isn't in the temporal dead zone
2066
2248
  // when an action callback runs during program.parse().
2249
+ const projectBlue = program.command("project-blue").description("Project Blue: dedicated iPhone lines (iMessage/SMS + FaceTime Audio), one line per agent (settings scope)");
2250
+ projectBlue
2251
+ .command("status")
2252
+ .description("Connection status (key masked) + current line assignments")
2253
+ .action(run(async (client) => client.get("/api/v1/settings/project-blue")));
2254
+ projectBlue
2255
+ .command("connect")
2256
+ .description("Connect or replace the account's Project Blue API key — verified with the vendor before it is stored")
2257
+ .option("--key <apiKey>", "Project Blue API key (proj_…); omit to read PROJECT_BLUE_API_KEY from the environment and keep the key out of shell history")
2258
+ .action(run(async (client, opts) => {
2259
+ const apiKey = (opts.key ?? process.env.PROJECT_BLUE_API_KEY ?? "").trim();
2260
+ if (!apiKey)
2261
+ throw new Error("Pass --key or set PROJECT_BLUE_API_KEY.");
2262
+ return client.put("/api/v1/settings/project-blue", { apiKey });
2263
+ }));
2264
+ projectBlue
2265
+ .command("disconnect")
2266
+ .description("Forget the API key and release every agent's line")
2267
+ .action(run(async (client) => client.del("/api/v1/settings/project-blue")));
2268
+ projectBlue
2269
+ .command("lines")
2270
+ .description("LIVE line inventory from the vendor with each line's assigned agent, plus assignments the vendor no longer lists (orphaned)")
2271
+ .action(run(async (client) => client.get("/api/v1/settings/project-blue/lines")));
2272
+ projectBlue
2273
+ .command("assign <lineId> <userId>")
2274
+ .description("Give a line to an agent (one line per agent, one agent per line — prior holders are released and reported)")
2275
+ .action(run(async (client, lineId, userId) => {
2276
+ assertUuid(lineId, "line id");
2277
+ assertUuid(userId, "user id");
2278
+ return client.put(`/api/v1/settings/project-blue/lines/${encodeURIComponent(lineId)}`, { userId });
2279
+ }));
2280
+ projectBlue
2281
+ .command("release <lineId>")
2282
+ .description("Release a line from whoever holds it")
2283
+ .action(run(async (client, lineId) => {
2284
+ assertUuid(lineId, "line id");
2285
+ return client.put(`/api/v1/settings/project-blue/lines/${encodeURIComponent(lineId)}`, { userId: null });
2286
+ }));
2067
2287
  function assertUuid(id, label = "id") {
2068
2288
  const uuidRe = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
2069
2289
  if (!uuidRe.test(id.trim())) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "conduyt",
3
- "version": "1.11.0",
3
+ "version": "1.14.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",