conduyt 1.9.0 → 1.11.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/index.js +252 -0
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1362,6 +1362,12 @@ users
|
|
|
1362
1362
|
.option("--email <email>", "email")
|
|
1363
1363
|
.option("--role <role>", "member | admin | owner")
|
|
1364
1364
|
.option("--phone <phone>", "phone")
|
|
1365
|
+
.option("--position <text>", "rep-profile position/title, merge-taggable as {{user.position}} (pass '' to clear)")
|
|
1366
|
+
.option("--mobile <phone>", "rep-profile mobile phone, merge-taggable as {{user.mobilePhone}} (pass '' to clear)")
|
|
1367
|
+
.option("--custom1 <text>", "rep-profile custom 1, {{user.custom1}} (pass '' to clear)")
|
|
1368
|
+
.option("--custom2 <text>", "rep-profile custom 2, {{user.custom2}} (pass '' to clear)")
|
|
1369
|
+
.option("--custom3 <text>", "rep-profile custom 3, {{user.custom3}} (pass '' to clear)")
|
|
1370
|
+
.option("--custom4 <text>", "rep-profile custom 4, {{user.custom4}} (pass '' to clear)")
|
|
1365
1371
|
.option("--json <json>", "full JSON body, merged over the flags")
|
|
1366
1372
|
.action(run(async (client, id, opts) => {
|
|
1367
1373
|
assertUuid(id, "user id");
|
|
@@ -1376,6 +1382,20 @@ users
|
|
|
1376
1382
|
body.role = opts.role;
|
|
1377
1383
|
if (opts.phone !== undefined)
|
|
1378
1384
|
body.phone = opts.phone;
|
|
1385
|
+
// Rep-profile fields (#42): an explicit empty string clears the field
|
|
1386
|
+
// (the API treats blank as null); omitted flags leave it untouched.
|
|
1387
|
+
if (opts.position !== undefined)
|
|
1388
|
+
body.position = opts.position || null;
|
|
1389
|
+
if (opts.mobile !== undefined)
|
|
1390
|
+
body.mobilePhone = opts.mobile || null;
|
|
1391
|
+
if (opts.custom1 !== undefined)
|
|
1392
|
+
body.custom1 = opts.custom1 || null;
|
|
1393
|
+
if (opts.custom2 !== undefined)
|
|
1394
|
+
body.custom2 = opts.custom2 || null;
|
|
1395
|
+
if (opts.custom3 !== undefined)
|
|
1396
|
+
body.custom3 = opts.custom3 || null;
|
|
1397
|
+
if (opts.custom4 !== undefined)
|
|
1398
|
+
body.custom4 = opts.custom4 || null;
|
|
1379
1399
|
if (opts.json !== undefined)
|
|
1380
1400
|
Object.assign(body, jsonArg(opts.json, "--json"));
|
|
1381
1401
|
if (Object.keys(body).length === 0)
|
|
@@ -1807,6 +1827,205 @@ lifecycle
|
|
|
1807
1827
|
throw new Error(`Invalid status name '${bad}' — 1-40 chars; letters, numbers, spaces, - and _ only.`);
|
|
1808
1828
|
return client.patch("/api/v1/settings", { masterStatuses: clean });
|
|
1809
1829
|
}));
|
|
1830
|
+
// ---- call flows + ring groups (#25 tri-surface parity, 2026-08-23) ----
|
|
1831
|
+
// IVR routing graphs and their companion ring sets. Settings-scoped at the
|
|
1832
|
+
// API-key boundary (same tier as the rest of the Twilio setup surface): a
|
|
1833
|
+
// dialer-only key gets 403 here. The flow graph (--graph) is the node/edge
|
|
1834
|
+
// JSON the API validates — `validate` previews problems without publishing.
|
|
1835
|
+
const callFlows = program.command("call-flows").description("Call flows: the IVR routing graphs inbound numbers execute (settings scope)");
|
|
1836
|
+
callFlows
|
|
1837
|
+
.command("list")
|
|
1838
|
+
.description("List call flows with status (draft/published) and attached numbers")
|
|
1839
|
+
.action(run(async (client) => client.get("/api/v1/call-flows")));
|
|
1840
|
+
callFlows
|
|
1841
|
+
.command("get <id>")
|
|
1842
|
+
.description("Get one call flow incl. draft + published graphs")
|
|
1843
|
+
.action(run(async (client, id) => { assertUuid(id, "call flow id"); return client.get(`/api/v1/call-flows/${encodeURIComponent(id)}`); }));
|
|
1844
|
+
callFlows
|
|
1845
|
+
.command("create <name>")
|
|
1846
|
+
.description("Create a call flow (starts as a DRAFT)")
|
|
1847
|
+
.option("--phone-number <e164>", "account number this flow answers")
|
|
1848
|
+
.option("--graph <json>", "initial flow graph (node/edge JSON)")
|
|
1849
|
+
.action(run(async (client, name, opts) => {
|
|
1850
|
+
const body = { name };
|
|
1851
|
+
if (opts.phoneNumber !== undefined)
|
|
1852
|
+
body.phoneNumber = opts.phoneNumber;
|
|
1853
|
+
if (opts.graph !== undefined)
|
|
1854
|
+
body.draftGraph = graphArg(opts.graph);
|
|
1855
|
+
return client.post("/api/v1/call-flows", body);
|
|
1856
|
+
}));
|
|
1857
|
+
callFlows
|
|
1858
|
+
.command("update <id>")
|
|
1859
|
+
.description("Update the DRAFT (published behavior changes only on the next publish)")
|
|
1860
|
+
.option("--name <name>", "new name")
|
|
1861
|
+
.option("--phone-number <e164>", "new number")
|
|
1862
|
+
.option("--graph <json>", "replacement draft graph (node/edge JSON) — REQUIRES --expected-draft-revision")
|
|
1863
|
+
.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
|
+
.action(run(async (client, id, opts) => {
|
|
1865
|
+
assertUuid(id, "call flow id");
|
|
1866
|
+
const body = {};
|
|
1867
|
+
if (opts.name !== undefined)
|
|
1868
|
+
body.name = opts.name;
|
|
1869
|
+
if (opts.phoneNumber !== undefined)
|
|
1870
|
+
body.phoneNumber = opts.phoneNumber;
|
|
1871
|
+
if (opts.graph !== undefined) {
|
|
1872
|
+
if (opts.expectedDraftRevision === undefined) {
|
|
1873
|
+
throw new Error("Replacing --graph requires --expected-draft-revision (the draftRevision you read the flow at) — `call-flows get` first.");
|
|
1874
|
+
}
|
|
1875
|
+
body.draftGraph = graphArg(opts.graph);
|
|
1876
|
+
}
|
|
1877
|
+
if (opts.expectedDraftRevision !== undefined) {
|
|
1878
|
+
// Strict decimal form BEFORE Number(): Number("") is 0, and a newly
|
|
1879
|
+
// created flow's revision IS 0 — an unset shell variable must refuse
|
|
1880
|
+
// locally, never perform a real graph replacement.
|
|
1881
|
+
const raw = String(opts.expectedDraftRevision).trim();
|
|
1882
|
+
const n = /^\d+$/.test(raw) ? Number(raw) : NaN;
|
|
1883
|
+
if (!Number.isSafeInteger(n) || n < 0) {
|
|
1884
|
+
throw new Error("--expected-draft-revision must be the non-negative integer draftRevision from your last read.");
|
|
1885
|
+
}
|
|
1886
|
+
body.expectedDraftRevision = n;
|
|
1887
|
+
}
|
|
1888
|
+
if (Object.keys(body).length === 0)
|
|
1889
|
+
throw new Error("Nothing to update. Pass at least one flag.");
|
|
1890
|
+
return client.patch(`/api/v1/call-flows/${encodeURIComponent(id)}`, body);
|
|
1891
|
+
}));
|
|
1892
|
+
callFlows
|
|
1893
|
+
.command("delete <id>")
|
|
1894
|
+
.description("Delete a call flow. A PUBLISHED flow refuses deletion (409) — run `call-flows unpublish` first")
|
|
1895
|
+
.action(run(async (client, id) => { assertUuid(id, "call flow id"); return client.del(`/api/v1/call-flows/${encodeURIComponent(id)}`); }));
|
|
1896
|
+
callFlows
|
|
1897
|
+
.command("validate <id>")
|
|
1898
|
+
.description("Validate the draft graph WITHOUT publishing — structured problems")
|
|
1899
|
+
.action(run(async (client, id) => { assertUuid(id, "call flow id"); return client.get(`/api/v1/call-flows/${encodeURIComponent(id)}/validate`); }));
|
|
1900
|
+
callFlows
|
|
1901
|
+
.command("publish <id>")
|
|
1902
|
+
.description("Publish the draft (refuses an invalid graph). REQUIRES --expected-updated-at (the API 428s without it): `call-flows get` first, pass its updatedAt — a concurrent edit then 409s instead of being published over")
|
|
1903
|
+
.requiredOption("--expected-updated-at <iso>", "updatedAt from your last `call-flows get` read (REQUIRED)")
|
|
1904
|
+
.action(run(async (client, id, opts) => {
|
|
1905
|
+
assertUuid(id, "call flow id");
|
|
1906
|
+
if (Number.isNaN(Date.parse(opts.expectedUpdatedAt))) {
|
|
1907
|
+
throw new Error("--expected-updated-at must be the ISO updatedAt timestamp from your last read.");
|
|
1908
|
+
}
|
|
1909
|
+
return client.post(`/api/v1/call-flows/${encodeURIComponent(id)}/publish`, { expectedUpdatedAt: opts.expectedUpdatedAt });
|
|
1910
|
+
}));
|
|
1911
|
+
callFlows
|
|
1912
|
+
.command("unpublish <id>")
|
|
1913
|
+
.description("Unpublish — the number falls back to default routing")
|
|
1914
|
+
.action(run(async (client, id) => { assertUuid(id, "call flow id"); return client.post(`/api/v1/call-flows/${encodeURIComponent(id)}/unpublish`, {}); }));
|
|
1915
|
+
callFlows
|
|
1916
|
+
.command("simulate <id>")
|
|
1917
|
+
.description('"If someone calls right now, who picks up?" — DRAFT graph vs LIVE availability, no side effects')
|
|
1918
|
+
.action(run(async (client, id) => { assertUuid(id, "call flow id"); return client.post(`/api/v1/call-flows/${encodeURIComponent(id)}/simulate`, {}); }));
|
|
1919
|
+
callFlows
|
|
1920
|
+
.command("roster")
|
|
1921
|
+
.description("Routing roster: members with DIDs + live availability")
|
|
1922
|
+
.action(run(async (client) => client.get("/api/v1/call-flows/roster")));
|
|
1923
|
+
const ringGroups = program.command("ring-groups").description("Ring groups: simultaneous/sequential ring sets over member DIDs (settings scope)");
|
|
1924
|
+
ringGroups
|
|
1925
|
+
.command("list")
|
|
1926
|
+
.description("List ring groups with numbers and members")
|
|
1927
|
+
.action(run(async (client) => client.get("/api/v1/ring-groups")));
|
|
1928
|
+
ringGroups
|
|
1929
|
+
.command("get <id>")
|
|
1930
|
+
.description("Get one ring group")
|
|
1931
|
+
.action(run(async (client, id) => { assertUuid(id, "ring group id"); return client.get(`/api/v1/ring-groups/${encodeURIComponent(id)}`); }));
|
|
1932
|
+
ringGroups
|
|
1933
|
+
.command("create <name>")
|
|
1934
|
+
.description("Create a ring group. --phone-number is REQUIRED; without --member-ids the group is created INACTIVE (memberless groups never ring)")
|
|
1935
|
+
.requiredOption("--phone-number <e164>", "account number that rings this group (REQUIRED)")
|
|
1936
|
+
.option("--member-ids <ids>", "comma-separated member user UUIDs (omit → created inactive)")
|
|
1937
|
+
.option("--strategy <strategy>", "ring strategy (e.g. simultaneous, sequential)")
|
|
1938
|
+
.option("--ring-seconds <n>", "seconds to ring before advancing/failing over (integer 5-120)")
|
|
1939
|
+
.option("--inactive", "create as inactive")
|
|
1940
|
+
.action(run(async (client, name, opts) => {
|
|
1941
|
+
// memberIds must ALWAYS be an array — the API rejects undefined
|
|
1942
|
+
// (validateRingGroupMemberIds requires an array; [] = memberless →
|
|
1943
|
+
// the group is created INACTIVE).
|
|
1944
|
+
const body = {
|
|
1945
|
+
name,
|
|
1946
|
+
phoneNumber: opts.phoneNumber,
|
|
1947
|
+
memberIds: opts.memberIds !== undefined ? memberIdsArg(opts.memberIds) : [],
|
|
1948
|
+
};
|
|
1949
|
+
if (opts.strategy !== undefined)
|
|
1950
|
+
body.strategy = opts.strategy;
|
|
1951
|
+
if (opts.ringSeconds !== undefined)
|
|
1952
|
+
body.ringSeconds = ringSecondsArg(opts.ringSeconds);
|
|
1953
|
+
if (opts.inactive)
|
|
1954
|
+
body.isActive = false;
|
|
1955
|
+
return client.post("/api/v1/ring-groups", body);
|
|
1956
|
+
}));
|
|
1957
|
+
ringGroups
|
|
1958
|
+
.command("update <id>")
|
|
1959
|
+
.description("Update a ring group. --expected-updated-at makes a concurrent edit 409 instead of being overwritten")
|
|
1960
|
+
.option("--name <name>", "new name")
|
|
1961
|
+
.option("--phone-number <e164>", "new number")
|
|
1962
|
+
.option("--member-ids <ids>", 'comma-separated replacement member UUIDs, or "none" to clear the roster (deactivates the group)')
|
|
1963
|
+
.option("--strategy <strategy>", "ring strategy")
|
|
1964
|
+
.option("--ring-seconds <n>", "seconds to ring")
|
|
1965
|
+
.option("--active <bool>", 'active flag — exactly "true" or "false"')
|
|
1966
|
+
.option("--expected-updated-at <iso>", "updatedAt from your last read (REQUIRED when replacing --member-ids)")
|
|
1967
|
+
.action(run(async (client, id, opts) => {
|
|
1968
|
+
assertUuid(id, "ring group id");
|
|
1969
|
+
const body = {};
|
|
1970
|
+
if (opts.name !== undefined)
|
|
1971
|
+
body.name = opts.name;
|
|
1972
|
+
if (opts.phoneNumber !== undefined)
|
|
1973
|
+
body.phoneNumber = opts.phoneNumber;
|
|
1974
|
+
if (opts.memberIds !== undefined) {
|
|
1975
|
+
if (opts.expectedUpdatedAt === undefined) {
|
|
1976
|
+
throw new Error("Replacing --member-ids requires --expected-updated-at (the updatedAt you read the group at) — `ring-groups get` first.");
|
|
1977
|
+
}
|
|
1978
|
+
const ids = memberIdsArg(opts.memberIds);
|
|
1979
|
+
body.memberIds = ids;
|
|
1980
|
+
if (ids.length === 0) {
|
|
1981
|
+
// An empty roster cannot stay active (the API 400s an active
|
|
1982
|
+
// memberless group) — clearing implies deactivation.
|
|
1983
|
+
if (opts.active === "true") {
|
|
1984
|
+
throw new Error('--member-ids none clears the roster and deactivates the group — it cannot combine with --active true.');
|
|
1985
|
+
}
|
|
1986
|
+
body.isActive = false;
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
if (opts.strategy !== undefined)
|
|
1990
|
+
body.strategy = opts.strategy;
|
|
1991
|
+
if (opts.ringSeconds !== undefined)
|
|
1992
|
+
body.ringSeconds = ringSecondsArg(opts.ringSeconds);
|
|
1993
|
+
if (opts.active !== undefined)
|
|
1994
|
+
body.isActive = strictBool(opts.active, "--active");
|
|
1995
|
+
if (opts.expectedUpdatedAt !== undefined) {
|
|
1996
|
+
if (Number.isNaN(Date.parse(opts.expectedUpdatedAt))) {
|
|
1997
|
+
throw new Error("--expected-updated-at must be the ISO updatedAt timestamp from your last read.");
|
|
1998
|
+
}
|
|
1999
|
+
body.expectedUpdatedAt = opts.expectedUpdatedAt;
|
|
2000
|
+
}
|
|
2001
|
+
if (Object.keys(body).length === 0)
|
|
2002
|
+
throw new Error("Nothing to update. Pass at least one flag.");
|
|
2003
|
+
return client.patch(`/api/v1/ring-groups/${encodeURIComponent(id)}`, body);
|
|
2004
|
+
}));
|
|
2005
|
+
ringGroups
|
|
2006
|
+
.command("delete <id>")
|
|
2007
|
+
.description("Delete a ring group (its number keeps default routing)")
|
|
2008
|
+
.action(run(async (client, id) => { assertUuid(id, "ring group id"); return client.del(`/api/v1/ring-groups/${encodeURIComponent(id)}`); }));
|
|
2009
|
+
ringGroups
|
|
2010
|
+
.command("release-number <e164>")
|
|
2011
|
+
.description("Release a number from routing (DESTRUCTIVE). --confirm must repeat the EXACT number. A number still referenced by an ACTIVE group cannot be released — delete or repoint that group first; --force does NOT bypass it (it exists only for numbers whose Twilio ownership cannot be verified, and then --force-reason is required)")
|
|
2012
|
+
.requiredOption("--confirm <e164>", "EXACT repetition of the number being released (REQUIRED)")
|
|
2013
|
+
.option("--force", "ONLY when Twilio ownership is unverifiable; requires --force-reason")
|
|
2014
|
+
.option("--force-reason <why>", "non-empty justification, required with --force")
|
|
2015
|
+
.action(run(async (client, phoneNumber, opts) => {
|
|
2016
|
+
if (opts.confirm !== phoneNumber) {
|
|
2017
|
+
throw new Error(`--confirm must repeat the exact number (${phoneNumber}) — got "${opts.confirm}".`);
|
|
2018
|
+
}
|
|
2019
|
+
if (opts.force && !(opts.forceReason && opts.forceReason.trim())) {
|
|
2020
|
+
throw new Error("--force requires a non-empty --force-reason.");
|
|
2021
|
+
}
|
|
2022
|
+
const body = { phoneNumber, confirm: opts.confirm };
|
|
2023
|
+
if (opts.force) {
|
|
2024
|
+
body.force = true;
|
|
2025
|
+
body.forceReason = opts.forceReason;
|
|
2026
|
+
}
|
|
2027
|
+
return client.post("/api/v1/ring-groups/release-number", body);
|
|
2028
|
+
}));
|
|
1810
2029
|
program.parseAsync(process.argv).catch(fail);
|
|
1811
2030
|
// helpers
|
|
1812
2031
|
// Parse a --json / --stages style argument, throwing a clear CLI error (caught
|
|
@@ -1880,6 +2099,39 @@ function jsonArrayArg(raw, flag) {
|
|
|
1880
2099
|
}
|
|
1881
2100
|
return v;
|
|
1882
2101
|
}
|
|
2102
|
+
function strictBool(raw, flag) {
|
|
2103
|
+
if (raw === "true")
|
|
2104
|
+
return true;
|
|
2105
|
+
if (raw === "false")
|
|
2106
|
+
return false;
|
|
2107
|
+
throw new Error(`${flag} must be exactly "true" or "false" (got "${raw}").`);
|
|
2108
|
+
}
|
|
2109
|
+
function ringSecondsArg(raw) {
|
|
2110
|
+
const n = Number(raw);
|
|
2111
|
+
if (!Number.isInteger(n) || n < 5 || n > 120) {
|
|
2112
|
+
throw new Error("--ring-seconds must be an integer between 5 and 120.");
|
|
2113
|
+
}
|
|
2114
|
+
return n;
|
|
2115
|
+
}
|
|
2116
|
+
function memberIdsArg(raw) {
|
|
2117
|
+
// "none" is the explicit empty roster: the API accepts memberIds: [] and
|
|
2118
|
+
// uses it to clear (and deactivate) a group — without a spelled form the
|
|
2119
|
+
// CLI could never remove the final member.
|
|
2120
|
+
if (raw.trim().toLowerCase() === "none")
|
|
2121
|
+
return [];
|
|
2122
|
+
const parts = raw.split(",").map((v) => v.trim());
|
|
2123
|
+
if (parts.length === 0 || parts.some((v) => v === "")) {
|
|
2124
|
+
throw new Error('--member-ids must be a comma-separated list of user UUIDs with no empty entries (or "none" to clear the roster).');
|
|
2125
|
+
}
|
|
2126
|
+
return parts;
|
|
2127
|
+
}
|
|
2128
|
+
function graphArg(raw) {
|
|
2129
|
+
const g = jsonArg(raw, "--graph");
|
|
2130
|
+
if (typeof g.entryId !== "string" || !Array.isArray(g.nodes)) {
|
|
2131
|
+
throw new Error("--graph must be a flow graph object with a string entryId and a nodes array — the API silently substitutes a default voicemail flow for anything else.");
|
|
2132
|
+
}
|
|
2133
|
+
return g;
|
|
2134
|
+
}
|
|
1883
2135
|
function jsonArg(raw, flag) {
|
|
1884
2136
|
if (raw.trim() === "") {
|
|
1885
2137
|
throw new Error(`${flag} must be valid JSON (got an empty string).`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "conduyt",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.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",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"build": "tsc",
|
|
19
19
|
"typecheck": "tsc --noEmit",
|
|
20
20
|
"start": "tsx src/index.ts",
|
|
21
|
-
"prepublishOnly": "npm run check:version && npm run build && node --import tsx --experimental-test-module-mocks --test src
|
|
21
|
+
"prepublishOnly": "npm run check:version && npm run build && node --import tsx --experimental-test-module-mocks --test src/*.test.ts",
|
|
22
22
|
"test": "node --import tsx --test src/*.test.ts",
|
|
23
23
|
"check:version": "node -e \"const p=require('./package.json').version,l=require('./package-lock.json');if(l.version!==p||l.packages[''].version!==p){console.error('lockfile version != '+p);process.exit(1)}console.log('Version sync OK: '+p)\""
|
|
24
24
|
},
|