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.
Files changed (2) hide show
  1. package/dist/index.js +81 -19
  2. 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("--cursor <cursor>", "pagination cursor")
84
- .option("--query <q>", "search query")
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) => client.get(`/api/v1/contacts${buildQuery({ limit: opts.limit, cursor: opts.cursor, query: opts.query, master_status: opts.masterStatus, exclude_master_status: opts.excludeMasterStatus })}`)));
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
- .option("--entity <type>", "entity type (default contact)")
2680
- .action(run(async (client, opts) => client.get(`/api/v1/saved-filters${buildQuery({ entity_type: opts.entity })}`)));
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
- .option("--entity <type>", "entity type (default contact)")
2689
- .option("--json <json>", "full JSON body, merged over the flags")
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
- return client.post("/api/v1/saved-filters", body);
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("--json <json>", "full JSON body, merged over the flags")
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
- return client.patch(`/api/v1/saved-filters/${encodeURIComponent(id)}`, body);
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
- return client.del(`/api/v1/saved-filters/${encodeURIComponent(id)}`);
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("Smart Views and their Smart Dialing priorities");
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "conduyt",
3
- "version": "1.72.0",
3
+ "version": "1.74.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",