conduyt 1.68.0 → 1.69.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
@@ -79,6 +79,8 @@ conduyt whatsapp templates # approved / pending / rejected template
79
79
  conduyt dialer voice-drop --contact <id> [--recording "Missed you"] [--if-human play] # a call that plays a Voicemail Drop recording to a machine
80
80
  conduyt messages send-sms --contact <id> --transport whatsapp --whatsapp-template HX… --whatsapp-variables '{"1":"Alex"}' --body "…"
81
81
  conduyt messages check --channel sms --body "…" [--context first|conversation] [--provider twilio] [--clean] [--body-file draft.txt] # grade a DRAFT before sending (score, grade, flagged words with safer alternatives, Unicode code-point offsets); --provider twilio (texts only) words the notices in Twilio's wording, --clean adds the cleaner's proposal; educates, never blocks, nothing stored or sent; 20 per minute; 400 / 403 / 413 / 429 exit 1 with the code
82
+ conduyt messages minimum-grade # the admin's Message check minimum grade to send (settings.messageCheck.minimumGrade): enabled, sms / email minimum (A-F, 0-100 or null), adminOverride; groundwork, stored but not enforced yet; owner / admin only (anyone else sees visible: false)
83
+ conduyt messages set-minimum-grade [--sms A|B|C|D|F|0-100|none] [--email ...] [--admin-override on|off] [--enabled on|off] # sends the whole block with the settings revision it read; --enabled on is refused 400 minimum_grade_not_available_yet; 403 forbidden for a non-owner / non-admin; all exit 1 with the code
82
84
  conduyt sms providers options # the workspace's active SMS providers, redacted: [{ id, name, color }] (needs email_templates.view or settings.view)
83
85
  conduyt templates list [--channel email|sms] [--active true] # email / SMS templates; each row carries bodyMode (blocks | html); the pasted html is not repeated in a list row
84
86
  conduyt templates get <id> # one template with its full content (an HTML-mode template answers its pasted source in `html`)
package/dist/index.js CHANGED
@@ -3,8 +3,9 @@ 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 } from "commander";
6
+ import { Command, CommanderError } from "commander";
7
7
  import { ConduytClient, buildQuery } from "./client.js";
8
+ import { minimumGradeOf, minimumGradePatch, parseGradeFlag } from "./message-check-minimum.js";
8
9
  import { splitList, stageRulePatch, validateCardFields, validateRuleChange } from "./board-settings.js";
9
10
  import { ARCHIVE_PIPELINE_TIMEOUT_MS, MAILCHIMP_ADMIN_TIMEOUT_MS, STATUS_METADATA_IMPORT_TIMEOUT_MS, PIPELINE_COLORS, boolFlag, intFlag, isoDate, lifecycleStage as lifecycleStageArg, MAX_PIPELINE_ROT_DAYS, MIN_PIPELINE_ROT_DAYS, pipelineColor, settingsRevision, slug as slugArg, uuid as uuidOf, uuidList, validateImportItems, validateStatusMetadataBody } from "./pipeline-settings.js";
10
11
  import { saveConfig, resolveConfig, configPath } from "./config.js";
@@ -41,7 +42,11 @@ const program = new Command();
41
42
  program
42
43
  .name("conduyt")
43
44
  .description("Command-line interface for Conduyt CRM — manage contacts, deals, pipelines, and run insight queries from your terminal.")
44
- .version(readVersion());
45
+ .version(readVersion())
46
+ // Commander ends --help / --version / usage errors with process.exit() right after writing, and a piped stdout is not drained by
47
+ // then (`conduyt messages set-minimum-grade --help | cat` was cut at exactly 512 bytes). exitOverride is set BEFORE any subcommand
48
+ // is defined, so every command inherits it; main() below drains both streams and only then exits with Commander's own exit code.
49
+ .exitOverride();
45
50
  // ---- config ----
46
51
  const config = program.command("config").description("Manage CLI configuration");
47
52
  config
@@ -85,7 +90,7 @@ contacts
85
90
  .action(run(async (client, id) => client.get(`/api/v1/contacts/${encodeURIComponent(id)}`)));
86
91
  contacts
87
92
  .command("create")
88
- .description("Create a contact")
93
+ .description("Create a contact. An impossible US/Canada number (area code or exchange starting 0 or 1) is refused 400 phone_invalid and nothing is saved; the refusal exits 1 with the status, message and code, sent once")
89
94
  .option("--first <name>", "first name")
90
95
  .option("--last <name>", "last name")
91
96
  .option("--email <email>", "email")
@@ -130,7 +135,7 @@ contacts
130
135
  }));
131
136
  contacts
132
137
  .command("update <id>")
133
- .description("Update a contact (PATCH — only the fields you pass change)")
138
+ .description("Update a contact (PATCH — only the fields you pass change). An impossible US/Canada number (area code or exchange starting 0 or 1) is refused 400 phone_invalid; a country is never null or blank (400 country_invalid, via --json); each refusal exits 1 with the status, message and code, sent once")
134
139
  .option("--first <name>", "first name")
135
140
  .option("--last <name>", "last name")
136
141
  .option("--email <email>", "email")
@@ -1890,6 +1895,36 @@ messages
1890
1895
  throw new Error("The draft is required. Pass --body '<text>' or --body-file <path>.");
1891
1896
  return client.post("/api/v1/messages/check", body);
1892
1897
  }));
1898
+ const MINIMUM_GRADE_NOTE = "Groundwork only: the minimum is stored and read back but NOT enforced yet (Send stays enabled for everyone), and --enabled on is refused 400 minimum_grade_not_available_yet until the feature is switched on.";
1899
+ messages
1900
+ .command("minimum-grade")
1901
+ .description(`Show the admin's Message check minimum grade to send (GET /settings, settings.messageCheck.minimumGrade). Prints { minimumGrade: { enabled, channel: { sms, email }, adminOverride } | null, visible } (the standard envelope is unwrapped). enabled is the master switch (default off); channel.sms / channel.email the lowest acceptable result for that channel (a letter A-F, a score 0-100 or null = no minimum); adminOverride whether an owner or admin may send below it (default on). ${MINIMUM_GRADE_NOTE} Owner / admin only: for any other member the API leaves the block out, so minimumGrade is null and visible false (a redaction, not an unset minimum). Needs the settings scope.`)
1902
+ .action(run(async (client) => {
1903
+ const result = (await client.get("/api/v1/settings"));
1904
+ const block = minimumGradeOf(result?.data);
1905
+ return { data: { minimumGrade: block, visible: block !== null } };
1906
+ }));
1907
+ messages
1908
+ .command("set-minimum-grade")
1909
+ .description(`Change the Message check minimum grade to send (PATCH /settings). Pass only what changes: --sms / --email <A-F|0-100|none> (a letter, a whole score, or none for no minimum; E, fractions and scores over 100 are refused), --admin-override on|off, --enabled on|off. Sends the WHOLE block (the API replaces it) with the settings revision it read; a 409 re-reads once, then exits 1 with the current settings. ${MINIMUM_GRADE_NOTE} Owner / admin only. Every refusal exits 1 with the status, the API's message and its code, sent once and never retried: 403 forbidden ("Only an owner or admin can change the Message check minimum grade"), 400 minimum_grade_not_available_yet, 422 invalid_body. No new scope: the settings scope, settings:edit and the owner or admin role.`)
1910
+ .option("--sms <grade>", "SMS minimum: A, B, C, D, F, a score 0-100, or none")
1911
+ .option("--email <grade>", "email minimum: A, B, C, D, F, a score 0-100, or none")
1912
+ .option("--admin-override <on|off>", "whether an owner or admin may send below the minimum (default on)")
1913
+ .option("--enabled <on|off>", "master switch; on is refused 400 minimum_grade_not_available_yet until the feature is switched on")
1914
+ .action(run(async (client, opts) => {
1915
+ const change = {};
1916
+ if (opts.sms !== undefined)
1917
+ change.sms = parseGradeFlag(opts.sms, "--sms");
1918
+ if (opts.email !== undefined)
1919
+ change.email = parseGradeFlag(opts.email, "--email");
1920
+ if (opts.adminOverride !== undefined)
1921
+ change.adminOverride = onOffArg(opts.adminOverride, "--admin-override");
1922
+ if (opts.enabled !== undefined)
1923
+ change.enabled = onOffArg(opts.enabled, "--enabled");
1924
+ if (Object.keys(change).length === 0)
1925
+ throw new Error("nothing to change: pass at least one of --sms, --email, --admin-override, --enabled");
1926
+ return patchGeneralSettingsMerged(client, (settings) => minimumGradePatch(settings, change));
1927
+ }));
1893
1928
  // ---- email / SMS templates (pasted-HTML email templates, #127, 2026-10-09) ----
1894
1929
  const templates = program.command("templates").description("Email and SMS templates (API key scope emails / templates; a member: email_templates.view / create / edit). Email templates are built in blocks or pasted as HTML (`--body-mode html --html-file page.html`)");
1895
1930
  const TEMPLATE_REFUSALS = "Every refusal exits 1 with the status, the API's message and its code, sent once and never retried: 400 invalid_body_mode (--body-mode is not blocks / html), html_without_html_mode (html needs --body-mode html), html_required (html mode with no html), html_too_large (html over 102,400 bytes), html_script (a <script> in the html), html_conditional_comment (an Outlook <!--[if ...]--> conditional comment), html_office_markup (Word / Office markup), html_duplicate_attribute (an element repeating an attribute), template_corrupt (an unreadable or doubled builder marker in bodyHtml), body_html_required (blocks mode with no --body-html), body_mode_conflict, template_meta_conflict";
@@ -5296,7 +5331,16 @@ smartViews
5296
5331
  assertUuid(id, "smart view id");
5297
5332
  return client.patch("/api/v1/smart-views/reorder", { order: ids });
5298
5333
  }));
5299
- program.parseAsync(process.argv).catch(fail);
5334
+ /** end the process only after stdout and stderr have flushed (a pipe is not drained at process.exit) */
5335
+ function exitAfterFlush(code) {
5336
+ process.stdout.write("", () => process.stderr.write("", () => process.exit(code)));
5337
+ }
5338
+ program.parseAsync(process.argv).catch((err) => {
5339
+ // help, version and usage errors: Commander already printed them; keep its exit code, but drain first
5340
+ if (err instanceof CommanderError)
5341
+ return exitAfterFlush(err.exitCode);
5342
+ fail(err);
5343
+ });
5300
5344
  // helpers
5301
5345
  // Parse a --json / --stages style argument, throwing a clear CLI error (caught
5302
5346
  // by run() -> non-zero exit) rather than a stack trace on bad JSON.
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Message check minimum grade, #134 groundwork (app 10-10, LIVE 9:19 AM): settings.messageCheck.minimumGrade, the admin's lowest
3
+ * acceptable Message check result per channel. Stored and read back, enforced nowhere yet; `enabled: true` is refused 400
4
+ * minimum_grade_not_available_yet. Owner / admin only: GET /settings leaves the block out for anyone else and PATCH answers 403.
5
+ * The validators mirror the API's messageCheckSettingsSchema (strict, whole block).
6
+ */
7
+ export const GRADE_LETTERS = ["A", "B", "C", "D", "F"];
8
+ export const DEFAULT_MINIMUM_GRADE = { enabled: false, channel: { sms: null, email: null }, adminOverride: true };
9
+ const isObj = (v) => !!v && typeof v === "object" && !Array.isArray(v);
10
+ /** a letter A-F (any case), a whole score 0-100, or the literal "none" (null = no minimum); E, fractions and out-of-range scores are refused */
11
+ export function parseGradeFlag(raw, flag) {
12
+ const v = raw.trim();
13
+ if (v.toLowerCase() === "none")
14
+ return null;
15
+ if (GRADE_LETTERS.includes(v.toUpperCase()))
16
+ return v.toUpperCase();
17
+ if (/^\d+$/.test(v) && Number(v) <= 100)
18
+ return Number(v);
19
+ throw new Error(`${flag} takes a letter A, B, C, D or F, a whole score 0-100, or none (got '${raw}').`);
20
+ }
21
+ /** the stored block with the defaults laid under it field by field; null when the API left the block out (the key's member is not an owner / admin) */
22
+ export function minimumGradeOf(settings) {
23
+ const mc = settings?.messageCheck;
24
+ if (!isObj(mc))
25
+ return null;
26
+ const s = isObj(mc.minimumGrade) ? mc.minimumGrade : {};
27
+ const ch = isObj(s.channel) ? s.channel : {};
28
+ const val = (v) => (typeof v === "string" || typeof v === "number" ? v : null);
29
+ return { enabled: s.enabled === true, channel: { sms: val(ch.sms), email: val(ch.email) }, adminOverride: s.adminOverride === false ? false : true };
30
+ }
31
+ /** the PATCH body: the WHOLE block (the API replaces it) with only the given fields changed; a block the key cannot see gets the defaults laid under the change (the API then answers 403 forbidden) */
32
+ export function minimumGradePatch(settings, change) {
33
+ const stored = minimumGradeOf(settings) ?? DEFAULT_MINIMUM_GRADE;
34
+ return {
35
+ messageCheck: {
36
+ minimumGrade: {
37
+ enabled: change.enabled ?? stored.enabled,
38
+ channel: { sms: change.sms === undefined ? stored.channel.sms : change.sms, email: change.email === undefined ? stored.channel.email : change.email },
39
+ adminOverride: change.adminOverride ?? stored.adminOverride,
40
+ },
41
+ },
42
+ };
43
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "conduyt",
3
- "version": "1.68.0",
3
+ "version": "1.69.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",