conduyt 1.58.1 → 1.60.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 +4 -0
- package/dist/index.js +108 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -34,6 +34,7 @@ conduyt whoami # show the authenticated user
|
|
|
34
34
|
conduyt contacts list --limit 25 # list contacts (supports --cursor, --query)
|
|
35
35
|
conduyt contacts get <id> # fetch one contact
|
|
36
36
|
conduyt deals list --pipeline <id> # list deals
|
|
37
|
+
conduyt deals bulk-update --file updates.json # update up to 100 deals at once ([{id, stageId, value, ...}]); per-item results, exit 1 if any failed
|
|
37
38
|
conduyt pipelines # list pipelines and stages
|
|
38
39
|
conduyt search "acme corp" # search across the CRM
|
|
39
40
|
conduyt insights summary # run an AI insight query
|
|
@@ -58,6 +59,9 @@ conduyt reply-capture status # inbound reply capture: state, DNS reco
|
|
|
58
59
|
conduyt reply-capture setup # create the receiving subdomain (--domain optional)
|
|
59
60
|
conduyt reply-capture verify # promote it to live once DNS is confirmed
|
|
60
61
|
conduyt reply-capture remove --confirm REMOVE # remove capture — replies to already-sent email are lost
|
|
62
|
+
conduyt integrations wordpress-forms list # WordPress form intake addresses: full URL of every active one, received counts
|
|
63
|
+
conduyt integrations wordpress-forms create --name "Contact page" --default-tag "Website lead" # prints the full URL
|
|
64
|
+
conduyt integrations wordpress-forms revoke <id> # the address answers 404 from now on; the row is kept
|
|
61
65
|
conduyt api GET /api/v1/companies # raw authenticated request (escape hatch)
|
|
62
66
|
conduyt config show # show resolved config (key masked)
|
|
63
67
|
```
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import { dirname, join } from "node:path";
|
|
|
6
6
|
import { Command } from "commander";
|
|
7
7
|
import { ConduytClient, buildQuery } from "./client.js";
|
|
8
8
|
import { saveConfig, resolveConfig, configPath } from "./config.js";
|
|
9
|
-
import { print, fail } from "./output.js";
|
|
9
|
+
import { print, fail, unwrap } from "./output.js";
|
|
10
10
|
// Exit cleanly when a downstream pipe closes early (e.g. `conduyt deals list |
|
|
11
11
|
// head`). Without this, the stdout write after the reader is gone throws an
|
|
12
12
|
// unhandled EPIPE and prints a Node stack trace — broken-pipe is normal Unix
|
|
@@ -561,6 +561,73 @@ deals
|
|
|
561
561
|
assertUuid(id, "deal id");
|
|
562
562
|
return client.del(`/api/v1/deals/${encodeURIComponent(id)}`);
|
|
563
563
|
}));
|
|
564
|
+
deals
|
|
565
|
+
.command("bulk-update")
|
|
566
|
+
.description("Update up to 100 deals in one request (POST /bulk/deals/update). Pass a JSON array of { id, ...editable fields (title, stageId, value, currency, status, priority, assignedTo, expectedCloseDate, lostReason, customFields) } via --updates or --file. Each item is applied on its own; prints the summary and one result per item, and exits 1 when any item failed (the full result is still printed). Rate limited to 5 requests a minute")
|
|
567
|
+
.option("--updates <json>", "the updates as a JSON array (or @file)")
|
|
568
|
+
.option("--file <path>", "read the updates JSON array from a file")
|
|
569
|
+
.action(async (opts) => {
|
|
570
|
+
try {
|
|
571
|
+
if (opts.updates !== undefined && opts.file !== undefined)
|
|
572
|
+
throw new Error("Pass either --updates or --file, not both.");
|
|
573
|
+
let raw;
|
|
574
|
+
if (opts.file !== undefined) {
|
|
575
|
+
if (opts.file.trim() === "")
|
|
576
|
+
throw new Error("--file must be a path (got an empty string).");
|
|
577
|
+
try {
|
|
578
|
+
raw = readFileSync(opts.file, "utf8");
|
|
579
|
+
}
|
|
580
|
+
catch {
|
|
581
|
+
throw new Error(`--file could not be read: ${opts.file}`);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
else if (opts.updates !== undefined) {
|
|
585
|
+
raw = opts.updates.startsWith("@") ? readFileSync(opts.updates.slice(1), "utf8") : opts.updates;
|
|
586
|
+
}
|
|
587
|
+
else {
|
|
588
|
+
throw new Error("The updates are required. Pass --updates '<json array>' or --file <path>.");
|
|
589
|
+
}
|
|
590
|
+
let updates;
|
|
591
|
+
try {
|
|
592
|
+
updates = JSON.parse(raw);
|
|
593
|
+
}
|
|
594
|
+
catch {
|
|
595
|
+
throw new Error("The updates must be valid JSON (an array of { id, ...fields }).");
|
|
596
|
+
}
|
|
597
|
+
if (!Array.isArray(updates))
|
|
598
|
+
throw new Error("The updates must be a JSON array of { id, ...fields } objects.");
|
|
599
|
+
const client = new ConduytClient();
|
|
600
|
+
const res = await client.post("/api/v1/bulk/deals/update", { updates });
|
|
601
|
+
print(res);
|
|
602
|
+
const data = unwrap(res);
|
|
603
|
+
const results = data && typeof data === "object" && Array.isArray(data.results) ? data.results : null;
|
|
604
|
+
const sm = data && typeof data === "object" && data.summary && typeof data.summary === "object" && !Array.isArray(data.summary) ? data.summary : null;
|
|
605
|
+
const n = updates.length;
|
|
606
|
+
const shapeOk = !!sm && !!results &&
|
|
607
|
+
typeof sm.total === "number" && typeof sm.succeeded === "number" && typeof sm.failed === "number" &&
|
|
608
|
+
sm.total === n && results.length === n &&
|
|
609
|
+
results.every((r) => r && typeof r === "object" && typeof r.success === "boolean" && typeof r.id === "string") &&
|
|
610
|
+
// The ids that came back must be exactly the ids submitted (order-independent, duplicates count);
|
|
611
|
+
// an item sent without a string id is answered as "unknown".
|
|
612
|
+
JSON.stringify(updates.map((u) => (u && typeof u.id === "string" ? u.id : "unknown")).sort()) ===
|
|
613
|
+
JSON.stringify(results.map((r) => r.id).sort());
|
|
614
|
+
if (!shapeOk) {
|
|
615
|
+
process.stderr.write(`Error: Unexpected response shape from the API: expected data.summary {total, succeeded, failed} and ${n} data.results with id and success.\n`);
|
|
616
|
+
process.exitCode = 1;
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
const failedItems = results.filter((r) => r.success === false);
|
|
620
|
+
if (failedItems.length > 0 || Number(sm.failed) > 0) {
|
|
621
|
+
for (const r of failedItems)
|
|
622
|
+
process.stderr.write(`Failed ${r.id}: ${r.error ?? "unknown error"}\n`);
|
|
623
|
+
process.stderr.write(`Error: ${failedItems.length || sm.failed} of ${sm.total} updates failed.\n`);
|
|
624
|
+
process.exitCode = 1;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
catch (err) {
|
|
628
|
+
fail(err);
|
|
629
|
+
}
|
|
630
|
+
});
|
|
564
631
|
// ---- pipelines ----
|
|
565
632
|
// Default action preserves the original published `conduyt pipelines` (list)
|
|
566
633
|
// invocation now that pipelines is a group — without it, commander prints help
|
|
@@ -2144,6 +2211,46 @@ webhooks
|
|
|
2144
2211
|
.option("--acknowledge-duplicate-risk", "you accept the step may be delivered twice")
|
|
2145
2212
|
.action(run(async (client, id, opts) => client.post(`/api/v1/webhook-logs/${uuidArg(id, "id")}/cancel`, barrierBody(opts))));
|
|
2146
2213
|
// ---------------------------------------------------------------------------
|
|
2214
|
+
// Integrations → WordPress forms (app 10-07): per-form intake addresses that Gravity Forms (Webhooks Add-On), WPForms
|
|
2215
|
+
// (Webhooks addon) and Elementor Pro (Webhook action) post submissions to; each submission becomes a contact. The full
|
|
2216
|
+
// address is the credential: `create` prints it and `list` shows it for every active intake. API key: list and create
|
|
2217
|
+
// need the form-intakes AND contacts scopes on a write or admin tier key; revoke needs form-intakes.
|
|
2218
|
+
// ---------------------------------------------------------------------------
|
|
2219
|
+
const integrations = program.command("integrations").description("Integrations managed from the CLI");
|
|
2220
|
+
const wordpressForms = integrations
|
|
2221
|
+
.command("wordpress-forms")
|
|
2222
|
+
.description("WordPress form intake addresses (Gravity Forms, WPForms, Elementor): submissions become contacts (form-intakes + contacts scopes, write tier; revoke: form-intakes)");
|
|
2223
|
+
wordpressForms
|
|
2224
|
+
.command("list")
|
|
2225
|
+
.description("List every active intake address (newest first), then the revoked history (the newest 200 revocations), with the full URL of every active intake and received counts")
|
|
2226
|
+
.action(run(async (client) => client.get("/api/v1/integrations/wordpress-forms/intakes")));
|
|
2227
|
+
wordpressForms
|
|
2228
|
+
.command("create")
|
|
2229
|
+
.description("Create an intake address for one form. Prints the full URL (the credential; list shows it too while active): paste it into the form plugin's webhook URL. Needs the form-intakes and contacts scopes")
|
|
2230
|
+
.option("--name <text>", "what the form is, e.g. \"Contact page, Smith Roofing\" (required, max 120)")
|
|
2231
|
+
.option("--default-tag <name>", "tag name applied to every submission unless the submission sends its own tags")
|
|
2232
|
+
.option("--source <text>", "contact source for submissions that do not send one (default wordpress)")
|
|
2233
|
+
.option("--assign-to <userId>", "user id (an active member) who owns new contacts; omit = lead routing decides")
|
|
2234
|
+
.action(run(async (client, opts) => {
|
|
2235
|
+
const name = (opts.name ?? "").trim();
|
|
2236
|
+
if (!name)
|
|
2237
|
+
throw new Error("--name is required");
|
|
2238
|
+
if (name.length > 120)
|
|
2239
|
+
throw new Error("--name must be at most 120 characters");
|
|
2240
|
+
const body = { name };
|
|
2241
|
+
if (opts.defaultTag !== undefined)
|
|
2242
|
+
body.defaultTag = opts.defaultTag;
|
|
2243
|
+
if (opts.source !== undefined)
|
|
2244
|
+
body.source = opts.source;
|
|
2245
|
+
if (opts.assignTo !== undefined)
|
|
2246
|
+
body.assignedTo = uuidArg(opts.assignTo, "--assign-to");
|
|
2247
|
+
return client.post("/api/v1/integrations/wordpress-forms/intakes", body);
|
|
2248
|
+
}));
|
|
2249
|
+
wordpressForms
|
|
2250
|
+
.command("revoke <id>")
|
|
2251
|
+
.description("Revoke an intake address: submissions to it answer 404 from now on. The row is kept (status revoked) for history")
|
|
2252
|
+
.action(run(async (client, id) => client.del(`/api/v1/integrations/wordpress-forms/intakes/${uuidArg(id, "id")}`)));
|
|
2253
|
+
// ---------------------------------------------------------------------------
|
|
2147
2254
|
// Sequences (one model for Email and SMS since 2026-09-30): steps send saved templates of the sequence's channel a
|
|
2148
2255
|
// number of days apart inside a send window; a run stops on a reply, a booked meeting, an opt-out, a closed deal or a
|
|
2149
2256
|
// removal. Provider drip timetables are the `drip-enrollments` group, a different thing.
|
package/package.json
CHANGED