@flowapt/flowiq-cli 0.3.8 → 0.3.9
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 +28 -1
- package/TEAM-GUIDE.md +3 -1
- package/package.json +1 -1
- package/src/commands/audit.js +7 -2
- package/src/commands/broadcast.js +32 -0
- package/src/index.js +7 -0
package/README.md
CHANGED
|
@@ -186,7 +186,7 @@ flowiq ct list
|
|
|
186
186
|
- **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`, …).
|
|
187
187
|
- `--agent` + filenames behave like `prompts`/`knowledge`; the file carries `agent_id`, so `push` targets the agent it was pulled from.
|
|
188
188
|
|
|
189
|
-
### Broadcast — `flowiq broadcast map|preview|send|resume|status|retry|list` (alias `bc`)
|
|
189
|
+
### Broadcast — `flowiq broadcast map|preview|send|resume|list-remote|status|retry|list` (alias `bc`)
|
|
190
190
|
|
|
191
191
|
Send an **APPROVED** WhatsApp template to every row of a CSV, filling the
|
|
192
192
|
template's variables **per row** from the CSV's own columns. The
|
|
@@ -220,6 +220,13 @@ flowiq bc resume <org_id> --campaign july-referrals --commit [--retry-failed]
|
|
|
220
220
|
`/api/send-template` as `headerMedia`, the same field the dashboard uses. A
|
|
221
221
|
media-header template with no resolvable image is refused (pass
|
|
222
222
|
`--header-media`).
|
|
223
|
+
- **`list-remote <org>` (v0.3.9)**: list the org's broadcasts **newest-first** with
|
|
224
|
+
the **full broadcastId** per row + template, status, recipient count and SAST
|
|
225
|
+
created time — the discovery step `status` / `retry` need (previously the id
|
|
226
|
+
existed only in the send output or the DB). `--limit <n>` (default 25, max 200),
|
|
227
|
+
`--template <substr>` (case-insensitive contains), `--since <date>`, `--json`.
|
|
228
|
+
Read-only. NOTE: plain `bc list` (no org) still lists your **local campaign
|
|
229
|
+
files** — the remote verb is named after `pinboard list-remote`.
|
|
223
230
|
- **`status <org> <broadcastId>` (v0.3.5)**: live delivery counts for a broadcast
|
|
224
231
|
by its id — `read` / `delivered` / `sent` / `failed` (+ % reached) from
|
|
225
232
|
`helpdesk_messages`. Built for the `--python` fire-and-forget engine (which
|
|
@@ -432,6 +439,26 @@ flowiq kw push <slug> --prune # ALSO deletes DB keywords missing from
|
|
|
432
439
|
- `action_config.link_preview: false` disables WhatsApp's link-preview card
|
|
433
440
|
on that action's text send (absent/`true` = preview on, the default).
|
|
434
441
|
Passed through verbatim; also toggleable per action in the dashboard.
|
|
442
|
+
- **`action_config.when` — conditional actions.** An action only runs if its
|
|
443
|
+
condition passes, checked against the **live contact row** when the keyword
|
|
444
|
+
fires (no tag to maintain, nothing goes stale):
|
|
445
|
+
```json
|
|
446
|
+
{ "type": "send_message", "when": { "field": "email", "op": "is_not_empty" } }
|
|
447
|
+
```
|
|
448
|
+
`field` ∈ `email` / `phone_number` / `full_name` / `city` (allowlist).
|
|
449
|
+
`op` ∈ `is_empty` / `is_not_empty` / `equals` / `contains` — the last two
|
|
450
|
+
need a `value` and are case-insensitive. Rejected on push otherwise
|
|
451
|
+
(V-21/V-22), because the runtime SKIPS an action it can't evaluate.
|
|
452
|
+
Typical pattern — one keyword, two possible replies, tag either way:
|
|
453
|
+
```
|
|
454
|
+
action 1 send_message when email is_not_empty
|
|
455
|
+
"I've got your email as {{contact.email}} — reply YES to use it."
|
|
456
|
+
action 2 send_message when email is_empty
|
|
457
|
+
"Pop in your email address to finish."
|
|
458
|
+
action 3 add_contact_tag (no when → always runs)
|
|
459
|
+
```
|
|
460
|
+
There is **no dashboard field** for conditions yet — the UI preserves them
|
|
461
|
+
on save but can't edit them, so treat `flowiq kw` as their owner.
|
|
435
462
|
- Scheduled keywords: the keyword-scheduler cron reconciles `active` from
|
|
436
463
|
`start_date`/`end_date` within ~30 min of your push.
|
|
437
464
|
|
package/TEAM-GUIDE.md
CHANGED
|
@@ -78,6 +78,7 @@ When you see it, run `npm i -g @flowapt/flowiq-cli` — a stale version also mea
|
|
|
78
78
|
| Edit the custom tools (API-call tools) | `flowiq ct pull <org_id>` → edit `tools[]` → `flowiq ct push <slug> --dry-run` → `flowiq ct push <slug>` |
|
|
79
79
|
| Turn one custom tool on/off | `flowiq ct enable\|disable <org_id> <tool_name>` |
|
|
80
80
|
| Edit keyword auto-replies (incl. competition entry keywords, add/remove-tag, set-agent and delay actions) | `flowiq kw pull <org_id>` → edit → `flowiq kw push <slug> --dry-run` → `flowiq kw push <slug>` |
|
|
81
|
+
| Send a **different auto-reply depending on the contact** (e.g. "we already have your email" vs "send us your email") | add `"when": {"field":"email","op":"is_not_empty"}` to one action and `is_empty` to the other — see *Keywords* in `flowiq guide --reference`. CLI-only, no dashboard field yet |
|
|
81
82
|
| See an org's agents / create one | `flowiq agent list <org_id>` / `flowiq agent create <org_id> --name "…"` |
|
|
82
83
|
| Change agent model / tool flags | `flowiq agent config <org_id> --model … --tool view_cart_tool=true` |
|
|
83
84
|
| Talk to the live agent safely (no real WhatsApp ever sent) | `flowiq test send <org_id> "hi, do you sell X?"` |
|
|
@@ -93,7 +94,8 @@ When you see it, run `npm i -g @flowapt/flowiq-cli` — a stale version also mea
|
|
|
93
94
|
| 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
95
|
| 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
96
|
| 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
|
-
|
|
|
97
|
+
| Find a broadcast's id (don't have the `broadcastId`?) | `flowiq bc list-remote <org_id>` — the org's broadcasts newest-first with full ids (`--template <substr>` / `--since <date>` / `--limit <n>` to narrow) |
|
|
98
|
+
| Check how a broadcast is landing (read/delivered/sent/failed) | `flowiq bc status <org_id> <broadcastId>` (from the send output, or `bc list-remote`) |
|
|
97
99
|
| See WHO failed on a broadcast + why (Meta error) | `flowiq bc status <org_id> <broadcastId> --failures` |
|
|
98
100
|
| Re-send a broadcast to only the ones that failed | `flowiq bc retry <org_id> <broadcastId>` (dry-run) → `… --commit` |
|
|
99
101
|
| **Get an OLD version of a prompt back** | `flowiq prompts history <org_id>` (pick the version) → `flowiq prompts restore <org_id> <audit_id>` (dry-run) → `… --commit` |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flowapt/flowiq-cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.9",
|
|
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": {
|
package/src/commands/audit.js
CHANGED
|
@@ -94,8 +94,13 @@ export async function list(orgId, opts = {}) {
|
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
96
|
console.log("");
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
// Print FULL ids: `audit show` requires a uuid, so an 8-char prefix here just
|
|
98
|
+
// sent you back to --json to fetch the real one (`prompts history` already
|
|
99
|
+
// prints full ids — this now matches it).
|
|
100
|
+
console.log(` detail: flowiq audit show <audit_id> [--content]`);
|
|
101
|
+
for (const e of entries) {
|
|
102
|
+
console.log(` ${e.id} ${e.endpoint} ${e.action}`);
|
|
103
|
+
}
|
|
99
104
|
console.log("");
|
|
100
105
|
}
|
|
101
106
|
|
|
@@ -955,6 +955,38 @@ export async function resume(orgId, opts = {}) {
|
|
|
955
955
|
await runPipeline(orgId, opts, { commitStage: !!opts.commit, isResume: true });
|
|
956
956
|
}
|
|
957
957
|
|
|
958
|
+
/** List an org's broadcasts newest-first — the discovery step for `status`/`retry`,
|
|
959
|
+
* which need a broadcastId you otherwise only have if you kept the send output.
|
|
960
|
+
* Read-only. (`list` without an org stays the LOCAL campaign-file listing; this is
|
|
961
|
+
* the remote verb, named after `pinboard list-remote`.) */
|
|
962
|
+
export async function listRemote(orgId, opts = {}) {
|
|
963
|
+
if (!UUID_RE.test(orgId)) { console.error(`Error: "${orgId}" is not a valid organization UUID.`); process.exit(1); }
|
|
964
|
+
let resp;
|
|
965
|
+
try {
|
|
966
|
+
resp = await http.post("broadcast", {
|
|
967
|
+
action: "list", organization_id: orgId,
|
|
968
|
+
...(opts.limit ? { limit: Number(opts.limit) } : {}),
|
|
969
|
+
...(opts.template ? { template: opts.template } : {}),
|
|
970
|
+
...(opts.since ? { since: opts.since } : {}),
|
|
971
|
+
});
|
|
972
|
+
} catch (e) { console.error(`Listing failed: ${e.message}`); if (e.body?.error) console.error(` ${e.body.error}`); process.exit(1); }
|
|
973
|
+
if (opts.json) { console.log(JSON.stringify(resp, null, 2)); return; }
|
|
974
|
+
const rows = resp.broadcasts || [];
|
|
975
|
+
if (!rows.length) { console.log(`No broadcasts found for ${resp.organization_name}${opts.template || opts.since ? " matching the filters" : ""}.`); return; }
|
|
976
|
+
// Full ids one per row, deliberately — an id you can't paste into `bc status`
|
|
977
|
+
// is useless (the same lesson the audit listing learned).
|
|
978
|
+
const sast = (iso) => { try { return new Date(iso).toLocaleString("sv-SE", { timeZone: "Africa/Johannesburg" }).slice(0, 16); } catch { return iso; } };
|
|
979
|
+
console.log(`Broadcasts — ${resp.organization_name} (${rows.length} shown, newest first; times SAST)`);
|
|
980
|
+
for (const b of rows) {
|
|
981
|
+
console.log("");
|
|
982
|
+
console.log(` ${b.id}`);
|
|
983
|
+
console.log(` ${sast(b.created_at)} ${b.template_name ?? "?"}${b.broadcast_name && b.broadcast_name !== b.template_name ? ` (${b.broadcast_name})` : ""}${b.media_url ? " · media header" : ""}`);
|
|
984
|
+
console.log(` status ${b.status ?? "?"} · ${b.total_recipients ?? "?"} recipient(s)`);
|
|
985
|
+
}
|
|
986
|
+
console.log("");
|
|
987
|
+
console.log(`Delivery detail: flowiq bc status ${orgId} <broadcast_id> [--failures]`);
|
|
988
|
+
}
|
|
989
|
+
|
|
958
990
|
/** Live delivery status for a broadcast by its broadcastId (e.g. from a --python
|
|
959
991
|
* fire-and-forget send). Read-only — reads the `broadcasts` row + message_status
|
|
960
992
|
* breakdown. This is the visibility the fire-and-forget engine otherwise loses. */
|
package/src/index.js
CHANGED
|
@@ -445,6 +445,13 @@ export function run(argv) {
|
|
|
445
445
|
.option("--rate <n>", "max messages per second (hard cap 10)", "8")
|
|
446
446
|
.option("--retry-failed", "also re-attempt rows previously marked failed (confirmed failures only)")
|
|
447
447
|
.action((orgId, opts) => broadcastCmd.resume(orgId, opts));
|
|
448
|
+
broadcast.command("list-remote <organization_id>")
|
|
449
|
+
.description("List the org's broadcasts newest-first (full broadcastId + template + status + recipients) — the discovery step for `bc status` / `bc retry`")
|
|
450
|
+
.option("--limit <n>", "how many to show (default 25, max 200)")
|
|
451
|
+
.option("--template <substr>", "filter: template_name contains this (case-insensitive)")
|
|
452
|
+
.option("--since <date>", "filter: created on/after this date, e.g. 2026-07-01")
|
|
453
|
+
.option("--json", "raw JSON")
|
|
454
|
+
.action((orgId, opts) => broadcastCmd.listRemote(orgId, opts));
|
|
448
455
|
broadcast.command("status <organization_id> <broadcast_id>")
|
|
449
456
|
.description("Live delivery counts (read/delivered/sent/failed) for a broadcast by its broadcastId — e.g. from a --python fire-and-forget send")
|
|
450
457
|
.option("--failures", "also list each FAILED recipient + the Meta error reason (makes a python send auditable)")
|