@flowapt/flowiq-cli 0.8.1 → 0.9.2
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 +156 -37
- package/TEAM-GUIDE.md +19 -4
- package/package.json +1 -1
- package/src/commands/broadcast.js +71 -1
- package/src/commands/groups.js +48 -2
- package/src/commands/hours.js +129 -14
- package/src/commands/plans.js +8 -1
- package/src/commands/popups.js +367 -0
- package/src/index.js +98 -8
package/README.md
CHANGED
|
@@ -189,7 +189,8 @@ flowiq ct list
|
|
|
189
189
|
- **Destructive pushes are blocked by default**: a push that removes a tool, disables one, or sends an empty `tools[]` (wiping everything) writes nothing and shows you exactly what it would strip — re-run with `--confirm` if intentional. Additive/no-op pushes are unaffected.
|
|
190
190
|
- Server-side validation is strict: tool `name` (`^[a-zA-Z0-9_-]{1,64}$`, no duplicates or built-in collisions), non-empty `description`, `http(s)` `endpoint`, method `GET/POST/PUT/PATCH/DELETE`, scalar headers, object-typed `parameters`/`injected_parameters`, valid auth/channels, timeout and response caps. Recursive schemas support strict nested objects, typed arrays, enums and numeric/string/item limits. Unknown tool/schema keys are rejected before anything writes.
|
|
191
191
|
- **`display_name` (optional, 10 Sep 2026):** the label the TEAM sees on the inbox tool-activity card (e.g. `Checked loyalty points`) — plain string, ≤80 chars, never sent to the AI. Leave it out and the card derives a label from the tool name.
|
|
192
|
-
- **Warnings (non-blocking):** an object with properties but no required fields. Unknown `{{placeholder}}` values are hard errors; injected parameters may additionally reference a declared top-level model parameter. Known runtime values include `organization_id`, `contact_id`, `agent_id`, `contact_whatsapp_id`, `contact_full_name`, `whatsapp_message_id`, `text`, `unique_message_id`, `unix_timestamp`, `operation_idempotency_key`, `supabase_anon_key`, and `openai_api_key`.
|
|
192
|
+
- **Warnings (non-blocking):** an object with properties but no required fields. Unknown `{{placeholder}}` values are hard errors; injected parameters may additionally reference a declared top-level model parameter. Known runtime values include `organization_id`, `contact_id`, `agent_id`, `contact_whatsapp_id`, `contact_full_name`, `whatsapp_message_id`, `text`, `unique_message_id`, `unix_timestamp`, `operation_idempotency_key`, `supabase_anon_key`, `flowmod_relay_secret`, `flowiq_queue_key`, and `openai_api_key`.
|
|
193
|
+
- **Queue credential:** `{{flowiq_queue_key}}` is super-admin-only and resolves to the service key only as the bearer token of a call to FlowIQ's own `add-to-api-queue` (`https://zvhwjpeeapujvuudfdps.supabase.co/functions/v1/add-to-api-queue`). The queue refuses the public anon key since 22 Sep 2026, so a tool that schedules a send (FlowMod Org's `schedule_reminder` / `schedule_a_task`) uses this. Validation and runtime both reject it anywhere else: another endpoint, another auth type, a body, a header.
|
|
193
194
|
- **Retailer gateway credential:** `{{retailer_tools_internal_key}}` is super-admin-only and resolves only as the `x-api-key` value for `https://express.chatcart.io/retailer-tools/*` (or loopback in local tests). Validation and runtime both reject putting it in a body or sending it to any other host.
|
|
194
195
|
- `--agent` + filenames behave like `prompts`/`knowledge`; the file carries `agent_id`, so `push` targets the agent it was pulled from.
|
|
195
196
|
|
|
@@ -234,7 +235,14 @@ flowiq bc send <org_id> --tag <batch-tag> --template <name> --body param1="…"
|
|
|
234
235
|
flowiq bc send … --at "2026-08-05 09:00" --needs-approval --commit # park it for approval
|
|
235
236
|
|
|
236
237
|
flowiq bc scheduled list <org_id> # open rows (pending + awaiting-approval)
|
|
237
|
-
flowiq bc scheduled list <org_id> --all # incl. fired / cancelled
|
|
238
|
+
flowiq bc scheduled list <org_id> --all # incl. fired / cancelled — an empty OPEN list now names what --all would show
|
|
239
|
+
flowiq bc scheduled list <org_id> --status completed
|
|
240
|
+
|
|
241
|
+
# SAVED SEND DRAFTS (v0.9.1) — the send dialog's stored configurations. A draft
|
|
242
|
+
# is NOT scheduled and never fires on its own; before this it was invisible
|
|
243
|
+
# outside the dashboard.
|
|
244
|
+
flowiq bc drafts <org_id> # newest first
|
|
245
|
+
flowiq bc drafts show <org_id> <draft_id> # the whole stored config
|
|
238
246
|
flowiq bc scheduled approve <org_id> <queue_id> # 'request' → 'pending' (--at re-times it)
|
|
239
247
|
flowiq bc scheduled cancel <org_id> <queue_id> --confirm
|
|
240
248
|
|
|
@@ -930,6 +938,7 @@ ids and prefixes — never a key.
|
|
|
930
938
|
| `agent-config` | config | before + after snapshot + the changed keys |
|
|
931
939
|
| `agents` / `org` / `meta-templates` | create | the created record / submitted request |
|
|
932
940
|
| `messaging-webhooks` / `webhooks` | push, reconnect | full before + after |
|
|
941
|
+
| `popups` | create / push / duplicate / delete / upload | **full before + after document** (popup + webhooks) |
|
|
933
942
|
| `pinboard` | push | full prior row + the new one |
|
|
934
943
|
| `agent-updates` | resolve | prior status + the client-facing note |
|
|
935
944
|
| `broadcast` | **send (committed)**, retry | the exact payload sent + the resulting broadcastId |
|
|
@@ -1024,6 +1033,85 @@ Push refuses an `auth_type` whose credentials are incomplete (e.g. `bearer`
|
|
|
1024
1033
|
with no `auth_config.token`), because that would make every delivery fail
|
|
1025
1034
|
closed — FlowIQ never falls back to an unauthenticated send.
|
|
1026
1035
|
|
|
1036
|
+
### Popups — `flowiq popups schema|list|pull|push|create|duplicate|activate|deactivate|live|delete|upload` (alias `pp`) (v0.9.0)
|
|
1037
|
+
|
|
1038
|
+
Every popup in FlowIQ — the ones built in the dashboard designer and the
|
|
1039
|
+
ones built here — is written through ONE endpoint, the popup service's
|
|
1040
|
+
`/api/popup-settings`. That endpoint owns the validator, the write rules
|
|
1041
|
+
and the schema; this CLI holds **no popup logic**. So a popup feature that
|
|
1042
|
+
ships in the popup service works from the terminal the same day, with no
|
|
1043
|
+
CLI release, and `flowiq popups schema` always prints what a popup may
|
|
1044
|
+
contain **today**.
|
|
1045
|
+
|
|
1046
|
+
```bash
|
|
1047
|
+
flowiq popups schema # what a document may contain (content, rules, design, webhooks); --json for enums + limits
|
|
1048
|
+
flowiq popups list <org_id> # every popup: id, active/live, views, signups, what a signup triggers
|
|
1049
|
+
flowiq popups pull <org_id> [popup_id] # → ./.flowiq/popups/<org-slug>/<name>-<id8>.json (popup + webhooks + etag); no id = all
|
|
1050
|
+
flowiq popups push <file> [--dry-run] # apply the document; refused (409) if someone edited it since the pull (--force overrides)
|
|
1051
|
+
flowiq popups create <org_id> <file> [--dry-run] # create from a document (a pulled file from ANY org works — ids are stripped); created INACTIVE
|
|
1052
|
+
flowiq popups duplicate <org_id> <popup_id> [--name …] [--from-org <id>] [--no-webhooks]
|
|
1053
|
+
flowiq popups activate|deactivate <org_id> <popup_id>
|
|
1054
|
+
flowiq popups live <org_id> <popup_id> [--off] # which ACTIVE popup the store embed shows (one per org; prints what it demoted)
|
|
1055
|
+
flowiq popups delete <org_id> <popup_id> [--confirm]
|
|
1056
|
+
flowiq popups upload <org_id> <popup_id> <file> --slot main | --font "Gilmer" [--weight 500] [--italic]
|
|
1057
|
+
```
|
|
1058
|
+
|
|
1059
|
+
**The document.** One JSON file per popup:
|
|
1060
|
+
|
|
1061
|
+
```json
|
|
1062
|
+
{
|
|
1063
|
+
"organization_id": "…",
|
|
1064
|
+
"etag": "6939ff535be8f3f6",
|
|
1065
|
+
"popup": { "id": "…", "name": "…", "slug": "spin", "is_active": true,
|
|
1066
|
+
"content_settings": { "welcome": {…}, "success": {…}, "wheel": {…} },
|
|
1067
|
+
"style_settings": { "design": {…} },
|
|
1068
|
+
"rules_settings": { "when_to_show": {…}, "whom_to_show": {…}, … },
|
|
1069
|
+
"stats": { "sales_stats": false } },
|
|
1070
|
+
"webhooks": [ { "id": "…", "name": "Discount", "url": "…/functions/v1/send-discount-code-popup",
|
|
1071
|
+
"discount_config": { "enabled": true, "template_name": "welcome_v1", … } } ]
|
|
1072
|
+
}
|
|
1073
|
+
```
|
|
1074
|
+
|
|
1075
|
+
- **A popup with no webhook sends NOTHING on signup.** The WhatsApp welcome +
|
|
1076
|
+
discount code is the `send-discount-code-popup` row; Klaviyo / Omnisend /
|
|
1077
|
+
Marsello / Passes / Flows are their own rows. `flowiq popups schema` lists
|
|
1078
|
+
every FlowIQ function with its URL and config keys. **One row per function
|
|
1079
|
+
per popup** — every row fires on every signup, so two rows = two messages;
|
|
1080
|
+
the server refuses a push that would create the duplicate.
|
|
1081
|
+
- **`push` is id-keyed for webhooks** and **MERGES integration configs**
|
|
1082
|
+
(`discount_config` / `pass_config` / `flows_config`) over what the row
|
|
1083
|
+
holds — a file that knows three keys can never wipe a fourth (`null`
|
|
1084
|
+
deletes a key on purpose). A webhook without an `id` but with a URL one
|
|
1085
|
+
existing row already has UPDATES that row. Webhooks missing from the file
|
|
1086
|
+
are left alone unless `--replace-webhooks --confirm-delete-webhooks`.
|
|
1087
|
+
- **`push` never changes which popup is live** (`currently_active_on_store`
|
|
1088
|
+
and `image_history` are not sent from the file). Use `popups live`.
|
|
1089
|
+
- **Validation is the server's** (the editor gets the same answers): answer
|
|
1090
|
+
keys (`choice_fields`, `questions`, deck cards, wheel segment ids) must
|
|
1091
|
+
match `[a-zA-Z0-9_-]{1,64}` or the submissions API drops them; an enabled
|
|
1092
|
+
wheel needs 2–12 wedges with at least one weight; an enabled teaser needs
|
|
1093
|
+
text; `size.width` is kept equal to `size.maxWidth`. Errors block, warnings
|
|
1094
|
+
ride back and are printed (`⚠`). Unknown keys are **stored with a
|
|
1095
|
+
warning**, never dropped — popups evolve faster than any allowlist.
|
|
1096
|
+
- **`create` and `duplicate` land INACTIVE** with no slug and zero stats;
|
|
1097
|
+
`activate` when checked. `duplicate` copies the webhooks too (`--from-org`
|
|
1098
|
+
copies a popup from another org, webhooks excluded — templates, lists and
|
|
1099
|
+
pass designs belong to the source client).
|
|
1100
|
+
- **Kill switch = `deactivate`** (`is_active=false`: store embed, pinned id
|
|
1101
|
+
and the `popup.flowapt.com/p/…` link all 404). `live --off` only unselects
|
|
1102
|
+
it — a store embed that pins its id still serves it while active.
|
|
1103
|
+
- **`delete` removes the signups, webhooks and delivery history with it**;
|
|
1104
|
+
a live popup or one with signups needs `--confirm`.
|
|
1105
|
+
- **`upload`** puts an image in the private `popups_images` bucket (1-year
|
|
1106
|
+
signed URL) or a font in the PUBLIC `popup_fonts` bucket, and prints the
|
|
1107
|
+
URL to paste into the design.
|
|
1108
|
+
- **Concurrency:** the file's `etag` is sent as `if_match`; if the popup was
|
|
1109
|
+
edited in the dashboard since the pull, push is refused with 409 — pull,
|
|
1110
|
+
re-apply, push. `--force` overwrites. Every push refreshes the file (new
|
|
1111
|
+
etag), and a rename moves it to its new file name.
|
|
1112
|
+
- **Audit:** create / push / duplicate / delete / upload are audited with the
|
|
1113
|
+
full before + after document (`flowiq audit <org> --endpoint popups`).
|
|
1114
|
+
|
|
1027
1115
|
### FlowMod prompts — `flowiq flowmod pull|push <slug>` (alias `fm`)
|
|
1028
1116
|
|
|
1029
1117
|
Round-trips a FlowMod org's **master-group** prompts + config. FlowMod groups
|
|
@@ -1049,29 +1137,32 @@ the prompt rows directly).
|
|
|
1049
1137
|
Common uses: edit a master-group's moderator prompt, or flip its reasoning
|
|
1050
1138
|
effort via `config.reasoningEffort` (`none` / `low` / `medium` / `high`).
|
|
1051
1139
|
|
|
1052
|
-
### Group chats — `flowiq groups
|
|
1140
|
+
### Group chats — ⛔ DEPRECATED (`flowiq groups`, alias `grp`)
|
|
1053
1141
|
|
|
1054
|
-
Read
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1142
|
+
**Evolution is retired (22 Sep 2026). Read a WhatsApp group chat with the
|
|
1143
|
+
WhatsApp MCP, not this command.** The flowapt Evolution instance logged out of
|
|
1144
|
+
WhatsApp on 29 Aug 2026 (`Instance.connectionStatus` = `close`, reason 401) and
|
|
1145
|
+
has stored nothing since, so this command answered from a frozen archive while
|
|
1146
|
+
looking current — on 22 Sep it reported the Johnson Fitness group's last
|
|
1147
|
+
message as 17 Aug while the group had traffic that morning.
|
|
1058
1148
|
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1149
|
+
```
|
|
1150
|
+
mcp__whatsapp-flowapt__list_chats { query: "<group name>" } → the JID
|
|
1151
|
+
mcp__whatsapp-flowapt__list_messages { chat_jid: "…@g.us", limit: 50 }
|
|
1152
|
+
mcp__whatsapp-flowmod__* if the group is not on the flowapt line
|
|
1153
|
+
```
|
|
1154
|
+
|
|
1155
|
+
Both verbs now **refuse unless `--archive` is passed**, because a stale read
|
|
1156
|
+
that looks complete is worse than an error. `--archive` prints the frozen data
|
|
1157
|
+
with a banner, and `groups list` shows the instance's connection state so the
|
|
1158
|
+
cut-off is visible:
|
|
1064
1159
|
|
|
1065
1160
|
```bash
|
|
1066
1161
|
export FLOWMOD_EVO_DB_URL='postgres://user:pass@app.flowmod.ai:5555/flowiq-db'
|
|
1067
1162
|
|
|
1068
|
-
flowiq groups list
|
|
1069
|
-
flowiq groups
|
|
1070
|
-
|
|
1071
|
-
flowiq groups pull "Phytoceutics x Flowapt" --last 7d
|
|
1072
|
-
flowiq groups pull "Zorora x Flowapt" --since 2026-06-24 --until "2026-06-25 12:00"
|
|
1073
|
-
flowiq grp pull 120363408455897717@g.us --last 48h # or pass the group JID
|
|
1074
|
-
# writes ./.flowiq/groups/<instance>-<group-slug>.json + prints a timeline
|
|
1163
|
+
flowiq groups list --archive # frozen: 38 groups, all ending 28 Aug 2026
|
|
1164
|
+
flowiq groups pull "Zorora x Flowapt" --archive --since 2026-06-24 --until 2026-06-25
|
|
1165
|
+
# still writes ./.flowiq/groups/<instance>-<group-slug>.json
|
|
1075
1166
|
```
|
|
1076
1167
|
|
|
1077
1168
|
- Group is matched by JID, exact name, or a unique case-insensitive substring
|
|
@@ -1099,23 +1190,30 @@ Editable fields: `name`, `description`, `status` (open / in_progress / done),
|
|
|
1099
1190
|
null), `organizations[]`, `media[]`, `messages[]`, `created_by`. Array columns
|
|
1100
1191
|
are full-replace within the row.
|
|
1101
1192
|
|
|
1102
|
-
### Client hours — `flowiq hours log|list|summary` (v0.5.0)
|
|
1193
|
+
### Client hours — `flowiq hours log|list|summary|package` (v0.5.0; packages v0.9.2)
|
|
1103
1194
|
|
|
1104
|
-
The client-hours ledger. Every
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1195
|
+
The client-hours ledger. Every piece of client work gets logged against the
|
|
1196
|
+
org's **monthly hours package** — manually here, or automatically by Claude
|
|
1197
|
+
sessions. **The CLI is the only way to log hours:** the FlowIQ MCP's
|
|
1198
|
+
`log_client_hours` tool was removed on 29 Aug 2026, because the MCP advertises
|
|
1199
|
+
every tool to every connection and a staff-only write does not belong on a
|
|
1200
|
+
client-facing surface. The MCP keeps `get_client_hours` (read, staff
|
|
1201
|
+
connections only). The Client Console card and the super-admin Changelog
|
|
1202
|
+
dialog → Client hours read the same figures.
|
|
1112
1203
|
|
|
1113
1204
|
```bash
|
|
1114
1205
|
flowiq hours log <org_id> --hours 1.5 --desc "Rebuilt the abandoned-cart copy" # fractions fine
|
|
1115
1206
|
flowiq hours log <org_id> --minutes 45 --desc "Fixed template header" --date 2026-08-18
|
|
1116
1207
|
flowiq hours list <org_id> # this month's entries + package standing
|
|
1117
1208
|
flowiq hours list <org_id> --month 2026-07 # a past month
|
|
1118
|
-
flowiq hours summary # every org with logged work
|
|
1209
|
+
flowiq hours summary # every org with logged work or a package
|
|
1210
|
+
|
|
1211
|
+
flowiq hours package show [<org_id>] # every package, or the one an org is on
|
|
1212
|
+
flowiq hours package set <org_id> --hours 5 --notes "Order Form 12 Aug 2026"
|
|
1213
|
+
flowiq hours package set <pt> <es> <it> --hours 10 --name Barkyn # ONE package SHARED by three orgs
|
|
1214
|
+
flowiq hours package set <org_id> --hours 0 # a plan with no hours included
|
|
1215
|
+
flowiq hours package clear <org_id> # back to "no package on file"
|
|
1216
|
+
# add --dry-run to set / clear to see the change first
|
|
1119
1217
|
```
|
|
1120
1218
|
|
|
1121
1219
|
- `log` takes exactly ONE of `--minutes <n>` (whole minutes) or `--hours <h>`
|
|
@@ -1124,10 +1222,25 @@ flowiq hours summary # every org with logged work this
|
|
|
1124
1222
|
- `--by` overrides who did the work (default: your staff login email);
|
|
1125
1223
|
`--date YYYY-MM-DD` backdates an entry (it counts toward THAT month);
|
|
1126
1224
|
`--session` tags the session/task name.
|
|
1127
|
-
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1225
|
+
- **Packages (v0.9.2).** Each org has its own package, shares one with sibling
|
|
1226
|
+
orgs, or has none on file. There is no default: an org with no package reads
|
|
1227
|
+
"no package on file", never an implied 10h. A **shared** package is measured
|
|
1228
|
+
on the time of ALL its orgs together (Barkyn Portugal + Spain + Italy draw on
|
|
1229
|
+
one 10h), and every org on it shows the shared total. `0` hours is a real
|
|
1230
|
+
plan (self-service, nothing included).
|
|
1231
|
+
- `package set` puts EXACTLY the orgs you name on one package. If they already
|
|
1232
|
+
form exactly one package it is updated in place; otherwise they leave their
|
|
1233
|
+
old packages (a package left empty is removed) and get a new one. The output
|
|
1234
|
+
warns when orgs are left behind on the old package. `--name` defaults to the
|
|
1235
|
+
org's name (or the names joined); `--notes` records where the figure comes
|
|
1236
|
+
from.
|
|
1237
|
+
- **Only matt@flowapt.com and gidon@flowapt.com can set or clear a package**
|
|
1238
|
+
(enforced on the server; other super admins get a 403, logged as a blocked
|
|
1239
|
+
attempt). `package show` tells you whether you can.
|
|
1240
|
+
- `list` / `summary` default to the current month and flag any org, or shared
|
|
1241
|
+
package, that has gone over.
|
|
1242
|
+
- Corrections (delete/edit) to entries live in the Changelog → Client hours
|
|
1243
|
+
page, not the CLI. Every `log`, `package set` and `package clear` is audited.
|
|
1131
1244
|
|
|
1132
1245
|
### Client deck — `flowiq report deck status|build|narrate|generate|inputs|approve|unapprove|send|print|pull` (v0.6.5)
|
|
1133
1246
|
|
|
@@ -1145,7 +1258,7 @@ from flowiq@flowapt.com.
|
|
|
1145
1258
|
flowiq report deck status <org_id> 2026-08 # what exists, workflow status, what blocks approval
|
|
1146
1259
|
flowiq report deck generate <org_id> 2026-08 # build the metrics (if missing) + write the copy
|
|
1147
1260
|
flowiq report deck generate <org_id> 2026-08 --refresh --force # rebuild everything
|
|
1148
|
-
flowiq report deck build <org_id> 2026-08 --refresh # metrics only
|
|
1261
|
+
flowiq report deck build <org_id> 2026-08 --refresh # metrics only; status then says "Copy is older than the numbers" until you narrate
|
|
1149
1262
|
flowiq report deck narrate <org_id> 2026-08 # copy only (hand edits are kept)
|
|
1150
1263
|
flowiq report deck inputs <org_id> 2026-08 --file inputs.json # the super-admin input form
|
|
1151
1264
|
flowiq report deck approve <org_id> 2026-08 # refused until every required input is present
|
|
@@ -1593,13 +1706,14 @@ flowiq agent config <organization_id> --use-settings-prompt --model gpt-5.6-luna
|
|
|
1593
1706
|
--rename Zara --tool woo_order_build=true --tool view_cart_tool=true --discount true
|
|
1594
1707
|
flowiq agent config <organization_id> --test-contact-number 27000000001 --test-contact-name "QA Bot"
|
|
1595
1708
|
flowiq agent config <organization_id> --model gpt-5.6-luna --reasoning-effort high
|
|
1709
|
+
flowiq agent config <organization_id> --model claude-sonnet-5 --reasoning-effort medium # Claude (needs the org's Claude key; warns if missing)
|
|
1596
1710
|
flowiq agent config <organization_id> --disable-base-tool get_product_info
|
|
1597
1711
|
flowiq agent config <organization_id> --enable-base-tool get_product_info
|
|
1598
1712
|
```
|
|
1599
1713
|
|
|
1600
1714
|
Settable: `settings.use_settings_prompt`, `settings.model`,
|
|
1601
|
-
`settings.reasoning_effort` (`--reasoning-effort low|medium|high`, `""` clears it —
|
|
1602
|
-
pairs with reasoning models like `gpt-5.6-luna`), agent `--rename`,
|
|
1715
|
+
`settings.reasoning_effort` (`--reasoning-effort low|medium|high|xhigh|max`, `""` clears it —
|
|
1716
|
+
pairs with reasoning models like `gpt-5.6-luna`; `max` is Claude only), agent `--rename`,
|
|
1603
1717
|
the tool-flag columns (`woo_order_build`, `woo_tip_field`, `woo_order_note_field`,
|
|
1604
1718
|
`view_cart_tool`, `restock_tool`, `block_tool_status`, `postal_code_tool_status`,
|
|
1605
1719
|
`shopify_products_web_chat`, `ticket_tool_status`, `product_lookup`,
|
|
@@ -1687,6 +1801,8 @@ and Flowapt reviews), from the terminal.
|
|
|
1687
1801
|
flowiq plans list # open plans across every ACTIVE client
|
|
1688
1802
|
flowiq plans list --status pending_review # waiting for Flowapt review
|
|
1689
1803
|
flowiq plans list <organization_id> --status all --since 2026-09-01
|
|
1804
|
+
flowiq plans list --created-since 2026-09-22 # what clients submitted TODAY (v0.9.1)
|
|
1805
|
+
flowiq plans list --touched-since 2026-09-20 # anything edited or reviewed since
|
|
1690
1806
|
flowiq plans show <plan_id> # copy, second message, buttons, creative, audience, notes
|
|
1691
1807
|
flowiq plans show <plan_id> --json --out plan.json
|
|
1692
1808
|
flowiq plans status <plan_id> --to approved # dry run
|
|
@@ -1699,6 +1815,9 @@ flowiq plans status <plan_id> --to sent --commit
|
|
|
1699
1815
|
`--status all` / a status / a comma list to widen or narrow, `--since` to
|
|
1700
1816
|
filter by send date, `--include-inactive` for inactive orgs. The header
|
|
1701
1817
|
shows the totals for every status, and each row prints the full plan id.
|
|
1818
|
+
**`--created-since` / `--touched-since` (v0.9.1)** filter the ROW's own
|
|
1819
|
+
timestamps instead of the send date (SAST day start) — "what did a client
|
|
1820
|
+
submit today", "what has moved since Friday", which `--since` cannot answer.
|
|
1702
1821
|
- **`show`** prints the plan the way the dialog does: message copy, the
|
|
1703
1822
|
second message sent when a button is tapped, buttons with their URLs, the
|
|
1704
1823
|
creative (file name + link), audience, template link, the response to the
|
|
@@ -1760,7 +1879,7 @@ version you have installed.
|
|
|
1760
1879
|
| `FLOWIQ_API_URL` | `https://api.flowiq.live` | Override the API host (local dev, staging). |
|
|
1761
1880
|
| `FLOWIQ_TOKEN` | (saved in `~/.config/flowiq/auth.json`) | Override the auth token, useful for CI. |
|
|
1762
1881
|
| `FLOWIQ_NO_UPDATE_CHECK` | (unset) | Set to `1` to silence the "you're behind" update hint (also honours `NO_UPDATE_NOTIFIER=1` and any `CI` env). |
|
|
1763
|
-
| `FLOWMOD_EVO_DB_URL` | (none) | Full `postgres://` URL for `groups` (Evolution DB).
|
|
1882
|
+
| `FLOWMOD_EVO_DB_URL` | (none) | Full `postgres://` URL for the DEPRECATED `groups --archive` (Evolution DB, frozen since 29 Aug 2026). Not needed for anything else. |
|
|
1764
1883
|
| `FLOWMOD_DB_HOST/PORT/USER/PASS/NAME` | (none) | Alternative to `FLOWMOD_EVO_DB_URL` — assembled into a connection string. |
|
|
1765
1884
|
|
|
1766
1885
|
## Upgrading
|
package/TEAM-GUIDE.md
CHANGED
|
@@ -86,9 +86,17 @@ When you see it, run `npm i -g @flowapt/flowiq-cli` — a stale version also mea
|
|
|
86
86
|
| 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 |
|
|
87
87
|
| Make a follow-up button **close the customer's ticket** ("Query solved") or **re-alert the team** ("I still need help") | keyword action `{"type":"update_ticket_status","status":"resolved"}` / `{"type":"renotify_ticket"}` — see *Keywords* in `flowiq guide --reference`. CLI-only, no dashboard field yet |
|
|
88
88
|
| Make a keyword/button **hand the chat to a team or person** (assign in the inbox + email/WhatsApp them) | keyword action `{"type":"assign_chat","team_id":"…","assignee_user_id":"…","notify_member":true}` — see *Keywords* in `flowiq guide --reference`. Also in the dashboard (action type "Assign Chat") |
|
|
89
|
+
| **See a client's popups** and what each one sends on signup | `flowiq popups list <org_id>` — "sends to: NOTHING" means a signup goes nowhere; add a `send-discount-code-popup` webhook |
|
|
90
|
+
| **Change a popup's copy, wheel, rules or webhooks** | `flowiq popups pull <org_id> <popup_id>` → edit the JSON → `flowiq popups push <file> --dry-run` → `… push <file>`. Same rules as the dashboard (it is the same endpoint); if someone edited it in between you get a 409 — pull again |
|
|
91
|
+
| **What can a popup contain right now?** (wheel, deck, intro page, scratch card, questions…) | `flowiq popups schema` — served by the popup service, so it is never stale |
|
|
92
|
+
| **Build a client's popup from a proven one** | `flowiq popups duplicate <client_org> <popup_id> --from-org <source_org> --name "Client — welcome"`, then `pull` → edit → `push`, add its webhooks, `activate`, `live` |
|
|
93
|
+
| Copy a popup inside the same client (webhooks included) | `flowiq popups duplicate <org_id> <popup_id>` — lands inactive with zero stats |
|
|
94
|
+
| Switch a popup on / off / choose which one the store shows | `flowiq popups activate\|deactivate <org_id> <popup_id>` · `flowiq popups live <org_id> <popup_id>` (prints which popup it demoted) |
|
|
95
|
+
| Put a picture or the client's own font on a popup | `flowiq popups upload <org_id> <popup_id> ./hero.jpg --slot main` · `… ./Gilmer-Medium.otf --font Gilmer --weight 500` — paste the printed URL into the design, then push |
|
|
89
96
|
| **Find an org's ID** (needed by nearly every command) | `flowiq org list` — or `flowiq org list african` to filter |
|
|
90
97
|
| See broadcast plans waiting for Flowapt review, across every client | `flowiq plans list --status pending_review`, then `flowiq plans show <plan_id>` for the copy, second message, buttons, creative and audience |
|
|
91
98
|
| See one client's broadcast plans | `flowiq plans list <org_id>` (open plans) or `flowiq plans list <org_id> --status all --since 2026-09-01` |
|
|
99
|
+
| "What did clients submit today?" | `flowiq plans list --created-since 2026-09-22` (v0.9.1) — `--since` filters the SEND date, `--created-since` / `--touched-since` filter when the plan itself was created or last changed |
|
|
92
100
|
| Mark a broadcast plan approved, scheduled or sent | `flowiq plans status <plan_id> --to sent` (a dry run), then add `--commit`. Rejecting needs `--comment "..."`, which the client reads |
|
|
93
101
|
| What's the stock on a product, per branch? | `flowiq shopify stock <org_id> "olive oil"` — shows each location by NAME, and says "not tracked" rather than a confusing 0 |
|
|
94
102
|
| Has this order shipped? What's the tracking? | `flowiq shopify order <org_id> '#14728'` — status, courier, tracking number + link |
|
|
@@ -102,6 +110,7 @@ When you see it, run `npm i -g @flowapt/flowiq-cli` — a stale version also mea
|
|
|
102
110
|
| See an org's agents / create one | `flowiq agent list <org_id>` / `flowiq agent create <org_id> --name "…"` — a new agent arrives ready to work (gpt-5.6-luna, high reasoning, prompt switched ON); no follow-up `agent config` needed |
|
|
103
111
|
| Change agent model / tool flags | `flowiq agent config <org_id> --model … --tool view_cart_tool=true` |
|
|
104
112
|
| Set the house model + reasoning tier | `flowiq agent config <org_id> --model gpt-5.6-luna --reasoning-effort high` |
|
|
113
|
+
| Run the agent on Claude (org needs a Claude key in Settings → Integrations) | `flowiq agent config <org_id> --model claude-sonnet-5 --reasoning-effort medium` |
|
|
105
114
|
| Remove a conflicting built-in tool from one agent | `flowiq agent config <org_id> --disable-base-tool get_product_info` (repeatable; safety/delivery tools cannot be disabled) |
|
|
106
115
|
| Agent says an in-stock product "isn't showing" | `flowiq agent config <org_id> --tool collapse_product_variants=true` — the search cap counts VARIANT rows until this is on |
|
|
107
116
|
| Agent can't quote ANY price / "I can't pull the live menu" | `flowiq agent config <org_id> --tool product_lookup=true` — name-based fuzzy lookup that works without embeddings or a live OpenAI key (semantic `get_product_info` needs both) |
|
|
@@ -163,7 +172,10 @@ several.
|
|
|
163
172
|
| Send the NEXT batch of a campaign that already has batches 01-12 | `flowiq seg plan <org_id> --tag-prefix f500 --ids-file next100.txt --batch-size 100 --start-index 13` → `flowiq seg apply <org_id> f500 --commit` (makes `f500-batch-13`; without `--start-index` the plan is REFUSED, because it would re-use `f500-batch-01` and merge the two cohorts) |
|
|
164
173
|
| Split a whole tagged audience into batches of N (e.g. 90k → 7000s) | `flowiq seg plan <org_id> --tag-prefix 3-aug-bc --from-tag "3-aug-bc" --batch-size 7000` → `flowiq seg apply <org_id> 3-aug-bc --commit --yes` (makes `…-batch-01…13`; works at any size — big applies are chunked internally) |
|
|
165
174
|
| Schedule a broadcast for later instead of sending now | `flowiq bc send <org_id> --tag <batch-tag> --template <name> --at "2026-08-05 09:00" --commit` (time is SAST; fires on its own; audience resolved at send time). **Works with --csv too (v0.6.1):** rows import+tag immediately, the send queues server-side, and it shows on the dashboard's Scheduled sends page. Nothing depends on your laptop being on at send time. |
|
|
166
|
-
| See / approve / cancel what's scheduled | `flowiq bc scheduled list <org_id>` → `flowiq bc scheduled approve <org_id> <queue_id>` or `flowiq bc scheduled cancel <org_id> <queue_id> --confirm` |
|
|
175
|
+
| See / approve / cancel what's scheduled | `flowiq bc scheduled list <org_id>` → `flowiq bc scheduled approve <org_id> <queue_id>` or `flowiq bc scheduled cancel <org_id> <queue_id> --confirm`. The plain list shows only what is still OPEN; it now tells you how many fired / cancelled rows it is hiding, and `--all` shows them with who scheduled each one and how big the audience was |
|
|
176
|
+
| "Did a broadcast go out, and who scheduled it?" | `flowiq bc scheduled list <org_id> --all` — every queued send ever, with `via flowiq-cli (name@flowapt.com)`, the audience at schedule time, when it fired and its broadcastId |
|
|
177
|
+
| Find a broadcast someone half-built and saved but never sent | `flowiq bc drafts <org_id>` → `flowiq bc drafts show <org_id> <draft_id>` (v0.9.1). These are the send dialog's SAVED DRAFTS — a draft is not scheduled and will never fire on its own |
|
|
178
|
+
| Read a client's WhatsApp GROUP chat | **Not the CLI.** `flowiq groups` is deprecated — Evolution was retired on 29 Aug 2026 and its copy is frozen. Ask Claude to read it with the WhatsApp MCP (`whatsapp-flowapt`, then `whatsapp-flowmod`) |
|
|
167
179
|
| 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>}}`). Sending a **different template to the SAME tag**? Add a distinct `--campaign <name>` — otherwise the CLI aborts (a campaign belongs to one template; reusing it would send the first template's image + skip everyone it already reached). |
|
|
168
180
|
| Send a broadcast whose template has an IMAGE/VIDEO/DOCUMENT header | Same as above — the media is automatic (the template's own stored header). Override with `--header-media <public-url>` if needed. The CLI verifies the resolved media's actual type against the header format — `header video (video ✓ video/mp4)` means verified; a mismatch (e.g. a video template whose stored default is secretly a png — templates made before 3 Aug 2026 can carry this) ABORTS and tells you to pass `--header-media` with the real file. |
|
|
169
181
|
| Send a **CAROUSEL** template (v0.4.9) | Tag mode only: `flowiq bc send <org_id> --tag <batch-tag> --template <carousel_name> --body param1="Hi {{first_name}}" --commit`. Card images are automatic (each card's stored template image, type-verified per card); override with repeatable `--card-media <url>` (one per card, in order). If the cards carry `{{n}}` body variables or URL-button variables, the CLI tells you exactly what to put in a `--cards-file <path>` JSON (one entry per card: `header_media` / `body_params` / `button_payloads` / `url_vars`). Always sends via the python engine (any size); no CSV mode for carousels. |
|
|
@@ -201,8 +213,9 @@ several.
|
|
|
201
213
|
| Store insights daily without emailing anyone (build history first) | `flowiq insights enable <org_id> --daily --store-only` |
|
|
202
214
|
| Send one insights report right now | `flowiq insights run <org_id> --period weekly --recipient you@flowapt.com` (dry run) → add `--commit` |
|
|
203
215
|
| Work a Pin Board task | `flowiq pin list-remote open` → `pull` → edit → `push` |
|
|
204
|
-
| Log hours you worked for a client (
|
|
205
|
-
| Check a client's package hours / the month across all clients | `flowiq hours list <org_id>` / `flowiq hours summary` |
|
|
216
|
+
| Log hours you worked for a client (against the client's own monthly package) | `flowiq hours log <org_id> --hours 1.5 --desc "what you did"` — plain language, the client sees it on their Client Console |
|
|
217
|
+
| Check a client's package hours / the month across all clients | `flowiq hours list <org_id>` / `flowiq hours summary` (a shared package, like Barkyn's three stores, shows the total across all of them) |
|
|
218
|
+
| See what hours package a client is on | `flowiq hours package show <org_id>` (no org = every package). Setting one is Matt or Gidon only: `flowiq hours package set <org_id> --hours 5` |
|
|
206
219
|
| Generate a client's monthly deck (metrics + copy) after the month has frozen | `flowiq report deck generate <org_id> 2026-08` — then review it at /reporting/deck |
|
|
207
220
|
| See whether a client deck can be approved, and what is still missing | `flowiq report deck status <org_id> 2026-08` |
|
|
208
221
|
| Send yourself a test copy of a client deck (PDF from flowiq@flowapt.com) | `flowiq report deck send <org_id> 2026-08 --test-to you@flowapt.com` — status untouched |
|
|
@@ -245,7 +258,9 @@ validates names, URLs, methods and parameter shapes. Unknown keys and unresolved
|
|
|
245
258
|
object with no required fields remains a warning because it can be intentional.
|
|
246
259
|
Nested object/array schemas are strict and supported. For ChatCart retailer tools, use
|
|
247
260
|
`{{retailer_tools_internal_key}}` only as the `x-api-key` auth value on the
|
|
248
|
-
trusted `express.chatcart.io/retailer-tools/*` gateway
|
|
261
|
+
trusted `express.chatcart.io/retailer-tools/*` gateway. A tool that schedules a
|
|
262
|
+
send through `add-to-api-queue` authenticates with `{{flowiq_queue_key}}` as its
|
|
263
|
+
bearer token (the queue refuses the public anon key); it works nowhere else; org/contact identity is
|
|
249
264
|
injected server-side and mutations use `{{operation_idempotency_key}}` for stable,
|
|
250
265
|
operation-scoped idempotency. Batch independent product requests in one
|
|
251
266
|
`search_retailer_products.queries` array (1–12) instead of repeated search calls.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flowapt/flowiq-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.2",
|
|
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": {
|
|
@@ -1791,7 +1791,21 @@ export async function scheduledList(orgId, opts = {}) {
|
|
|
1791
1791
|
});
|
|
1792
1792
|
} catch (e) { console.error(`Scheduled list failed: ${e.message}`); if (e.body?.error) console.error(` ${e.body.error}`); process.exit(1); }
|
|
1793
1793
|
if (opts.json) { console.log(JSON.stringify(resp, null, 2)); return; }
|
|
1794
|
-
|
|
1794
|
+
// An empty list must say what it is HIDING. "No open scheduled broadcasts"
|
|
1795
|
+
// read as "this org has never scheduled anything" on an org with four fired
|
|
1796
|
+
// rows (Johnson Fitness, 22 Sep 2026) and sent that session to raw SQL.
|
|
1797
|
+
const counts = resp.status_counts ?? {};
|
|
1798
|
+
const shownFilter = opts.all ? "all" : (opts.status || "open");
|
|
1799
|
+
const hidden = Object.entries(counts)
|
|
1800
|
+
.filter(([st]) => shownFilter === "all" ? false : (shownFilter === "open" ? !["pending", "request"].includes(st) : st !== shownFilter))
|
|
1801
|
+
.map(([st, n]) => `${n} ${st}`);
|
|
1802
|
+
if (!resp.scheduled?.length) {
|
|
1803
|
+
console.log(`No ${opts.all ? "" : `${shownFilter} `}scheduled broadcasts on ${resp.organization_name}.`);
|
|
1804
|
+
if (hidden.length) {
|
|
1805
|
+
console.log(` ${hidden.join(" · ")} row(s) exist on this org — see them with: flowiq bc scheduled list ${orgId} --all`);
|
|
1806
|
+
}
|
|
1807
|
+
return;
|
|
1808
|
+
}
|
|
1795
1809
|
console.log(`Scheduled broadcasts on ${resp.organization_name}:`);
|
|
1796
1810
|
for (const s of resp.scheduled) {
|
|
1797
1811
|
const flag = s.stalled ? " ⚠ AWAITING APPROVAL — will NOT fire" : "";
|
|
@@ -1806,6 +1820,62 @@ export async function scheduledList(orgId, opts = {}) {
|
|
|
1806
1820
|
}
|
|
1807
1821
|
console.log("");
|
|
1808
1822
|
console.log(` ${resp.count} row(s). Approve: flowiq bc scheduled approve <org> <id> · Cancel: … cancel <org> <id>`);
|
|
1823
|
+
if (hidden.length) console.log(` not shown: ${hidden.join(" · ")} (add --all)`);
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
/** List the org's SAVED SEND DRAFTS (broadcast_send_drafts). Read-only. */
|
|
1827
|
+
export async function draftsList(orgId, opts = {}) {
|
|
1828
|
+
if (!UUID_RE.test(orgId)) { console.error(`Error: "${orgId}" is not a valid organization UUID.`); process.exit(1); }
|
|
1829
|
+
let resp;
|
|
1830
|
+
try {
|
|
1831
|
+
resp = await http.post("broadcast", { action: "drafts-list", organization_id: orgId, limit: opts.limit });
|
|
1832
|
+
} catch (e) { console.error(`Drafts list failed: ${e.message}`); if (e.body?.error) console.error(` ${e.body.error}`); process.exit(1); }
|
|
1833
|
+
if (opts.json) { console.log(JSON.stringify(resp, null, 2)); return; }
|
|
1834
|
+
if (!resp.drafts?.length) { console.log(`No saved send drafts on ${resp.organization_name}.`); return; }
|
|
1835
|
+
console.log(`Saved send drafts on ${resp.organization_name} (newest first):`);
|
|
1836
|
+
for (const d of resp.drafts) {
|
|
1837
|
+
console.log("");
|
|
1838
|
+
console.log(` ${d.id}`);
|
|
1839
|
+
console.log(` ${d.name || "(unnamed)"} · template ${d.template_name ?? "?"}${d.mode ? ` · ${d.mode}` : ""}${d.engine ? ` · ${d.engine}` : ""}`);
|
|
1840
|
+
const aud = [];
|
|
1841
|
+
if (d.tags?.length) aud.push(`tags ${d.tags.join(", ")}`);
|
|
1842
|
+
if (d.segment) aud.push(`segment ${d.segment}`);
|
|
1843
|
+
if (d.recipient_count != null) aud.push(`${d.recipient_count} recipient(s)`);
|
|
1844
|
+
if (!aud.length) aud.push("no audience chosen yet");
|
|
1845
|
+
if (aud.length) console.log(` ${aud.join(" · ")}`);
|
|
1846
|
+
if (d.scheduled_for) console.log(` scheduled for ${fmtSast(d.scheduled_for)} SAST`);
|
|
1847
|
+
console.log(` saved ${fmtSast(d.updated_at)} SAST${d.created_by_name ? ` by ${d.created_by_name}` : ""}`);
|
|
1848
|
+
}
|
|
1849
|
+
console.log("");
|
|
1850
|
+
console.log(` ${resp.count} draft(s). Full config: flowiq bc drafts show ${orgId} <draft_id>`);
|
|
1851
|
+
console.log(" A draft is NOT scheduled and will never fire on its own — it is a saved form in the send dialog.");
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
/** One saved send draft in full. Read-only. */
|
|
1855
|
+
export async function draftsShow(orgId, draftId, opts = {}) {
|
|
1856
|
+
if (!UUID_RE.test(orgId)) { console.error(`Error: "${orgId}" is not a valid organization UUID.`); process.exit(1); }
|
|
1857
|
+
if (!UUID_RE.test(draftId || "")) { console.error(`Error: "${draftId}" is not a valid draft id (get it from: flowiq bc drafts <org>).`); process.exit(1); }
|
|
1858
|
+
let resp;
|
|
1859
|
+
try {
|
|
1860
|
+
resp = await http.post("broadcast", { action: "drafts-show", organization_id: orgId, draft_id: draftId });
|
|
1861
|
+
} catch (e) { console.error(`Draft show failed: ${e.message}`); if (e.body?.error) console.error(` ${e.body.error}`); process.exit(1); }
|
|
1862
|
+
if (opts.json) { console.log(JSON.stringify(resp, null, 2)); return; }
|
|
1863
|
+
const d = resp.draft;
|
|
1864
|
+
console.log(`${d.name || "(unnamed)"} — ${resp.organization_name}`);
|
|
1865
|
+
console.log(` id: ${d.id}`);
|
|
1866
|
+
console.log(` template: ${d.template_name ?? "?"}`);
|
|
1867
|
+
if (d.mode) console.log(` mode: ${d.mode}`);
|
|
1868
|
+
if (d.tags?.length) console.log(` tags: ${d.tags.join(", ")}`);
|
|
1869
|
+
if (d.segment) console.log(` segment: ${d.segment}`);
|
|
1870
|
+
if (d.recipient_count != null) console.log(` audience: ${d.recipient_count} recipient(s)`);
|
|
1871
|
+
if (d.engine) console.log(` engine: ${d.engine}`);
|
|
1872
|
+
if (d.carousel_cards) console.log(` carousel: ${d.carousel_cards} card override(s)`);
|
|
1873
|
+
if (d.scheduled_for) console.log(` scheduled: ${fmtSast(d.scheduled_for)} SAST (in the draft only — nothing is queued)`);
|
|
1874
|
+
if (d.header_media) console.log(` header: ${d.header_media}`);
|
|
1875
|
+
console.log(` saved: ${fmtSast(d.updated_at)} SAST${d.created_by_name ? ` by ${d.created_by_name}` : ""} · created ${fmtSast(d.created_at)} SAST`);
|
|
1876
|
+
console.log("");
|
|
1877
|
+
console.log("Stored config:");
|
|
1878
|
+
console.log(JSON.stringify(d.config ?? {}, null, 2).split("\n").map((l) => ` ${l}`).join("\n"));
|
|
1809
1879
|
}
|
|
1810
1880
|
|
|
1811
1881
|
/** Approve (request → pending) or cancel a scheduled broadcast. */
|
package/src/commands/groups.js
CHANGED
|
@@ -11,6 +11,16 @@
|
|
|
11
11
|
// FLOWMOD_DB_HOST / FLOWMOD_DB_PORT / FLOWMOD_DB_USER / FLOWMOD_DB_PASS / FLOWMOD_DB_NAME
|
|
12
12
|
//
|
|
13
13
|
// Read-only: there is no `push` — group history is a system of record.
|
|
14
|
+
//
|
|
15
|
+
// ⛔ DEPRECATED (Matt, 22 Sep 2026): Evolution is fully deprecated. The flowapt
|
|
16
|
+
// instance logged out of WhatsApp on 29 Aug 2026 (`Instance.connectionStatus`
|
|
17
|
+
// 'close', reason 401) and has stored NOTHING since, so this command answers
|
|
18
|
+
// from a frozen archive while looking current — on 22 Sep it reported the
|
|
19
|
+
// Johnson Fitness group's last message as 17 Aug when the group had traffic
|
|
20
|
+
// that morning. READ A GROUP CHAT WITH THE WHATSAPP MCP:
|
|
21
|
+
// mcp__whatsapp-flowapt__list_chats / list_messages (then whatsapp-flowmod)
|
|
22
|
+
// Both verbs now REFUSE unless --archive is passed, because a stale read that
|
|
23
|
+
// looks complete is worse than an error.
|
|
14
24
|
|
|
15
25
|
import fs from "node:fs/promises";
|
|
16
26
|
import path from "node:path";
|
|
@@ -64,6 +74,32 @@ async function connect() {
|
|
|
64
74
|
return client;
|
|
65
75
|
}
|
|
66
76
|
|
|
77
|
+
// ---------- deprecation guard ----------
|
|
78
|
+
// Evolution is deprecated; the store is frozen. Refuse rather than answer with
|
|
79
|
+
// a stale window that reads as complete.
|
|
80
|
+
const ARCHIVE_FROZEN_NOTE =
|
|
81
|
+
"Evolution stopped syncing on 29 Aug 2026 (the flowapt instance was logged out), so anything after that date is MISSING here.";
|
|
82
|
+
|
|
83
|
+
function refuseUnlessArchive(opts, what) {
|
|
84
|
+
if (opts.archive) {
|
|
85
|
+
console.error("⚠ ARCHIVE READ — Evolution is deprecated and this data is FROZEN.");
|
|
86
|
+
console.error(` ${ARCHIVE_FROZEN_NOTE}`);
|
|
87
|
+
console.error(" Anything recent must come from the WhatsApp MCP (whatsapp-flowapt, then whatsapp-flowmod).");
|
|
88
|
+
console.error("");
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
console.error(`⛔ \`flowiq groups ${what}\` is DEPRECATED — Evolution is no longer in use.`);
|
|
92
|
+
console.error(` ${ARCHIVE_FROZEN_NOTE}`);
|
|
93
|
+
console.error("");
|
|
94
|
+
console.error(" Read a group chat with the WhatsApp MCP instead:");
|
|
95
|
+
console.error(" mcp__whatsapp-flowapt__list_chats { query: \"<group name>\" }");
|
|
96
|
+
console.error(" mcp__whatsapp-flowapt__list_messages { chat_jid: \"…@g.us\", limit: 50 }");
|
|
97
|
+
console.error(" …and mcp__whatsapp-flowmod__* if the group is not on the flowapt line.");
|
|
98
|
+
console.error("");
|
|
99
|
+
console.error(" To read the frozen pre-29-Aug archive anyway, pass --archive.");
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
|
|
67
103
|
// ---------- helpers ----------
|
|
68
104
|
function slugify(name, fallback) {
|
|
69
105
|
const s = String(name || "")
|
|
@@ -132,7 +168,10 @@ function extractText(m) {
|
|
|
132
168
|
}
|
|
133
169
|
|
|
134
170
|
async function resolveInstanceId(client, instanceName) {
|
|
135
|
-
const r = await client.query(
|
|
171
|
+
const r = await client.query(
|
|
172
|
+
'SELECT id, "profileName", "connectionStatus", "updatedAt" FROM "Instance" WHERE name = $1',
|
|
173
|
+
[instanceName]
|
|
174
|
+
);
|
|
136
175
|
if (!r.rows.length) {
|
|
137
176
|
const all = await client.query('SELECT name FROM "Instance" ORDER BY name');
|
|
138
177
|
console.error(
|
|
@@ -145,6 +184,7 @@ async function resolveInstanceId(client, instanceName) {
|
|
|
145
184
|
|
|
146
185
|
// ---------- groups list ----------
|
|
147
186
|
export async function list(opts = {}) {
|
|
187
|
+
refuseUnlessArchive(opts, "list");
|
|
148
188
|
const instanceName = opts.instance || DEFAULT_INSTANCE;
|
|
149
189
|
const client = await connect();
|
|
150
190
|
try {
|
|
@@ -161,7 +201,12 @@ export async function list(opts = {}) {
|
|
|
161
201
|
ORDER BY last_ts DESC NULLS LAST`,
|
|
162
202
|
[inst.id]
|
|
163
203
|
);
|
|
164
|
-
|
|
204
|
+
// The instance's own state explains every stale row below it.
|
|
205
|
+
const connLabel = inst.connectionStatus === "open" ? "connected" : `${inst.connectionStatus} (not syncing)`;
|
|
206
|
+
console.log(
|
|
207
|
+
`Instance "${instanceName}" (${inst.profileName ?? "?"}) — ${r.rows.length} group chats · ${connLabel}` +
|
|
208
|
+
(inst.updatedAt ? ` since ${new Date(inst.updatedAt).toISOString().slice(0, 10)}` : "") + "\n"
|
|
209
|
+
);
|
|
165
210
|
const rows = r.rows.map((x) => ({
|
|
166
211
|
group: x.name || "(no name)",
|
|
167
212
|
msgs: Number(x.msgs || 0),
|
|
@@ -181,6 +226,7 @@ export async function list(opts = {}) {
|
|
|
181
226
|
|
|
182
227
|
// ---------- groups pull ----------
|
|
183
228
|
export async function pull(nameOrJid, opts = {}) {
|
|
229
|
+
refuseUnlessArchive(opts, "pull");
|
|
184
230
|
const instanceName = opts.instance || DEFAULT_INSTANCE;
|
|
185
231
|
const limit = Math.max(1, Math.min(Number(opts.limit) || DEFAULT_LIMIT, 5000));
|
|
186
232
|
|