conduyt 1.2.1 → 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 +1203 -2
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -99,6 +99,41 @@ 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
|
+
}));
|
|
130
|
+
contacts
|
|
131
|
+
.command("delete <id>")
|
|
132
|
+
.description("Delete a contact by id (soft-delete; retained for audit, the contact's deals are unaffected)")
|
|
133
|
+
.action(run(async (client, id) => {
|
|
134
|
+
assertUuid(id, "contact id");
|
|
135
|
+
return client.del(`/api/v1/contacts/${encodeURIComponent(id)}`);
|
|
136
|
+
}));
|
|
102
137
|
// ---- deals ----
|
|
103
138
|
const deals = program.command("deals").description("Manage deals");
|
|
104
139
|
deals
|
|
@@ -265,6 +300,64 @@ deals
|
|
|
265
300
|
assertFiniteValue(body.value);
|
|
266
301
|
return client.post("/api/v1/deals", body);
|
|
267
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
|
+
}));
|
|
354
|
+
deals
|
|
355
|
+
.command("delete <id>")
|
|
356
|
+
.description("Delete a deal by id (soft-delete; drops out of lists/boards and pipeline totals, retained for audit)")
|
|
357
|
+
.action(run(async (client, id) => {
|
|
358
|
+
assertUuid(id, "deal id");
|
|
359
|
+
return client.del(`/api/v1/deals/${encodeURIComponent(id)}`);
|
|
360
|
+
}));
|
|
268
361
|
// ---- pipelines ----
|
|
269
362
|
// Default action preserves the original published `conduyt pipelines` (list)
|
|
270
363
|
// invocation now that pipelines is a group — without it, commander prints help
|
|
@@ -321,7 +414,9 @@ program
|
|
|
321
414
|
// ---- insights ----
|
|
322
415
|
program
|
|
323
416
|
.command("insights <type>")
|
|
324
|
-
.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")
|
|
325
420
|
.option("--input <json>", "JSON payload to send with the request")
|
|
326
421
|
.action(run(async (client, type, opts) => {
|
|
327
422
|
let extra = {};
|
|
@@ -333,7 +428,10 @@ program
|
|
|
333
428
|
fail("--input must be valid JSON");
|
|
334
429
|
}
|
|
335
430
|
}
|
|
336
|
-
|
|
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 });
|
|
337
435
|
}));
|
|
338
436
|
// ---- users (team members) ----
|
|
339
437
|
const users = program.command("users").description("Manage team members");
|
|
@@ -422,6 +520,1030 @@ customFields
|
|
|
422
520
|
assertOptionsShape(body.options);
|
|
423
521
|
return client.post("/api/v1/custom-fields", body);
|
|
424
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
|
+
}));
|
|
425
1547
|
// ---- raw escape hatch ----
|
|
426
1548
|
program
|
|
427
1549
|
.command("api <method> <path>")
|
|
@@ -485,6 +1607,17 @@ function assertFiniteValue(v) {
|
|
|
485
1607
|
throw new Error("value must be a finite number.");
|
|
486
1608
|
}
|
|
487
1609
|
}
|
|
1610
|
+
// Validate an id is a UUID before it is interpolated into a DELETE path, so a
|
|
1611
|
+
// malformed value with slashes can't form a different (destructive) subroute.
|
|
1612
|
+
// The path is also URL-encoded at the call site as defence in depth. The regex
|
|
1613
|
+
// is inlined (not a module-level const) so it isn't in the temporal dead zone
|
|
1614
|
+
// when an action callback runs during program.parse().
|
|
1615
|
+
function assertUuid(id, label = "id") {
|
|
1616
|
+
const uuidRe = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1617
|
+
if (!uuidRe.test(id.trim())) {
|
|
1618
|
+
throw new Error(`${label} must be a UUID.`);
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
488
1621
|
// Reject prototype-polluting keys in untrusted JSON. JSON.parse creates an OWN
|
|
489
1622
|
// "__proto__" property, but Object.assign'ing it onto a {} body invokes the
|
|
490
1623
|
// setter and changes the body's PROTOTYPE — so a validator could read an
|
|
@@ -498,6 +1631,22 @@ function rejectProtoPollution(obj, flag) {
|
|
|
498
1631
|
}
|
|
499
1632
|
}
|
|
500
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
|
+
}
|
|
501
1650
|
function jsonArg(raw, flag) {
|
|
502
1651
|
if (raw.trim() === "") {
|
|
503
1652
|
throw new Error(`${flag} must be valid JSON (got an empty string).`);
|
|
@@ -518,6 +1667,58 @@ function jsonArg(raw, flag) {
|
|
|
518
1667
|
rejectProtoPollution(v, flag);
|
|
519
1668
|
return v;
|
|
520
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
|
+
}
|
|
521
1722
|
function run(handler) {
|
|
522
1723
|
return async (...args) => {
|
|
523
1724
|
try {
|