@flowapt/flowiq-cli 0.3.4 → 0.3.6
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 +10 -1
- package/TEAM-GUIDE.md +2 -1
- package/package.json +1 -1
- package/src/commands/broadcast.js +40 -5
- package/src/index.js +5 -1
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|list` (alias `bc`)
|
|
163
|
+
### Broadcast — `flowiq broadcast map|preview|send|resume|status|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
|
|
@@ -194,6 +194,15 @@ flowiq bc resume <org_id> --campaign july-referrals --commit [--retry-failed]
|
|
|
194
194
|
`/api/send-template` as `headerMedia`, the same field the dashboard uses. A
|
|
195
195
|
media-header template with no resolvable image is refused (pass
|
|
196
196
|
`--header-media`).
|
|
197
|
+
- **`status <org> <broadcastId>` (v0.3.5)**: live delivery counts for a broadcast
|
|
198
|
+
by its id — `read` / `delivered` / `sent` / `failed` (+ % reached) from
|
|
199
|
+
`helpdesk_messages`. Built for the `--python` fire-and-forget engine (which
|
|
200
|
+
returns a `broadcastId` but has no CLI status log), but works for any broadcast
|
|
201
|
+
id (dashboard sends included). `flowiq bc status <org> <broadcastId>`.
|
|
202
|
+
- **Engine auto-routing (v0.3.6):** any `send --tag` of **more than 10** eligible
|
|
203
|
+
recipients **always uses the python engine** (a `>2000` tag routes there too).
|
|
204
|
+
Only a ≤10 send stays on the resumable per-row Node engine. `--python` forces
|
|
205
|
+
python at any size. (CSV per-row sends stay on Node — python takes uniform params.)
|
|
197
206
|
- **`--python` engine (v0.3.4, `send --tag` only)**: hand the whole send to the
|
|
198
207
|
**python `/meta-broadcast` endpoint** — the *same* sender the dashboard's "Python
|
|
199
208
|
endpoint" toggle uses — via a staff-gated proxy (the master key stays server-side
|
package/TEAM-GUIDE.md
CHANGED
|
@@ -92,7 +92,8 @@ When you see it, run `npm i -g @flowapt/flowiq-cli` — a stale version also mea
|
|
|
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
93
|
| Send to one batch tag | `flowiq bc send <org_id> --tag <batch-tag> --template … --body param1="Hi {{first_name}}" --commit` |
|
|
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
|
-
| 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) |
|
|
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
|
+
| Check how a broadcast is landing (read/delivered/sent/failed) | `flowiq bc status <org_id> <broadcastId>` (the `broadcastId` a `--python` send prints) |
|
|
96
97
|
| Read a contact's chat | `flowiq m pull <contact_id>` then open the JSON |
|
|
97
98
|
| Export an org's full chat history | `flowiq export chats <org_id>` |
|
|
98
99
|
| 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.6",
|
|
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": {
|
|
@@ -812,17 +812,29 @@ async function runTagPipeline(orgId, opts, { commitStage }) {
|
|
|
812
812
|
if (template.url_button?.present && !buttonLiteral) aborts.push("template has a dynamic URL button — pass --button param1=<code>");
|
|
813
813
|
if (aborts.length) { for (const a of aborts) console.error(`ABORT — ${a}`); process.exit(1); }
|
|
814
814
|
|
|
815
|
-
//
|
|
816
|
-
//
|
|
817
|
-
//
|
|
815
|
+
// ENGINE ROUTING (policy): any send of MORE THAN 10 recipients ALWAYS uses the
|
|
816
|
+
// python /meta-broadcast engine (the proven bulk sender). --python forces it at
|
|
817
|
+
// any size; only a ≤10 send stays on the resumable per-row Node engine.
|
|
818
|
+
const PYTHON_MIN = 10;
|
|
818
819
|
if (opts.python) {
|
|
819
820
|
return runPythonTagSend(orgId, { opts, tag, templateName, template, headerMedia, bodyLiterals, buttonLiteral, commitStage });
|
|
820
821
|
}
|
|
821
822
|
|
|
822
|
-
// resolve the tag server-side (broadcast-safe recipients only)
|
|
823
|
+
// resolve the tag server-side (broadcast-safe recipients only). A >2000 tag
|
|
824
|
+
// 413s the Node resolver — that's by definition >10, so route straight to python.
|
|
823
825
|
let resolved;
|
|
824
826
|
try { resolved = await http.post("broadcast", { action: "resolve-tag", organization_id: orgId, tag }); }
|
|
825
|
-
catch (e) {
|
|
827
|
+
catch (e) {
|
|
828
|
+
if (e.status === 413) {
|
|
829
|
+
console.log(`Tag "${tag}" has >2000 contacts → PYTHON engine (any send >${PYTHON_MIN} uses python).`);
|
|
830
|
+
return runPythonTagSend(orgId, { opts, tag, templateName, template, headerMedia, bodyLiterals, buttonLiteral, commitStage });
|
|
831
|
+
}
|
|
832
|
+
console.error(`Tag resolution failed: ${e.message}`); if (e.body?.error) console.error(` ${e.body.error}`); process.exit(1);
|
|
833
|
+
}
|
|
834
|
+
if (resolved.recipients.length > PYTHON_MIN) {
|
|
835
|
+
console.log(`Tag "${tag}": ${resolved.recipients.length} sendable (>${PYTHON_MIN}) → PYTHON engine (any send >${PYTHON_MIN} uses python).`);
|
|
836
|
+
return runPythonTagSend(orgId, { opts, tag, templateName, template, headerMedia, bodyLiterals, buttonLiteral, commitStage });
|
|
837
|
+
}
|
|
826
838
|
console.log(`Tag "${tag}" on ${resolved.organization_name}: ${resolved.tagged_total} contact(s), ${resolved.recipients.length} sendable, ${resolved.excluded.length} excluded (opt-out/archived/blocked).`);
|
|
827
839
|
if (!resolved.recipients.length) { console.log("Nothing sendable under this tag."); return; }
|
|
828
840
|
|
|
@@ -943,6 +955,29 @@ export async function resume(orgId, opts = {}) {
|
|
|
943
955
|
await runPipeline(orgId, opts, { commitStage: !!opts.commit, isResume: true });
|
|
944
956
|
}
|
|
945
957
|
|
|
958
|
+
/** Live delivery status for a broadcast by its broadcastId (e.g. from a --python
|
|
959
|
+
* fire-and-forget send). Read-only — reads the `broadcasts` row + message_status
|
|
960
|
+
* breakdown. This is the visibility the fire-and-forget engine otherwise loses. */
|
|
961
|
+
export async function status(orgId, broadcastId, opts = {}) {
|
|
962
|
+
if (!UUID_RE.test(orgId)) { console.error(`Error: "${orgId}" is not a valid organization UUID.`); process.exit(1); }
|
|
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
|
+
let resp;
|
|
965
|
+
try { resp = await http.post("broadcast", { action: "status", organization_id: orgId, broadcast_id: broadcastId }); }
|
|
966
|
+
catch (e) { console.error(`Status failed: ${e.message}`); if (e.body?.error) console.error(` ${e.body.error}`); process.exit(1); }
|
|
967
|
+
if (opts.json) { console.log(JSON.stringify(resp, null, 2)); return; }
|
|
968
|
+
const b = resp.broadcast, d = resp.delivery;
|
|
969
|
+
const reached = d.read + d.delivered + d.sent;
|
|
970
|
+
console.log(`Broadcast ${b.id} — ${resp.organization_name}`);
|
|
971
|
+
console.log(` template: ${b.template_name}${b.broadcast_name ? ` (${b.broadcast_name})` : ""}${b.media_url ? " · media header" : ""}`);
|
|
972
|
+
console.log(` status: ${b.status ?? "?"} · created ${b.created_at}`);
|
|
973
|
+
console.log(` recipients: ${b.total_recipients ?? "?"} · ${d.linked} message(s) linked`);
|
|
974
|
+
console.log(` ✅ reached ${reached} (read ${d.read} · delivered ${d.delivered} · sent ${d.sent})`);
|
|
975
|
+
console.log(` ❌ failed ${d.failed}${d.other ? ` · other/pending ${d.other}` : ""}`);
|
|
976
|
+
if (b.total_recipients) {
|
|
977
|
+
console.log(` ${((reached / b.total_recipients) * 100).toFixed(1)}% of ${b.total_recipients} recipients reached${d.linked < b.total_recipients ? " (still sending?)" : ""}`);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
|
|
946
981
|
export async function list() {
|
|
947
982
|
let files;
|
|
948
983
|
try { files = (await fs.readdir(CAMPAIGN_DIR)).filter((f) => f.endsWith(".json") && !f.endsWith(".report.json")); }
|
package/src/index.js
CHANGED
|
@@ -391,7 +391,7 @@ export function run(argv) {
|
|
|
391
391
|
.option("--button <k=v>", "with --tag: dynamic URL button param, e.g. --button param1=<short-code>", broadcastCmd.collectKV, {})
|
|
392
392
|
.option("--campaign <name>", "campaign id / config file slug (default: CSV filename / tag)")
|
|
393
393
|
.option("--header-media <url>", "header image/video/doc URL for a media-header template (default: the template's own stored image)")
|
|
394
|
-
.option("--python", "with --tag:
|
|
394
|
+
.option("--python", "with --tag: force the python /meta-broadcast engine (same as the dashboard's Python toggle). NOTE: any tag send >10 recipients ALWAYS uses python automatically")
|
|
395
395
|
.option("--commit", "actually send (omit to dry-run)")
|
|
396
396
|
.option("--yes", "skip the type-the-campaign-name confirm gate (CI)")
|
|
397
397
|
.option("--force-remap", "ignore the saved mapping and rebuild interactively")
|
|
@@ -411,6 +411,10 @@ export function run(argv) {
|
|
|
411
411
|
.option("--rate <n>", "max messages per second (hard cap 10)", "8")
|
|
412
412
|
.option("--retry-failed", "also re-attempt rows previously marked failed (confirmed failures only)")
|
|
413
413
|
.action((orgId, opts) => broadcastCmd.resume(orgId, opts));
|
|
414
|
+
broadcast.command("status <organization_id> <broadcast_id>")
|
|
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("--json", "raw JSON")
|
|
417
|
+
.action((orgId, broadcastId, opts) => broadcastCmd.status(orgId, broadcastId, opts));
|
|
414
418
|
broadcast.command("list")
|
|
415
419
|
.description("List local campaigns + sent counts")
|
|
416
420
|
.action(() => broadcastCmd.list());
|