@botozap/cli 0.1.4 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -0
- package/dist/commands/agenda.js +58 -0
- package/dist/commands/ai.js +73 -0
- package/dist/commands/attendance.js +139 -0
- package/dist/commands/calendar.js +14 -0
- package/dist/commands/contact-configuration.js +18 -0
- package/dist/commands/contacts.js +18 -1
- package/dist/index.js +10 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -206,3 +206,28 @@ pnpm --filter @botozap/cli test # vitest
|
|
|
206
206
|
pnpm --filter @botozap/cli build # tsc -> dist/
|
|
207
207
|
node packages/cli/dist/index.js --help
|
|
208
208
|
```
|
|
209
|
+
|
|
210
|
+
## Atendimento, CRM e Agenda
|
|
211
|
+
|
|
212
|
+
Comandos: `saved-replies`, `inbox-tools`, `opportunities`, `demands`, `radar`,
|
|
213
|
+
`journeys`, `appointments`, `calendar`, `assignments`, `contact-stages` e
|
|
214
|
+
`contact-fields`. Cada subcomando mostra campos e CAS em `--help`; os campos e
|
|
215
|
+
filtros são lidos de `--input-file arquivo.json`. Use `-o json` para scripts.
|
|
216
|
+
O arquivo mantém listas, objetos, null e timestamps precisos sem escape de shell.
|
|
217
|
+
|
|
218
|
+
```sh
|
|
219
|
+
botozap saved-replies list --input-file filtros.json -o json
|
|
220
|
+
botozap opportunities create --input-file oportunidade.json -o json
|
|
221
|
+
botozap appointments availability --input-file disponibilidade.json -o json
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
[Contratos e exemplos dos arquivos JSON](https://github.com/bytecraft-fernando/botozap-js/blob/main/docs/attendance-release.md).
|
|
225
|
+
|
|
226
|
+
IA usa exclusivamente credenciais próprias (BYOK): SDK `client.ai`, CLI `botozap ai`
|
|
227
|
+
e ferramentas MCP `ai_*`. Agentes versionados, provedores, credenciais, conhecimento,
|
|
228
|
+
memória, skills, follow-ups e retornos prometidos, roteadores, casos, alertas,
|
|
229
|
+
avisos, propostas de aprendizado e comerciais, controle de acesso (elegibilidade),
|
|
230
|
+
inferências, promessas do operador, catálogo de modelos, execuções e uso. Scopes
|
|
231
|
+
`agents:read/write`; aprovação exige chave criada por
|
|
232
|
+
owner/admin ainda autorizado. Prévia não envia WhatsApp. Não há carteira, créditos
|
|
233
|
+
ou compra de vagas de IA. [Contratos e exemplos IA](https://github.com/bytecraft-fernando/botozap-js/blob/main/docs/ai.md).
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { operation } from "./attendance.js";
|
|
2
|
+
export function registerAgenda(program) {
|
|
3
|
+
const group = program
|
|
4
|
+
.command("appointments")
|
|
5
|
+
.description("Agenda completa; scopes appointments:read/write");
|
|
6
|
+
operation(group, "list", "Filtros: customer_id,contact_id,owner_user_id,service_id,status,from,to,date(UTC),page,per_page", (c, _id, p) => c.appointments.list(p));
|
|
7
|
+
operation(group, "get <id>", "Detalha compromisso e revision", (c, id) => c.appointments.get(id));
|
|
8
|
+
operation(group, "create", "Cria compromisso; idempotency_key opcional no header; title,ends_at,time_zone,owner_user_id,service_id,links CRM e meeting_requested opcionais", (c, _id, p) => {
|
|
9
|
+
const { idempotency_key, ...body } = p;
|
|
10
|
+
return c.appointments.create(body, {
|
|
11
|
+
idempotencyKey: idempotency_key,
|
|
12
|
+
});
|
|
13
|
+
}, ["contact_id", "scheduled_at"]);
|
|
14
|
+
operation(group, "update <id>", "Edita compromisso com expected_revision; legado scheduled_at/note continua aceito", (c, id, p) => c.appointments.update(id, p));
|
|
15
|
+
operation(group, "delete <id>", "Exclui compromisso; Google deve estar cancelado e sincronizado antes", (c, id) => c.appointments.delete(id));
|
|
16
|
+
operation(group, "history <id>", "Histórico paginado (page)", (c, id, p) => c.appointments.history(id, p));
|
|
17
|
+
operation(group, "availability", "Horários livres calculados incluindo jornadas, exceções, buffers e calendários", (c, _id, p) => c.appointments.availability(p), ["customer_id", "owner_user_id", "service_id", "from", "to"]);
|
|
18
|
+
for (const kind of ["services", "schedules", "exceptions"]) {
|
|
19
|
+
const configuration = group
|
|
20
|
+
.command(kind)
|
|
21
|
+
.description(`Configuração de ${kind}`);
|
|
22
|
+
operation(configuration, "list", "customer_id e page", (c, _id, p) => c.appointments[kind].list(p), ["customer_id"]);
|
|
23
|
+
const required = kind === "services"
|
|
24
|
+
? [
|
|
25
|
+
"customer_id",
|
|
26
|
+
"name",
|
|
27
|
+
"duration_minutes",
|
|
28
|
+
"slot_minutes",
|
|
29
|
+
"buffer_before_minutes",
|
|
30
|
+
"buffer_after_minutes",
|
|
31
|
+
"minimum_notice_minutes",
|
|
32
|
+
"booking_horizon_days",
|
|
33
|
+
"active",
|
|
34
|
+
]
|
|
35
|
+
: kind === "schedules"
|
|
36
|
+
? ["customer_id", "owner_user_id", "time_zone", "windows"]
|
|
37
|
+
: [
|
|
38
|
+
"customer_id",
|
|
39
|
+
"owner_user_id",
|
|
40
|
+
"local_date",
|
|
41
|
+
"start_minute",
|
|
42
|
+
"end_minute",
|
|
43
|
+
"kind",
|
|
44
|
+
];
|
|
45
|
+
operation(configuration, "create", "Cria configuração completa; consulte tipos públicos do SDK", (c, _id, p) => kind === "services"
|
|
46
|
+
? c.appointments.services.create(p)
|
|
47
|
+
: kind === "schedules"
|
|
48
|
+
? c.appointments.schedules.create(p)
|
|
49
|
+
: c.appointments.exceptions.create(p), required);
|
|
50
|
+
operation(configuration, "update <id>", "Altera campos da configuração com expected_revision", (c, id, p) => kind === "services"
|
|
51
|
+
? c.appointments.services.update(id, p)
|
|
52
|
+
: kind === "schedules"
|
|
53
|
+
? c.appointments.schedules.update(id, p)
|
|
54
|
+
: c.appointments.exceptions.update(id, p), ["expected_revision"]);
|
|
55
|
+
if (kind === "exceptions")
|
|
56
|
+
operation(configuration, "delete <id>", "Exclui exceção com expected_revision", (c, id, p) => c.appointments.exceptions.delete(id, Number(p.expected_revision)), ["expected_revision"]);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { readFile, stat } from "node:fs/promises";
|
|
2
|
+
import { basename } from "node:path";
|
|
3
|
+
import { AI_CONFIGURABLE_PURPOSES, AI_OPERATIONS } from "@botozap/sdk";
|
|
4
|
+
import { operation } from "./attendance.js";
|
|
5
|
+
const kebab = (value) => value.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
|
|
6
|
+
/** Every command maps to one declared /ai endpoint; inputs preserve exact revisions. */
|
|
7
|
+
export function registerAi(program) {
|
|
8
|
+
const root = program
|
|
9
|
+
.command("ai")
|
|
10
|
+
.description("IA BYOK: agentes, bibliotecas, roteamento e operação; agents:read/write");
|
|
11
|
+
operation(root, "conversation-control <id>", "Pausa/retoma agente na conversa; resume pode gerar respostas.", (client, id, input) => client.conversations.controlAgent(id, input.action), ["action"]);
|
|
12
|
+
for (const group of new Set(AI_OPERATIONS.map((o) => o.group))) {
|
|
13
|
+
const command = root
|
|
14
|
+
.command(kebab(group))
|
|
15
|
+
.description(`Operações de ${group}`);
|
|
16
|
+
for (const op of AI_OPERATIONS.filter((o) => o.group === group)) {
|
|
17
|
+
const required = Object.entries(op.fields)
|
|
18
|
+
.filter(([, v]) => !v.optional)
|
|
19
|
+
.map(([k]) => k);
|
|
20
|
+
const fields = op.shape === "multipart"
|
|
21
|
+
? required
|
|
22
|
+
.filter((k) => k !== "file" && k !== "file_name")
|
|
23
|
+
.concat("file_path")
|
|
24
|
+
: required;
|
|
25
|
+
operation(command, kebab(op.name), `${op.method} ${op.path}. ${op.description} Campos: ${Object.keys(op.fields)
|
|
26
|
+
.filter((k) => k !== "file")
|
|
27
|
+
.join(", ")}${op.shape === "multipart" ? ", file_path (arquivo local)" : ""}.`, async (client, _id, raw) => {
|
|
28
|
+
const input = { ...raw };
|
|
29
|
+
if (op.shape === "multipart") {
|
|
30
|
+
const path = String(input.file_path);
|
|
31
|
+
const limit = op.group === "skills" ? 5 * 1024 * 1024 : 20 * 1024 * 1024;
|
|
32
|
+
const size = (await stat(path)).size;
|
|
33
|
+
if (!size || size > limit)
|
|
34
|
+
throw new Error("Arquivo vazio ou acima do limite de upload.");
|
|
35
|
+
const bytes = await readFile(path);
|
|
36
|
+
if (!bytes.length || bytes.length > limit)
|
|
37
|
+
throw new Error("Arquivo vazio ou acima do limite de upload.");
|
|
38
|
+
delete input.file_path;
|
|
39
|
+
input.file = new Blob([bytes], {
|
|
40
|
+
type: typeof input.mime_type === "string"
|
|
41
|
+
? input.mime_type
|
|
42
|
+
: /\.pdf$/i.test(String(input.file_name ?? path))
|
|
43
|
+
? "application/pdf"
|
|
44
|
+
: "application/octet-stream",
|
|
45
|
+
});
|
|
46
|
+
delete input.mime_type;
|
|
47
|
+
input.file_name ??= basename(path);
|
|
48
|
+
}
|
|
49
|
+
return client.ai.invoke(group, op.name, input);
|
|
50
|
+
}, fields, {
|
|
51
|
+
maxBytes: 1024 * 1024,
|
|
52
|
+
validate(input) {
|
|
53
|
+
for (const [name, field] of Object.entries(op.fields)) {
|
|
54
|
+
const value = input[name];
|
|
55
|
+
if (value === undefined)
|
|
56
|
+
continue;
|
|
57
|
+
if (field.type === "configurablePurpose" &&
|
|
58
|
+
!AI_CONFIGURABLE_PURPOSES.includes(String(value)))
|
|
59
|
+
throw new Error("Esta finalidade é configurada na versão do agente ou no roteador.");
|
|
60
|
+
if ((field.type === "revision" || field.type === "revisionZero") &&
|
|
61
|
+
(typeof value !== "string" || !/^\d+$/.test(value)))
|
|
62
|
+
throw new Error(`${name} deve ser a string de revisão recebida da API.`);
|
|
63
|
+
if (field.type === "number" &&
|
|
64
|
+
(typeof value !== "number" || !Number.isFinite(value)))
|
|
65
|
+
throw new Error(`${name} deve ser número.`);
|
|
66
|
+
if (field.type === "true" && value !== true)
|
|
67
|
+
throw new Error(`${name} exige confirmação true.`);
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { context } from "./shared.js";
|
|
3
|
+
import { printJson, printDetail, printTable, printLine } from "../output.js";
|
|
4
|
+
/** JSON files preserve nested fields, null, Unicode and microsecond CAS timestamps. */
|
|
5
|
+
export async function readInput(path, maxBytes = 256 * 1024) {
|
|
6
|
+
if (!path)
|
|
7
|
+
return {};
|
|
8
|
+
const raw = await readFile(path, "utf8");
|
|
9
|
+
if (Buffer.byteLength(raw) > maxBytes)
|
|
10
|
+
throw new Error(`JSON excede ${maxBytes} bytes.`);
|
|
11
|
+
const value = JSON.parse(raw);
|
|
12
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
13
|
+
throw new Error("O arquivo deve conter um objeto JSON.");
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
function display(value, format) {
|
|
17
|
+
if (format === "json")
|
|
18
|
+
return printJson(value ?? { success: true });
|
|
19
|
+
if (value === undefined)
|
|
20
|
+
return printLine("Operação concluída.");
|
|
21
|
+
if (Array.isArray(value))
|
|
22
|
+
return printTable(value, [
|
|
23
|
+
{ header: "ID", key: "id" },
|
|
24
|
+
{ header: "NOME", key: "name" },
|
|
25
|
+
{ header: "TÍTULO", key: "title" },
|
|
26
|
+
]);
|
|
27
|
+
if (value && typeof value === "object") {
|
|
28
|
+
const row = value;
|
|
29
|
+
if (Array.isArray(row.data)) {
|
|
30
|
+
printTable(row.data, [
|
|
31
|
+
{ header: "ID", key: "id" },
|
|
32
|
+
{ header: "NOME", key: "name" },
|
|
33
|
+
{ header: "TÍTULO", key: "title" },
|
|
34
|
+
{ header: "ESTADO", key: "status" },
|
|
35
|
+
]);
|
|
36
|
+
if (row.meta)
|
|
37
|
+
printDetail(row.meta);
|
|
38
|
+
if (row.paging)
|
|
39
|
+
printDetail(row.paging);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
return printDetail(row);
|
|
43
|
+
}
|
|
44
|
+
printJson(value);
|
|
45
|
+
}
|
|
46
|
+
export function operation(parent, signature, description, run, required = [], inputOptions) {
|
|
47
|
+
const cmd = parent
|
|
48
|
+
.command(signature)
|
|
49
|
+
.description(description)
|
|
50
|
+
.option("--input-file <path>", "arquivo JSON com campos/filtros da operação; consulte --help");
|
|
51
|
+
if (required.length)
|
|
52
|
+
cmd.addHelpText("after", `\nCampos obrigatórios no JSON: ${required.join(", ")}.`);
|
|
53
|
+
cmd.action(async (...args) => {
|
|
54
|
+
const command = args.at(-1);
|
|
55
|
+
const opts = command.opts();
|
|
56
|
+
const data = await readInput(opts.inputFile, inputOptions?.maxBytes);
|
|
57
|
+
for (const key of required)
|
|
58
|
+
if (data[key] === undefined)
|
|
59
|
+
throw new Error(`Campo obrigatório no JSON: ${key}.`);
|
|
60
|
+
if (inputOptions)
|
|
61
|
+
inputOptions.validate(data);
|
|
62
|
+
else
|
|
63
|
+
for (const key of ["expected_version", "version", "expected_revision"])
|
|
64
|
+
if (data[key] !== undefined &&
|
|
65
|
+
(!Number.isInteger(data[key]) ||
|
|
66
|
+
Number(data[key]) < (signature.startsWith("stage-rule") ? 0 : 1)))
|
|
67
|
+
throw new Error(`${key} deve ser inteiro positivo.`);
|
|
68
|
+
const { client, format } = context(command);
|
|
69
|
+
const id = signature.includes("<id>") ? String(args[0]) : undefined;
|
|
70
|
+
display(await run(client, id, data), format);
|
|
71
|
+
});
|
|
72
|
+
return cmd;
|
|
73
|
+
}
|
|
74
|
+
export function registerAttendance(program) {
|
|
75
|
+
const saved = program
|
|
76
|
+
.command("saved-replies")
|
|
77
|
+
.description("Respostas compartilhadas (scope saved_replies:read/write); pessoais pertencem à sessão");
|
|
78
|
+
operation(saved, "list", "Filtros: query, customer_id (UUID ou account), include_account, page, per_page", (c, _id, p) => c.savedReplies.list(p));
|
|
79
|
+
operation(saved, "get <id>", "Lê resposta e updated_at para edição", (c, id) => c.savedReplies.get(id));
|
|
80
|
+
operation(saved, "create", "Cria resposta: title, body, shortcut, customer_id", (c, _id, p) => c.savedReplies.create(p), ["title", "body"]);
|
|
81
|
+
operation(saved, "update <id>", "Atualiza campos com CAS (expected_updated_at exato, sem arredondar)", (c, id, p) => c.savedReplies.update(id, p), ["expected_updated_at"]);
|
|
82
|
+
operation(saved, "delete <id>", "Exclui resposta com CAS", (c, id, p) => c.savedReplies.delete(id, p), ["expected_updated_at"]);
|
|
83
|
+
const inbox = program
|
|
84
|
+
.command("inbox-tools")
|
|
85
|
+
.description("Notas, retornos, arquivo e adiamento; scopes inbox:read/write");
|
|
86
|
+
operation(inbox, "get <id>", "Ferramentas da conversa; page/per_page para notas e retornos", (c, id, p) => c.inbox.get(id, p));
|
|
87
|
+
operation(inbox, "mutate <id>", "operation: archive, unarchive, snooze, unsnooze, note_create/update/delete, reminder_create/update; CAS expected_version nas alterações", (c, id, p) => c.inbox.mutate(id, p), ["operation"]);
|
|
88
|
+
for (const resource of ["opportunities", "demands"]) {
|
|
89
|
+
const group = program
|
|
90
|
+
.command(resource)
|
|
91
|
+
.description(`${resource === "opportunities" ? "Oportunidades" : "Demandas"} do CRM; scopes crm:read/write`);
|
|
92
|
+
operation(group, "list", "customer_id obrigatório; q,status,contact_id,owner_user_id,stage_id,page,per_page", (c, _id, p) => c[resource].list(p), ["customer_id"]);
|
|
93
|
+
operation(group, "get <id>", "Lê registro e versão", (c, id) => c[resource].get(id));
|
|
94
|
+
operation(group, "create", "Cria registro completo; campos conforme SDK", (c, _id, p) => resource === "opportunities"
|
|
95
|
+
? c.opportunities.create(p)
|
|
96
|
+
: c.demands.create(p), ["customer_id", "contact_id", "title"]);
|
|
97
|
+
operation(group, "update <id>", "Altera campos, responsável, estado e próximo passo com CAS", (c, id, p) => c[resource].update(id, p), ["expected_version"]);
|
|
98
|
+
operation(group, "activities <id>", "Histórico paginado (page)", (c, id, p) => c[resource].activities(id, p));
|
|
99
|
+
operation(group, "conversations <id>", "Conversas vinculadas (page)", (c, id, p) => c[resource].conversations(id, p));
|
|
100
|
+
operation(group, "link-conversation <id>", "Vincula conversa do mesmo contato", (c, id, p) => c[resource].linkConversation(id, String(p.conversation_id)), ["conversation_id"]);
|
|
101
|
+
operation(group, "unlink-conversation <id>", "Remove vínculo com conversa", (c, id, p) => c[resource].unlinkConversation(id, String(p.conversation_id)), ["conversation_id"]);
|
|
102
|
+
}
|
|
103
|
+
const radar = program
|
|
104
|
+
.command("radar")
|
|
105
|
+
.description("Pendências do CRM; scopes crm:read/write");
|
|
106
|
+
operation(radar, "list", "customer_id; filtros bucket,entity_type,owner_user_id,reason,page,per_page", (c, _id, p) => c.radar.list(p), ["customer_id"]);
|
|
107
|
+
operation(radar, "stage-rules", "Critérios das etapas do Cliente", (c, _id, p) => c.radar.stageRules(String(p.customer_id)), ["customer_id"]);
|
|
108
|
+
operation(radar, "stage-rule <id>", "Configura critério: expected_version=0 cria; cold_hours,critical_hours,require_owner,require_next_step", (c, id, p) => c.radar.configureStageRule(id, p), [
|
|
109
|
+
"customer_id",
|
|
110
|
+
"expected_version",
|
|
111
|
+
"cold_hours",
|
|
112
|
+
"critical_hours",
|
|
113
|
+
"require_owner",
|
|
114
|
+
"require_next_step",
|
|
115
|
+
]);
|
|
116
|
+
const journey = program
|
|
117
|
+
.command("journeys")
|
|
118
|
+
.description("Réguas e execuções; scopes journeys:read/write; ambiente live");
|
|
119
|
+
operation(journey, "list", "Filtros customer_id,limit,after", (c, _id, p) => c.journeys.list(p));
|
|
120
|
+
operation(journey, "get <id>", "Definição e passos da régua", (c, id, p) => c.journeys.get(id, p));
|
|
121
|
+
operation(journey, "create", "Configuração completa da régua; steps é lista de template_id,delay_minutes,variable_map", (c, _id, p) => c.journeys.create(p), ["customer_id", "name", "steps"]);
|
|
122
|
+
operation(journey, "update <id>", "Substitui configuração completa com CAS version", (c, id, p) => c.journeys.update(id, p), ["customer_id", "name", "steps", "version"]);
|
|
123
|
+
operation(journey, "delete <id>", "Arquiva régua com CAS version (preserva histórico)", (c, id, p) => c.journeys.delete(id, Number(p.version)), ["version"]);
|
|
124
|
+
operation(journey, "control <id>", "action: pause/resume/archive e version", (c, id, p) => c.journeys.control(id, p), ["action", "version"]);
|
|
125
|
+
operation(journey, "runs <id>", "Lista execuções da régua; customer_id,limit,after", (c, id, p) => c.journeys.runs(id, p));
|
|
126
|
+
operation(journey, "enroll <id>", "Programa ocorrência única para contato (pode gerar envio automático)", (c, id, p) => c.journeys.enroll(id, p), ["contact_id", "occurrence_key"]);
|
|
127
|
+
operation(journey, "get-run <id>", "Execução com passos, falhas e comprovantes", (c, id) => c.journeys.getRun(id));
|
|
128
|
+
operation(journey, "control-run <id>", "action: stop ou acknowledge (com note)", (c, id, p) => c.journeys.controlRun(id, p), ["action"]);
|
|
129
|
+
const assignments = program
|
|
130
|
+
.command("assignments")
|
|
131
|
+
.description("Atribuições de conversas a membros da Conta");
|
|
132
|
+
operation(assignments, "list <id>", "Atribuições da conversa; page,per_page", (c, id, p) => c.conversations.listAssignments(id, p));
|
|
133
|
+
operation(assignments, "create <id>", "Atribui conversa a user_id; notes opcional", (c, id, p) => c.conversations.createAssignment(id, p), ["user_id"]);
|
|
134
|
+
operation(assignments, "get <id>", "Lê assignment_id da conversa", (c, id, p) => c.conversations.getAssignment(id, String(p.assignment_id)), ["assignment_id"]);
|
|
135
|
+
operation(assignments, "update <id>", "Atualiza atribuição: assignment_id + campos permitidos pela API", (c, id, p) => {
|
|
136
|
+
const { assignment_id, ...patch } = p;
|
|
137
|
+
return c.conversations.updateAssignment(id, String(assignment_id), patch);
|
|
138
|
+
}, ["assignment_id"]);
|
|
139
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { operation } from "./attendance.js";
|
|
2
|
+
export function registerCalendar(program) {
|
|
3
|
+
const group = program
|
|
4
|
+
.command("calendar")
|
|
5
|
+
.description("Google Calendar: autorize primeiro em /calendarios no Painel; scopes calendar:read/write");
|
|
6
|
+
operation(group, "connections", "Lista conexões (page,per_page)", (c, _id, p) => c.calendar.connections(p));
|
|
7
|
+
operation(group, "disconnect <id>", "Desconecta conta Google e seus calendários", (c, id) => c.calendar.disconnect(id));
|
|
8
|
+
operation(group, "calendars <id>", "Lista calendários da conexão", (c, id) => c.calendar.calendars(id));
|
|
9
|
+
operation(group, "refresh <id>", "Atualiza catálogo de calendários Google da conexão", (c, id) => c.calendar.refresh(id));
|
|
10
|
+
operation(group, "select <id>", "Configura destino/ocupação com CAS expected_revision", (c, id, p) => c.calendar.select(id, p), ["destination", "include_busy", "expected_revision"]);
|
|
11
|
+
operation(group, "jobs", "Lista sincronizações (connection_id,page,per_page)", (c, _id, p) => c.calendar.jobs(p));
|
|
12
|
+
operation(group, "retry-job <id>", "Reagenda sincronização com falha", (c, id) => c.calendar.retryJob(id));
|
|
13
|
+
operation(group, "resolve-conflict <id>", "ID do compromisso; choice local/remote e expected_revision", (c, id, p) => c.calendar.resolveConflict(id, p), ["choice", "expected_revision"]);
|
|
14
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { operation } from "./attendance.js";
|
|
2
|
+
export function registerContactConfiguration(program) {
|
|
3
|
+
const stages = program
|
|
4
|
+
.command("contact-stages")
|
|
5
|
+
.description("Etapas do funil por Cliente; contacts:read/write");
|
|
6
|
+
operation(stages, "list", "customer_id recomendado; omitir só em Conta com exatamente um Cliente", (c, _id, p) => c.contactStages.list(p));
|
|
7
|
+
operation(stages, "create", "label e color (zinc,green,amber,pink); customer_id recomendado", (c, _id, p) => c.contactStages.create(p), ["label", "color"]);
|
|
8
|
+
operation(stages, "update <id>", "Altera label,color,position; customer_id recomendado", (c, id, p) => c.contactStages.update(id, p));
|
|
9
|
+
operation(stages, "delete <id>", "Exclui etapa do Cliente; customer_id recomendado", (c, id, p) => c.contactStages.delete(id, p));
|
|
10
|
+
operation(stages, "reorder", "stage_ids deve conter todas as etapas exatamente uma vez; customer_id recomendado", (c, _id, p) => c.contactStages.reorder(p), ["stage_ids"]);
|
|
11
|
+
const fields = program
|
|
12
|
+
.command("contact-fields")
|
|
13
|
+
.description("Campos personalizados da Conta; contacts:read/write");
|
|
14
|
+
operation(fields, "list", "Lista definições e tipos de campos disponíveis", (c) => c.contactFields.list());
|
|
15
|
+
operation(fields, "create", "label; type text/number/boolean/date e key opcionais", (c, _id, p) => c.contactFields.create(p), ["label"]);
|
|
16
|
+
operation(fields, "update <id>", "Altera label e position; tipo e chave são imutáveis", (c, id, p) => c.contactFields.update(id, p));
|
|
17
|
+
operation(fields, "delete <id>", "Exclui definição do campo", (c, id) => c.contactFields.delete(id));
|
|
18
|
+
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { context, toInt, toBool } from "./shared.js";
|
|
2
2
|
import { printJson, printTable, printDetail, printCursorFooter, printLine, } from "../output.js";
|
|
3
|
+
function collect(value, previous) {
|
|
4
|
+
return [...(previous ?? []), value];
|
|
5
|
+
}
|
|
3
6
|
export function registerContacts(program) {
|
|
4
7
|
const contacts = program
|
|
5
8
|
.command("contacts")
|
|
@@ -60,6 +63,7 @@ export function registerContacts(program) {
|
|
|
60
63
|
.option("--username <username>", "username")
|
|
61
64
|
.option("--customer-id <id>", "cliente vinculado")
|
|
62
65
|
.option("--phone-number-id <id>", "número vinculado")
|
|
66
|
+
.option("--tag <tag>", "tag do contato (repetível; até 20 × 40 caracteres)", collect)
|
|
63
67
|
.action(async (opts, cmd) => {
|
|
64
68
|
const { client, format } = context(cmd);
|
|
65
69
|
const data = await client.contacts.create({
|
|
@@ -70,6 +74,7 @@ export function registerContacts(program) {
|
|
|
70
74
|
username: opts.username,
|
|
71
75
|
customer_id: opts.customerId,
|
|
72
76
|
phone_number_id: opts.phoneNumberId,
|
|
77
|
+
...(opts.tag ? { tags: opts.tag } : {}),
|
|
73
78
|
});
|
|
74
79
|
if (format === "json")
|
|
75
80
|
return printJson(data);
|
|
@@ -81,6 +86,9 @@ export function registerContacts(program) {
|
|
|
81
86
|
.description("Atualiza um contato")
|
|
82
87
|
.option("--profile-name <nome>", "novo nome do perfil")
|
|
83
88
|
.option("--username <username>", "novo username")
|
|
89
|
+
.option("--tag <tag>", "substitui todas as tags (repetível)", collect)
|
|
90
|
+
.option("--add-tag <tag>", "acrescenta tag (repetível)", collect)
|
|
91
|
+
.option("--remove-tag <tag>", "remove tag (repetível)", collect)
|
|
84
92
|
.action(async (id, opts, cmd) => {
|
|
85
93
|
const { client, format } = context(cmd);
|
|
86
94
|
const body = {};
|
|
@@ -88,8 +96,17 @@ export function registerContacts(program) {
|
|
|
88
96
|
body.profile_name = opts.profileName;
|
|
89
97
|
if (opts.username !== undefined)
|
|
90
98
|
body.username = opts.username;
|
|
99
|
+
if (opts.tag)
|
|
100
|
+
body.tags = opts.tag;
|
|
101
|
+
if (opts.addTag)
|
|
102
|
+
body.add_tags = opts.addTag;
|
|
103
|
+
if (opts.removeTag)
|
|
104
|
+
body.remove_tags = opts.removeTag;
|
|
105
|
+
if (opts.tag && (opts.addTag || opts.removeTag)) {
|
|
106
|
+
throw new Error("Use --tag para substituir a lista ou --add-tag/--remove-tag para alterá-la, não os dois.");
|
|
107
|
+
}
|
|
91
108
|
if (Object.keys(body).length === 0) {
|
|
92
|
-
throw new Error("Informe ao menos --profile-name ou --
|
|
109
|
+
throw new Error("Informe ao menos --profile-name, --username, --tag, --add-tag ou --remove-tag.");
|
|
93
110
|
}
|
|
94
111
|
const data = await client.contacts.update(id, body);
|
|
95
112
|
if (format === "json")
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,11 @@ import { Command } from "commander";
|
|
|
4
4
|
import pc from "picocolors";
|
|
5
5
|
import { resolveFormat } from "./output.js";
|
|
6
6
|
import { renderErrorText, renderErrorJson } from "./render-error.js";
|
|
7
|
+
import { registerContactConfiguration } from "./commands/contact-configuration.js";
|
|
8
|
+
import { registerCalendar } from "./commands/calendar.js";
|
|
9
|
+
import { registerAi } from "./commands/ai.js";
|
|
10
|
+
import { registerAgenda } from "./commands/agenda.js";
|
|
11
|
+
import { registerAttendance } from "./commands/attendance.js";
|
|
7
12
|
import { registerMessages } from "./commands/messages.js";
|
|
8
13
|
import { registerConversations } from "./commands/conversations.js";
|
|
9
14
|
import { registerContacts } from "./commands/contacts.js";
|
|
@@ -38,6 +43,11 @@ program.hook("preAction", (_thisCommand, actionCommand) => {
|
|
|
38
43
|
selectedFormat = resolveFormat(actionCommand.optsWithGlobals().output);
|
|
39
44
|
});
|
|
40
45
|
// Recursos
|
|
46
|
+
registerAttendance(program);
|
|
47
|
+
registerAgenda(program);
|
|
48
|
+
registerAi(program);
|
|
49
|
+
registerCalendar(program);
|
|
50
|
+
registerContactConfiguration(program);
|
|
41
51
|
registerMessages(program);
|
|
42
52
|
registerConversations(program);
|
|
43
53
|
registerContacts(program);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@botozap/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "CLI dev-first para a API pública do BotoZap (WhatsApp Cloud API multi-tenant).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"commander": "^12.1.0",
|
|
32
32
|
"picocolors": "^1.1.1",
|
|
33
|
-
"@botozap/sdk": "0.
|
|
33
|
+
"@botozap/sdk": "0.4.0"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@types/node": "^20.19.0",
|