@flowapt/flowiq-cli 0.3.6 → 0.3.7
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 +16 -4
- package/TEAM-GUIDE.md +3 -1
- package/package.json +1 -1
- package/src/commands/broadcast.js +42 -1
- package/src/index.js +7 -0
package/README.md
CHANGED
|
@@ -160,7 +160,7 @@ flowiq ct list
|
|
|
160
160
|
- **Warnings (non-blocking):** unknown keys (likely typos the runtime would silently ignore) and unknown `{{placeholders}}` (they will NOT be substituted at runtime — known: `organization_id`, `contact_id`, `agent_id`, `contact_whatsapp_id`, `contact_full_name`, `supabase_anon_key`, `openai_api_key`, …).
|
|
161
161
|
- `--agent` + filenames behave like `prompts`/`knowledge`; the file carries `agent_id`, so `push` targets the agent it was pulled from.
|
|
162
162
|
|
|
163
|
-
### Broadcast — `flowiq broadcast map|preview|send|resume|status|list` (alias `bc`)
|
|
163
|
+
### Broadcast — `flowiq broadcast map|preview|send|resume|status|retry|list` (alias `bc`)
|
|
164
164
|
|
|
165
165
|
Send an **APPROVED** WhatsApp template to every row of a CSV, filling the
|
|
166
166
|
template's variables **per row** from the CSV's own columns. The
|
|
@@ -198,7 +198,15 @@ flowiq bc resume <org_id> --campaign july-referrals --commit [--retry-failed]
|
|
|
198
198
|
by its id — `read` / `delivered` / `sent` / `failed` (+ % reached) from
|
|
199
199
|
`helpdesk_messages`. Built for the `--python` fire-and-forget engine (which
|
|
200
200
|
returns a `broadcastId` but has no CLI status log), but works for any broadcast
|
|
201
|
-
id (dashboard sends included). `
|
|
201
|
+
id (dashboard sends included). **`--failures` (v0.3.7)** additionally lists each
|
|
202
|
+
failed recipient + the Meta error reason (`error_code` / `error_title`) with a
|
|
203
|
+
by-reason rollup — makes a fire-and-forget send fully auditable.
|
|
204
|
+
- **`retry <org> <broadcastId>` (v0.3.7)**: re-send a broadcast to **only its
|
|
205
|
+
failed recipients** — reconstructs the send from the `broadcasts` row (template +
|
|
206
|
+
params + header) and re-fires via the python engine, creating a NEW broadcast.
|
|
207
|
+
DRY-RUN by default (shows the failed count + reason breakdown); `--commit` fires.
|
|
208
|
+
Note: permanent failures (e.g. `131026` undeliverable, opt-outs) just fail again —
|
|
209
|
+
retry earns its keep on transient (throttle/throughput) failures. Capped at 500.
|
|
202
210
|
- **Engine auto-routing (v0.3.6):** any `send --tag` of **more than 10** eligible
|
|
203
211
|
recipients **always uses the python engine** (a `>2000` tag routes there too).
|
|
204
212
|
Only a ≤10 send stays on the resumable per-row Node engine. `--python` forces
|
|
@@ -242,8 +250,12 @@ flowiq bc send <org_id> --tag july-promo-batch-01 --template fresh_drop_v1 \
|
|
|
242
250
|
```
|
|
243
251
|
|
|
244
252
|
Values are shared across the tag (use `{{first_name}}` etc. for per-contact
|
|
245
|
-
personalization — resolved server-side).
|
|
246
|
-
|
|
253
|
+
personalization — resolved server-side). Supported per-contact tokens:
|
|
254
|
+
`{{first_name}}` `{{full_name}}` `{{email}}` `{{phone_number}}`
|
|
255
|
+
`{{whatsapp_id}}` `{{contact_id}}`, plus `{{attributes.<key>}}` for any
|
|
256
|
+
top-level key of the contact's `attributes` JSON (missing / object-valued
|
|
257
|
+
keys render as empty — both engines, since 21 Jul 2026). Opted-out /
|
|
258
|
+
archived / blocked contacts under the tag are excluded automatically. Tags with >2000 contacts
|
|
247
259
|
are refused — slice them with `flowiq segments` first.
|
|
248
260
|
|
|
249
261
|
### Segments — `flowiq segments plan|apply|list|untag` (alias `seg`)
|
package/TEAM-GUIDE.md
CHANGED
|
@@ -90,10 +90,12 @@ When you see it, run `npm i -g @flowapt/flowiq-cli` — a stale version also mea
|
|
|
90
90
|
| Split your whole broadcast list into N even cohorts (e.g. 3 for A/B/C or waves) | `flowiq seg plan <org_id> --tag-prefix bcast --from-attribute allow_broadcast_true --split 3` → `flowiq seg apply <org_id> bcast --commit` (makes `bcast-batch-01/02/03`, ~even, shuffled) |
|
|
91
91
|
| Combine existing tags → a batched send list (include some tags, drop others, split into batches of N) | `flowiq seg plan <org_id> --tag-prefix clearance-bc --from-tag "loyalty-list" --exclude "recent-campaign" --batch-size 1000` → `flowiq seg apply <org_id> clearance-bc --commit` (makes `clearance-bc-batch-01/02/…`) |
|
|
92
92
|
| Split a big id-list cohort into send-safe batch tags | `flowiq seg plan <org_id> --tag-prefix … --ids-file …` → `flowiq seg apply … --commit` |
|
|
93
|
-
| Send to one batch tag | `flowiq bc send <org_id> --tag <batch-tag> --template … --body param1="Hi {{first_name}}" --commit` |
|
|
93
|
+
| Send to one batch tag | `flowiq bc send <org_id> --tag <batch-tag> --template … --body param1="Hi {{first_name}}" --commit` (per-contact tokens: the 6 contact fields + `{{attributes.<key>}}`) |
|
|
94
94
|
| Send a broadcast whose template has an IMAGE header | Same as above — the image is automatic (the template's own header image). Override with `--header-media <public-image-url>` if needed. |
|
|
95
95
|
| Send via the SAME engine as the dashboard's "Python" toggle | add `--python` to a `bc send --tag …` (fire-and-forget; python resolves the tag + sends + tracks; no CLI resume for this engine). **Any tag send over 10 recipients uses python automatically.** |
|
|
96
96
|
| Check how a broadcast is landing (read/delivered/sent/failed) | `flowiq bc status <org_id> <broadcastId>` (the `broadcastId` a `--python` send prints) |
|
|
97
|
+
| See WHO failed on a broadcast + why (Meta error) | `flowiq bc status <org_id> <broadcastId> --failures` |
|
|
98
|
+
| Re-send a broadcast to only the ones that failed | `flowiq bc retry <org_id> <broadcastId>` (dry-run) → `… --commit` |
|
|
97
99
|
| Read a contact's chat | `flowiq m pull <contact_id>` then open the JSON |
|
|
98
100
|
| Export an org's full chat history | `flowiq export chats <org_id>` |
|
|
99
101
|
| Check / create WhatsApp templates | `flowiq tpl pull <org_id>` / `flowiq tpl create <org_id> --request-file req.json` |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flowapt/flowiq-cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.7",
|
|
4
4
|
"description": "Command-line tool for FlowIQ staff: round-trip agent prompts, questionnaires, fine-tuning, pin-board tasks, webhooks, templates, agent-updates, chat exports, and live agent testing without ever touching service-role credentials.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -962,7 +962,7 @@ export async function status(orgId, broadcastId, opts = {}) {
|
|
|
962
962
|
if (!UUID_RE.test(orgId)) { console.error(`Error: "${orgId}" is not a valid organization UUID.`); process.exit(1); }
|
|
963
963
|
if (!UUID_RE.test(broadcastId)) { console.error("Error: broadcast id must be a UUID (the broadcastId a --python send printed)."); process.exit(1); }
|
|
964
964
|
let resp;
|
|
965
|
-
try { resp = await http.post("broadcast", { action: "status", organization_id: orgId, broadcast_id: broadcastId }); }
|
|
965
|
+
try { resp = await http.post("broadcast", { action: "status", organization_id: orgId, broadcast_id: broadcastId, include_failures: !!opts.failures }); }
|
|
966
966
|
catch (e) { console.error(`Status failed: ${e.message}`); if (e.body?.error) console.error(` ${e.body.error}`); process.exit(1); }
|
|
967
967
|
if (opts.json) { console.log(JSON.stringify(resp, null, 2)); return; }
|
|
968
968
|
const b = resp.broadcast, d = resp.delivery;
|
|
@@ -976,6 +976,47 @@ export async function status(orgId, broadcastId, opts = {}) {
|
|
|
976
976
|
if (b.total_recipients) {
|
|
977
977
|
console.log(` ${((reached / b.total_recipients) * 100).toFixed(1)}% of ${b.total_recipients} recipients reached${d.linked < b.total_recipients ? " (still sending?)" : ""}`);
|
|
978
978
|
}
|
|
979
|
+
if (opts.failures && resp.failures) {
|
|
980
|
+
console.log("");
|
|
981
|
+
console.log(` Failures by reason:`);
|
|
982
|
+
for (const [reason, n] of Object.entries(resp.failure_reasons || {}).sort((a, b2) => b2[1] - a[1])) console.log(` ${String(n).padStart(5)} × ${reason}`);
|
|
983
|
+
const cap = opts.limit ? Number(opts.limit) : 40;
|
|
984
|
+
console.log("");
|
|
985
|
+
console.log(` Failed recipients${resp.failures.length > cap ? ` (showing ${cap} of ${resp.failures.length}; --json for all)` : ` (${resp.failures.length})`}:`);
|
|
986
|
+
for (const f of resp.failures.slice(0, cap)) {
|
|
987
|
+
console.log(` ${(f.number || "—").padEnd(14)} ${(f.name || "").slice(0, 22).padEnd(22)} ${f.error_code ?? "?"} ${f.error_title || ""}`);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
/** Re-send a broadcast to ONLY its failed recipients (transient-failure recovery).
|
|
993
|
+
* Reconstructs the send from the broadcasts row + re-fires via python. The dry-run
|
|
994
|
+
* shows the failed count + reason breakdown; --commit creates a NEW broadcast. */
|
|
995
|
+
export async function retry(orgId, broadcastId, opts = {}) {
|
|
996
|
+
if (!UUID_RE.test(orgId)) { console.error(`Error: "${orgId}" is not a valid organization UUID.`); process.exit(1); }
|
|
997
|
+
if (!UUID_RE.test(broadcastId)) { console.error("Error: broadcast id must be a UUID (the broadcastId to retry)."); process.exit(1); }
|
|
998
|
+
let dry;
|
|
999
|
+
try { dry = await http.post("broadcast", { action: "retry", organization_id: orgId, broadcast_id: broadcastId, dry_run: true }); }
|
|
1000
|
+
catch (e) { console.error(`Retry check failed: ${e.message}`); if (e.body?.error) console.error(` ${e.body.error}`); process.exit(1); }
|
|
1001
|
+
console.log(`Broadcast ${broadcastId} (${dry.broadcast?.template_name ?? "?"}) — ${dry.failed_count} failed recipient(s).`);
|
|
1002
|
+
for (const [reason, n] of Object.entries(dry.failure_reasons || {}).sort((a, b) => b[1] - a[1])) console.log(` ${String(n).padStart(5)} × ${reason}`);
|
|
1003
|
+
if (!dry.failed_count) { console.log("Nothing to retry."); return; }
|
|
1004
|
+
console.log("");
|
|
1005
|
+
console.log("Note: permanent failures (131026 undeliverable / opt-outs) just fail again — retry helps transient (throttle/throughput) failures.");
|
|
1006
|
+
if (!opts.commit) {
|
|
1007
|
+
console.log(`DRY RUN — would re-send to ${dry.failed_count} failed recipient(s) via the python engine. Add --commit to retry.`);
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
if (!opts.yes) {
|
|
1011
|
+
const a = await ask(`Type "retry" to re-send to ${dry.failed_count} failed recipient(s): `);
|
|
1012
|
+
if (a !== "retry") { console.log("Aborted — nothing re-sent."); process.exit(0); }
|
|
1013
|
+
}
|
|
1014
|
+
let out;
|
|
1015
|
+
try { out = await http.post("broadcast", { action: "retry", organization_id: orgId, broadcast_id: broadcastId, dry_run: false }); }
|
|
1016
|
+
catch (e) { console.error(`Retry failed: ${e.message}`); if (e.body?.error) console.error(` ${e.body.error}`); process.exit(1); }
|
|
1017
|
+
console.log("");
|
|
1018
|
+
console.log(`✅ ${out.message || `Retry fired for ${out.retried} recipient(s)`}${out.broadcastId ? ` · new broadcastId ${out.broadcastId}` : ""}`);
|
|
1019
|
+
if (out.broadcastId) console.log(` Watch it: flowiq bc status ${orgId} ${out.broadcastId} --failures`);
|
|
979
1020
|
}
|
|
980
1021
|
|
|
981
1022
|
export async function list() {
|
package/src/index.js
CHANGED
|
@@ -413,8 +413,15 @@ export function run(argv) {
|
|
|
413
413
|
.action((orgId, opts) => broadcastCmd.resume(orgId, opts));
|
|
414
414
|
broadcast.command("status <organization_id> <broadcast_id>")
|
|
415
415
|
.description("Live delivery counts (read/delivered/sent/failed) for a broadcast by its broadcastId — e.g. from a --python fire-and-forget send")
|
|
416
|
+
.option("--failures", "also list each FAILED recipient + the Meta error reason (makes a python send auditable)")
|
|
417
|
+
.option("--limit <n>", "with --failures: how many failed rows to print", "40")
|
|
416
418
|
.option("--json", "raw JSON")
|
|
417
419
|
.action((orgId, broadcastId, opts) => broadcastCmd.status(orgId, broadcastId, opts));
|
|
420
|
+
broadcast.command("retry <organization_id> <broadcast_id>")
|
|
421
|
+
.description("Re-send a broadcast to ONLY its failed recipients (transient-failure recovery). DRY-RUN by default; --commit re-fires via python")
|
|
422
|
+
.option("--commit", "actually re-send (omit to see the failed count + reasons)")
|
|
423
|
+
.option("--yes", "skip the type-'retry' confirm gate")
|
|
424
|
+
.action((orgId, broadcastId, opts) => broadcastCmd.retry(orgId, broadcastId, opts));
|
|
418
425
|
broadcast.command("list")
|
|
419
426
|
.description("List local campaigns + sent counts")
|
|
420
427
|
.action(() => broadcastCmd.list());
|