conduyt 1.62.0 → 1.63.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
@@ -32,7 +32,10 @@ The saved config lives at `~/.conduyt/config.json` and is written owner-only (`c
32
32
  ```bash
33
33
  conduyt whoami # show the authenticated user
34
34
  conduyt contacts list --limit 25 # list contacts (supports --cursor, --query)
35
- conduyt contacts get <id> # fetch one contact
35
+ conduyt contacts get <id> # fetch one contact (carries `revision`)
36
+ conduyt contacts update <id> --first Ann --expected-revision <n> # PATCH; a contact changed since revision n is refused: 409 contact_changed, exit 1, code and current revision printed
37
+ conduyt contacts collaborators add <contactId> <userId> [--expected-revision <n>] # POST /contacts/{id}/collaborators; prints { added, revision }
38
+ conduyt contacts collaborators remove <contactId> <userId> [--expected-revision <n>] # DELETE with a body; prints { removed, revision }; 404 when not a collaborator
36
39
  conduyt deals list --pipeline <id> # list deals
37
40
  conduyt deals list --health-summary --pipeline <id> # Pipeline health strip for the filtered set (totalOpenDeals, atRiskValue, checks[]); add --health-check stale --health-limit 20 for one check's deals, --health-cursor <nextCursor> to page, --health-rows-only for just the page
38
41
  conduyt deals health --pipeline <id> --check needs_action # GET /deals/inspection: the six checks over open deals; --check adds its deals, --cursor/--offset pages
package/dist/client.js CHANGED
@@ -57,7 +57,14 @@ export class ConduytClient {
57
57
  // STAGE_REQUIREMENTS 422 carries lostReasonOptions) keep the context —
58
58
  // same principle as `structured` above, bounded against huge bodies.
59
59
  if (json && typeof json === "object" && json.error) {
60
- const extras = Object.fromEntries(Object.entries(json).filter(([k]) => k !== "error"));
60
+ // Scalars first (code, revision), nested objects last: a 409 contact_changed also carries the whole current
61
+ // `contact`, and the bound below may cut that, never the code or the current revision.
62
+ const isScalar = (v) => v === null || typeof v !== "object";
63
+ const entries = Object.entries(json).filter(([k]) => k !== "error");
64
+ const extras = Object.fromEntries([
65
+ ...entries.filter(([, v]) => isScalar(v)),
66
+ ...entries.filter(([, v]) => !isScalar(v)),
67
+ ]);
61
68
  if (Object.keys(extras).length > 0) {
62
69
  msg = `${msg} ${JSON.stringify(extras).slice(0, 1000)}`;
63
70
  }
@@ -78,8 +85,9 @@ export class ConduytClient {
78
85
  put(path, body, opts) {
79
86
  return this.request("PUT", path, body, opts);
80
87
  }
81
- del(path) {
82
- return this.request("DELETE", path);
88
+ /** `body` is for the few DELETE routes that take one (contact collaborators with an --expected-revision pin). */
89
+ del(path, body) {
90
+ return this.request("DELETE", path, body);
83
91
  }
84
92
  /** A POST whose body comes back as text (a CSV or XLSX export), not JSON. */
85
93
  async postText(path, body) {
package/dist/index.js CHANGED
@@ -9,6 +9,8 @@ import { splitList, stageRulePatch, validateCardFields, validateRuleChange } fro
9
9
  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
10
  import { saveConfig, resolveConfig, configPath } from "./config.js";
11
11
  import { print, fail, unwrap } from "./output.js";
12
+ /** the contact save revision bound (catalog: integer 0..2147483647) */
13
+ const CONTACT_REVISION_MAX = 2147483647;
12
14
  // Exit cleanly when a downstream pipe closes early (e.g. `conduyt deals list |
13
15
  // head`). Without this, the stdout write after the reader is gone throws an
14
16
  // unhandled EPIPE and prints a Node stack trace — broken-pipe is normal Unix
@@ -77,7 +79,7 @@ contacts
77
79
  .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 })}`)));
78
80
  contacts
79
81
  .command("get <id>")
80
- .description("Get a single contact by id")
82
+ .description("Get a single contact by id (includes `revision`: pass it as --expected-revision on `contacts update` / `contacts collaborators`)")
81
83
  .action(run(async (client, id) => client.get(`/api/v1/contacts/${encodeURIComponent(id)}`)));
82
84
  contacts
83
85
  .command("create")
@@ -135,10 +137,13 @@ contacts
135
137
  .option("--company <company>", "company name (auto-creates/links a company; pass '' to clear)")
136
138
  .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)")
137
139
  .option("--master-status <status>", "lead lifecycle status: open|won|lost|abandoned|disqualified or an account custom (terminal statuses silence all outbound automation)")
140
+ .option("--expected-revision <n>", "the contact's `revision` as you read it (contacts get / list; 0-2147483647): a contact changed since is refused with 409 contact_changed, nothing is written, exit 1 with the code and the current revision printed")
138
141
  .option("--json <json>", "full JSON body, merged over the flags above (for any field)")
139
142
  .action(run(async (client, id, opts) => {
140
143
  assertUuid(id, "contact id");
141
144
  const body = {};
145
+ if (opts.expectedRevision !== undefined)
146
+ body.expectedRevision = intFlag(opts.expectedRevision, "--expected-revision", 0, CONTACT_REVISION_MAX);
142
147
  if (opts.first !== undefined)
143
148
  body.firstName = opts.first;
144
149
  if (opts.last !== undefined)
@@ -175,6 +180,25 @@ contacts
175
180
  assertContactSource(body.source, { allowNull: true });
176
181
  return client.patch(`/api/v1/contacts/${encodeURIComponent(id)}`, body);
177
182
  }));
183
+ const collaborators = contacts.command("collaborators").description("Team members collaborating on a contact");
184
+ for (const [verb, method] of [["add", "post"], ["remove", "del"]]) {
185
+ collaborators
186
+ .command(`${verb} <contactId> <userId>`)
187
+ .description(verb === "add"
188
+ ? "Add a team member as a collaborator (POST /contacts/{id}/collaborators; needs edit authority). Prints { added, revision } with the contact's NEW revision. A stale --expected-revision is 409 contact_changed (exit 1, code and current revision printed); 409 without a code: already a collaborator"
189
+ : "Remove a collaborator (DELETE /contacts/{id}/collaborators with a body; needs edit authority). Prints { removed, revision } with the contact's NEW revision. A stale --expected-revision is 409 contact_changed (exit 1, code and current revision printed); 404 when the user is not a collaborator")
190
+ .option("--expected-revision <n>", "the contact's `revision` as you read it (0-2147483647); omit for no pin")
191
+ .action(run(async (client, contactId, userId, opts) => {
192
+ assertUuid(contactId, "contact id");
193
+ if (userId.trim() === "")
194
+ throw new Error("userId must not be blank.");
195
+ const body = { userId };
196
+ if (opts.expectedRevision !== undefined)
197
+ body.expectedRevision = intFlag(opts.expectedRevision, "--expected-revision", 0, CONTACT_REVISION_MAX);
198
+ const path = `/api/v1/contacts/${encodeURIComponent(contactId)}/collaborators`;
199
+ return method === "post" ? client.post(path, body) : client.del(path, body);
200
+ }));
201
+ }
178
202
  contacts
179
203
  .command("delete <id>")
180
204
  .description("Delete a contact by id (soft-delete; retained for audit, the contact's deals are unaffected)")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "conduyt",
3
- "version": "1.62.0",
3
+ "version": "1.63.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",