conduyt 1.72.0 → 1.74.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 +81 -19
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { readFileSync, writeFileSync } from "node:fs";
|
|
|
3
3
|
import { addPoolFilterSet, listPoolFilterSets, removePoolFilterSet, updatePoolFilterSet } from "./pool-filter-sets.js";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { dirname, join } from "node:path";
|
|
6
|
-
import { Command, CommanderError } from "commander";
|
|
6
|
+
import { Command, CommanderError, Option } from "commander";
|
|
7
7
|
import { ConduytClient, buildQuery } from "./client.js";
|
|
8
8
|
import { minimumGradeOf, minimumGradePatch, parseGradeFlag } from "./message-check-minimum.js";
|
|
9
9
|
import { splitList, stageRulePatch, validateCardFields, validateRuleChange } from "./board-settings.js";
|
|
@@ -80,11 +80,23 @@ contacts
|
|
|
80
80
|
.command("list")
|
|
81
81
|
.description("List contacts")
|
|
82
82
|
.option("--limit <n>", "max results")
|
|
83
|
-
.option("--
|
|
84
|
-
.option("--
|
|
83
|
+
.option("--page <n>", "page number (default 1; the contacts list pages with page and per_page)")
|
|
84
|
+
.option("--cursor <cursor>", "not supported on contacts list (the API pages with --page); passing it exits 1 and sends no request")
|
|
85
|
+
.option("--query <q>", "search text (name, email, phone; sent to the API as `search`)")
|
|
86
|
+
.option("--email <address>", "exact email lookup, repeatable (--email a@x.com --email b@y.com): matches the primary email or any secondary email slot, trimmed and lower-cased; each address is at most 254 characters; an empty or invalid address, or more than 20 distinct addresses, is a 400; on a workspace whose identifier index is still being built, a lookup whose stored spellings exceed its stored-spelling bind budget is a 422 (a count of spellings, not contacts; other filters do not help)", (v, prev = []) => [...prev, v])
|
|
85
87
|
.option("--master-status <list>", "only these lead statuses, comma-separated (open, won, lost, abandoned, disqualified, or an account custom status)")
|
|
86
88
|
.option("--exclude-master-status <list>", "hide these lead statuses, comma-separated — e.g. won,lost,abandoned,disqualified is the Active view")
|
|
87
|
-
.action(run(async (client, opts) =>
|
|
89
|
+
.action(run(async (client, opts) => {
|
|
90
|
+
// the route never reads `cursor` (it pages with page / per_page): refuse before any request instead of silently returning page 1
|
|
91
|
+
if (opts.cursor !== undefined)
|
|
92
|
+
throw new Error("contacts list does not support --cursor (the API pages with page and per_page); use --page <n>");
|
|
93
|
+
// the API reads `search` (there is no `query` param), and every repeated `email` occurrence
|
|
94
|
+
const qs = new URLSearchParams(buildQuery({ limit: opts.limit, page: opts.page, search: opts.query, master_status: opts.masterStatus, exclude_master_status: opts.excludeMasterStatus }).slice(1));
|
|
95
|
+
for (const address of opts.email ?? [])
|
|
96
|
+
qs.append("email", address);
|
|
97
|
+
const q = qs.toString();
|
|
98
|
+
return client.get(`/api/v1/contacts${q ? `?${q}` : ""}`);
|
|
99
|
+
}));
|
|
88
100
|
contacts
|
|
89
101
|
.command("get <id>")
|
|
90
102
|
.description("Get a single contact by id (includes `revision`: pass it as --expected-revision on `contacts update` / `contacts collaborators`; `hasOutboundSms` is true when the workspace has ever PROVABLY texted the contact: a proven send only, not a draft, a queued row or a pre-send failure; use it to pick `messages check --context`: false = first, true = conversation; `hasOutboundEmail` is the same for email: true when at least one outbound email to the contact was proven sent, so a genuine \"Re:\" reply is a conversation and not flagged)")
|
|
@@ -2670,57 +2682,104 @@ companies
|
|
|
2670
2682
|
}));
|
|
2671
2683
|
// ---- tags ----
|
|
2672
2684
|
// ---- saved filters (personal, Velocify-style) ----
|
|
2685
|
+
// Smart View icons (app branch fable/smart-view-icons): the 30 current ids in picker order. The API also accepts 17 legacy ids as
|
|
2686
|
+
// deprecated inputs (saved as the mapped new id); they are deliberately not listed here.
|
|
2687
|
+
const SMART_VIEW_ICON_IDS = [
|
|
2688
|
+
"heart", "star", "trophy", "badge", "target", "eye", "clock", "hourglass", "siren", "bolt",
|
|
2689
|
+
"fire", "rain", "funnel", "pyramid", "flag", "pin", "push-pin", "thumbs-up", "thumbs-down", "coffee",
|
|
2690
|
+
"rocket", "number-1", "number-2", "number-3", "number-4", "number-5", "number-6", "number-7", "number-8", "number-9",
|
|
2691
|
+
];
|
|
2692
|
+
const SMART_VIEW_ICON_HELP = (omitted) => `the view's icon, one of (picker order): ${SMART_VIEW_ICON_IDS.join(", ")}. ${omitted}. Legacy icon ids are accepted but saved as the new id; anything else is 400 invalid_icon`;
|
|
2693
|
+
const SAVED_FILTER_ENTITY_ERROR = "saved filters are contact Smart Views; --entity is no longer supported";
|
|
2694
|
+
function rejectEntity(body) {
|
|
2695
|
+
if ("entity" in body || "entityType" in body)
|
|
2696
|
+
throw new Error(SAVED_FILTER_ENTITY_ERROR);
|
|
2697
|
+
}
|
|
2698
|
+
/** saved-filters update/delete take a Smart View id, but the generic routes also edit/delete SHARED views for admin keys: read the view first and
|
|
2699
|
+
* refuse anything that is not one of the caller's personal views (the GET is the only request made in the refusal path). A missing view throws the server's reason. */
|
|
2700
|
+
async function requirePersonalSmartView(client, id) {
|
|
2701
|
+
const res = (await client.get(`/api/v1/smart-views/${encodeURIComponent(id)}`));
|
|
2702
|
+
if (res?.data?.isPersonal !== true) {
|
|
2703
|
+
throw new Error("this is a shared Smart View; saved filters are personal views only (use `conduyt api PATCH /smart-views/<id> --body '{...}'` or `conduyt api DELETE /smart-views/<id>`)");
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2673
2706
|
const savedFilters = program
|
|
2674
2707
|
.command("saved-filters")
|
|
2675
2708
|
.description("Manage YOUR personal saved contact filters (the web app's Saved dropdown; requires the 'saved-filters' key scope)");
|
|
2676
2709
|
savedFilters
|
|
2677
2710
|
.command("list")
|
|
2678
|
-
.description("List your saved filters")
|
|
2679
|
-
.
|
|
2680
|
-
|
|
2711
|
+
.description("List your saved filters: your PERSONAL Smart Views (GET /smart-views, every page, filtered to isPersonal). The printed id is the Smart View id that update and delete take")
|
|
2712
|
+
.action(run(async (client) => {
|
|
2713
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
2714
|
+
const first = (await client.get("/api/v1/smart-views?counts=false"));
|
|
2715
|
+
const rows = [...first.data.data];
|
|
2716
|
+
let meta = first.data.meta;
|
|
2717
|
+
let shifted = false;
|
|
2718
|
+
for (let page = 2; meta.has_more && page <= 50; page += 1) {
|
|
2719
|
+
const next = (await client.get(`/api/v1/smart-views?page=${page}&counts=false`));
|
|
2720
|
+
if (next.data.meta.order_stamp !== first.data.meta.order_stamp) {
|
|
2721
|
+
shifted = true;
|
|
2722
|
+
break;
|
|
2723
|
+
}
|
|
2724
|
+
rows.push(...next.data.data);
|
|
2725
|
+
meta = next.data.meta;
|
|
2726
|
+
}
|
|
2727
|
+
if (!shifted)
|
|
2728
|
+
return rows.filter((v) => v.isPersonal === true);
|
|
2729
|
+
}
|
|
2730
|
+
throw new Error("Smart Views kept changing while they were listed; try again.");
|
|
2731
|
+
}));
|
|
2681
2732
|
savedFilters
|
|
2682
2733
|
.command("create")
|
|
2683
|
-
.description("Save a named filter (grouped payload via --filters JSON)")
|
|
2734
|
+
.description("Save a named filter (a personal Smart View, private to you; grouped payload via --filters JSON)")
|
|
2684
2735
|
.option("--name <name>", "display name (required, unique per user, max 60 chars)")
|
|
2685
2736
|
.option("--filters <json>", `grouped filter payload JSON, e.g. [{"conjunction":"AND","rules":[{"field":"source","operator":"equals","value":"facebook"}]}]`)
|
|
2686
2737
|
.option("--sort-field <field>", "sort field (default createdAt)")
|
|
2687
2738
|
.option("--sort-order <dir>", "asc | desc (default desc)")
|
|
2688
|
-
.
|
|
2689
|
-
.option("--
|
|
2739
|
+
.addOption(new Option("--entity <type>", "removed").hideHelp()) // not in the help; passing it exits with SAVED_FILTER_ENTITY_ERROR instead of being ignored
|
|
2740
|
+
.option("--icon <id>", SMART_VIEW_ICON_HELP("Omitted: heart (the default)"))
|
|
2741
|
+
.option("--json <json>", "full JSON body, merged over the flags (may carry `icon`: same ids, same default and same 400 invalid_icon as --icon)")
|
|
2690
2742
|
.action(run(async (client, opts) => {
|
|
2743
|
+
if (opts.entity !== undefined)
|
|
2744
|
+
throw new Error(SAVED_FILTER_ENTITY_ERROR);
|
|
2691
2745
|
const body = {};
|
|
2692
2746
|
if (opts.name !== undefined)
|
|
2693
2747
|
body.name = opts.name;
|
|
2748
|
+
if (opts.icon !== undefined)
|
|
2749
|
+
body.icon = opts.icon;
|
|
2694
2750
|
if (opts.filters !== undefined)
|
|
2695
2751
|
body.filters = jsonArrayArg(opts.filters, "--filters");
|
|
2696
2752
|
if (opts.sortField !== undefined)
|
|
2697
2753
|
body.sortField = opts.sortField;
|
|
2698
2754
|
if (opts.sortOrder !== undefined)
|
|
2699
2755
|
body.sortOrder = opts.sortOrder;
|
|
2700
|
-
if (opts.entity !== undefined)
|
|
2701
|
-
body.entityType = opts.entity;
|
|
2702
2756
|
if (opts.json !== undefined)
|
|
2703
2757
|
Object.assign(body, jsonArg(opts.json, "--json"));
|
|
2758
|
+
rejectEntity(body);
|
|
2704
2759
|
// A --json merge can clobber filters with a non-array; re-assert the
|
|
2705
2760
|
// shape after ALL merges so a malformed payload fails locally.
|
|
2706
2761
|
if (body.filters !== undefined && !Array.isArray(body.filters)) {
|
|
2707
2762
|
throw new Error("filters must be a JSON ARRAY (grouped payload or flat rules).");
|
|
2708
2763
|
}
|
|
2709
|
-
|
|
2764
|
+
// a saved filter IS a personal Smart View (saved-filters write routes retired 2026-09-24, 410): personal:true keeps it private for every key
|
|
2765
|
+
return client.post("/api/v1/smart-views", { ...body, personal: true });
|
|
2710
2766
|
}));
|
|
2711
2767
|
savedFilters
|
|
2712
2768
|
.command("update <id>")
|
|
2713
|
-
.description("Rename a saved filter or replace its filters/sort (owner-only)")
|
|
2769
|
+
.description("Rename a saved filter (a personal Smart View) or replace its filters/sort (owner-only). The id must be one of YOUR personal views (see `saved-filters list`); a shared Smart View id is refused after one GET, nothing is changed (edit a shared view with `conduyt api PATCH /smart-views/<id> --body '{...}'`)")
|
|
2714
2770
|
.option("--name <name>", "new display name")
|
|
2715
2771
|
.option("--filters <json>", "grouped filter payload JSON")
|
|
2716
2772
|
.option("--sort-field <field>", "sort field")
|
|
2717
2773
|
.option("--sort-order <dir>", "asc | desc")
|
|
2718
|
-
.option("--
|
|
2774
|
+
.option("--icon <id>", SMART_VIEW_ICON_HELP("Omitted: unchanged"))
|
|
2775
|
+
.option("--json <json>", "full JSON body, merged over the flags (may carry `icon`: same ids and same 400 invalid_icon as --icon)")
|
|
2719
2776
|
.action(run(async (client, id, opts) => {
|
|
2720
2777
|
assertUuid(id, "saved filter id");
|
|
2721
2778
|
const body = {};
|
|
2722
2779
|
if (opts.name !== undefined)
|
|
2723
2780
|
body.name = opts.name;
|
|
2781
|
+
if (opts.icon !== undefined)
|
|
2782
|
+
body.icon = opts.icon;
|
|
2724
2783
|
if (opts.filters !== undefined)
|
|
2725
2784
|
body.filters = jsonArrayArg(opts.filters, "--filters");
|
|
2726
2785
|
if (opts.sortField !== undefined)
|
|
@@ -2729,18 +2788,21 @@ savedFilters
|
|
|
2729
2788
|
body.sortOrder = opts.sortOrder;
|
|
2730
2789
|
if (opts.json !== undefined)
|
|
2731
2790
|
Object.assign(body, jsonArg(opts.json, "--json"));
|
|
2791
|
+
rejectEntity(body);
|
|
2732
2792
|
// Same post-merge shape assertion as create — see above.
|
|
2733
2793
|
if (body.filters !== undefined && !Array.isArray(body.filters)) {
|
|
2734
2794
|
throw new Error("filters must be a JSON ARRAY (grouped payload or flat rules).");
|
|
2735
2795
|
}
|
|
2736
|
-
|
|
2796
|
+
await requirePersonalSmartView(client, id);
|
|
2797
|
+
return client.patch(`/api/v1/smart-views/${encodeURIComponent(id)}`, body);
|
|
2737
2798
|
}));
|
|
2738
2799
|
savedFilters
|
|
2739
2800
|
.command("delete <id>")
|
|
2740
|
-
.description("Delete one of your saved filters (owner-only)")
|
|
2801
|
+
.description("Delete one of your saved filters (a personal Smart View; owner-only). The id must be one of YOUR personal views (see `saved-filters list`); a shared Smart View id is refused after one GET, nothing is deleted (delete a shared view with `conduyt api DELETE /smart-views/<id>`)")
|
|
2741
2802
|
.action(run(async (client, id) => {
|
|
2742
2803
|
assertUuid(id, "saved filter id");
|
|
2743
|
-
|
|
2804
|
+
await requirePersonalSmartView(client, id);
|
|
2805
|
+
return client.del(`/api/v1/smart-views/${encodeURIComponent(id)}`);
|
|
2744
2806
|
}));
|
|
2745
2807
|
const tags = program.command("tags").description("Manage tags");
|
|
2746
2808
|
tags
|
|
@@ -5233,7 +5295,7 @@ reports
|
|
|
5233
5295
|
.command("deliveries <sid>")
|
|
5234
5296
|
.description("The delivery history of a schedule with download URLs")
|
|
5235
5297
|
.action(run(async (client, sid) => { assertUuid(sid, "schedule id"); return client.get(`/api/v1/reports/subscriptions/${sid}/deliveries`); }));
|
|
5236
|
-
const smartViews = program.command("smart-views").description(
|
|
5298
|
+
const smartViews = program.command("smart-views").description(`Smart Views and their Smart Dialing priorities. Creating or updating a view (conduyt api POST /smart-views, PATCH /smart-views/<id>, or saved-filters create/update) takes an optional icon, one of (picker order): ${SMART_VIEW_ICON_IDS.join(", ")}. heart is the default on create. Legacy icon ids are accepted but saved as the new id; anything else is 400 invalid_icon`);
|
|
5237
5299
|
smartViews
|
|
5238
5300
|
.command("list")
|
|
5239
5301
|
.description("List Smart Views in sidebar order (= Smart Dialing priority) with lead counts, audience (groupId / members / isPersonal) and dialing settings")
|
package/package.json
CHANGED