@botozap/cli 0.1.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/LICENSE +21 -0
- package/README.md +215 -0
- package/dist/client.js +24 -0
- package/dist/commands/config.js +53 -0
- package/dist/commands/contacts.js +110 -0
- package/dist/commands/conversations.js +60 -0
- package/dist/commands/customers.js +86 -0
- package/dist/commands/deliveries.js +40 -0
- package/dist/commands/login.js +61 -0
- package/dist/commands/logs.js +38 -0
- package/dist/commands/media.js +28 -0
- package/dist/commands/messages.js +113 -0
- package/dist/commands/numbers.js +51 -0
- package/dist/commands/setup-links.js +63 -0
- package/dist/commands/shared.js +37 -0
- package/dist/commands/status.js +32 -0
- package/dist/commands/templates.js +87 -0
- package/dist/commands/users.js +28 -0
- package/dist/commands/webhooks.js +116 -0
- package/dist/config.js +77 -0
- package/dist/index.js +74 -0
- package/dist/output.js +101 -0
- package/dist/render-error.js +100 -0
- package/package.json +47 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Bytecraft Consultoria em Tecnologia LTDA
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# @botozap/cli
|
|
2
|
+
|
|
3
|
+
CLI dev-first para a **API pública do BotoZap** — a plataforma multi-tenant sobre a
|
|
4
|
+
WhatsApp Cloud API oficial (a "Kapso brasileira"). Envie mensagens, gerencie contatos,
|
|
5
|
+
clientes, números, templates e webhooks direto do terminal ou de scripts.
|
|
6
|
+
|
|
7
|
+
A CLI é uma casca fina sobre o SDK oficial [`@botozap/sdk`](../sdk) — toda chamada de
|
|
8
|
+
rede passa por ele.
|
|
9
|
+
|
|
10
|
+
> Requer **Node ≥ 20.19**. Projeto usa **pnpm** (nunca npm/npx/yarn/bun).
|
|
11
|
+
|
|
12
|
+
> **Status: preview `0.x`, ainda não publicado no npm.** Sem promessa de
|
|
13
|
+
> estabilidade de comandos/flags até a `1.0`. A [API REST](https://botozap.com.br/docs)
|
|
14
|
+
> é a interface oficial e estável — se algo faltar na CLI, chame
|
|
15
|
+
> `https://botozap.com.br/api/v1` direto (`curl` + `Authorization: Bearer`).
|
|
16
|
+
> A CLI cobre hoje um SUBCONJUNTO da API: transmissões (`broadcasts`) e `flows`
|
|
17
|
+
> existem no SDK/REST, mas ainda não têm comando. Bugs:
|
|
18
|
+
> [issues do monorepo](https://github.com/bytecraft-fernando/botozap-js/issues)
|
|
19
|
+
> — nunca inclua sua chave de API no relato.
|
|
20
|
+
|
|
21
|
+
## Instalação
|
|
22
|
+
|
|
23
|
+
Ainda **não publicado no npm** (em preparação). Por ora, use dentro do monorepo
|
|
24
|
+
`botozap-js`:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# na raiz do monorepo
|
|
28
|
+
pnpm install
|
|
29
|
+
pnpm --filter @botozap/cli build
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Isso gera `packages/cli/dist/` com o binário `botozap` (campo `bin`). Para usar a partir
|
|
33
|
+
do checkout:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# na pasta do pacote
|
|
37
|
+
cd packages/cli
|
|
38
|
+
pnpm link --global # disponibiliza o comando `botozap`
|
|
39
|
+
# ou rode direto:
|
|
40
|
+
node dist/index.js --help
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Autenticação
|
|
44
|
+
|
|
45
|
+
A CLI fala com `/api/v1` autenticando com o header `Authorization: Bearer bz_live_…`
|
|
46
|
+
(herdado do SDK). O servidor também aceita o legado `X-API-Key`, mas a CLI agora envia
|
|
47
|
+
**Bearer**. A chave é criada no painel do BotoZap, em **/chaves**.
|
|
48
|
+
|
|
49
|
+
A chave (e a URL base) são resolvidas nesta **ordem de prioridade**:
|
|
50
|
+
|
|
51
|
+
| Item | 1º (maior) | 2º | 3º | padrão |
|
|
52
|
+
| -------- | ----------------- | -------------------- | --------------------------- | ------------------------------- |
|
|
53
|
+
| API key | `--api-key` | `BOTOZAP_API_KEY` | `~/.botozap/cli/config.json`| — |
|
|
54
|
+
| URL base | `--api-url` | `BOTOZAP_API_URL` | `~/.botozap/cli/config.json`| `https://botozap.com.br/api/v1` |
|
|
55
|
+
|
|
56
|
+
> **Segurança:** prefira `BOTOZAP_API_KEY` (env) ou `botozap login` a passar
|
|
57
|
+
> `--api-key` na linha de comando — argumentos de CLI ficam no histórico do shell
|
|
58
|
+
> e visíveis em `ps`/argv. A flag existe para conveniência e CI, mas trata a chave
|
|
59
|
+
> como segredo em argv. O `config.json` é gravado com permissão `0600` (diretório `0700`).
|
|
60
|
+
|
|
61
|
+
### Gravando a chave
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# stub amigável: explica /chaves e oferece colar a chave
|
|
65
|
+
botozap login
|
|
66
|
+
|
|
67
|
+
# direto
|
|
68
|
+
botozap config set apiKey bz_live_xxxxxxxxxxxx
|
|
69
|
+
botozap config set baseUrl https://botozap.com.br/api/v1
|
|
70
|
+
|
|
71
|
+
botozap config get # mostra config (apiKey mascarada)
|
|
72
|
+
botozap config path # caminho do config.json
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
> **`botozap login` é um stub.** O BotoZap ainda não tem login OAuth por navegador
|
|
76
|
+
> (como `kapso login`); o comando apenas instrui a criar a chave em `/chaves` e grava
|
|
77
|
+
> a chave colada no config local.
|
|
78
|
+
|
|
79
|
+
### Verificando
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
botozap status
|
|
83
|
+
# Autenticado. Conta acessível.
|
|
84
|
+
# URL base https://botozap.com.br/api/v1
|
|
85
|
+
# Origem da chave config
|
|
86
|
+
# Números 3
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Saída: humano vs. JSON
|
|
90
|
+
|
|
91
|
+
Por padrão a saída é **humana** (tabelas/linhas compactas). Para scripts, use
|
|
92
|
+
`-o json` (ou `--output json`), que imprime o JSON **cru** da resposta:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
botozap messages list -o json | jq '.data[].id'
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
> **Listas** preservam o envelope da API (`{ data, paging }` por cursor, `{ data, meta }`
|
|
99
|
+
> por offset). **Itens** (`get`, `create`, `update`, `media ingest`, `webhooks test`)
|
|
100
|
+
> saem **desembrulhados** — o objeto direto, sem a chave `data` em volta — porque os
|
|
101
|
+
> métodos de item do SDK já entregam a entidade. `messages send` sempre respondeu o
|
|
102
|
+
> objeto direto (a rota `POST /messages` não usa envelope).
|
|
103
|
+
|
|
104
|
+
## Comandos
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
botozap messages send | list | get
|
|
108
|
+
botozap conversations list | get | update
|
|
109
|
+
botozap contacts list | get | create | update | delete
|
|
110
|
+
botozap media ingest
|
|
111
|
+
botozap customers list | get | create | update | delete
|
|
112
|
+
botozap setup-links list | create | update (--customer <id>)
|
|
113
|
+
botozap numbers list | get | health
|
|
114
|
+
botozap templates list | get | create
|
|
115
|
+
botozap webhooks list | get | create | update | delete | test
|
|
116
|
+
botozap deliveries list (webhook_deliveries)
|
|
117
|
+
botozap logs list (api_logs)
|
|
118
|
+
botozap users list
|
|
119
|
+
botozap config set | get | path
|
|
120
|
+
botozap login
|
|
121
|
+
botozap status
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Flags globais
|
|
125
|
+
|
|
126
|
+
- `--api-key <chave>` / `--api-url <url>` — sobrepõem env e config.
|
|
127
|
+
- `-o, --output <human|json>` — formato de saída (padrão `human`).
|
|
128
|
+
- Listas por **cursor**: `--limit`, `--after`, `--before`.
|
|
129
|
+
- Listas por **offset**: `--page`, `--per-page`.
|
|
130
|
+
|
|
131
|
+
### Exemplos
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
# Enviar texto simples
|
|
135
|
+
botozap messages send --to 5511999999999 --text "Olá do BotoZap!"
|
|
136
|
+
|
|
137
|
+
# Enviar payload completo (template, mídia, etc.) via arquivo ou stdin
|
|
138
|
+
botozap messages send --input ./mensagem.json
|
|
139
|
+
cat ./mensagem.json | botozap messages send --stdin
|
|
140
|
+
|
|
141
|
+
# Listar mensagens com filtro e paginação por cursor
|
|
142
|
+
botozap messages list --direction inbound --limit 20
|
|
143
|
+
botozap messages list --after <cursor>
|
|
144
|
+
|
|
145
|
+
# Encerrar uma conversa
|
|
146
|
+
botozap conversations update <id> --status ended
|
|
147
|
+
|
|
148
|
+
# Criar contato
|
|
149
|
+
botozap contacts create --wa-id 5511999999999 --profile-name "Maria"
|
|
150
|
+
|
|
151
|
+
# Ingerir mídia por URL
|
|
152
|
+
botozap media ingest --phone-number-id <id> --source https://exemplo.com/foto.jpg
|
|
153
|
+
|
|
154
|
+
# Clientes (offset)
|
|
155
|
+
botozap customers list --page 1 --per-page 25
|
|
156
|
+
botozap customers create --name "Acme LTDA" --external-customer-id acme-001
|
|
157
|
+
|
|
158
|
+
# Setup links de um cliente
|
|
159
|
+
botozap setup-links list --customer <customerId>
|
|
160
|
+
botozap setup-links create --customer <customerId>
|
|
161
|
+
|
|
162
|
+
# Números e saúde
|
|
163
|
+
botozap numbers list
|
|
164
|
+
botozap numbers health <phoneNumberId>
|
|
165
|
+
|
|
166
|
+
# Templates (componentes via arquivo JSON)
|
|
167
|
+
botozap templates create --name boas_vindas --language pt_BR \
|
|
168
|
+
--category UTILITY --components ./components.json
|
|
169
|
+
|
|
170
|
+
# Webhooks (eventos válidos: messages, statuses)
|
|
171
|
+
botozap webhooks create --url https://meu.app/webhook \
|
|
172
|
+
--events messages,statuses --secret s3cr3t
|
|
173
|
+
botozap webhooks test <id>
|
|
174
|
+
|
|
175
|
+
# Entregas e logs
|
|
176
|
+
botozap deliveries list --status failed
|
|
177
|
+
botozap logs list --status-code 500
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Tratamento de erros
|
|
181
|
+
|
|
182
|
+
A API usa o envelope `{ "error": { "code", "message" } }`. A CLI imprime a `message`
|
|
183
|
+
(em PT-BR) no **stderr** e sai com **código 1**:
|
|
184
|
+
|
|
185
|
+
```
|
|
186
|
+
Erro [authentication_error]: Chave de API inválida ou expirada.
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Num `429 rate_limited`, a CLI acrescenta o `Retry-After` (e `X-RateLimit-*`,
|
|
190
|
+
quando presentes) à mensagem — espere esse tempo antes de reenviar.
|
|
191
|
+
|
|
192
|
+
Com `-o json`, erros saem **estruturados no stderr** (stdout fica limpo para
|
|
193
|
+
pipes): `{ "error": { "code", "message", "status", "rate_limit"? } }`. O exit
|
|
194
|
+
code é **1** para qualquer erro (`0` só no sucesso) — trate classes de erro pelo
|
|
195
|
+
`code` do JSON, não pelo exit code.
|
|
196
|
+
|
|
197
|
+
## Estrutura
|
|
198
|
+
|
|
199
|
+
```
|
|
200
|
+
src/
|
|
201
|
+
index.ts # entrada (bin), parsing com commander, tratamento de erro
|
|
202
|
+
client.ts # resolve config e instancia `BotoZap` de @botozap/sdk
|
|
203
|
+
config.ts # leitura/escrita de ~/.botozap/cli/config.json + resolução
|
|
204
|
+
output.ts # formatação human/json (tabelas, detalhes, paginação)
|
|
205
|
+
commands/*.ts # um arquivo por recurso
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## Desenvolvimento
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
pnpm --filter @botozap/cli typecheck # tsc --noEmit
|
|
212
|
+
pnpm --filter @botozap/cli test # vitest
|
|
213
|
+
pnpm --filter @botozap/cli build # tsc -> dist/
|
|
214
|
+
node packages/cli/dist/index.js --help
|
|
215
|
+
```
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { BotoZap } from "@botozap/sdk";
|
|
2
|
+
import { resolveAuth } from "./config.js";
|
|
3
|
+
// `BotoZapError` (erro de API: `.code`/`.message`/`.status`) vem do SDK — a CLI
|
|
4
|
+
// não tem mais cliente HTTP próprio. Re-exporta para o handler central de erro.
|
|
5
|
+
export { BotoZapError } from "@botozap/sdk";
|
|
6
|
+
/** Erro de configuração local (ex.: falta de API key). Não sai do SDK. */
|
|
7
|
+
export class ConfigError extends Error {
|
|
8
|
+
constructor(message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "ConfigError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Resolve credenciais (flag > env > config) e instancia o cliente do SDK.
|
|
15
|
+
* A ausência de chave é um erro LOCAL (`ConfigError`) — nunca chega a rede.
|
|
16
|
+
*/
|
|
17
|
+
export function createClient(global) {
|
|
18
|
+
const auth = resolveAuth({ apiKey: global.apiKey, apiUrl: global.apiUrl });
|
|
19
|
+
if (!auth.apiKey) {
|
|
20
|
+
throw new ConfigError("Nenhuma API key encontrada. Defina com `botozap config set apiKey bz_live_…`, " +
|
|
21
|
+
"exporte BOTOZAP_API_KEY ou passe --api-key. Crie uma chave em /chaves no painel.");
|
|
22
|
+
}
|
|
23
|
+
return new BotoZap({ apiKey: auth.apiKey, baseUrl: auth.baseUrl });
|
|
24
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { readConfig, setConfigValue, configPath, } from "../config.js";
|
|
2
|
+
import { resolveFormat, printJson, printLine, dim } from "../output.js";
|
|
3
|
+
const ALLOWED_KEYS = ["apiKey", "baseUrl"];
|
|
4
|
+
/** Mascara a API key, mostrando só o prefixo e os últimos 4 caracteres. */
|
|
5
|
+
function maskKey(key) {
|
|
6
|
+
if (!key)
|
|
7
|
+
return "(não definida)";
|
|
8
|
+
if (key.length <= 12)
|
|
9
|
+
return key.slice(0, 4) + "…";
|
|
10
|
+
return key.slice(0, 8) + "…" + key.slice(-4);
|
|
11
|
+
}
|
|
12
|
+
export function registerConfig(program) {
|
|
13
|
+
const config = program
|
|
14
|
+
.command("config")
|
|
15
|
+
.description("Ler e gravar a configuração local da CLI");
|
|
16
|
+
config
|
|
17
|
+
.command("set <chave> <valor>")
|
|
18
|
+
.description("Define uma chave de config (apiKey | baseUrl)")
|
|
19
|
+
.action((key, value) => {
|
|
20
|
+
if (!ALLOWED_KEYS.includes(key)) {
|
|
21
|
+
throw new Error(`Chave inválida: "${key}". Use uma de: ${ALLOWED_KEYS.join(", ")}.`);
|
|
22
|
+
}
|
|
23
|
+
setConfigValue(key, value);
|
|
24
|
+
printLine(`Gravado em ${configPath()}`);
|
|
25
|
+
if (key === "apiKey") {
|
|
26
|
+
printLine(dim(`apiKey = ${maskKey(value)}`));
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
printLine(dim(`${key} = ${value}`));
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
config
|
|
33
|
+
.command("get")
|
|
34
|
+
.description("Mostra a config atual (apiKey mascarada)")
|
|
35
|
+
.action((_opts, cmd) => {
|
|
36
|
+
const format = resolveFormat(cmd.optsWithGlobals().output);
|
|
37
|
+
const stored = readConfig();
|
|
38
|
+
if (format === "json") {
|
|
39
|
+
return printJson({
|
|
40
|
+
apiKey: stored.apiKey ? maskKey(stored.apiKey) : null,
|
|
41
|
+
baseUrl: stored.baseUrl ?? null,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
printLine(`apiKey ${maskKey(stored.apiKey)}`);
|
|
45
|
+
printLine(`baseUrl ${stored.baseUrl ?? dim("(padrão)")}`);
|
|
46
|
+
});
|
|
47
|
+
config
|
|
48
|
+
.command("path")
|
|
49
|
+
.description("Mostra o caminho do arquivo de configuração")
|
|
50
|
+
.action(() => {
|
|
51
|
+
printLine(configPath());
|
|
52
|
+
});
|
|
53
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { context, toInt, toBool } from "./shared.js";
|
|
2
|
+
import { printJson, printTable, printDetail, printCursorFooter, printLine, } from "../output.js";
|
|
3
|
+
export function registerContacts(program) {
|
|
4
|
+
const contacts = program
|
|
5
|
+
.command("contacts")
|
|
6
|
+
.description("Gerenciar contatos");
|
|
7
|
+
contacts
|
|
8
|
+
.command("list")
|
|
9
|
+
.description("Lista contatos (paginação por cursor)")
|
|
10
|
+
.option("--customer-id <id>", "filtra por cliente")
|
|
11
|
+
.option("--profile-name-contains <texto>", "filtra por nome do perfil")
|
|
12
|
+
.option("--wa-id-contains <texto>", "filtra por wa_id")
|
|
13
|
+
.option("--has-customer <bool>", "true | false")
|
|
14
|
+
.option("--created-after <data>", "ISO 8601")
|
|
15
|
+
.option("--created-before <data>", "ISO 8601")
|
|
16
|
+
.option("--limit <n>", "quantidade por página")
|
|
17
|
+
.option("--after <cursor>", "cursor da próxima página")
|
|
18
|
+
.option("--before <cursor>", "cursor da página anterior")
|
|
19
|
+
.action(async (opts, cmd) => {
|
|
20
|
+
const { client, format } = context(cmd);
|
|
21
|
+
const res = await client.contacts.list({
|
|
22
|
+
customer_id: opts.customerId,
|
|
23
|
+
profile_name_contains: opts.profileNameContains,
|
|
24
|
+
wa_id_contains: opts.waIdContains,
|
|
25
|
+
has_customer: toBool(opts.hasCustomer),
|
|
26
|
+
created_after: opts.createdAfter,
|
|
27
|
+
created_before: opts.createdBefore,
|
|
28
|
+
limit: toInt(opts.limit),
|
|
29
|
+
after: opts.after,
|
|
30
|
+
before: opts.before,
|
|
31
|
+
});
|
|
32
|
+
if (format === "json")
|
|
33
|
+
return printJson(res);
|
|
34
|
+
printTable(res.data, [
|
|
35
|
+
{ header: "ID", key: "id", max: 36 },
|
|
36
|
+
{ header: "WA_ID", key: "wa_id" },
|
|
37
|
+
{ header: "NOME", key: "profile_name" },
|
|
38
|
+
{ header: "USERNAME", key: "username" },
|
|
39
|
+
{ header: "CLIENTE", key: "customer_id", max: 36 },
|
|
40
|
+
]);
|
|
41
|
+
printCursorFooter(res.paging);
|
|
42
|
+
});
|
|
43
|
+
contacts
|
|
44
|
+
.command("get <id>")
|
|
45
|
+
.description("Detalha um contato")
|
|
46
|
+
.action(async (id, _opts, cmd) => {
|
|
47
|
+
const { client, format } = context(cmd);
|
|
48
|
+
const data = await client.contacts.get(id);
|
|
49
|
+
if (format === "json")
|
|
50
|
+
return printJson(data);
|
|
51
|
+
printDetail(data);
|
|
52
|
+
});
|
|
53
|
+
contacts
|
|
54
|
+
.command("create")
|
|
55
|
+
.description("Cria um contato")
|
|
56
|
+
.requiredOption("--wa-id <wa_id>", "WhatsApp ID (obrigatório)")
|
|
57
|
+
.option("--profile-name <nome>", "nome do perfil")
|
|
58
|
+
.option("--phone <telefone>", "telefone")
|
|
59
|
+
.option("--user-id <id>", "id externo do usuário")
|
|
60
|
+
.option("--username <username>", "username")
|
|
61
|
+
.option("--customer-id <id>", "cliente vinculado")
|
|
62
|
+
.option("--phone-number-id <id>", "número vinculado")
|
|
63
|
+
.action(async (opts, cmd) => {
|
|
64
|
+
const { client, format } = context(cmd);
|
|
65
|
+
const data = await client.contacts.create({
|
|
66
|
+
wa_id: opts.waId,
|
|
67
|
+
profile_name: opts.profileName,
|
|
68
|
+
phone: opts.phone,
|
|
69
|
+
user_id: opts.userId,
|
|
70
|
+
username: opts.username,
|
|
71
|
+
customer_id: opts.customerId,
|
|
72
|
+
phone_number_id: opts.phoneNumberId,
|
|
73
|
+
});
|
|
74
|
+
if (format === "json")
|
|
75
|
+
return printJson(data);
|
|
76
|
+
printLine("Contato criado.");
|
|
77
|
+
printDetail(data);
|
|
78
|
+
});
|
|
79
|
+
contacts
|
|
80
|
+
.command("update <id>")
|
|
81
|
+
.description("Atualiza um contato")
|
|
82
|
+
.option("--profile-name <nome>", "novo nome do perfil")
|
|
83
|
+
.option("--username <username>", "novo username")
|
|
84
|
+
.action(async (id, opts, cmd) => {
|
|
85
|
+
const { client, format } = context(cmd);
|
|
86
|
+
const body = {};
|
|
87
|
+
if (opts.profileName !== undefined)
|
|
88
|
+
body.profile_name = opts.profileName;
|
|
89
|
+
if (opts.username !== undefined)
|
|
90
|
+
body.username = opts.username;
|
|
91
|
+
if (Object.keys(body).length === 0) {
|
|
92
|
+
throw new Error("Informe ao menos --profile-name ou --username.");
|
|
93
|
+
}
|
|
94
|
+
const data = await client.contacts.update(id, body);
|
|
95
|
+
if (format === "json")
|
|
96
|
+
return printJson(data);
|
|
97
|
+
printLine("Contato atualizado.");
|
|
98
|
+
printDetail(data);
|
|
99
|
+
});
|
|
100
|
+
contacts
|
|
101
|
+
.command("delete <id>")
|
|
102
|
+
.description("Remove um contato")
|
|
103
|
+
.action(async (id, _opts, cmd) => {
|
|
104
|
+
const { client, format } = context(cmd);
|
|
105
|
+
await client.contacts.delete(id);
|
|
106
|
+
if (format === "json")
|
|
107
|
+
return printJson({ deleted: true });
|
|
108
|
+
printLine("Contato removido.");
|
|
109
|
+
});
|
|
110
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { context, toInt } from "./shared.js";
|
|
2
|
+
import { printJson, printTable, printDetail, printCursorFooter, printLine, } from "../output.js";
|
|
3
|
+
export function registerConversations(program) {
|
|
4
|
+
const conv = program
|
|
5
|
+
.command("conversations")
|
|
6
|
+
.description("Listar e gerenciar conversas");
|
|
7
|
+
conv
|
|
8
|
+
.command("list")
|
|
9
|
+
.description("Lista conversas (paginação por cursor)")
|
|
10
|
+
.option("--phone-number-id <id>", "filtra por número")
|
|
11
|
+
.option("--status <status>", "active | ended")
|
|
12
|
+
.option("--phone-number <e164>", "filtra pelo número do contato")
|
|
13
|
+
.option("--limit <n>", "quantidade por página")
|
|
14
|
+
.option("--after <cursor>", "cursor da próxima página")
|
|
15
|
+
.option("--before <cursor>", "cursor da página anterior")
|
|
16
|
+
.action(async (opts, cmd) => {
|
|
17
|
+
const { client, format } = context(cmd);
|
|
18
|
+
const res = await client.conversations.list({
|
|
19
|
+
phone_number_id: opts.phoneNumberId,
|
|
20
|
+
status: opts.status,
|
|
21
|
+
phone_number: opts.phoneNumber,
|
|
22
|
+
limit: toInt(opts.limit),
|
|
23
|
+
after: opts.after,
|
|
24
|
+
before: opts.before,
|
|
25
|
+
});
|
|
26
|
+
if (format === "json")
|
|
27
|
+
return printJson(res);
|
|
28
|
+
printTable(res.data, [
|
|
29
|
+
{ header: "ID", key: "id", max: 36 },
|
|
30
|
+
{ header: "CONTATO", key: "phone_number" },
|
|
31
|
+
{ header: "STATUS", key: "status" },
|
|
32
|
+
{ header: "ATUALIZADA", key: "updated_at" },
|
|
33
|
+
]);
|
|
34
|
+
printCursorFooter(res.paging);
|
|
35
|
+
});
|
|
36
|
+
conv
|
|
37
|
+
.command("get <id>")
|
|
38
|
+
.description("Detalha uma conversa")
|
|
39
|
+
.action(async (id, _opts, cmd) => {
|
|
40
|
+
const { client, format } = context(cmd);
|
|
41
|
+
const data = await client.conversations.get(id);
|
|
42
|
+
if (format === "json")
|
|
43
|
+
return printJson(data);
|
|
44
|
+
printDetail(data);
|
|
45
|
+
});
|
|
46
|
+
conv
|
|
47
|
+
.command("update <id>")
|
|
48
|
+
.description("Atualiza o status de uma conversa (active | ended)")
|
|
49
|
+
.requiredOption("--status <status>", "active | ended")
|
|
50
|
+
.action(async (id, opts, cmd) => {
|
|
51
|
+
const { client, format } = context(cmd);
|
|
52
|
+
const data = await client.conversations.update(id, {
|
|
53
|
+
status: opts.status,
|
|
54
|
+
});
|
|
55
|
+
if (format === "json")
|
|
56
|
+
return printJson(data);
|
|
57
|
+
printLine("Conversa atualizada.");
|
|
58
|
+
printDetail(data);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { context, toInt } from "./shared.js";
|
|
2
|
+
import { printJson, printTable, printDetail, printOffsetFooter, printLine, } from "../output.js";
|
|
3
|
+
export function registerCustomers(program) {
|
|
4
|
+
const customers = program
|
|
5
|
+
.command("customers")
|
|
6
|
+
.description("Gerenciar clientes (do seu workspace)");
|
|
7
|
+
customers
|
|
8
|
+
.command("list")
|
|
9
|
+
.description("Lista clientes (paginação por offset)")
|
|
10
|
+
.option("--page <n>", "página")
|
|
11
|
+
.option("--per-page <n>", "itens por página")
|
|
12
|
+
.action(async (opts, cmd) => {
|
|
13
|
+
const { client, format } = context(cmd);
|
|
14
|
+
const res = await client.customers.list({
|
|
15
|
+
page: toInt(opts.page),
|
|
16
|
+
per_page: toInt(opts.perPage),
|
|
17
|
+
});
|
|
18
|
+
if (format === "json")
|
|
19
|
+
return printJson(res);
|
|
20
|
+
printTable(res.data, [
|
|
21
|
+
{ header: "ID", key: "id", max: 36 },
|
|
22
|
+
{ header: "NOME", key: "name" },
|
|
23
|
+
{ header: "EXTERNO", key: "external_customer_id" },
|
|
24
|
+
{ header: "CRIADO", key: "created_at" },
|
|
25
|
+
]);
|
|
26
|
+
printOffsetFooter(res.meta);
|
|
27
|
+
});
|
|
28
|
+
customers
|
|
29
|
+
.command("get <id>")
|
|
30
|
+
.description("Detalha um cliente")
|
|
31
|
+
.action(async (id, _opts, cmd) => {
|
|
32
|
+
const { client, format } = context(cmd);
|
|
33
|
+
const data = await client.customers.get(id);
|
|
34
|
+
if (format === "json")
|
|
35
|
+
return printJson(data);
|
|
36
|
+
printDetail(data);
|
|
37
|
+
});
|
|
38
|
+
customers
|
|
39
|
+
.command("create")
|
|
40
|
+
.description("Cria um cliente")
|
|
41
|
+
.requiredOption("--name <nome>", "nome do cliente (obrigatório)")
|
|
42
|
+
.option("--external-customer-id <id>", "id externo")
|
|
43
|
+
.action(async (opts, cmd) => {
|
|
44
|
+
const { client, format } = context(cmd);
|
|
45
|
+
const data = await client.customers.create({
|
|
46
|
+
name: opts.name,
|
|
47
|
+
external_customer_id: opts.externalCustomerId,
|
|
48
|
+
});
|
|
49
|
+
if (format === "json")
|
|
50
|
+
return printJson(data);
|
|
51
|
+
printLine("Cliente criado.");
|
|
52
|
+
printDetail(data);
|
|
53
|
+
});
|
|
54
|
+
customers
|
|
55
|
+
.command("update <id>")
|
|
56
|
+
.description("Atualiza um cliente")
|
|
57
|
+
.option("--name <nome>", "novo nome")
|
|
58
|
+
.option("--external-customer-id <id>", "novo id externo")
|
|
59
|
+
.action(async (id, opts, cmd) => {
|
|
60
|
+
const { client, format } = context(cmd);
|
|
61
|
+
const body = {};
|
|
62
|
+
if (opts.name !== undefined)
|
|
63
|
+
body.name = opts.name;
|
|
64
|
+
if (opts.externalCustomerId !== undefined) {
|
|
65
|
+
body.external_customer_id = opts.externalCustomerId;
|
|
66
|
+
}
|
|
67
|
+
if (Object.keys(body).length === 0) {
|
|
68
|
+
throw new Error("Informe ao menos --name ou --external-customer-id.");
|
|
69
|
+
}
|
|
70
|
+
const data = await client.customers.update(id, body);
|
|
71
|
+
if (format === "json")
|
|
72
|
+
return printJson(data);
|
|
73
|
+
printLine("Cliente atualizado.");
|
|
74
|
+
printDetail(data);
|
|
75
|
+
});
|
|
76
|
+
customers
|
|
77
|
+
.command("delete <id>")
|
|
78
|
+
.description("Remove um cliente")
|
|
79
|
+
.action(async (id, _opts, cmd) => {
|
|
80
|
+
const { client, format } = context(cmd);
|
|
81
|
+
await client.customers.delete(id);
|
|
82
|
+
if (format === "json")
|
|
83
|
+
return printJson({ deleted: true });
|
|
84
|
+
printLine("Cliente removido.");
|
|
85
|
+
});
|
|
86
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { context, toInt } from "./shared.js";
|
|
2
|
+
import { printJson, printTable, printCursorFooter } from "../output.js";
|
|
3
|
+
export function registerDeliveries(program) {
|
|
4
|
+
const deliveries = program
|
|
5
|
+
.command("deliveries")
|
|
6
|
+
.description("Entregas de webhook (webhook_deliveries)");
|
|
7
|
+
deliveries
|
|
8
|
+
.command("list")
|
|
9
|
+
.description("Lista entregas de webhook (paginação por cursor)")
|
|
10
|
+
.option("--endpoint-id <id>", "filtra por endpoint")
|
|
11
|
+
.option("--webhook-id <id>", "alias de --endpoint-id")
|
|
12
|
+
.option("--status <status>", "filtra por status")
|
|
13
|
+
.option("--event-type <tipo>", "filtra por tipo de evento")
|
|
14
|
+
.option("--limit <n>", "quantidade por página")
|
|
15
|
+
.option("--after <cursor>", "cursor da próxima página")
|
|
16
|
+
.option("--before <cursor>", "cursor da página anterior")
|
|
17
|
+
.action(async (opts, cmd) => {
|
|
18
|
+
const { client, format } = context(cmd);
|
|
19
|
+
// `webhook_id` é alias de `endpoint_id` na rota; --endpoint-id/--webhook-id
|
|
20
|
+
// colapsam no mesmo param (endpoint tem precedência).
|
|
21
|
+
const res = await client.webhookDeliveries.list({
|
|
22
|
+
webhook_id: opts.endpointId ?? opts.webhookId,
|
|
23
|
+
status: opts.status,
|
|
24
|
+
event_type: opts.eventType,
|
|
25
|
+
limit: toInt(opts.limit),
|
|
26
|
+
after: opts.after,
|
|
27
|
+
before: opts.before,
|
|
28
|
+
});
|
|
29
|
+
if (format === "json")
|
|
30
|
+
return printJson(res);
|
|
31
|
+
printTable(res.data, [
|
|
32
|
+
{ header: "ID", key: "id", max: 36 },
|
|
33
|
+
{ header: "EVENTO", key: "event_type" },
|
|
34
|
+
{ header: "STATUS", key: "status" },
|
|
35
|
+
{ header: "HTTP", key: "response_status" },
|
|
36
|
+
{ header: "CRIADA", key: "created_at" },
|
|
37
|
+
]);
|
|
38
|
+
printCursorFooter(res.paging);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
import { stdin, stdout } from "node:process";
|
|
3
|
+
import { setConfigValue, configPath } from "../config.js";
|
|
4
|
+
import { printLine, dim } from "../output.js";
|
|
5
|
+
import pc from "picocolors";
|
|
6
|
+
/**
|
|
7
|
+
* `botozap login` — STUB amigável.
|
|
8
|
+
*
|
|
9
|
+
* O BotoZap ainda não tem login OAuth por navegador (como o `kapso login`).
|
|
10
|
+
* Por isso este comando apenas (1) explica como criar uma API key no painel,
|
|
11
|
+
* em /chaves, e (2) oferece colar a chave para gravá-la no config local.
|
|
12
|
+
*/
|
|
13
|
+
export function registerLogin(program) {
|
|
14
|
+
program
|
|
15
|
+
.command("login")
|
|
16
|
+
.description("Instruções para autenticar e gravar sua API key (stub)")
|
|
17
|
+
.option("--api-key <chave>", "grava a chave informada sem perguntar")
|
|
18
|
+
.action(async (opts, _cmd) => {
|
|
19
|
+
printLine(pc.bold("Autenticação do BotoZap CLI"));
|
|
20
|
+
printLine("");
|
|
21
|
+
printLine("Ainda não há login por navegador (OAuth). Use uma API key do painel:");
|
|
22
|
+
printLine(` 1. Acesse ${pc.cyan("https://botozap.com.br/chaves")}`);
|
|
23
|
+
printLine(" 2. Crie uma chave (formato bz_live_…)");
|
|
24
|
+
printLine(" 3. Cole a chave abaixo (ou rode com --api-key).");
|
|
25
|
+
printLine("");
|
|
26
|
+
let key = opts.apiKey;
|
|
27
|
+
if (!key) {
|
|
28
|
+
const interativo = Boolean(stdin.isTTY);
|
|
29
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
30
|
+
try {
|
|
31
|
+
const pergunta = rl.question("Cole sua API key: ");
|
|
32
|
+
if (interativo) {
|
|
33
|
+
// Mascara a digitação: o prompt acima já foi impresso, então a
|
|
34
|
+
// partir daqui suprimimos o eco de cada caractere da chave. Assim
|
|
35
|
+
// ela não aparece na tela nem fica no scrollback do terminal.
|
|
36
|
+
// (Num pipe/redirect não há eco de terminal, então não mascaramos —
|
|
37
|
+
// o `botozap login < chave.txt` / `echo … | botozap login` funciona.)
|
|
38
|
+
rl._writeToOutput = () => { };
|
|
39
|
+
}
|
|
40
|
+
key = (await pergunta).trim();
|
|
41
|
+
// O Enter final também foi suprimido pela máscara: reponha a quebra.
|
|
42
|
+
if (interativo)
|
|
43
|
+
stdout.write("\n");
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
rl.close();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (!key) {
|
|
50
|
+
printLine(dim("Nenhuma chave informada. Nada foi gravado."));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (!key.startsWith("bz_")) {
|
|
54
|
+
printLine(pc.yellow("Aviso: a chave não começa com `bz_`. Gravando mesmo assim."));
|
|
55
|
+
}
|
|
56
|
+
setConfigValue("apiKey", key);
|
|
57
|
+
printLine(pc.green("Chave gravada com sucesso."));
|
|
58
|
+
printLine(dim(`Arquivo: ${configPath()}`));
|
|
59
|
+
printLine(dim("Verifique com `botozap status`."));
|
|
60
|
+
});
|
|
61
|
+
}
|