conduyt 1.3.0 → 1.4.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/client.js +87 -1
- package/dist/index.js +1178 -2
- package/package.json +1 -1
package/dist/client.js
CHANGED
|
@@ -20,7 +20,14 @@ export class ConduytClient {
|
|
|
20
20
|
const text = await res.text();
|
|
21
21
|
const json = text ? safeParse(text) : null;
|
|
22
22
|
if (!res.ok) {
|
|
23
|
-
|
|
23
|
+
// Preserve structured non-2xx bodies: several endpoints return the data
|
|
24
|
+
// a caller needs to proceed IN the error body (e.g. custom-field delete
|
|
25
|
+
// 409 carries valueCount/dependencyCount for the --confirm flow).
|
|
26
|
+
// Reducing those to statusText would strand the user.
|
|
27
|
+
const structured = json && typeof json === "object" && !json.error
|
|
28
|
+
? JSON.stringify(json)
|
|
29
|
+
: null;
|
|
30
|
+
const msg = json?.error || structured || res.statusText || `HTTP ${res.status}`;
|
|
24
31
|
throw new Error(`Conduyt API ${res.status}: ${msg}`);
|
|
25
32
|
}
|
|
26
33
|
return json;
|
|
@@ -37,6 +44,85 @@ export class ConduytClient {
|
|
|
37
44
|
del(path) {
|
|
38
45
|
return this.request("DELETE", path);
|
|
39
46
|
}
|
|
47
|
+
// Stream `ai chat`'s text/event-stream response to stdout as clean answer
|
|
48
|
+
// text. The server frames SSE records as `data: {"text":"..."}\n\n`, ends
|
|
49
|
+
// with `data: [DONE]\n\n`, and reports in-stream failures as
|
|
50
|
+
// `data: {"error":"..."}\n\n` — so we parse events (buffering across chunk
|
|
51
|
+
// splits), print only the text deltas, stop cleanly on [DONE], and THROW on
|
|
52
|
+
// an error event so the command exits non-zero instead of "succeeding" with
|
|
53
|
+
// raw protocol noise.
|
|
54
|
+
async stream(method, path, body) {
|
|
55
|
+
const url = `${this.baseUrl}${path.startsWith("/") ? path : `/${path}`}`;
|
|
56
|
+
const res = await fetch(url, {
|
|
57
|
+
method,
|
|
58
|
+
headers: {
|
|
59
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
60
|
+
"Content-Type": "application/json",
|
|
61
|
+
Accept: "text/event-stream",
|
|
62
|
+
},
|
|
63
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
64
|
+
});
|
|
65
|
+
if (!res.ok || !res.body) {
|
|
66
|
+
const text = await res.text();
|
|
67
|
+
const json = text ? safeParse(text) : null;
|
|
68
|
+
const msg = json?.error || res.statusText || `HTTP ${res.status}`;
|
|
69
|
+
throw new Error(`Conduyt API ${res.status}: ${msg}`);
|
|
70
|
+
}
|
|
71
|
+
const reader = res.body.getReader();
|
|
72
|
+
const decoder = new TextDecoder();
|
|
73
|
+
let buffer = "";
|
|
74
|
+
let wroteAny = false;
|
|
75
|
+
const handleEvent = (payload) => {
|
|
76
|
+
if (payload === "[DONE]")
|
|
77
|
+
return "done";
|
|
78
|
+
const parsed = safeParse(payload);
|
|
79
|
+
if (parsed && typeof parsed === "object") {
|
|
80
|
+
const rec = parsed;
|
|
81
|
+
if (typeof rec.error === "string" && rec.error) {
|
|
82
|
+
if (wroteAny)
|
|
83
|
+
process.stdout.write("\n");
|
|
84
|
+
throw new Error(`AI stream error: ${rec.error}`);
|
|
85
|
+
}
|
|
86
|
+
if (typeof rec.text === "string" && rec.text) {
|
|
87
|
+
process.stdout.write(rec.text);
|
|
88
|
+
wroteAny = true;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return undefined;
|
|
92
|
+
};
|
|
93
|
+
const drainBuffer = () => {
|
|
94
|
+
// SSE events are separated by a blank line; fields we care about are
|
|
95
|
+
// `data: <payload>` lines (multi-line data joins with \n per spec).
|
|
96
|
+
let sep;
|
|
97
|
+
while ((sep = buffer.indexOf("\n\n")) !== -1) {
|
|
98
|
+
const rawEvent = buffer.slice(0, sep);
|
|
99
|
+
buffer = buffer.slice(sep + 2);
|
|
100
|
+
const dataLines = rawEvent
|
|
101
|
+
.split("\n")
|
|
102
|
+
.filter((l) => l.startsWith("data:"))
|
|
103
|
+
.map((l) => l.slice(5).replace(/^ /, ""));
|
|
104
|
+
if (dataLines.length === 0)
|
|
105
|
+
continue;
|
|
106
|
+
if (handleEvent(dataLines.join("\n")) === "done")
|
|
107
|
+
return "done";
|
|
108
|
+
}
|
|
109
|
+
return undefined;
|
|
110
|
+
};
|
|
111
|
+
outer: for (;;) {
|
|
112
|
+
const { done, value } = await reader.read();
|
|
113
|
+
if (value) {
|
|
114
|
+
buffer += decoder.decode(value, { stream: true });
|
|
115
|
+
if (drainBuffer() === "done")
|
|
116
|
+
break outer;
|
|
117
|
+
}
|
|
118
|
+
if (done)
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
buffer += decoder.decode();
|
|
122
|
+
drainBuffer();
|
|
123
|
+
if (wroteAny)
|
|
124
|
+
process.stdout.write("\n");
|
|
125
|
+
}
|
|
40
126
|
}
|
|
41
127
|
function safeParse(text) {
|
|
42
128
|
try {
|
package/dist/index.js
CHANGED
|
@@ -99,6 +99,34 @@ contacts
|
|
|
99
99
|
Object.assign(body, jsonArg(opts.json, "--json"));
|
|
100
100
|
return client.post("/api/v1/contacts", body);
|
|
101
101
|
}));
|
|
102
|
+
contacts
|
|
103
|
+
.command("update <id>")
|
|
104
|
+
.description("Update a contact (PATCH — only the fields you pass change)")
|
|
105
|
+
.option("--first <name>", "first name")
|
|
106
|
+
.option("--last <name>", "last name")
|
|
107
|
+
.option("--email <email>", "email")
|
|
108
|
+
.option("--phone <phone>", "phone")
|
|
109
|
+
.option("--company <company>", "company name (auto-creates/links a company; pass '' to clear)")
|
|
110
|
+
.option("--json <json>", "full JSON body, merged over the flags above (for any field)")
|
|
111
|
+
.action(run(async (client, id, opts) => {
|
|
112
|
+
assertUuid(id, "contact id");
|
|
113
|
+
const body = {};
|
|
114
|
+
if (opts.first !== undefined)
|
|
115
|
+
body.firstName = opts.first;
|
|
116
|
+
if (opts.last !== undefined)
|
|
117
|
+
body.lastName = opts.last;
|
|
118
|
+
if (opts.email !== undefined)
|
|
119
|
+
body.email = opts.email;
|
|
120
|
+
if (opts.phone !== undefined)
|
|
121
|
+
body.phone = opts.phone;
|
|
122
|
+
if (opts.company !== undefined)
|
|
123
|
+
body.company = opts.company;
|
|
124
|
+
if (opts.json !== undefined)
|
|
125
|
+
Object.assign(body, jsonArg(opts.json, "--json"));
|
|
126
|
+
if (Object.keys(body).length === 0)
|
|
127
|
+
throw new Error("Nothing to update. Pass at least one field flag or --json.");
|
|
128
|
+
return client.patch(`/api/v1/contacts/${encodeURIComponent(id)}`, body);
|
|
129
|
+
}));
|
|
102
130
|
contacts
|
|
103
131
|
.command("delete <id>")
|
|
104
132
|
.description("Delete a contact by id (soft-delete; retained for audit, the contact's deals are unaffected)")
|
|
@@ -272,6 +300,57 @@ deals
|
|
|
272
300
|
assertFiniteValue(body.value);
|
|
273
301
|
return client.post("/api/v1/deals", body);
|
|
274
302
|
}));
|
|
303
|
+
deals
|
|
304
|
+
.command("get <id>")
|
|
305
|
+
.description("Get a single deal by id")
|
|
306
|
+
.action(run(async (client, id) => {
|
|
307
|
+
assertUuid(id, "deal id");
|
|
308
|
+
return client.get(`/api/v1/deals/${encodeURIComponent(id)}`);
|
|
309
|
+
}));
|
|
310
|
+
deals
|
|
311
|
+
.command("update <id>")
|
|
312
|
+
.description("Update a deal (PATCH — only the fields you pass change). --stage moves the deal (won/lost status auto-derives from the target stage's flags)")
|
|
313
|
+
.option("--title <title>", "deal title")
|
|
314
|
+
.option("--value <n>", "deal value (number)")
|
|
315
|
+
.option("--currency <code>", "ISO currency, e.g. GBP")
|
|
316
|
+
.option("--stage <name-or-uuid>", "move to this stage — accepts a stage name (resolved via /automations/resolve) or a stage UUID")
|
|
317
|
+
.option("--pipeline <id>", "move to this pipeline UUID")
|
|
318
|
+
.option("--contact <id>", "primary contact UUID")
|
|
319
|
+
.option("--status <status>", "explicit status (open|won|lost); overrides the status auto-derived from --stage")
|
|
320
|
+
.option("--json <json>", "full JSON body, merged over the flags")
|
|
321
|
+
.action(run(async (client, id, opts) => {
|
|
322
|
+
assertUuid(id, "deal id");
|
|
323
|
+
const body = {};
|
|
324
|
+
if (opts.title !== undefined)
|
|
325
|
+
body.title = opts.title;
|
|
326
|
+
if (opts.value !== undefined) {
|
|
327
|
+
const trimmed = opts.value.trim();
|
|
328
|
+
const n = Number(trimmed);
|
|
329
|
+
if (trimmed === "" || !Number.isFinite(n)) {
|
|
330
|
+
throw new Error("--value must be a finite number.");
|
|
331
|
+
}
|
|
332
|
+
body.value = n;
|
|
333
|
+
}
|
|
334
|
+
if (opts.currency !== undefined)
|
|
335
|
+
body.currency = opts.currency;
|
|
336
|
+
if (opts.pipeline !== undefined)
|
|
337
|
+
body.pipelineId = opts.pipeline;
|
|
338
|
+
if (opts.contact !== undefined)
|
|
339
|
+
body.contactId = opts.contact;
|
|
340
|
+
if (opts.status !== undefined)
|
|
341
|
+
body.status = opts.status;
|
|
342
|
+
if (opts.stage !== undefined) {
|
|
343
|
+
if (opts.stage.trim() === "")
|
|
344
|
+
throw new Error("--stage was provided but is blank. Omit it or pass a stage name/UUID.");
|
|
345
|
+
body.stageId = await resolveStageId(client, opts.stage);
|
|
346
|
+
}
|
|
347
|
+
if (opts.json !== undefined)
|
|
348
|
+
Object.assign(body, jsonArg(opts.json, "--json"));
|
|
349
|
+
assertFiniteValue(body.value);
|
|
350
|
+
if (Object.keys(body).length === 0)
|
|
351
|
+
throw new Error("Nothing to update. Pass at least one field flag or --json.");
|
|
352
|
+
return client.patch(`/api/v1/deals/${encodeURIComponent(id)}`, body);
|
|
353
|
+
}));
|
|
275
354
|
deals
|
|
276
355
|
.command("delete <id>")
|
|
277
356
|
.description("Delete a deal by id (soft-delete; drops out of lists/boards and pipeline totals, retained for audit)")
|
|
@@ -335,7 +414,9 @@ program
|
|
|
335
414
|
// ---- insights ----
|
|
336
415
|
program
|
|
337
416
|
.command("insights <type>")
|
|
338
|
-
.description("Run an AI insight query
|
|
417
|
+
.description("Run an AI insight query. Types: sms_drip_status, sms_responses_untouched, " +
|
|
418
|
+
"agent_followups, pipeline_health, email_performance, smart_view_summary, " +
|
|
419
|
+
"team_activity, contact_import_history")
|
|
339
420
|
.option("--input <json>", "JSON payload to send with the request")
|
|
340
421
|
.action(run(async (client, type, opts) => {
|
|
341
422
|
let extra = {};
|
|
@@ -347,7 +428,10 @@ program
|
|
|
347
428
|
fail("--input must be valid JSON");
|
|
348
429
|
}
|
|
349
430
|
}
|
|
350
|
-
|
|
431
|
+
// The /ai/insights endpoint reads the insight name from `query`, not
|
|
432
|
+
// `type` — sending `type` returned "Unsupported query type" for every
|
|
433
|
+
// call, so the CLI's insights command was fully broken.
|
|
434
|
+
return client.post("/api/v1/ai/insights", { query: type, ...extra });
|
|
351
435
|
}));
|
|
352
436
|
// ---- users (team members) ----
|
|
353
437
|
const users = program.command("users").description("Manage team members");
|
|
@@ -436,6 +520,1030 @@ customFields
|
|
|
436
520
|
assertOptionsShape(body.options);
|
|
437
521
|
return client.post("/api/v1/custom-fields", body);
|
|
438
522
|
}));
|
|
523
|
+
// ---- tasks ----
|
|
524
|
+
const tasks = program.command("tasks").description("Manage tasks");
|
|
525
|
+
tasks
|
|
526
|
+
.command("list")
|
|
527
|
+
.description("List tasks")
|
|
528
|
+
.option("--status <status>", "filter by status, e.g. todo|in_progress|done")
|
|
529
|
+
.option("--priority <priority>", "filter by priority, e.g. low|medium|high|urgent")
|
|
530
|
+
.option("--assigned <id>", "filter by assignee user UUID")
|
|
531
|
+
.option("--contact <id>", "filter by contact UUID")
|
|
532
|
+
.option("--deal <id>", "filter by deal UUID")
|
|
533
|
+
.option("--search <q>", "search title/description")
|
|
534
|
+
.option("--tab <tab>", "my_tasks | all | completed")
|
|
535
|
+
.option("--overdue", "only overdue, not-done tasks")
|
|
536
|
+
.option("--due-from <date>", "due on/after this date")
|
|
537
|
+
.option("--due-to <date>", "due on/before this date")
|
|
538
|
+
.option("--sort <field>", "sort field (e.g. dueDate, priority, createdAt)")
|
|
539
|
+
.option("--order <dir>", "asc | desc")
|
|
540
|
+
.option("--page <n>", "page number")
|
|
541
|
+
.option("--limit <n>", "results per page (per_page)")
|
|
542
|
+
.action(run(async (client, opts) => client.get(`/api/v1/tasks${buildQuery({
|
|
543
|
+
status: opts.status,
|
|
544
|
+
priority: opts.priority,
|
|
545
|
+
assigned_to: opts.assigned,
|
|
546
|
+
contact_id: opts.contact,
|
|
547
|
+
deal_id: opts.deal,
|
|
548
|
+
search: opts.search,
|
|
549
|
+
tab: opts.tab,
|
|
550
|
+
overdue: opts.overdue ? "true" : undefined,
|
|
551
|
+
dueFrom: opts.dueFrom,
|
|
552
|
+
dueTo: opts.dueTo,
|
|
553
|
+
sort: opts.sort,
|
|
554
|
+
order: opts.order,
|
|
555
|
+
page: opts.page,
|
|
556
|
+
per_page: opts.limit,
|
|
557
|
+
})}`)));
|
|
558
|
+
tasks
|
|
559
|
+
.command("get <id>")
|
|
560
|
+
.description("Get a single task by id")
|
|
561
|
+
.action(run(async (client, id) => {
|
|
562
|
+
assertUuid(id, "task id");
|
|
563
|
+
return client.get(`/api/v1/tasks/${encodeURIComponent(id)}`);
|
|
564
|
+
}));
|
|
565
|
+
tasks
|
|
566
|
+
.command("create")
|
|
567
|
+
.description("Create a task (--assigned accepts a user UUID, or the magic strings @me / @owner)")
|
|
568
|
+
.option("--title <title>", "task title (required)")
|
|
569
|
+
.option("--description <text>", "description")
|
|
570
|
+
.option("--status <status>", "todo|in_progress|done (default todo)")
|
|
571
|
+
.option("--priority <priority>", "low|medium|high|urgent (default medium)")
|
|
572
|
+
.option("--due <date>", "due date (ISO or date string)")
|
|
573
|
+
.option("--assigned <id>", "assignee user UUID, or @me / @owner")
|
|
574
|
+
.option("--contact <id>", "link to contact UUID")
|
|
575
|
+
.option("--deal <id>", "link to deal UUID")
|
|
576
|
+
.option("--json <json>", "full JSON body, merged over the flags")
|
|
577
|
+
.action(run(async (client, opts) => {
|
|
578
|
+
const body = {};
|
|
579
|
+
if (opts.title !== undefined)
|
|
580
|
+
body.title = opts.title;
|
|
581
|
+
if (opts.description !== undefined)
|
|
582
|
+
body.description = opts.description;
|
|
583
|
+
if (opts.status !== undefined)
|
|
584
|
+
body.status = opts.status;
|
|
585
|
+
if (opts.priority !== undefined)
|
|
586
|
+
body.priority = opts.priority;
|
|
587
|
+
if (opts.due !== undefined)
|
|
588
|
+
body.dueDate = opts.due;
|
|
589
|
+
if (opts.assigned !== undefined)
|
|
590
|
+
body.assignedTo = opts.assigned;
|
|
591
|
+
if (opts.contact !== undefined)
|
|
592
|
+
body.contactId = opts.contact;
|
|
593
|
+
if (opts.deal !== undefined)
|
|
594
|
+
body.dealId = opts.deal;
|
|
595
|
+
if (opts.json !== undefined)
|
|
596
|
+
Object.assign(body, jsonArg(opts.json, "--json"));
|
|
597
|
+
return client.post("/api/v1/tasks", body);
|
|
598
|
+
}));
|
|
599
|
+
tasks
|
|
600
|
+
.command("update <id>")
|
|
601
|
+
.description("Update a task (PATCH — only the fields you pass change)")
|
|
602
|
+
.option("--title <title>", "task title")
|
|
603
|
+
.option("--description <text>", "description")
|
|
604
|
+
.option("--status <status>", "todo|in_progress|done")
|
|
605
|
+
.option("--priority <priority>", "low|medium|high|urgent")
|
|
606
|
+
.option("--due <date>", "due date")
|
|
607
|
+
.option("--assigned <id>", "assignee user UUID, or @me / @owner")
|
|
608
|
+
.option("--contact <id>", "link to contact UUID")
|
|
609
|
+
.option("--deal <id>", "link to deal UUID")
|
|
610
|
+
.option("--json <json>", "full JSON body, merged over the flags")
|
|
611
|
+
.action(run(async (client, id, opts) => {
|
|
612
|
+
assertUuid(id, "task id");
|
|
613
|
+
const body = {};
|
|
614
|
+
if (opts.title !== undefined)
|
|
615
|
+
body.title = opts.title;
|
|
616
|
+
if (opts.description !== undefined)
|
|
617
|
+
body.description = opts.description;
|
|
618
|
+
if (opts.status !== undefined)
|
|
619
|
+
body.status = opts.status;
|
|
620
|
+
if (opts.priority !== undefined)
|
|
621
|
+
body.priority = opts.priority;
|
|
622
|
+
if (opts.due !== undefined)
|
|
623
|
+
body.dueDate = opts.due;
|
|
624
|
+
if (opts.assigned !== undefined)
|
|
625
|
+
body.assignedTo = opts.assigned;
|
|
626
|
+
if (opts.contact !== undefined)
|
|
627
|
+
body.contactId = opts.contact;
|
|
628
|
+
if (opts.deal !== undefined)
|
|
629
|
+
body.dealId = opts.deal;
|
|
630
|
+
if (opts.json !== undefined)
|
|
631
|
+
Object.assign(body, jsonArg(opts.json, "--json"));
|
|
632
|
+
if (Object.keys(body).length === 0)
|
|
633
|
+
throw new Error("Nothing to update. Pass at least one field flag or --json.");
|
|
634
|
+
return client.patch(`/api/v1/tasks/${encodeURIComponent(id)}`, body);
|
|
635
|
+
}));
|
|
636
|
+
tasks
|
|
637
|
+
.command("complete <id>")
|
|
638
|
+
.description("Mark a task done (PATCH status=done)")
|
|
639
|
+
.action(run(async (client, id) => {
|
|
640
|
+
assertUuid(id, "task id");
|
|
641
|
+
return client.patch(`/api/v1/tasks/${encodeURIComponent(id)}`, { status: "done" });
|
|
642
|
+
}));
|
|
643
|
+
tasks
|
|
644
|
+
.command("delete <id>")
|
|
645
|
+
.description("Delete a task by id (hard delete)")
|
|
646
|
+
.action(run(async (client, id) => {
|
|
647
|
+
assertUuid(id, "task id");
|
|
648
|
+
return client.del(`/api/v1/tasks/${encodeURIComponent(id)}`);
|
|
649
|
+
}));
|
|
650
|
+
// ---- messages / conversations ----
|
|
651
|
+
const messages = program.command("messages").description("Send and read messages");
|
|
652
|
+
messages
|
|
653
|
+
.command("list")
|
|
654
|
+
.description("List logged messages")
|
|
655
|
+
.option("--contact <id>", "filter by contact UUID")
|
|
656
|
+
.option("--channel <channel>", "sms | email")
|
|
657
|
+
.option("--direction <direction>", "inbound | outbound")
|
|
658
|
+
.option("--status <status>", "filter by status")
|
|
659
|
+
.option("--page <n>", "page number")
|
|
660
|
+
.option("--limit <n>", "results per page (per_page)")
|
|
661
|
+
.action(run(async (client, opts) => client.get(`/api/v1/messages${buildQuery({
|
|
662
|
+
contactId: opts.contact,
|
|
663
|
+
channel: opts.channel,
|
|
664
|
+
direction: opts.direction,
|
|
665
|
+
status: opts.status,
|
|
666
|
+
page: opts.page,
|
|
667
|
+
per_page: opts.limit,
|
|
668
|
+
})}`)));
|
|
669
|
+
messages
|
|
670
|
+
.command("send")
|
|
671
|
+
.description("Log/create a message (POST /messages). NOTE: outbound SMS is rejected here — use `messages send-sms` for real SMS delivery")
|
|
672
|
+
.option("--contact <id>", "contact UUID (required)")
|
|
673
|
+
.option("--channel <channel>", "sms | email (required)")
|
|
674
|
+
.option("--direction <direction>", "inbound | outbound (required)")
|
|
675
|
+
.option("--body <text>", "message body (required)")
|
|
676
|
+
.option("--subject <subject>", "subject (email)")
|
|
677
|
+
.option("--from <from>", "from number/email")
|
|
678
|
+
.option("--to <to>", "to number/email")
|
|
679
|
+
.option("--json <json>", "full JSON body, merged over the flags")
|
|
680
|
+
.action(run(async (client, opts) => {
|
|
681
|
+
const body = {};
|
|
682
|
+
if (opts.contact !== undefined)
|
|
683
|
+
body.contactId = opts.contact;
|
|
684
|
+
if (opts.channel !== undefined)
|
|
685
|
+
body.channel = opts.channel;
|
|
686
|
+
if (opts.direction !== undefined)
|
|
687
|
+
body.direction = opts.direction;
|
|
688
|
+
if (opts.body !== undefined)
|
|
689
|
+
body.body = opts.body;
|
|
690
|
+
if (opts.subject !== undefined)
|
|
691
|
+
body.subject = opts.subject;
|
|
692
|
+
if (opts.from !== undefined) {
|
|
693
|
+
if (opts.channel === "email")
|
|
694
|
+
body.fromEmail = opts.from;
|
|
695
|
+
else
|
|
696
|
+
body.fromNumber = opts.from;
|
|
697
|
+
}
|
|
698
|
+
if (opts.to !== undefined) {
|
|
699
|
+
if (opts.channel === "email")
|
|
700
|
+
body.toEmail = opts.to;
|
|
701
|
+
else
|
|
702
|
+
body.toNumber = opts.to;
|
|
703
|
+
}
|
|
704
|
+
if (opts.json !== undefined)
|
|
705
|
+
Object.assign(body, jsonArg(opts.json, "--json"));
|
|
706
|
+
return client.post("/api/v1/messages", body);
|
|
707
|
+
}));
|
|
708
|
+
messages
|
|
709
|
+
.command("send-sms")
|
|
710
|
+
.description("Send an outbound SMS via Twilio (POST /messages/sms/send)")
|
|
711
|
+
.option("--contact <id>", "contact UUID (required)")
|
|
712
|
+
.option("--body <text>", "SMS body, 1-1600 chars (required)")
|
|
713
|
+
.option("--from <number>", "from number — must be an account-owned Twilio number/agent DID")
|
|
714
|
+
.option("--provider <id>", "smsProviderId UUID")
|
|
715
|
+
.option("--json <json>", "full JSON body, merged over the flags")
|
|
716
|
+
.action(run(async (client, opts) => {
|
|
717
|
+
const body = {};
|
|
718
|
+
if (opts.contact !== undefined)
|
|
719
|
+
body.contactId = opts.contact;
|
|
720
|
+
if (opts.body !== undefined)
|
|
721
|
+
body.body = opts.body;
|
|
722
|
+
if (opts.from !== undefined)
|
|
723
|
+
body.fromNumber = opts.from;
|
|
724
|
+
if (opts.provider !== undefined)
|
|
725
|
+
body.smsProviderId = opts.provider;
|
|
726
|
+
if (opts.json !== undefined)
|
|
727
|
+
Object.assign(body, jsonArg(opts.json, "--json"));
|
|
728
|
+
return client.post("/api/v1/messages/sms/send", body);
|
|
729
|
+
}));
|
|
730
|
+
messages
|
|
731
|
+
.command("conversations")
|
|
732
|
+
.description("List conversation threads")
|
|
733
|
+
.option("--channel <channel>", "sms | email")
|
|
734
|
+
.option("--search <q>", "search query")
|
|
735
|
+
.option("--status <status>", "filter by conversation status")
|
|
736
|
+
.option("--page <n>", "page number")
|
|
737
|
+
.option("--limit <n>", "results per page (per_page)")
|
|
738
|
+
.action(run(async (client, opts) => client.get(`/api/v1/conversations${buildQuery({
|
|
739
|
+
channel: opts.channel,
|
|
740
|
+
search: opts.search,
|
|
741
|
+
status: opts.status,
|
|
742
|
+
page: opts.page,
|
|
743
|
+
per_page: opts.limit,
|
|
744
|
+
})}`)));
|
|
745
|
+
messages
|
|
746
|
+
.command("conversation <contactId>")
|
|
747
|
+
.description("Read a contact's conversation (messages + notes)")
|
|
748
|
+
.option("--channel <channel>", "sms | email")
|
|
749
|
+
.option("--cursor <messageId>", "paginate backward from this message id")
|
|
750
|
+
.option("--around <messageId>", "center the window on this message id")
|
|
751
|
+
.option("--limit <n>", "messages per page (per_page)")
|
|
752
|
+
.action(run(async (client, contactId, opts) => {
|
|
753
|
+
assertUuid(contactId, "contact id");
|
|
754
|
+
return client.get(`/api/v1/conversations/${encodeURIComponent(contactId)}${buildQuery({
|
|
755
|
+
channel: opts.channel,
|
|
756
|
+
cursor: opts.cursor,
|
|
757
|
+
around: opts.around,
|
|
758
|
+
per_page: opts.limit,
|
|
759
|
+
})}`);
|
|
760
|
+
}));
|
|
761
|
+
// ---- appointments ----
|
|
762
|
+
const appointments = program.command("appointments").description("Manage appointments");
|
|
763
|
+
appointments
|
|
764
|
+
.command("list")
|
|
765
|
+
.description("List appointments")
|
|
766
|
+
.option("--status <status>", "filter by status")
|
|
767
|
+
.option("--calendar <id>", "filter by calendar UUID")
|
|
768
|
+
.option("--contact <id>", "filter by contact UUID")
|
|
769
|
+
.option("--assigned <id>", "filter by assignee user UUID")
|
|
770
|
+
.option("--booking-page <id>", "filter by booking page UUID")
|
|
771
|
+
.option("--from <date>", "start of date range")
|
|
772
|
+
.option("--to <date>", "end of date range")
|
|
773
|
+
.option("--page <n>", "page number")
|
|
774
|
+
.option("--limit <n>", "results per page (per_page)")
|
|
775
|
+
.action(run(async (client, opts) => client.get(`/api/v1/appointments${buildQuery({
|
|
776
|
+
status: opts.status,
|
|
777
|
+
calendar_id: opts.calendar,
|
|
778
|
+
contact_id: opts.contact,
|
|
779
|
+
assigned_to: opts.assigned,
|
|
780
|
+
booking_page_id: opts.bookingPage,
|
|
781
|
+
from: opts.from,
|
|
782
|
+
to: opts.to,
|
|
783
|
+
page: opts.page,
|
|
784
|
+
per_page: opts.limit,
|
|
785
|
+
})}`)));
|
|
786
|
+
appointments
|
|
787
|
+
.command("get <id>")
|
|
788
|
+
.description("Get a single appointment by id")
|
|
789
|
+
.action(run(async (client, id) => {
|
|
790
|
+
assertUuid(id, "appointment id");
|
|
791
|
+
return client.get(`/api/v1/appointments/${encodeURIComponent(id)}`);
|
|
792
|
+
}));
|
|
793
|
+
appointments
|
|
794
|
+
.command("create")
|
|
795
|
+
.description("Create an appointment. --start/--end must be ISO-8601 WITH a timezone offset (e.g. 2026-07-10T15:00:00-07:00)")
|
|
796
|
+
.option("--title <title>", "title (required)")
|
|
797
|
+
.option("--start <iso>", "start time, ISO-8601 with tz offset (required)")
|
|
798
|
+
.option("--end <iso>", "end time, ISO-8601 with tz offset (required)")
|
|
799
|
+
.option("--description <text>", "description")
|
|
800
|
+
.option("--calendar <id>", "calendar UUID")
|
|
801
|
+
.option("--contact <id>", "contact UUID")
|
|
802
|
+
.option("--assigned <id>", "assignee user UUID")
|
|
803
|
+
.option("--location <location>", "location")
|
|
804
|
+
.option("--status <status>", "status (default confirmed)")
|
|
805
|
+
.option("--json <json>", "full JSON body, merged over the flags")
|
|
806
|
+
.action(run(async (client, opts) => {
|
|
807
|
+
const body = {};
|
|
808
|
+
if (opts.title !== undefined)
|
|
809
|
+
body.title = opts.title;
|
|
810
|
+
if (opts.start !== undefined)
|
|
811
|
+
body.startTime = opts.start;
|
|
812
|
+
if (opts.end !== undefined)
|
|
813
|
+
body.endTime = opts.end;
|
|
814
|
+
if (opts.description !== undefined)
|
|
815
|
+
body.description = opts.description;
|
|
816
|
+
if (opts.calendar !== undefined)
|
|
817
|
+
body.calendarId = opts.calendar;
|
|
818
|
+
if (opts.contact !== undefined)
|
|
819
|
+
body.contactId = opts.contact;
|
|
820
|
+
if (opts.assigned !== undefined)
|
|
821
|
+
body.assignedTo = opts.assigned;
|
|
822
|
+
if (opts.location !== undefined)
|
|
823
|
+
body.location = opts.location;
|
|
824
|
+
if (opts.status !== undefined)
|
|
825
|
+
body.status = opts.status;
|
|
826
|
+
if (opts.json !== undefined)
|
|
827
|
+
Object.assign(body, jsonArg(opts.json, "--json"));
|
|
828
|
+
return client.post("/api/v1/appointments", body);
|
|
829
|
+
}));
|
|
830
|
+
appointments
|
|
831
|
+
.command("update <id>")
|
|
832
|
+
.description("Update an appointment (PATCH). --start/--end reschedule; ISO-8601 with tz offset")
|
|
833
|
+
.option("--title <title>", "title")
|
|
834
|
+
.option("--start <iso>", "start time, ISO-8601 with tz offset")
|
|
835
|
+
.option("--end <iso>", "end time, ISO-8601 with tz offset")
|
|
836
|
+
.option("--description <text>", "description")
|
|
837
|
+
.option("--location <location>", "location")
|
|
838
|
+
.option("--status <status>", "status (e.g. confirmed, cancelled)")
|
|
839
|
+
.option("--assigned <id>", "assignee user UUID (or empty to unassign)")
|
|
840
|
+
.option("--json <json>", "full JSON body, merged over the flags")
|
|
841
|
+
.action(run(async (client, id, opts) => {
|
|
842
|
+
assertUuid(id, "appointment id");
|
|
843
|
+
const body = {};
|
|
844
|
+
if (opts.title !== undefined)
|
|
845
|
+
body.title = opts.title;
|
|
846
|
+
if (opts.start !== undefined)
|
|
847
|
+
body.startTime = opts.start;
|
|
848
|
+
if (opts.end !== undefined)
|
|
849
|
+
body.endTime = opts.end;
|
|
850
|
+
if (opts.description !== undefined)
|
|
851
|
+
body.description = opts.description;
|
|
852
|
+
if (opts.location !== undefined)
|
|
853
|
+
body.location = opts.location;
|
|
854
|
+
if (opts.status !== undefined)
|
|
855
|
+
body.status = opts.status;
|
|
856
|
+
if (opts.assigned !== undefined)
|
|
857
|
+
body.assignedTo = opts.assigned;
|
|
858
|
+
if (opts.json !== undefined)
|
|
859
|
+
Object.assign(body, jsonArg(opts.json, "--json"));
|
|
860
|
+
if (Object.keys(body).length === 0)
|
|
861
|
+
throw new Error("Nothing to update. Pass at least one field flag or --json.");
|
|
862
|
+
return client.patch(`/api/v1/appointments/${encodeURIComponent(id)}`, body);
|
|
863
|
+
}));
|
|
864
|
+
appointments
|
|
865
|
+
.command("cancel <id>")
|
|
866
|
+
.description("Cancel an appointment (soft-cancel; sets status=cancelled)")
|
|
867
|
+
.action(run(async (client, id) => {
|
|
868
|
+
assertUuid(id, "appointment id");
|
|
869
|
+
return client.del(`/api/v1/appointments/${encodeURIComponent(id)}`);
|
|
870
|
+
}));
|
|
871
|
+
// ---- calendars ----
|
|
872
|
+
const calendars = program.command("calendars").description("Manage calendars");
|
|
873
|
+
calendars
|
|
874
|
+
.command("list")
|
|
875
|
+
.description("List calendars")
|
|
876
|
+
.option("--page <n>", "page number")
|
|
877
|
+
.option("--limit <n>", "results per page (per_page)")
|
|
878
|
+
.action(run(async (client, opts) => client.get(`/api/v1/calendars${buildQuery({ page: opts.page, per_page: opts.limit })}`)));
|
|
879
|
+
calendars
|
|
880
|
+
.command("create")
|
|
881
|
+
.description("Create a calendar")
|
|
882
|
+
.option("--name <name>", "calendar name (required)")
|
|
883
|
+
.option("--description <text>", "description")
|
|
884
|
+
.option("--timezone <tz>", "IANA timezone (default America/Los_Angeles)")
|
|
885
|
+
.option("--default", "make this the default calendar")
|
|
886
|
+
.option("--json <json>", "full JSON body, merged over the flags")
|
|
887
|
+
.action(run(async (client, opts) => {
|
|
888
|
+
const body = {};
|
|
889
|
+
if (opts.name !== undefined)
|
|
890
|
+
body.name = opts.name;
|
|
891
|
+
if (opts.description !== undefined)
|
|
892
|
+
body.description = opts.description;
|
|
893
|
+
if (opts.timezone !== undefined)
|
|
894
|
+
body.timezone = opts.timezone;
|
|
895
|
+
if (opts.default)
|
|
896
|
+
body.isDefault = true;
|
|
897
|
+
if (opts.json !== undefined)
|
|
898
|
+
Object.assign(body, jsonArg(opts.json, "--json"));
|
|
899
|
+
return client.post("/api/v1/calendars", body);
|
|
900
|
+
}));
|
|
901
|
+
// ---- automations (the "AI builds a workflow" loop) ----
|
|
902
|
+
const automations = program.command("automations").description("Build and run automations/workflows");
|
|
903
|
+
automations
|
|
904
|
+
.command("list")
|
|
905
|
+
.description("List automations")
|
|
906
|
+
.option("--page <n>", "page number")
|
|
907
|
+
.option("--limit <n>", "results per page (per_page)")
|
|
908
|
+
.action(run(async (client, opts) => client.get(`/api/v1/automations${buildQuery({ page: opts.page, per_page: opts.limit })}`)));
|
|
909
|
+
automations
|
|
910
|
+
.command("schema")
|
|
911
|
+
.description("Get the node/field/operator schema catalog (node types, action rules, graph shape)")
|
|
912
|
+
.action(run(async (client) => client.get("/api/v1/automations/schema")));
|
|
913
|
+
automations
|
|
914
|
+
.command("resolve")
|
|
915
|
+
.description("Resolve human-readable names to UUIDs. Pass comma lists via flags and/or a full JSON body")
|
|
916
|
+
.option("--tags <list>", "comma-separated tag names")
|
|
917
|
+
.option("--pipelines <list>", "comma-separated pipeline names")
|
|
918
|
+
.option("--stages <list>", "comma-separated stage names")
|
|
919
|
+
.option("--users <list>", "comma-separated user names or emails")
|
|
920
|
+
.option("--json <json>", "full JSON body (e.g. {\"templates\":[\"Welcome\"]}), merged over the flags")
|
|
921
|
+
.action(run(async (client, opts) => {
|
|
922
|
+
const body = {};
|
|
923
|
+
if (opts.tags !== undefined)
|
|
924
|
+
body.tags = csvArg(opts.tags, "--tags");
|
|
925
|
+
if (opts.pipelines !== undefined)
|
|
926
|
+
body.pipelines = csvArg(opts.pipelines, "--pipelines");
|
|
927
|
+
if (opts.stages !== undefined)
|
|
928
|
+
body.stages = csvArg(opts.stages, "--stages");
|
|
929
|
+
if (opts.users !== undefined)
|
|
930
|
+
body.users = csvArg(opts.users, "--users");
|
|
931
|
+
if (opts.json !== undefined)
|
|
932
|
+
Object.assign(body, jsonArg(opts.json, "--json"));
|
|
933
|
+
if (Object.keys(body).length === 0)
|
|
934
|
+
throw new Error("Nothing to resolve. Pass --tags/--pipelines/--stages/--users or --json.");
|
|
935
|
+
return client.post("/api/v1/automations/resolve", body);
|
|
936
|
+
}));
|
|
937
|
+
automations
|
|
938
|
+
.command("validate")
|
|
939
|
+
.description("Validate a workflow graph before saving. Provide the body via --json or --file (needs triggerEvent + graph)")
|
|
940
|
+
.option("--json <json>", "full request body as JSON")
|
|
941
|
+
.option("--file <path>", "read the request body from a JSON file")
|
|
942
|
+
.action(run(async (client, opts) => client.post("/api/v1/automations/validate", bodyFromJsonOrFile(opts))));
|
|
943
|
+
automations
|
|
944
|
+
.command("import")
|
|
945
|
+
.description("Create a workflow (with optional name resolution). Provide the body via --json or --file (needs name + triggerEvent + graph)")
|
|
946
|
+
.option("--json <json>", "full request body as JSON")
|
|
947
|
+
.option("--file <path>", "read the request body from a JSON file")
|
|
948
|
+
.option("--resolve-names", "set resolveNames=true so name fields in node configs are converted to ids")
|
|
949
|
+
.action(run(async (client, opts) => {
|
|
950
|
+
const body = bodyFromJsonOrFile(opts);
|
|
951
|
+
if (opts.resolveNames)
|
|
952
|
+
body.resolveNames = true;
|
|
953
|
+
return client.post("/api/v1/automations/import", body);
|
|
954
|
+
}));
|
|
955
|
+
automations
|
|
956
|
+
.command("dry-run <id>")
|
|
957
|
+
.description("Test a workflow without side effects")
|
|
958
|
+
.option("--contact <id>", "run against this contact UUID")
|
|
959
|
+
.option("--appointment <id>", "seed with this appointment UUID")
|
|
960
|
+
.option("--source <source>", "draft | published (default draft)")
|
|
961
|
+
.option("--mode <mode>", "single | all-paths (default single)")
|
|
962
|
+
.action(run(async (client, id, opts) => {
|
|
963
|
+
assertUuid(id, "automation id");
|
|
964
|
+
const body = {};
|
|
965
|
+
if (opts.contact !== undefined)
|
|
966
|
+
body.contactId = opts.contact;
|
|
967
|
+
if (opts.appointment !== undefined)
|
|
968
|
+
body.appointmentId = opts.appointment;
|
|
969
|
+
return client.post(`/api/v1/automations/${encodeURIComponent(id)}/dry-run${buildQuery({ source: opts.source, mode: opts.mode })}`, body);
|
|
970
|
+
}));
|
|
971
|
+
automations
|
|
972
|
+
.command("publish <id>")
|
|
973
|
+
.description("Publish a draft workflow to live")
|
|
974
|
+
.action(run(async (client, id) => {
|
|
975
|
+
assertUuid(id, "automation id");
|
|
976
|
+
return client.post(`/api/v1/automations/${encodeURIComponent(id)}/publish`, {});
|
|
977
|
+
}));
|
|
978
|
+
automations
|
|
979
|
+
.command("step-logs [id]")
|
|
980
|
+
.description("Execution step logs. With an automation id, logs for that one; without, account-wide (filter with --automation)")
|
|
981
|
+
.option("--automation <id>", "account-wide: filter to this automation UUID (ignored when [id] is given)")
|
|
982
|
+
.option("--status <status>", "filter by step status")
|
|
983
|
+
.option("--action-type <type>", "filter by action type")
|
|
984
|
+
.option("--search <q>", "fuzzy search on contact / automation name")
|
|
985
|
+
.option("--date-from <iso>", "startedAt >= this")
|
|
986
|
+
.option("--date-to <iso>", "startedAt <= this")
|
|
987
|
+
.option("--limit <n>", "max results (default 50, max 200)")
|
|
988
|
+
.option("--offset <n>", "pagination offset")
|
|
989
|
+
.action(run(async (client, id, opts) => {
|
|
990
|
+
if (id !== undefined)
|
|
991
|
+
assertUuid(id, "automation id");
|
|
992
|
+
const q = buildQuery({
|
|
993
|
+
automationId: id === undefined ? opts.automation : undefined,
|
|
994
|
+
status: opts.status,
|
|
995
|
+
actionType: opts.actionType,
|
|
996
|
+
search: opts.search,
|
|
997
|
+
dateFrom: opts.dateFrom,
|
|
998
|
+
dateTo: opts.dateTo,
|
|
999
|
+
limit: opts.limit,
|
|
1000
|
+
offset: opts.offset,
|
|
1001
|
+
});
|
|
1002
|
+
const path = id !== undefined
|
|
1003
|
+
? `/api/v1/automations/${encodeURIComponent(id)}/step-logs${q}`
|
|
1004
|
+
: `/api/v1/automations/step-logs${q}`;
|
|
1005
|
+
return client.get(path);
|
|
1006
|
+
}));
|
|
1007
|
+
// ---- companies ----
|
|
1008
|
+
const companies = program.command("companies").description("Manage companies");
|
|
1009
|
+
companies
|
|
1010
|
+
.command("list")
|
|
1011
|
+
.description("List companies")
|
|
1012
|
+
.option("--search <q>", "search by name/domain")
|
|
1013
|
+
.option("--industry <industry>", "filter by industry")
|
|
1014
|
+
.option("--size <size>", "filter by size band (e.g. 11-50)")
|
|
1015
|
+
.option("--sort <field>", "createdAt|updatedAt|name|healthScore|annualRevenue|totalValue|dealCount")
|
|
1016
|
+
.option("--order <dir>", "asc | desc")
|
|
1017
|
+
.option("--page <n>", "page number")
|
|
1018
|
+
.option("--limit <n>", "results per page (per_page)")
|
|
1019
|
+
.action(run(async (client, opts) => client.get(`/api/v1/companies${buildQuery({
|
|
1020
|
+
search: opts.search,
|
|
1021
|
+
industry: opts.industry,
|
|
1022
|
+
size: opts.size,
|
|
1023
|
+
sort: opts.sort,
|
|
1024
|
+
order: opts.order,
|
|
1025
|
+
page: opts.page,
|
|
1026
|
+
per_page: opts.limit,
|
|
1027
|
+
})}`)));
|
|
1028
|
+
companies
|
|
1029
|
+
.command("get <id>")
|
|
1030
|
+
.description("Get a single company by id")
|
|
1031
|
+
.action(run(async (client, id) => {
|
|
1032
|
+
assertUuid(id, "company id");
|
|
1033
|
+
return client.get(`/api/v1/companies/${encodeURIComponent(id)}`);
|
|
1034
|
+
}));
|
|
1035
|
+
companies
|
|
1036
|
+
.command("create")
|
|
1037
|
+
.description("Create a company")
|
|
1038
|
+
.option("--name <name>", "company name (required)")
|
|
1039
|
+
.option("--domain <domain>", "domain")
|
|
1040
|
+
.option("--industry <industry>", "industry")
|
|
1041
|
+
.option("--size <size>", "size band: 1-10|11-50|51-200|201-500|501-1000|1001+")
|
|
1042
|
+
.option("--website <url>", "website URL")
|
|
1043
|
+
.option("--phone <phone>", "phone")
|
|
1044
|
+
.option("--city <city>", "city")
|
|
1045
|
+
.option("--state <state>", "state")
|
|
1046
|
+
.option("--country <cc>", "ISO-3166 alpha-2 country code (default US)")
|
|
1047
|
+
.option("--description <text>", "description")
|
|
1048
|
+
.option("--json <json>", "full JSON body, merged over the flags")
|
|
1049
|
+
.action(run(async (client, opts) => {
|
|
1050
|
+
const body = {};
|
|
1051
|
+
if (opts.name !== undefined)
|
|
1052
|
+
body.name = opts.name;
|
|
1053
|
+
if (opts.domain !== undefined)
|
|
1054
|
+
body.domain = opts.domain;
|
|
1055
|
+
if (opts.industry !== undefined)
|
|
1056
|
+
body.industry = opts.industry;
|
|
1057
|
+
if (opts.size !== undefined)
|
|
1058
|
+
body.size = opts.size;
|
|
1059
|
+
if (opts.website !== undefined)
|
|
1060
|
+
body.website = opts.website;
|
|
1061
|
+
if (opts.phone !== undefined)
|
|
1062
|
+
body.phone = opts.phone;
|
|
1063
|
+
if (opts.city !== undefined)
|
|
1064
|
+
body.city = opts.city;
|
|
1065
|
+
if (opts.state !== undefined)
|
|
1066
|
+
body.state = opts.state;
|
|
1067
|
+
if (opts.country !== undefined)
|
|
1068
|
+
body.country = opts.country;
|
|
1069
|
+
if (opts.description !== undefined)
|
|
1070
|
+
body.description = opts.description;
|
|
1071
|
+
if (opts.json !== undefined)
|
|
1072
|
+
Object.assign(body, jsonArg(opts.json, "--json"));
|
|
1073
|
+
return client.post("/api/v1/companies", body);
|
|
1074
|
+
}));
|
|
1075
|
+
companies
|
|
1076
|
+
.command("update <id>")
|
|
1077
|
+
.description("Update a company (PATCH — only the fields you pass change)")
|
|
1078
|
+
.option("--name <name>", "company name")
|
|
1079
|
+
.option("--domain <domain>", "domain")
|
|
1080
|
+
.option("--industry <industry>", "industry")
|
|
1081
|
+
.option("--size <size>", "size band")
|
|
1082
|
+
.option("--website <url>", "website URL")
|
|
1083
|
+
.option("--phone <phone>", "phone")
|
|
1084
|
+
.option("--city <city>", "city")
|
|
1085
|
+
.option("--state <state>", "state")
|
|
1086
|
+
.option("--country <cc>", "ISO-3166 alpha-2 country code")
|
|
1087
|
+
.option("--description <text>", "description")
|
|
1088
|
+
.option("--json <json>", "full JSON body, merged over the flags")
|
|
1089
|
+
.action(run(async (client, id, opts) => {
|
|
1090
|
+
assertUuid(id, "company id");
|
|
1091
|
+
const body = {};
|
|
1092
|
+
if (opts.name !== undefined)
|
|
1093
|
+
body.name = opts.name;
|
|
1094
|
+
if (opts.domain !== undefined)
|
|
1095
|
+
body.domain = opts.domain;
|
|
1096
|
+
if (opts.industry !== undefined)
|
|
1097
|
+
body.industry = opts.industry;
|
|
1098
|
+
if (opts.size !== undefined)
|
|
1099
|
+
body.size = opts.size;
|
|
1100
|
+
if (opts.website !== undefined)
|
|
1101
|
+
body.website = opts.website;
|
|
1102
|
+
if (opts.phone !== undefined)
|
|
1103
|
+
body.phone = opts.phone;
|
|
1104
|
+
if (opts.city !== undefined)
|
|
1105
|
+
body.city = opts.city;
|
|
1106
|
+
if (opts.state !== undefined)
|
|
1107
|
+
body.state = opts.state;
|
|
1108
|
+
if (opts.country !== undefined)
|
|
1109
|
+
body.country = opts.country;
|
|
1110
|
+
if (opts.description !== undefined)
|
|
1111
|
+
body.description = opts.description;
|
|
1112
|
+
if (opts.json !== undefined)
|
|
1113
|
+
Object.assign(body, jsonArg(opts.json, "--json"));
|
|
1114
|
+
if (Object.keys(body).length === 0)
|
|
1115
|
+
throw new Error("Nothing to update. Pass at least one field flag or --json.");
|
|
1116
|
+
return client.patch(`/api/v1/companies/${encodeURIComponent(id)}`, body);
|
|
1117
|
+
}));
|
|
1118
|
+
// ---- tags ----
|
|
1119
|
+
// ---- saved filters (personal, Velocify-style) ----
|
|
1120
|
+
const savedFilters = program
|
|
1121
|
+
.command("saved-filters")
|
|
1122
|
+
.description("Manage YOUR personal saved contact filters (the web app's Saved dropdown; requires the 'saved-filters' key scope)");
|
|
1123
|
+
savedFilters
|
|
1124
|
+
.command("list")
|
|
1125
|
+
.description("List your saved filters")
|
|
1126
|
+
.option("--entity <type>", "entity type (default contact)")
|
|
1127
|
+
.action(run(async (client, opts) => client.get(`/api/v1/saved-filters${buildQuery({ entity_type: opts.entity })}`)));
|
|
1128
|
+
savedFilters
|
|
1129
|
+
.command("create")
|
|
1130
|
+
.description("Save a named filter (grouped payload via --filters JSON)")
|
|
1131
|
+
.option("--name <name>", "display name (required, unique per user, max 60 chars)")
|
|
1132
|
+
.option("--filters <json>", `grouped filter payload JSON, e.g. [{"conjunction":"AND","rules":[{"field":"source","operator":"equals","value":"facebook"}]}]`)
|
|
1133
|
+
.option("--sort-field <field>", "sort field (default createdAt)")
|
|
1134
|
+
.option("--sort-order <dir>", "asc | desc (default desc)")
|
|
1135
|
+
.option("--entity <type>", "entity type (default contact)")
|
|
1136
|
+
.option("--json <json>", "full JSON body, merged over the flags")
|
|
1137
|
+
.action(run(async (client, opts) => {
|
|
1138
|
+
const body = {};
|
|
1139
|
+
if (opts.name !== undefined)
|
|
1140
|
+
body.name = opts.name;
|
|
1141
|
+
if (opts.filters !== undefined)
|
|
1142
|
+
body.filters = jsonArrayArg(opts.filters, "--filters");
|
|
1143
|
+
if (opts.sortField !== undefined)
|
|
1144
|
+
body.sortField = opts.sortField;
|
|
1145
|
+
if (opts.sortOrder !== undefined)
|
|
1146
|
+
body.sortOrder = opts.sortOrder;
|
|
1147
|
+
if (opts.entity !== undefined)
|
|
1148
|
+
body.entityType = opts.entity;
|
|
1149
|
+
if (opts.json !== undefined)
|
|
1150
|
+
Object.assign(body, jsonArg(opts.json, "--json"));
|
|
1151
|
+
// A --json merge can clobber filters with a non-array; re-assert the
|
|
1152
|
+
// shape after ALL merges so a malformed payload fails locally.
|
|
1153
|
+
if (body.filters !== undefined && !Array.isArray(body.filters)) {
|
|
1154
|
+
throw new Error("filters must be a JSON ARRAY (grouped payload or flat rules).");
|
|
1155
|
+
}
|
|
1156
|
+
return client.post("/api/v1/saved-filters", body);
|
|
1157
|
+
}));
|
|
1158
|
+
savedFilters
|
|
1159
|
+
.command("update <id>")
|
|
1160
|
+
.description("Rename a saved filter or replace its filters/sort (owner-only)")
|
|
1161
|
+
.option("--name <name>", "new display name")
|
|
1162
|
+
.option("--filters <json>", "grouped filter payload JSON")
|
|
1163
|
+
.option("--sort-field <field>", "sort field")
|
|
1164
|
+
.option("--sort-order <dir>", "asc | desc")
|
|
1165
|
+
.option("--json <json>", "full JSON body, merged over the flags")
|
|
1166
|
+
.action(run(async (client, id, opts) => {
|
|
1167
|
+
assertUuid(id, "saved filter id");
|
|
1168
|
+
const body = {};
|
|
1169
|
+
if (opts.name !== undefined)
|
|
1170
|
+
body.name = opts.name;
|
|
1171
|
+
if (opts.filters !== undefined)
|
|
1172
|
+
body.filters = jsonArrayArg(opts.filters, "--filters");
|
|
1173
|
+
if (opts.sortField !== undefined)
|
|
1174
|
+
body.sortField = opts.sortField;
|
|
1175
|
+
if (opts.sortOrder !== undefined)
|
|
1176
|
+
body.sortOrder = opts.sortOrder;
|
|
1177
|
+
if (opts.json !== undefined)
|
|
1178
|
+
Object.assign(body, jsonArg(opts.json, "--json"));
|
|
1179
|
+
// Same post-merge shape assertion as create — see above.
|
|
1180
|
+
if (body.filters !== undefined && !Array.isArray(body.filters)) {
|
|
1181
|
+
throw new Error("filters must be a JSON ARRAY (grouped payload or flat rules).");
|
|
1182
|
+
}
|
|
1183
|
+
return client.patch(`/api/v1/saved-filters/${encodeURIComponent(id)}`, body);
|
|
1184
|
+
}));
|
|
1185
|
+
savedFilters
|
|
1186
|
+
.command("delete <id>")
|
|
1187
|
+
.description("Delete one of your saved filters (owner-only)")
|
|
1188
|
+
.action(run(async (client, id) => {
|
|
1189
|
+
assertUuid(id, "saved filter id");
|
|
1190
|
+
return client.del(`/api/v1/saved-filters/${encodeURIComponent(id)}`);
|
|
1191
|
+
}));
|
|
1192
|
+
const tags = program.command("tags").description("Manage tags");
|
|
1193
|
+
tags
|
|
1194
|
+
.command("list")
|
|
1195
|
+
.description("List tags")
|
|
1196
|
+
.option("--name <name>", "exact (case-insensitive) name match")
|
|
1197
|
+
.option("--search <q>", "name contains")
|
|
1198
|
+
.option("--page <n>", "page number")
|
|
1199
|
+
.option("--limit <n>", "results per page (per_page)")
|
|
1200
|
+
.action(run(async (client, opts) => client.get(`/api/v1/tags${buildQuery({ name: opts.name, search: opts.search, page: opts.page, per_page: opts.limit })}`)));
|
|
1201
|
+
tags
|
|
1202
|
+
.command("create")
|
|
1203
|
+
.description("Create a tag")
|
|
1204
|
+
.option("--name <name>", "tag name (required)")
|
|
1205
|
+
.option("--color <hex>", "hex color (default #6B7280)")
|
|
1206
|
+
.action(run(async (client, opts) => {
|
|
1207
|
+
const body = {};
|
|
1208
|
+
if (opts.name !== undefined)
|
|
1209
|
+
body.name = opts.name;
|
|
1210
|
+
if (opts.color !== undefined)
|
|
1211
|
+
body.color = opts.color;
|
|
1212
|
+
return client.post("/api/v1/tags", body);
|
|
1213
|
+
}));
|
|
1214
|
+
// ---- notes ----
|
|
1215
|
+
const notes = program.command("notes").description("Manage notes");
|
|
1216
|
+
notes
|
|
1217
|
+
.command("list")
|
|
1218
|
+
.description("List notes (filter by contact or deal)")
|
|
1219
|
+
.option("--contact <id>", "filter by contact UUID")
|
|
1220
|
+
.option("--deal <id>", "filter by deal UUID")
|
|
1221
|
+
.option("--page <n>", "page number")
|
|
1222
|
+
.option("--limit <n>", "results per page (per_page)")
|
|
1223
|
+
.action(run(async (client, opts) => client.get(`/api/v1/notes${buildQuery({ contact_id: opts.contact, deal_id: opts.deal, page: opts.page, per_page: opts.limit })}`)));
|
|
1224
|
+
notes
|
|
1225
|
+
.command("create")
|
|
1226
|
+
.description("Create a note on a contact and/or deal")
|
|
1227
|
+
.option("--body <text>", "note body (required)")
|
|
1228
|
+
.option("--contact <id>", "attach to contact UUID")
|
|
1229
|
+
.option("--deal <id>", "attach to deal UUID")
|
|
1230
|
+
.option("--pinned", "pin the note")
|
|
1231
|
+
.option("--json <json>", "full JSON body, merged over the flags")
|
|
1232
|
+
.action(run(async (client, opts) => {
|
|
1233
|
+
const body = {};
|
|
1234
|
+
if (opts.body !== undefined)
|
|
1235
|
+
body.body = opts.body;
|
|
1236
|
+
if (opts.contact !== undefined)
|
|
1237
|
+
body.contactId = opts.contact;
|
|
1238
|
+
if (opts.deal !== undefined)
|
|
1239
|
+
body.dealId = opts.deal;
|
|
1240
|
+
if (opts.pinned)
|
|
1241
|
+
body.isPinned = true;
|
|
1242
|
+
if (opts.json !== undefined)
|
|
1243
|
+
Object.assign(body, jsonArg(opts.json, "--json"));
|
|
1244
|
+
return client.post("/api/v1/notes", body);
|
|
1245
|
+
}));
|
|
1246
|
+
// ---- activities ----
|
|
1247
|
+
const activities = program.command("activities").description("Manage activities (timeline events)");
|
|
1248
|
+
activities
|
|
1249
|
+
.command("list")
|
|
1250
|
+
.description("List activities")
|
|
1251
|
+
.option("--contact <id>", "filter by contact UUID")
|
|
1252
|
+
.option("--deal <id>", "filter by deal UUID")
|
|
1253
|
+
.option("--type <type>", "filter by type (buckets: task, deal_update)")
|
|
1254
|
+
.option("--search <q>", "search")
|
|
1255
|
+
.option("--from <date>", "fromDate YYYY-MM-DD")
|
|
1256
|
+
.option("--to <date>", "toDate YYYY-MM-DD")
|
|
1257
|
+
.option("--page <n>", "page number")
|
|
1258
|
+
.option("--limit <n>", "results per page (per_page)")
|
|
1259
|
+
.action(run(async (client, opts) => client.get(`/api/v1/activities${buildQuery({
|
|
1260
|
+
contact_id: opts.contact,
|
|
1261
|
+
deal_id: opts.deal,
|
|
1262
|
+
type: opts.type,
|
|
1263
|
+
search: opts.search,
|
|
1264
|
+
fromDate: opts.from,
|
|
1265
|
+
toDate: opts.to,
|
|
1266
|
+
page: opts.page,
|
|
1267
|
+
per_page: opts.limit,
|
|
1268
|
+
})}`)));
|
|
1269
|
+
activities
|
|
1270
|
+
.command("create")
|
|
1271
|
+
.description("Log an activity (type call/email/sms/meeting are stored as manual activities)")
|
|
1272
|
+
.option("--type <type>", "activity type (required)")
|
|
1273
|
+
.option("--title <title>", "title (required)")
|
|
1274
|
+
.option("--description <text>", "description")
|
|
1275
|
+
.option("--contact <id>", "contact UUID")
|
|
1276
|
+
.option("--deal <id>", "deal UUID")
|
|
1277
|
+
.option("--json <json>", "full JSON body, merged over the flags")
|
|
1278
|
+
.action(run(async (client, opts) => {
|
|
1279
|
+
const body = {};
|
|
1280
|
+
if (opts.type !== undefined)
|
|
1281
|
+
body.type = opts.type;
|
|
1282
|
+
if (opts.title !== undefined)
|
|
1283
|
+
body.title = opts.title;
|
|
1284
|
+
if (opts.description !== undefined)
|
|
1285
|
+
body.description = opts.description;
|
|
1286
|
+
if (opts.contact !== undefined)
|
|
1287
|
+
body.contactId = opts.contact;
|
|
1288
|
+
if (opts.deal !== undefined)
|
|
1289
|
+
body.dealId = opts.deal;
|
|
1290
|
+
if (opts.json !== undefined)
|
|
1291
|
+
Object.assign(body, jsonArg(opts.json, "--json"));
|
|
1292
|
+
return client.post("/api/v1/activities", body);
|
|
1293
|
+
}));
|
|
1294
|
+
// ---- users: update / deactivate (added to the existing `users` group) ----
|
|
1295
|
+
users
|
|
1296
|
+
.command("update <id>")
|
|
1297
|
+
.description("Update a team member (PATCH). Cannot change active status here — use `users deactivate`")
|
|
1298
|
+
.option("--first <name>", "first name")
|
|
1299
|
+
.option("--last <name>", "last name")
|
|
1300
|
+
.option("--email <email>", "email")
|
|
1301
|
+
.option("--role <role>", "member | admin | owner")
|
|
1302
|
+
.option("--phone <phone>", "phone")
|
|
1303
|
+
.option("--json <json>", "full JSON body, merged over the flags")
|
|
1304
|
+
.action(run(async (client, id, opts) => {
|
|
1305
|
+
assertUuid(id, "user id");
|
|
1306
|
+
const body = {};
|
|
1307
|
+
if (opts.first !== undefined)
|
|
1308
|
+
body.firstName = opts.first;
|
|
1309
|
+
if (opts.last !== undefined)
|
|
1310
|
+
body.lastName = opts.last;
|
|
1311
|
+
if (opts.email !== undefined)
|
|
1312
|
+
body.email = opts.email;
|
|
1313
|
+
if (opts.role !== undefined)
|
|
1314
|
+
body.role = opts.role;
|
|
1315
|
+
if (opts.phone !== undefined)
|
|
1316
|
+
body.phone = opts.phone;
|
|
1317
|
+
if (opts.json !== undefined)
|
|
1318
|
+
Object.assign(body, jsonArg(opts.json, "--json"));
|
|
1319
|
+
if (Object.keys(body).length === 0)
|
|
1320
|
+
throw new Error("Nothing to update. Pass at least one field flag or --json.");
|
|
1321
|
+
return client.patch(`/api/v1/users/${encodeURIComponent(id)}`, body);
|
|
1322
|
+
}));
|
|
1323
|
+
users
|
|
1324
|
+
.command("deactivate <id>")
|
|
1325
|
+
.description("Soft-deactivate a team member (revokes access; use --reassign to hand off their records)")
|
|
1326
|
+
.option("--reassign <id>", "reassign this user's records to this user UUID")
|
|
1327
|
+
.action(run(async (client, id, opts) => {
|
|
1328
|
+
assertUuid(id, "user id");
|
|
1329
|
+
return client.post(`/api/v1/users/${encodeURIComponent(id)}/deactivate${buildQuery({ reassign_to: opts.reassign })}`, {});
|
|
1330
|
+
}));
|
|
1331
|
+
// ---- custom-fields: list / update / delete (added to the existing group) ----
|
|
1332
|
+
customFields
|
|
1333
|
+
.command("list")
|
|
1334
|
+
.description("List custom field definitions")
|
|
1335
|
+
.option("--entity <type>", "entityType: contact | deal | company")
|
|
1336
|
+
.option("--page <n>", "page number")
|
|
1337
|
+
.option("--limit <n>", "results per page (per_page)")
|
|
1338
|
+
.action(run(async (client, opts) => client.get(`/api/v1/custom-fields${buildQuery({ entityType: opts.entity, page: opts.page, per_page: opts.limit })}`)));
|
|
1339
|
+
customFields
|
|
1340
|
+
.command("update <id>")
|
|
1341
|
+
.description("Update a custom field definition (label/section/options/required/order are mutable; type & key are not)")
|
|
1342
|
+
.option("--label <label>", "display label")
|
|
1343
|
+
.option("--section <section>", "section grouping")
|
|
1344
|
+
.option("--options <json>", "JSON array of options (for select/radio/multiselect)")
|
|
1345
|
+
.option("--required", "mark the field required")
|
|
1346
|
+
.option("--not-required", "mark the field not required")
|
|
1347
|
+
.option("--sort <n>", "sort order (integer)")
|
|
1348
|
+
.option("--json <json>", "full JSON body, merged over the flags")
|
|
1349
|
+
.action(run(async (client, id, opts) => {
|
|
1350
|
+
assertUuid(id, "custom field id");
|
|
1351
|
+
const body = {};
|
|
1352
|
+
if (opts.label !== undefined)
|
|
1353
|
+
body.label = opts.label;
|
|
1354
|
+
if (opts.section !== undefined)
|
|
1355
|
+
body.section = opts.section;
|
|
1356
|
+
if (opts.options !== undefined) {
|
|
1357
|
+
if (opts.options.trim() === "")
|
|
1358
|
+
throw new Error("--options must be valid JSON (got an empty string).");
|
|
1359
|
+
let o;
|
|
1360
|
+
try {
|
|
1361
|
+
o = JSON.parse(opts.options);
|
|
1362
|
+
}
|
|
1363
|
+
catch {
|
|
1364
|
+
throw new Error("--options must be valid JSON.");
|
|
1365
|
+
}
|
|
1366
|
+
if (!Array.isArray(o) || o.length === 0) {
|
|
1367
|
+
throw new Error("--options must be a non-empty JSON array.");
|
|
1368
|
+
}
|
|
1369
|
+
body.options = o;
|
|
1370
|
+
}
|
|
1371
|
+
if (opts.required)
|
|
1372
|
+
body.isRequired = true;
|
|
1373
|
+
if (opts.notRequired)
|
|
1374
|
+
body.isRequired = false;
|
|
1375
|
+
if (opts.sort !== undefined) {
|
|
1376
|
+
const n = Number(opts.sort);
|
|
1377
|
+
if (!Number.isInteger(n))
|
|
1378
|
+
throw new Error("--sort must be an integer.");
|
|
1379
|
+
body.sortOrder = n;
|
|
1380
|
+
}
|
|
1381
|
+
if (opts.json !== undefined)
|
|
1382
|
+
Object.assign(body, jsonArg(opts.json, "--json"));
|
|
1383
|
+
if (Object.keys(body).length === 0)
|
|
1384
|
+
throw new Error("Nothing to update. Pass at least one field flag or --json.");
|
|
1385
|
+
return client.patch(`/api/v1/custom-fields/${encodeURIComponent(id)}`, body);
|
|
1386
|
+
}));
|
|
1387
|
+
customFields
|
|
1388
|
+
.command("delete <id>")
|
|
1389
|
+
.description("Delete a custom field definition. If it has stored values/dependencies the API returns a 409 with the counts — re-run with --confirm --values <n> --deps <n>")
|
|
1390
|
+
.option("--confirm", "confirm deletion of a field that has impact")
|
|
1391
|
+
.option("--values <n>", "expectedValueCount (from the 409 response)")
|
|
1392
|
+
.option("--deps <n>", "expectedDependencyCount (from the 409 response)")
|
|
1393
|
+
.action(run(async (client, id, opts) => {
|
|
1394
|
+
assertUuid(id, "custom field id");
|
|
1395
|
+
let body;
|
|
1396
|
+
if (opts.confirm || opts.values !== undefined || opts.deps !== undefined) {
|
|
1397
|
+
body = { confirmImpact: true };
|
|
1398
|
+
if (opts.values !== undefined) {
|
|
1399
|
+
const n = Number(opts.values);
|
|
1400
|
+
if (!Number.isInteger(n) || n < 0)
|
|
1401
|
+
throw new Error("--values must be a non-negative integer.");
|
|
1402
|
+
body.expectedValueCount = n;
|
|
1403
|
+
}
|
|
1404
|
+
if (opts.deps !== undefined) {
|
|
1405
|
+
const n = Number(opts.deps);
|
|
1406
|
+
if (!Number.isInteger(n) || n < 0)
|
|
1407
|
+
throw new Error("--deps must be a non-negative integer.");
|
|
1408
|
+
body.expectedDependencyCount = n;
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
return client.request("DELETE", `/api/v1/custom-fields/${encodeURIComponent(id)}`, body);
|
|
1412
|
+
}));
|
|
1413
|
+
// ---- webhooks ----
|
|
1414
|
+
const webhooks = program.command("webhooks").description("Manage outbound webhooks");
|
|
1415
|
+
webhooks
|
|
1416
|
+
.command("list")
|
|
1417
|
+
.description("List registered webhooks (secrets omitted)")
|
|
1418
|
+
.option("--page <n>", "page number")
|
|
1419
|
+
.option("--limit <n>", "results per page (per_page)")
|
|
1420
|
+
.action(run(async (client, opts) => client.get(`/api/v1/webhooks/manage${buildQuery({ page: opts.page, per_page: opts.limit })}`)));
|
|
1421
|
+
webhooks
|
|
1422
|
+
.command("register")
|
|
1423
|
+
.description("Register an outbound webhook. The signing secret (whsec_...) is returned ONCE in the response — store it now")
|
|
1424
|
+
.option("--url <url>", "delivery URL (https in prod) (required)")
|
|
1425
|
+
.option("--events <list>", "comma-separated event names (required)")
|
|
1426
|
+
.option("--description <text>", "description")
|
|
1427
|
+
.option("--inactive", "create it disabled (isActive=false)")
|
|
1428
|
+
.option("--json <json>", "full JSON body, merged over the flags")
|
|
1429
|
+
.action(run(async (client, opts) => {
|
|
1430
|
+
const body = {};
|
|
1431
|
+
if (opts.url !== undefined)
|
|
1432
|
+
body.url = opts.url;
|
|
1433
|
+
if (opts.events !== undefined)
|
|
1434
|
+
body.events = csvArg(opts.events, "--events");
|
|
1435
|
+
if (opts.description !== undefined)
|
|
1436
|
+
body.description = opts.description;
|
|
1437
|
+
if (opts.inactive)
|
|
1438
|
+
body.isActive = false;
|
|
1439
|
+
if (opts.json !== undefined)
|
|
1440
|
+
Object.assign(body, jsonArg(opts.json, "--json"));
|
|
1441
|
+
return client.post("/api/v1/webhooks/manage", body);
|
|
1442
|
+
}));
|
|
1443
|
+
// ---- dnc (Do Not Call) ----
|
|
1444
|
+
// NOTE: an AI agent's API key must be granted the `dnc` scope to manage the DNC
|
|
1445
|
+
// list. Reading also requires dialer:view; adding a litigator entry and removals
|
|
1446
|
+
// are admin/owner. A non-admin agent may only DNC a number on a contact it owns
|
|
1447
|
+
// or a number it recently called.
|
|
1448
|
+
const dnc = program.command("dnc").description("Manage the Do Not Call list (API key needs the `dnc` scope)");
|
|
1449
|
+
dnc
|
|
1450
|
+
.command("list")
|
|
1451
|
+
.description("List/search DNC entries. Non-admins must pass --phone (block check only); account-wide browse needs broader access")
|
|
1452
|
+
.option("--phone <number>", "phone number to check/search")
|
|
1453
|
+
.option("--tier <tier>", "dnc | litigator")
|
|
1454
|
+
.option("--search <q>", "search by contact name/phone (admin)")
|
|
1455
|
+
.option("--page <n>", "page number")
|
|
1456
|
+
.option("--limit <n>", "results per page (per_page)")
|
|
1457
|
+
.action(run(async (client, opts) => client.get(`/api/v1/dnc${buildQuery({ phone: opts.phone, tier: opts.tier, search: opts.search, page: opts.page, per_page: opts.limit })}`)));
|
|
1458
|
+
dnc
|
|
1459
|
+
.command("add")
|
|
1460
|
+
.description("Add a number/contact to the DNC list (needs the `dnc` scope; litigator tier is admin-only)")
|
|
1461
|
+
.option("--contact <id>", "contact UUID (contactId or --phone required)")
|
|
1462
|
+
.option("--phone <number>", "phone number (contactId or --phone required)")
|
|
1463
|
+
.option("--tier <tier>", "dnc | litigator (default dnc)")
|
|
1464
|
+
.option("--reason <text>", "reason")
|
|
1465
|
+
.action(run(async (client, opts) => {
|
|
1466
|
+
const body = {};
|
|
1467
|
+
if (opts.contact !== undefined)
|
|
1468
|
+
body.contactId = opts.contact;
|
|
1469
|
+
if (opts.phone !== undefined)
|
|
1470
|
+
body.phone = opts.phone;
|
|
1471
|
+
body.tier = opts.tier ?? "dnc";
|
|
1472
|
+
if (opts.reason !== undefined)
|
|
1473
|
+
body.reason = opts.reason;
|
|
1474
|
+
return client.post("/api/v1/dnc", body);
|
|
1475
|
+
}));
|
|
1476
|
+
dnc
|
|
1477
|
+
.command("remove <id>")
|
|
1478
|
+
.description("Remove a DNC entry by id (admin/owner)")
|
|
1479
|
+
.action(run(async (client, id) => {
|
|
1480
|
+
assertUuid(id, "dnc id");
|
|
1481
|
+
return client.del(`/api/v1/dnc/${encodeURIComponent(id)}`);
|
|
1482
|
+
}));
|
|
1483
|
+
// ---- ai ----
|
|
1484
|
+
const ai = program.command("ai").description("AI assistant and insight endpoints");
|
|
1485
|
+
ai
|
|
1486
|
+
.command("chat <message>")
|
|
1487
|
+
.description("Ask the CRM AI assistant. Streams a server-sent event (SSE) response straight to stdout")
|
|
1488
|
+
.option("--context <text>", "extra page/context string (treated as untrusted data by the assistant)")
|
|
1489
|
+
.action(async (message, opts) => {
|
|
1490
|
+
try {
|
|
1491
|
+
if (message.trim() === "")
|
|
1492
|
+
throw new Error("message must not be empty.");
|
|
1493
|
+
const client = new ConduytClient();
|
|
1494
|
+
const body = { messages: [{ role: "user", content: message }] };
|
|
1495
|
+
if (opts.context !== undefined)
|
|
1496
|
+
body.context = opts.context;
|
|
1497
|
+
await client.stream("POST", "/api/v1/ai/chat", body);
|
|
1498
|
+
}
|
|
1499
|
+
catch (err) {
|
|
1500
|
+
fail(err);
|
|
1501
|
+
}
|
|
1502
|
+
});
|
|
1503
|
+
ai
|
|
1504
|
+
.command("next-actions")
|
|
1505
|
+
.description("AI-recommended next actions for a user (defaults to you; other users need admin/owner)")
|
|
1506
|
+
.option("--user <id>", "user UUID")
|
|
1507
|
+
.action(run(async (client, opts) => client.get(`/api/v1/ai/next-actions${buildQuery({ userId: opts.user })}`)));
|
|
1508
|
+
ai
|
|
1509
|
+
.command("daily-brief")
|
|
1510
|
+
.description("Your AI daily brief (task-focused summary)")
|
|
1511
|
+
.action(run(async (client) => client.get("/api/v1/ai/daily-brief")));
|
|
1512
|
+
ai
|
|
1513
|
+
.command("summarize-contact <contactId>")
|
|
1514
|
+
.description("Generate an AI summary of a contact")
|
|
1515
|
+
.action(run(async (client, contactId) => {
|
|
1516
|
+
assertUuid(contactId, "contact id");
|
|
1517
|
+
return client.post("/api/v1/ai/summarize-contact", { contactId });
|
|
1518
|
+
}));
|
|
1519
|
+
ai
|
|
1520
|
+
.command("enrich")
|
|
1521
|
+
.description("AI-enrich a contact identity (needs at least one of --email / --first / --company)")
|
|
1522
|
+
.option("--email <email>", "email")
|
|
1523
|
+
.option("--first <name>", "first name")
|
|
1524
|
+
.option("--last <name>", "last name")
|
|
1525
|
+
.option("--company <company>", "company")
|
|
1526
|
+
.option("--title <title>", "job title")
|
|
1527
|
+
.option("--phone <phone>", "phone")
|
|
1528
|
+
.option("--json <json>", "full JSON body, merged over the flags")
|
|
1529
|
+
.action(run(async (client, opts) => {
|
|
1530
|
+
const body = {};
|
|
1531
|
+
if (opts.email !== undefined)
|
|
1532
|
+
body.email = opts.email;
|
|
1533
|
+
if (opts.first !== undefined)
|
|
1534
|
+
body.firstName = opts.first;
|
|
1535
|
+
if (opts.last !== undefined)
|
|
1536
|
+
body.lastName = opts.last;
|
|
1537
|
+
if (opts.company !== undefined)
|
|
1538
|
+
body.company = opts.company;
|
|
1539
|
+
if (opts.title !== undefined)
|
|
1540
|
+
body.jobTitle = opts.title;
|
|
1541
|
+
if (opts.phone !== undefined)
|
|
1542
|
+
body.phone = opts.phone;
|
|
1543
|
+
if (opts.json !== undefined)
|
|
1544
|
+
Object.assign(body, jsonArg(opts.json, "--json"));
|
|
1545
|
+
return client.post("/api/v1/ai/enrich-contact", body);
|
|
1546
|
+
}));
|
|
439
1547
|
// ---- raw escape hatch ----
|
|
440
1548
|
program
|
|
441
1549
|
.command("api <method> <path>")
|
|
@@ -523,6 +1631,22 @@ function rejectProtoPollution(obj, flag) {
|
|
|
523
1631
|
}
|
|
524
1632
|
}
|
|
525
1633
|
}
|
|
1634
|
+
function jsonArrayArg(raw, flag) {
|
|
1635
|
+
if (raw.trim() === "") {
|
|
1636
|
+
throw new Error(`${flag} must be valid JSON (got an empty string).`);
|
|
1637
|
+
}
|
|
1638
|
+
let v;
|
|
1639
|
+
try {
|
|
1640
|
+
v = JSON.parse(raw);
|
|
1641
|
+
}
|
|
1642
|
+
catch {
|
|
1643
|
+
throw new Error(`${flag} must be valid JSON.`);
|
|
1644
|
+
}
|
|
1645
|
+
if (!Array.isArray(v)) {
|
|
1646
|
+
throw new Error(`${flag} must be a JSON ARRAY (e.g. [{"conjunction":"AND","rules":[...]}]).`);
|
|
1647
|
+
}
|
|
1648
|
+
return v;
|
|
1649
|
+
}
|
|
526
1650
|
function jsonArg(raw, flag) {
|
|
527
1651
|
if (raw.trim() === "") {
|
|
528
1652
|
throw new Error(`${flag} must be valid JSON (got an empty string).`);
|
|
@@ -543,6 +1667,58 @@ function jsonArg(raw, flag) {
|
|
|
543
1667
|
rejectProtoPollution(v, flag);
|
|
544
1668
|
return v;
|
|
545
1669
|
}
|
|
1670
|
+
// Non-throwing UUID check (assertUuid throws; this just tests). Used to decide
|
|
1671
|
+
// whether a value is already an id or a human name that needs resolving.
|
|
1672
|
+
function looksLikeUuid(s) {
|
|
1673
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(s.trim());
|
|
1674
|
+
}
|
|
1675
|
+
// Resolve a stage name-or-UUID to a stage UUID, mirroring how the API/MCP let an
|
|
1676
|
+
// agent use human-readable names: a UUID passes through; a name is resolved via
|
|
1677
|
+
// POST /automations/resolve (the canonical name->id endpoint). Fails closed with
|
|
1678
|
+
// a clear message rather than PATCHing a bad stageId.
|
|
1679
|
+
async function resolveStageId(client, value) {
|
|
1680
|
+
const v = value.trim();
|
|
1681
|
+
if (looksLikeUuid(v))
|
|
1682
|
+
return v;
|
|
1683
|
+
const res = (await client.post("/api/v1/automations/resolve", { stages: [v] }));
|
|
1684
|
+
const id = res?.data?.stages?.[v];
|
|
1685
|
+
if (typeof id === "string" && looksLikeUuid(id))
|
|
1686
|
+
return id;
|
|
1687
|
+
throw new Error(`Could not resolve stage "${v}" to a stage id. Check the name (see \`conduyt pipelines list\`) or pass a stage UUID to --stage.`);
|
|
1688
|
+
}
|
|
1689
|
+
// Split a comma-separated flag value into a non-empty array of trimmed strings.
|
|
1690
|
+
// Used for array-valued request fields (webhook events, resolve name lists).
|
|
1691
|
+
function csvArg(raw, flag) {
|
|
1692
|
+
const items = raw.split(",").map((s) => s.trim()).filter((s) => s !== "");
|
|
1693
|
+
if (items.length === 0) {
|
|
1694
|
+
throw new Error(`${flag} must be a non-empty comma-separated list.`);
|
|
1695
|
+
}
|
|
1696
|
+
return items;
|
|
1697
|
+
}
|
|
1698
|
+
// Build a full request body from either --json (inline) or --file (a path to a
|
|
1699
|
+
// JSON file), requiring exactly one. Used by automations validate/import whose
|
|
1700
|
+
// bodies (a workflow graph) are too large to express as flat flags. The parsed
|
|
1701
|
+
// value must be a plain JSON object; proto-polluting keys are rejected.
|
|
1702
|
+
function bodyFromJsonOrFile(opts) {
|
|
1703
|
+
if (opts.json !== undefined && opts.file !== undefined) {
|
|
1704
|
+
throw new Error("Pass either --json or --file, not both.");
|
|
1705
|
+
}
|
|
1706
|
+
if (opts.json !== undefined)
|
|
1707
|
+
return jsonArg(opts.json, "--json");
|
|
1708
|
+
if (opts.file !== undefined) {
|
|
1709
|
+
if (opts.file.trim() === "")
|
|
1710
|
+
throw new Error("--file must be a path (got an empty string).");
|
|
1711
|
+
let raw;
|
|
1712
|
+
try {
|
|
1713
|
+
raw = readFileSync(opts.file, "utf8");
|
|
1714
|
+
}
|
|
1715
|
+
catch {
|
|
1716
|
+
throw new Error(`--file could not be read: ${opts.file}`);
|
|
1717
|
+
}
|
|
1718
|
+
return jsonArg(raw, "--file");
|
|
1719
|
+
}
|
|
1720
|
+
throw new Error("A request body is required. Pass --json '<json>' or --file <path>.");
|
|
1721
|
+
}
|
|
546
1722
|
function run(handler) {
|
|
547
1723
|
return async (...args) => {
|
|
548
1724
|
try {
|
package/package.json
CHANGED