@flowapt/flowiq-cli 0.9.0 → 0.9.3
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 +108 -43
- package/TEAM-GUIDE.md +14 -5
- package/package.json +1 -1
- package/src/commands/agent-updates.js +116 -3
- 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/index.js +65 -9
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
|
|
|
@@ -1129,29 +1137,32 @@ the prompt rows directly).
|
|
|
1129
1137
|
Common uses: edit a master-group's moderator prompt, or flip its reasoning
|
|
1130
1138
|
effort via `config.reasoningEffort` (`none` / `low` / `medium` / `high`).
|
|
1131
1139
|
|
|
1132
|
-
### Group chats — `flowiq groups
|
|
1140
|
+
### Group chats — ⛔ DEPRECATED (`flowiq groups`, alias `grp`)
|
|
1133
1141
|
|
|
1134
|
-
Read
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
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.
|
|
1138
1148
|
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
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:
|
|
1144
1159
|
|
|
1145
1160
|
```bash
|
|
1146
1161
|
export FLOWMOD_EVO_DB_URL='postgres://user:pass@app.flowmod.ai:5555/flowiq-db'
|
|
1147
1162
|
|
|
1148
|
-
flowiq groups list
|
|
1149
|
-
flowiq groups
|
|
1150
|
-
|
|
1151
|
-
flowiq groups pull "Phytoceutics x Flowapt" --last 7d
|
|
1152
|
-
flowiq groups pull "Zorora x Flowapt" --since 2026-06-24 --until "2026-06-25 12:00"
|
|
1153
|
-
flowiq grp pull 120363408455897717@g.us --last 48h # or pass the group JID
|
|
1154
|
-
# 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
|
|
1155
1166
|
```
|
|
1156
1167
|
|
|
1157
1168
|
- Group is matched by JID, exact name, or a unique case-insensitive substring
|
|
@@ -1179,23 +1190,30 @@ Editable fields: `name`, `description`, `status` (open / in_progress / done),
|
|
|
1179
1190
|
null), `organizations[]`, `media[]`, `messages[]`, `created_by`. Array columns
|
|
1180
1191
|
are full-replace within the row.
|
|
1181
1192
|
|
|
1182
|
-
### 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)
|
|
1183
1194
|
|
|
1184
|
-
The client-hours ledger. Every
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
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.
|
|
1192
1203
|
|
|
1193
1204
|
```bash
|
|
1194
1205
|
flowiq hours log <org_id> --hours 1.5 --desc "Rebuilt the abandoned-cart copy" # fractions fine
|
|
1195
1206
|
flowiq hours log <org_id> --minutes 45 --desc "Fixed template header" --date 2026-08-18
|
|
1196
1207
|
flowiq hours list <org_id> # this month's entries + package standing
|
|
1197
1208
|
flowiq hours list <org_id> --month 2026-07 # a past month
|
|
1198
|
-
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
|
|
1199
1217
|
```
|
|
1200
1218
|
|
|
1201
1219
|
- `log` takes exactly ONE of `--minutes <n>` (whole minutes) or `--hours <h>`
|
|
@@ -1204,10 +1222,25 @@ flowiq hours summary # every org with logged work this
|
|
|
1204
1222
|
- `--by` overrides who did the work (default: your staff login email);
|
|
1205
1223
|
`--date YYYY-MM-DD` backdates an entry (it counts toward THAT month);
|
|
1206
1224
|
`--session` tags the session/task name.
|
|
1207
|
-
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
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.
|
|
1211
1244
|
|
|
1212
1245
|
### Client deck — `flowiq report deck status|build|narrate|generate|inputs|approve|unapprove|send|print|pull` (v0.6.5)
|
|
1213
1246
|
|
|
@@ -1225,7 +1258,7 @@ from flowiq@flowapt.com.
|
|
|
1225
1258
|
flowiq report deck status <org_id> 2026-08 # what exists, workflow status, what blocks approval
|
|
1226
1259
|
flowiq report deck generate <org_id> 2026-08 # build the metrics (if missing) + write the copy
|
|
1227
1260
|
flowiq report deck generate <org_id> 2026-08 --refresh --force # rebuild everything
|
|
1228
|
-
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
|
|
1229
1262
|
flowiq report deck narrate <org_id> 2026-08 # copy only (hand edits are kept)
|
|
1230
1263
|
flowiq report deck inputs <org_id> 2026-08 --file inputs.json # the super-admin input form
|
|
1231
1264
|
flowiq report deck approve <org_id> 2026-08 # refused until every required input is present
|
|
@@ -1673,13 +1706,14 @@ flowiq agent config <organization_id> --use-settings-prompt --model gpt-5.6-luna
|
|
|
1673
1706
|
--rename Zara --tool woo_order_build=true --tool view_cart_tool=true --discount true
|
|
1674
1707
|
flowiq agent config <organization_id> --test-contact-number 27000000001 --test-contact-name "QA Bot"
|
|
1675
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)
|
|
1676
1710
|
flowiq agent config <organization_id> --disable-base-tool get_product_info
|
|
1677
1711
|
flowiq agent config <organization_id> --enable-base-tool get_product_info
|
|
1678
1712
|
```
|
|
1679
1713
|
|
|
1680
1714
|
Settable: `settings.use_settings_prompt`, `settings.model`,
|
|
1681
|
-
`settings.reasoning_effort` (`--reasoning-effort low|medium|high`, `""` clears it —
|
|
1682
|
-
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`,
|
|
1683
1717
|
the tool-flag columns (`woo_order_build`, `woo_tip_field`, `woo_order_note_field`,
|
|
1684
1718
|
`view_cart_tool`, `restock_tool`, `block_tool_status`, `postal_code_tool_status`,
|
|
1685
1719
|
`shopify_products_web_chat`, `ticket_tool_status`, `product_lookup`,
|
|
@@ -1734,18 +1768,30 @@ number already maps to an existing contact, the command prints a `⚠` warning
|
|
|
1734
1768
|
naming it. Pass `--test-contact-number ""` (empty) to clear the override — the
|
|
1735
1769
|
harness then falls back to a synthetic per-agent `test_<agent_id>` contact.
|
|
1736
1770
|
|
|
1737
|
-
### Agent updates — `flowiq agent-updates pull|list|resolve` (alias `au`)
|
|
1771
|
+
### Agent updates — `flowiq agent-updates all|pull|list|resolve` (alias `au`)
|
|
1738
1772
|
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1773
|
+
Client-raised "change the agent" requests (`agent_updates`). **`all`** is the
|
|
1774
|
+
cross-org queue: every org in ONE call (about a second), grouped by org, no
|
|
1775
|
+
UUID needed. **`pull`** is the deep read for one org: each update with the chat
|
|
1776
|
+
context around the triggering message + the linked contact (attachments
|
|
1777
|
+
downloaded locally). Once a fix is verified, resolve the ticket from the same
|
|
1778
|
+
terminal.
|
|
1743
1779
|
|
|
1744
1780
|
```bash
|
|
1781
|
+
# every org at once (v0.9.3)
|
|
1782
|
+
flowiq au all # open = pending + in_progress, grouped by org
|
|
1783
|
+
flowiq au all --status all --since 14d # everything raised in the last 14 days
|
|
1784
|
+
flowiq au all --priority urgent,high --summary # just the per-org counts
|
|
1785
|
+
flowiq au all --search wallet --status all # title OR description contains "wallet"
|
|
1786
|
+
flowiq au all --org <uuid>,<uuid> --context # a few orgs, with the chat window around each trigger
|
|
1787
|
+
flowiq au all --json | jq '.updates[] | {org: .organization_name, title}'
|
|
1788
|
+
# → ./.flowiq/agent-updates/_all.<status>.json (aliases: `au queue`, `au list-remote`)
|
|
1789
|
+
|
|
1790
|
+
# one org in depth
|
|
1745
1791
|
flowiq au pull <organization_id> # pending, with context
|
|
1746
1792
|
flowiq au pull <organization_id> --status all --titles "EFT, bank"
|
|
1747
1793
|
flowiq au pull <organization_id> --before 20 --after 10 --no-files
|
|
1748
|
-
# → ./.flowiq/agent-updates/<slug>.json + files/<update-id>/<attachment>
|
|
1794
|
+
# → ./.flowiq/agent-updates/<slug>.json (pending) or <slug>.<status>.json + files/<update-id>/<attachment>
|
|
1749
1795
|
|
|
1750
1796
|
flowiq au resolve <update_id> --note "We updated the agent so it no longer …"
|
|
1751
1797
|
flowiq au resolve <update_id> --status declined --internal "duplicate of …"
|
|
@@ -1757,6 +1803,20 @@ flowiq au resolve <update_id> --status declined --internal "duplicate of …"
|
|
|
1757
1803
|
the LAST step, after a live test proves the fix. Don't resolve on hope.
|
|
1758
1804
|
- An interactive confirm shows exactly what the client will read; `--yes`
|
|
1759
1805
|
skips the confirm (but never the `--note` requirement).
|
|
1806
|
+
- `all` flags: `--status` (`open` default, or any of `pending,in_progress,
|
|
1807
|
+
resolved,declined`, or `all`), `--since` / `--until` (`14d`, `12h`, `2w` or
|
|
1808
|
+
`YYYY-MM-DD`, SAST day start), `--priority`, `--org` (repeatable),
|
|
1809
|
+
`--titles`, `--search`, `--limit` (default 1000, max 5000; a ⚠ TRUNCATED
|
|
1810
|
+
line says when more matched), `--context` (+ `--before/--after/--recent`),
|
|
1811
|
+
`--files`, `--summary`, `--no-desc`, `--out`, `--json`. No context window and
|
|
1812
|
+
no attachment download unless asked, which is what keeps it fast.
|
|
1813
|
+
- Every update (in `all` and `pull`) names the **agent that answers that
|
|
1814
|
+
conversation**: the contact's binding, else the agent on the update, else the
|
|
1815
|
+
org's active agent, with a ⚠ when it is not the active one, so a fix goes to
|
|
1816
|
+
the right agent.
|
|
1817
|
+
- `pull` builds the context windows in parallel (v0.9.3), so `--status all` no
|
|
1818
|
+
longer times out on orgs with long ticket histories, and `--no-files` really
|
|
1819
|
+
skips downloads.
|
|
1760
1820
|
|
|
1761
1821
|
### Broadcast planning — `flowiq plans list|show|status` (alias `planning`) (v0.7.3)
|
|
1762
1822
|
|
|
@@ -1767,6 +1827,8 @@ and Flowapt reviews), from the terminal.
|
|
|
1767
1827
|
flowiq plans list # open plans across every ACTIVE client
|
|
1768
1828
|
flowiq plans list --status pending_review # waiting for Flowapt review
|
|
1769
1829
|
flowiq plans list <organization_id> --status all --since 2026-09-01
|
|
1830
|
+
flowiq plans list --created-since 2026-09-22 # what clients submitted TODAY (v0.9.1)
|
|
1831
|
+
flowiq plans list --touched-since 2026-09-20 # anything edited or reviewed since
|
|
1770
1832
|
flowiq plans show <plan_id> # copy, second message, buttons, creative, audience, notes
|
|
1771
1833
|
flowiq plans show <plan_id> --json --out plan.json
|
|
1772
1834
|
flowiq plans status <plan_id> --to approved # dry run
|
|
@@ -1779,6 +1841,9 @@ flowiq plans status <plan_id> --to sent --commit
|
|
|
1779
1841
|
`--status all` / a status / a comma list to widen or narrow, `--since` to
|
|
1780
1842
|
filter by send date, `--include-inactive` for inactive orgs. The header
|
|
1781
1843
|
shows the totals for every status, and each row prints the full plan id.
|
|
1844
|
+
**`--created-since` / `--touched-since` (v0.9.1)** filter the ROW's own
|
|
1845
|
+
timestamps instead of the send date (SAST day start) — "what did a client
|
|
1846
|
+
submit today", "what has moved since Friday", which `--since` cannot answer.
|
|
1782
1847
|
- **`show`** prints the plan the way the dialog does: message copy, the
|
|
1783
1848
|
second message sent when a button is tapped, buttons with their URLs, the
|
|
1784
1849
|
creative (file name + link), audience, template link, the response to the
|
|
@@ -1840,7 +1905,7 @@ version you have installed.
|
|
|
1840
1905
|
| `FLOWIQ_API_URL` | `https://api.flowiq.live` | Override the API host (local dev, staging). |
|
|
1841
1906
|
| `FLOWIQ_TOKEN` | (saved in `~/.config/flowiq/auth.json`) | Override the auth token, useful for CI. |
|
|
1842
1907
|
| `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). |
|
|
1843
|
-
| `FLOWMOD_EVO_DB_URL` | (none) | Full `postgres://` URL for `groups` (Evolution DB).
|
|
1908
|
+
| `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. |
|
|
1844
1909
|
| `FLOWMOD_DB_HOST/PORT/USER/PASS/NAME` | (none) | Alternative to `FLOWMOD_EVO_DB_URL` — assembled into a connection string. |
|
|
1845
1910
|
|
|
1846
1911
|
## Upgrading
|
package/TEAM-GUIDE.md
CHANGED
|
@@ -96,6 +96,7 @@ When you see it, run `npm i -g @flowapt/flowiq-cli` — a stale version also mea
|
|
|
96
96
|
| **Find an org's ID** (needed by nearly every command) | `flowiq org list` — or `flowiq org list african` to filter |
|
|
97
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 |
|
|
98
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 |
|
|
99
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 |
|
|
100
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 |
|
|
101
102
|
| Has this order shipped? What's the tracking? | `flowiq shopify order <org_id> '#14728'` — status, courier, tracking number + link |
|
|
@@ -109,6 +110,7 @@ When you see it, run `npm i -g @flowapt/flowiq-cli` — a stale version also mea
|
|
|
109
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 |
|
|
110
111
|
| Change agent model / tool flags | `flowiq agent config <org_id> --model … --tool view_cart_tool=true` |
|
|
111
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` |
|
|
112
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) |
|
|
113
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 |
|
|
114
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) |
|
|
@@ -170,7 +172,10 @@ several.
|
|
|
170
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) |
|
|
171
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) |
|
|
172
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. |
|
|
173
|
-
| 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`) |
|
|
174
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). |
|
|
175
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. |
|
|
176
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. |
|
|
@@ -196,7 +201,8 @@ several.
|
|
|
196
201
|
| Manage Shopify/Woo platform webhooks | `flowiq wh pull <org_id>` → edit → `flowiq wh push <slug>` |
|
|
197
202
|
| Manage outbound messaging webhooks (incl. their auth) | `flowiq mw pull <org_id>` → `flowiq mw push <slug> --dry-run` → push |
|
|
198
203
|
| Create a brand-new client org | `flowiq org create --name "Client Name"` → then `agent create` on the printed id |
|
|
199
|
-
| See
|
|
204
|
+
| See EVERY client's open change requests at once | `flowiq au all` (grouped by org; `--since 14d`, `--priority urgent,high`, `--search <text>`, `--status all`, `--summary`, `--json`) |
|
|
205
|
+
| See one client's pending change requests, with the chat around each | `flowiq au pull <org_id>` |
|
|
200
206
|
| Close a client's change request (after verifying the fix!) | `flowiq au resolve <update_id> --note "what changed"` — the client reads the note |
|
|
201
207
|
| Check an org's platform + active agent | `flowiq org info <org_id>` |
|
|
202
208
|
| **See every Settings → Profile switch on an org** | `flowiq org flags show <org_id>` (credentials are never shown) |
|
|
@@ -208,8 +214,9 @@ several.
|
|
|
208
214
|
| Store insights daily without emailing anyone (build history first) | `flowiq insights enable <org_id> --daily --store-only` |
|
|
209
215
|
| Send one insights report right now | `flowiq insights run <org_id> --period weekly --recipient you@flowapt.com` (dry run) → add `--commit` |
|
|
210
216
|
| Work a Pin Board task | `flowiq pin list-remote open` → `pull` → edit → `push` |
|
|
211
|
-
| Log hours you worked for a client (
|
|
212
|
-
| Check a client's package hours / the month across all clients | `flowiq hours list <org_id>` / `flowiq hours summary` |
|
|
217
|
+
| 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 |
|
|
218
|
+
| 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) |
|
|
219
|
+
| 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` |
|
|
213
220
|
| 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 |
|
|
214
221
|
| See whether a client deck can be approved, and what is still missing | `flowiq report deck status <org_id> 2026-08` |
|
|
215
222
|
| 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 |
|
|
@@ -252,7 +259,9 @@ validates names, URLs, methods and parameter shapes. Unknown keys and unresolved
|
|
|
252
259
|
object with no required fields remains a warning because it can be intentional.
|
|
253
260
|
Nested object/array schemas are strict and supported. For ChatCart retailer tools, use
|
|
254
261
|
`{{retailer_tools_internal_key}}` only as the `x-api-key` auth value on the
|
|
255
|
-
trusted `express.chatcart.io/retailer-tools/*` gateway
|
|
262
|
+
trusted `express.chatcart.io/retailer-tools/*` gateway. A tool that schedules a
|
|
263
|
+
send through `add-to-api-queue` authenticates with `{{flowiq_queue_key}}` as its
|
|
264
|
+
bearer token (the queue refuses the public anon key); it works nowhere else; org/contact identity is
|
|
256
265
|
injected server-side and mutations use `{{operation_idempotency_key}}` for stable,
|
|
257
266
|
operation-scoped idempotency. Batch independent product requests in one
|
|
258
267
|
`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.9.
|
|
3
|
+
"version": "0.9.3",
|
|
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": {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
// `flowiq agent-updates pull <org_id> [flags]` / `list`.
|
|
1
|
+
// `flowiq agent-updates pull <org_id> [flags]` / `all` / `list` / `resolve`.
|
|
2
2
|
// Read-only: fetches an org's agent_updates + chat context via /cli/agent-updates,
|
|
3
3
|
// then downloads any attachments client-side into ./.flowiq/agent-updates/files/.
|
|
4
|
+
// `all` (v0.9.3) is the cross-org queue: one server query over every org.
|
|
4
5
|
|
|
5
6
|
import fs from "node:fs/promises";
|
|
6
7
|
import path from "node:path";
|
|
@@ -81,7 +82,9 @@ export async function pull(orgId, opts = {}) {
|
|
|
81
82
|
process.exit(1);
|
|
82
83
|
}
|
|
83
84
|
|
|
84
|
-
|
|
85
|
+
// Commander turns --no-files into `files: false` (the old `noFiles` read was
|
|
86
|
+
// never set, so attachments downloaded regardless — gap row 14 Sep 2026).
|
|
87
|
+
const downloadFiles = opts.files !== false && !opts.noFiles;
|
|
85
88
|
const maxMb = opts.maxMb != null ? Number.parseFloat(opts.maxMb) : DEFAULT_MAX_MB;
|
|
86
89
|
if (!Number.isFinite(maxMb) || maxMb <= 0) {
|
|
87
90
|
console.error(`Invalid --max-mb: ${opts.maxMb}`);
|
|
@@ -116,7 +119,10 @@ export async function pull(orgId, opts = {}) {
|
|
|
116
119
|
|
|
117
120
|
await fs.mkdir(AU_DIR, { recursive: true });
|
|
118
121
|
const slug = slugify(resp.organization_name, resp.organization_id);
|
|
119
|
-
|
|
122
|
+
// A non-default status gets its own file, so pulling in_progress no longer
|
|
123
|
+
// overwrites the pending snapshot (gap rows 18 + 21 Sep 2026).
|
|
124
|
+
const statusKey = String(resp.status_filter || "pending");
|
|
125
|
+
const filePath = path.join(AU_DIR, statusKey === "pending" ? `${slug}.json` : `${slug}.${slugify(statusKey, "status")}.json`);
|
|
120
126
|
const overwriting = await fileExists(filePath);
|
|
121
127
|
await fs.writeFile(filePath, JSON.stringify(resp, null, 2) + "\n", "utf8");
|
|
122
128
|
|
|
@@ -138,6 +144,7 @@ export async function pull(orgId, opts = {}) {
|
|
|
138
144
|
console.log(` priority=${u.priority} status=${u.status} raised=${date}${u.created_by_name ? ` by ${u.created_by_name}` : ""}`);
|
|
139
145
|
console.log(` desc: ${oneLine(u.description, 140)}`);
|
|
140
146
|
if (u.contact) console.log(` contact: ${u.contact.full_name ?? "(no name)"} (${u.contact.whatsapp_id ?? u.contact.id})`);
|
|
147
|
+
if (u.agent) console.log(` agent: ${agentLine(u.agent)}`);
|
|
141
148
|
if (u.attachments && u.attachments.length) {
|
|
142
149
|
console.log(` attachments: ${u.attachments.length} (${u.attachments.map((a) => a.filename || a.type || "file").join(", ")})`);
|
|
143
150
|
for (const a of u.attachments) {
|
|
@@ -158,6 +165,112 @@ export async function pull(orgId, opts = {}) {
|
|
|
158
165
|
console.log(`${path.join(AU_DIR, FILES_DIRNAME)}/<update-id>/ — Read those files for visual context, then act.`);
|
|
159
166
|
}
|
|
160
167
|
|
|
168
|
+
function agentLine(a) {
|
|
169
|
+
const src = { update: "set on the update", contact_binding: "contact is bound to it", org_active: "org's active agent" }[a.source] || a.source;
|
|
170
|
+
const warn = a.is_active ? "" : ` ⚠ NOT the active agent (${a.active_agent_name ?? a.active_agent_id ?? "none"}) — fix THIS agent`;
|
|
171
|
+
return `${a.name ?? a.id} (${src})${warn}`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function sastStamp(iso) {
|
|
175
|
+
if (!iso) return "?";
|
|
176
|
+
const d = new Date(new Date(iso).getTime() + 2 * 3600e3);
|
|
177
|
+
return d.toISOString().slice(0, 16).replace("T", " ");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const PRIORITY_MARK = { urgent: "‼", high: "!", medium: "·", low: " " };
|
|
181
|
+
|
|
182
|
+
// --- all (cross-org queue) --------------------------------------------------
|
|
183
|
+
// One server query across every org: no per-org UUIDs, no context window and
|
|
184
|
+
// no attachment downloads unless asked, so the whole open queue is one call.
|
|
185
|
+
export async function all(opts = {}) {
|
|
186
|
+
const query = { scope: "all", status: opts.status || "open" };
|
|
187
|
+
if (opts.since) query.since = opts.since;
|
|
188
|
+
if (opts.until) query.until = opts.until;
|
|
189
|
+
if (opts.priority) query.priority = opts.priority;
|
|
190
|
+
if (opts.org && opts.org.length) query.org = [].concat(opts.org).join(",");
|
|
191
|
+
if (opts.titles) query.titles = opts.titles;
|
|
192
|
+
if (opts.search) query.search = opts.search;
|
|
193
|
+
if (opts.limit) query.limit = String(opts.limit);
|
|
194
|
+
if (opts.context) {
|
|
195
|
+
query.context = "1";
|
|
196
|
+
if (opts.before != null) query.before = String(opts.before);
|
|
197
|
+
if (opts.after != null) query.after = String(opts.after);
|
|
198
|
+
if (opts.recent != null) query.recent = String(opts.recent);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
let resp;
|
|
202
|
+
try {
|
|
203
|
+
resp = await http.get("agent-updates", query);
|
|
204
|
+
} catch (e) {
|
|
205
|
+
console.error(`Pull failed: ${e.message}`);
|
|
206
|
+
process.exit(1);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (opts.files) {
|
|
210
|
+
const maxMb = opts.maxMb != null ? Number.parseFloat(opts.maxMb) : DEFAULT_MAX_MB;
|
|
211
|
+
const maxBytes = Math.round(maxMb * 1024 * 1024);
|
|
212
|
+
for (const u of resp.updates || []) {
|
|
213
|
+
if (!(u.attachments || []).length && !u.superadmin_response_image) continue;
|
|
214
|
+
try { await downloadUpdateFiles(u, maxBytes); } catch (e) { u._file_download_error = e.message; }
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (opts.json) {
|
|
219
|
+
process.stdout.write(JSON.stringify(resp, null, 2) + "\n");
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
await fs.mkdir(AU_DIR, { recursive: true });
|
|
224
|
+
const statusKey = Array.isArray(resp.filters?.status) ? resp.filters.status.join("-") : String(resp.filters?.status ?? "all");
|
|
225
|
+
const filePath = opts.out ? path.resolve(opts.out) : path.join(AU_DIR, `_all.${slugify(statusKey, "all")}.json`);
|
|
226
|
+
await fs.writeFile(filePath, JSON.stringify(resp, null, 2) + "\n", "utf8");
|
|
227
|
+
|
|
228
|
+
const updates = resp.updates || [];
|
|
229
|
+
const f = resp.filters || {};
|
|
230
|
+
console.log(`Wrote ${filePath} (${resp.elapsed_ms ?? "?"} ms server)`);
|
|
231
|
+
console.log(` status: ${Array.isArray(f.status) ? f.status.join(", ") : f.status}` +
|
|
232
|
+
`${f.since ? ` · since ${sastStamp(f.since)} SAST` : ""}${f.until ? ` · until ${sastStamp(f.until)}` : ""}` +
|
|
233
|
+
`${f.priority ? ` · priority ${f.priority.join(",")}` : ""}${f.search ? ` · search "${f.search}"` : ""}` +
|
|
234
|
+
`${f.titles ? ` · titles ${f.titles.join("|")}` : ""}${f.org ? ` · ${f.org.length} org(s)` : ""}`);
|
|
235
|
+
console.log(` updates: ${resp.returned} across ${(resp.by_org || []).length} org(s)` +
|
|
236
|
+
(resp.truncated ? ` ⚠ TRUNCATED: ${resp.total_matched} matched, raise --limit` : ""));
|
|
237
|
+
if (!updates.length) { console.log(" (nothing matches)"); return; }
|
|
238
|
+
|
|
239
|
+
console.log("");
|
|
240
|
+
console.log(" By org (count · pending · in progress · urgent/high · oldest):");
|
|
241
|
+
for (const b of resp.by_org || []) {
|
|
242
|
+
const age = Math.floor((Date.now() - new Date(b.oldest_created_at).getTime()) / 86400e3);
|
|
243
|
+
console.log(` ${String(b.count).padStart(4)} ${String(b.pending).padStart(3)} ${String(b.in_progress).padStart(3)} ${String(b.urgent_or_high).padStart(3)} ${String(age + "d").padStart(5)} ${b.organization_name ?? b.organization_id}`);
|
|
244
|
+
}
|
|
245
|
+
if (opts.summary) return;
|
|
246
|
+
|
|
247
|
+
const byOrg = new Map();
|
|
248
|
+
for (const u of updates) {
|
|
249
|
+
if (!byOrg.has(u.organization_id)) byOrg.set(u.organization_id, []);
|
|
250
|
+
byOrg.get(u.organization_id).push(u);
|
|
251
|
+
}
|
|
252
|
+
for (const b of resp.by_org || []) {
|
|
253
|
+
const list = byOrg.get(b.organization_id) || [];
|
|
254
|
+
console.log("");
|
|
255
|
+
console.log(` ── ${b.organization_name ?? b.organization_id} (${b.organization_id})`);
|
|
256
|
+
for (const u of list) {
|
|
257
|
+
const mark = PRIORITY_MARK[u.priority] ?? "?";
|
|
258
|
+
const st = u.status === "pending" ? "" : ` [${u.status}]`;
|
|
259
|
+
console.log(` ${mark} ${u.id.slice(0, 8)} ${String(u.age_days + "d").padStart(4)} ${oneLine(u.title, 70)}${st}`);
|
|
260
|
+
const bits = [];
|
|
261
|
+
if (u.created_by_name) bits.push(`by ${u.created_by_name}`);
|
|
262
|
+
if (u.contact) bits.push(`contact ${u.contact.full_name ?? u.contact.whatsapp_id ?? u.contact.id}`);
|
|
263
|
+
if ((u.attachments || []).length) bits.push(`${u.attachments.length} attachment(s)`);
|
|
264
|
+
if (u.agent && !u.agent.is_active) bits.push(`⚠ agent ${u.agent.name ?? u.agent.id} is NOT active`);
|
|
265
|
+
if (bits.length) console.log(` ${bits.join(" · ")}`);
|
|
266
|
+
if (opts.desc !== false && u.description) console.log(` ${oneLine(u.description, 150)}`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
console.log("");
|
|
270
|
+
console.log("Full text, contacts, agents" + (opts.context ? ", chat context" : "") + " are in the JSON. Drill into one org with");
|
|
271
|
+
console.log("`flowiq au pull <org_id>` (chat context + attachments), then `flowiq au resolve <id> --note …`.");
|
|
272
|
+
}
|
|
273
|
+
|
|
161
274
|
export async function list() {
|
|
162
275
|
let files;
|
|
163
276
|
try {
|
|
@@ -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
|
|
package/src/commands/hours.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
// `flowiq hours log <org> --minutes N|--hours H --desc "…"` / `list <org>` /
|
|
2
|
-
// `summary`. The client-hours ledger: every
|
|
3
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// `summary` / `package show|set|clear`. The client-hours ledger: every piece
|
|
3
|
+
// of client work gets logged against the org's monthly hours PACKAGE. Since
|
|
4
|
+
// v0.9.2 each org has its own package (or shares one with sibling orgs, e.g.
|
|
5
|
+
// Barkyn PT/ES/IT), set with `hours package set` by Matt or Gidon only; an org
|
|
6
|
+
// with no package reads "no package on file", never an implied 10h.
|
|
5
7
|
// All writes happen server-side (/cli/hours) with the staff token.
|
|
6
8
|
|
|
7
9
|
import { http } from "../http.js";
|
|
@@ -14,6 +16,24 @@ function fmtHours(minutes) {
|
|
|
14
16
|
return `${Number.isInteger(h) ? h : h.toFixed(1)}h`;
|
|
15
17
|
}
|
|
16
18
|
|
|
19
|
+
/** "5h package" / "10h package shared with Barkyn - Spain, Barkyn - Italy" / "no package on file". */
|
|
20
|
+
function describePackage(pkg, orgName) {
|
|
21
|
+
if (!pkg) return "no package on file (set one with `flowiq hours package set`)";
|
|
22
|
+
const size = `${fmtHours(pkg.allowance_minutes)}/month`;
|
|
23
|
+
if (!pkg.shared) return `${size} package`;
|
|
24
|
+
const others = (pkg.organizations || []).filter((n) => n !== orgName);
|
|
25
|
+
return `${size} package "${pkg.name}", shared with ${others.join(", ")}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** "3h of 5h used (2h left)" / "⚠ 1.5h OVER" against a package; "" with none. */
|
|
29
|
+
function standingLine(pkg) {
|
|
30
|
+
if (!pkg) return "";
|
|
31
|
+
const used = pkg.used_minutes;
|
|
32
|
+
const allowance = pkg.allowance_minutes;
|
|
33
|
+
const tail = used > allowance ? ` ⚠ ${fmtHours(used - allowance)} OVER` : ` (${fmtHours(allowance - used)} left)`;
|
|
34
|
+
return `${fmtHours(used)} of ${fmtHours(allowance)} used${pkg.shared ? " across the shared package" : ""}${tail}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
17
37
|
function requireOrg(orgId) {
|
|
18
38
|
if (!UUID_RE.test(orgId || "")) {
|
|
19
39
|
console.error(`Error: "${orgId}" is not a valid organization UUID (find it with \`flowiq org list <search>\`).`);
|
|
@@ -88,11 +108,11 @@ export async function log(orgId, opts) {
|
|
|
88
108
|
process.exit(1);
|
|
89
109
|
}
|
|
90
110
|
|
|
91
|
-
const used = resp.month_total_minutes;
|
|
92
|
-
const allowance = resp.allowance_hours * 60;
|
|
93
111
|
console.log(`Logged ${fmtHours(minutes)} for ${resp.organization.name}`);
|
|
94
|
-
console.log(` entry:
|
|
95
|
-
console.log(` month:
|
|
112
|
+
console.log(` entry: ${resp.id}`);
|
|
113
|
+
console.log(` month: ${resp.month}: ${fmtHours(resp.month_total_minutes)} logged for this org`);
|
|
114
|
+
console.log(` package: ${describePackage(resp.package, resp.organization.name)}`);
|
|
115
|
+
if (resp.package) console.log(` ${standingLine(resp.package)}`);
|
|
96
116
|
}
|
|
97
117
|
|
|
98
118
|
export async function list(orgId, opts) {
|
|
@@ -109,9 +129,10 @@ export async function list(orgId, opts) {
|
|
|
109
129
|
console.log(JSON.stringify(resp, null, 2));
|
|
110
130
|
return;
|
|
111
131
|
}
|
|
112
|
-
const over = resp.remaining_minutes < 0;
|
|
113
132
|
console.log(`${resp.organization.name} — ${resp.month}`);
|
|
114
|
-
console.log(`
|
|
133
|
+
console.log(` logged: ${fmtHours(resp.total_minutes)} for this org`);
|
|
134
|
+
console.log(` package: ${describePackage(resp.package, resp.organization.name)}`);
|
|
135
|
+
if (resp.package) console.log(` ${standingLine(resp.package)}`);
|
|
115
136
|
if (!resp.entries.length) {
|
|
116
137
|
console.log(" (no entries this month)");
|
|
117
138
|
return;
|
|
@@ -138,13 +159,107 @@ export async function summary(opts) {
|
|
|
138
159
|
return;
|
|
139
160
|
}
|
|
140
161
|
if (!resp.organizations.length) {
|
|
141
|
-
console.log(`(no hours logged
|
|
162
|
+
console.log(`(no hours logged and no packages set for ${resp.month})`);
|
|
142
163
|
return;
|
|
143
164
|
}
|
|
144
|
-
|
|
145
|
-
console.log(`Client hours — ${resp.month} (package: ${resp.allowance_hours}h/month per org)\n`);
|
|
165
|
+
console.log(`Client hours — ${resp.month}\n`);
|
|
146
166
|
for (const o of resp.organizations) {
|
|
147
|
-
const
|
|
148
|
-
|
|
167
|
+
const p = o.package;
|
|
168
|
+
let pkgCol;
|
|
169
|
+
if (!p) pkgCol = "no package on file";
|
|
170
|
+
else if (p.shared) {
|
|
171
|
+
const flag = p.used_minutes > p.allowance_minutes ? ` ⚠ ${fmtHours(p.used_minutes - p.allowance_minutes)} OVER` : "";
|
|
172
|
+
pkgCol = `shared "${p.name}": ${fmtHours(p.used_minutes)} / ${fmtHours(p.allowance_minutes)}${flag}`;
|
|
173
|
+
} else {
|
|
174
|
+
const flag = p.used_minutes > p.allowance_minutes ? ` ⚠ ${fmtHours(p.used_minutes - p.allowance_minutes)} OVER` : "";
|
|
175
|
+
pkgCol = `of ${fmtHours(p.allowance_minutes)}${flag}`;
|
|
176
|
+
}
|
|
177
|
+
console.log(` ${fmtHours(o.total_minutes).padStart(6)} (${String(o.entries).padStart(3)} entr${o.entries === 1 ? "y " : "ies"}) ${(o.name || o.id).padEnd(28)} ${pkgCol}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ── packages ───────────────────────────────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
function printPackage(p) {
|
|
184
|
+
const who = p.updated_by ? ` · set by ${p.updated_by} ${String(p.updated_at).slice(0, 10)}` : "";
|
|
185
|
+
console.log(` ${p.name} · ${fmtHours(p.monthly_minutes)}/month${p.organizations.length > 1 ? " · SHARED" : ""}${who}`);
|
|
186
|
+
for (const o of p.organizations) console.log(` ${o.name} ${o.id}`);
|
|
187
|
+
if (p.notes) console.log(` notes: ${p.notes}`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export async function packageShow(orgId, opts) {
|
|
191
|
+
if (orgId) requireOrg(orgId);
|
|
192
|
+
let resp;
|
|
193
|
+
try {
|
|
194
|
+
resp = await http.get("hours", { packages: "1" });
|
|
195
|
+
} catch (e) {
|
|
196
|
+
console.error(`Package lookup failed: ${e.message}`);
|
|
197
|
+
process.exit(1);
|
|
198
|
+
}
|
|
199
|
+
let packages = resp.packages;
|
|
200
|
+
if (orgId) packages = packages.filter((p) => p.organizations.some((o) => o.id === orgId));
|
|
201
|
+
if (opts.json) {
|
|
202
|
+
console.log(JSON.stringify(orgId ? { packages } : resp, null, 2));
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (!packages.length) {
|
|
206
|
+
console.log(orgId ? "No package on file for this org." : "No packages set yet.");
|
|
207
|
+
} else {
|
|
208
|
+
console.log(`Hours packages (${packages.length})\n`);
|
|
209
|
+
for (const p of packages) printPackage(p);
|
|
210
|
+
}
|
|
211
|
+
console.log(`\nChanges: ${resp.editors.join(", ")} only${resp.can_edit ? " (you can)" : " (not you)"}.`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export async function packageSet(orgIds, opts) {
|
|
215
|
+
for (const id of orgIds) requireOrg(id);
|
|
216
|
+
const hours = Number(opts.hours);
|
|
217
|
+
if (!Number.isFinite(hours) || hours < 0 || hours > 1000) {
|
|
218
|
+
console.error(`Error: --hours must be a number between 0 and 1000 (got "${opts.hours}").`);
|
|
219
|
+
process.exit(1);
|
|
220
|
+
}
|
|
221
|
+
let resp;
|
|
222
|
+
try {
|
|
223
|
+
resp = await http.post("hours", {
|
|
224
|
+
action: "package_set",
|
|
225
|
+
organization_ids: orgIds,
|
|
226
|
+
hours,
|
|
227
|
+
name: opts.name || undefined,
|
|
228
|
+
notes: opts.notes || undefined,
|
|
229
|
+
dry_run: !!opts.dryRun,
|
|
230
|
+
});
|
|
231
|
+
} catch (e) {
|
|
232
|
+
console.error(`Package change failed: ${e.message}`);
|
|
233
|
+
process.exit(1);
|
|
234
|
+
}
|
|
235
|
+
const plan = resp.plan;
|
|
236
|
+
const who = plan.organizations.map((o) => o.name).join(", ");
|
|
237
|
+
console.log(`${resp.dry_run ? "DRY RUN — nothing written.\n" : ""}${plan.mode === "update" ? "Update" : "New"} package "${plan.name}": ${fmtHours(plan.monthly_minutes)}/month for ${who}`);
|
|
238
|
+
for (const b of plan.before) {
|
|
239
|
+
console.log(` before: "${b.name}" ${fmtHours(b.monthly_minutes)}/month (${b.organizations.map((o) => o.name).join(", ")})`);
|
|
240
|
+
}
|
|
241
|
+
if (!plan.before.length) console.log(" before: no package on file");
|
|
242
|
+
for (const l of plan.left_behind) {
|
|
243
|
+
console.log(` ⚠ ${l.remaining.join(", ")} stay${l.remaining.length === 1 ? "s" : ""} on "${l.package}" without ${plan.organizations.length === 1 ? "this org" : "these orgs"}`);
|
|
244
|
+
}
|
|
245
|
+
if (resp.dry_run) console.log("\nRun again without --dry-run to apply.");
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export async function packageClear(orgId, opts) {
|
|
249
|
+
requireOrg(orgId);
|
|
250
|
+
let resp;
|
|
251
|
+
try {
|
|
252
|
+
resp = await http.post("hours", { action: "package_clear", organization_id: orgId, dry_run: !!opts.dryRun });
|
|
253
|
+
} catch (e) {
|
|
254
|
+
console.error(`Package change failed: ${e.message}`);
|
|
255
|
+
process.exit(1);
|
|
256
|
+
}
|
|
257
|
+
if (resp.changed === false) {
|
|
258
|
+
console.log(`${resp.organization.name}: no package on file, nothing to clear.`);
|
|
259
|
+
return;
|
|
149
260
|
}
|
|
261
|
+
const plan = resp.plan;
|
|
262
|
+
console.log(`${resp.dry_run ? "DRY RUN — nothing written.\n" : ""}${plan.organization.name} ${resp.dry_run ? "would leave" : "left"} package "${plan.package.name}" (${fmtHours(plan.package.monthly_minutes)}/month)`);
|
|
263
|
+
if (plan.package_removed) console.log(" the package had no other orgs, so it is removed");
|
|
264
|
+
else console.log(` still on it: ${plan.remaining.join(", ")}`);
|
|
150
265
|
}
|
package/src/commands/plans.js
CHANGED
|
@@ -65,6 +65,8 @@ export async function list(orgId, opts = {}) {
|
|
|
65
65
|
if (orgId) query.organization_id = orgId;
|
|
66
66
|
if (opts.status) query.status = opts.status;
|
|
67
67
|
if (opts.since) query.since = opts.since;
|
|
68
|
+
if (opts.createdSince) query.created_since = opts.createdSince;
|
|
69
|
+
if (opts.touchedSince) query.touched_since = opts.touchedSince;
|
|
68
70
|
if (opts.includeInactive) query.include_inactive = "1";
|
|
69
71
|
if (opts.limit != null) query.limit = String(opts.limit);
|
|
70
72
|
|
|
@@ -83,7 +85,12 @@ export async function list(orgId, opts = {}) {
|
|
|
83
85
|
? resp.organization_name
|
|
84
86
|
: `all ${resp.include_inactive ? "" : "active "}orgs`;
|
|
85
87
|
const totals = STATUS_ORDER.filter((s) => resp.counts?.[s]).map((s) => `${s} ${resp.counts[s]}`).join(" · ");
|
|
86
|
-
|
|
88
|
+
const windowBits = [
|
|
89
|
+
resp.since ? `send date from ${resp.since}` : null,
|
|
90
|
+
resp.created_since ? `created from ${resp.created_since}` : null,
|
|
91
|
+
resp.touched_since ? `touched from ${resp.touched_since}` : null,
|
|
92
|
+
].filter(Boolean);
|
|
93
|
+
console.log(`Broadcast plans · ${scope} · status ${resp.status_filter}${windowBits.length ? ` · ${windowBits.join(" · ")}` : ""}`);
|
|
87
94
|
console.log(` every status in this scope: ${totals || "none"}`);
|
|
88
95
|
|
|
89
96
|
const plans = resp.plans || [];
|
package/src/index.js
CHANGED
|
@@ -223,13 +223,15 @@ export function run(argv) {
|
|
|
223
223
|
// groups (read-only: pull WhatsApp GROUP chat history from the FlowMod Evolution DB)
|
|
224
224
|
const groups = program.command("groups")
|
|
225
225
|
.alias("grp")
|
|
226
|
-
.description("
|
|
226
|
+
.description("DEPRECATED — Evolution is retired and frozen (nothing since 29 Aug 2026). Read group chats with the whatsapp-flowapt / whatsapp-flowmod MCP instead.");
|
|
227
227
|
groups.command("list")
|
|
228
|
-
.description("List
|
|
228
|
+
.description("[archive only] List the frozen Evolution instance's group chats — needs --archive")
|
|
229
229
|
.option("--instance <name>", "Evolution instance name", "flowapt")
|
|
230
|
+
.option("--archive", "read the FROZEN pre-29-Aug-2026 archive anyway")
|
|
230
231
|
.action((opts) => groupsCmd.list(opts));
|
|
231
232
|
groups.command("pull <name-or-jid>")
|
|
232
|
-
.description("Pull a group's
|
|
233
|
+
.description("[archive only] Pull a group's FROZEN message history into a local JSON file — needs --archive")
|
|
234
|
+
.option("--archive", "read the FROZEN pre-29-Aug-2026 archive anyway")
|
|
233
235
|
.option("--instance <name>", "Evolution instance name", "flowapt")
|
|
234
236
|
.option("--last <window>", "rolling window back from now, e.g. 48h, 7d, 2w, 90m", "48h")
|
|
235
237
|
.option("--since <date>", "window start (SAST), YYYY-MM-DD or \"YYYY-MM-DD HH:MM\" (overrides --last)")
|
|
@@ -255,9 +257,9 @@ export function run(argv) {
|
|
|
255
257
|
.description("List every task in the DB (optionally filter: open | in_progress | done) to find an id")
|
|
256
258
|
.action((status) => pinboardCmd.listRemote(status));
|
|
257
259
|
|
|
258
|
-
// hours (client-hours ledger
|
|
260
|
+
// hours (client-hours ledger + each org's monthly hours package, v0.9.2)
|
|
259
261
|
const hours = program.command("hours")
|
|
260
|
-
.description("Client-hours ledger: log + report Flowapt work against each org's
|
|
262
|
+
.description("Client-hours ledger: log + report Flowapt work against each org's monthly hours package");
|
|
261
263
|
hours.command("log <organization_id>")
|
|
262
264
|
.description("Log work done for a client org (exactly one of --minutes / --hours)")
|
|
263
265
|
.option("--minutes <n>", "duration in whole minutes")
|
|
@@ -273,10 +275,27 @@ export function run(argv) {
|
|
|
273
275
|
.option("--json", "raw JSON output")
|
|
274
276
|
.action((orgId, opts) => hoursCmd.list(orgId, opts));
|
|
275
277
|
hours.command("summary")
|
|
276
|
-
.description("Cross-org month totals
|
|
278
|
+
.description("Cross-org month totals: every org with logged work or a package, vs its package")
|
|
277
279
|
.option("--month <YYYY-MM>", "which month to report")
|
|
278
280
|
.option("--json", "raw JSON output")
|
|
279
281
|
.action((opts) => hoursCmd.summary(opts));
|
|
282
|
+
const hoursPkg = hours.command("package")
|
|
283
|
+
.description("Each org's monthly hours package (one org, or several sharing one). Changes: Matt + Gidon only");
|
|
284
|
+
hoursPkg.command("show [organization_id]")
|
|
285
|
+
.description("Every package, or the one an org is on")
|
|
286
|
+
.option("--json", "raw JSON output")
|
|
287
|
+
.action((orgId, opts) => hoursCmd.packageShow(orgId, opts));
|
|
288
|
+
hoursPkg.command("set <organization_ids...>")
|
|
289
|
+
.description("Put these orgs on ONE package of --hours a month (several orgs = a shared package)")
|
|
290
|
+
.requiredOption("--hours <h>", "hours included per month (0 = none included, e.g. a self-service plan)")
|
|
291
|
+
.option("--name <text>", "package name (default: the org's name, or the names joined)")
|
|
292
|
+
.option("--notes <text>", "where the figure comes from, e.g. \"Order Form 12 Aug 2026\"")
|
|
293
|
+
.option("--dry-run", "show what would change, write nothing")
|
|
294
|
+
.action((orgIds, opts) => hoursCmd.packageSet(orgIds, opts));
|
|
295
|
+
hoursPkg.command("clear <organization_id>")
|
|
296
|
+
.description("Take an org off its package (back to 'no package on file')")
|
|
297
|
+
.option("--dry-run", "show what would change, write nothing")
|
|
298
|
+
.action((orgId, opts) => hoursCmd.packageClear(orgId, opts));
|
|
280
299
|
|
|
281
300
|
// report deck — the monthly client deck (build / copy / approve / send)
|
|
282
301
|
const report = program.command("report").description("Client reporting: the monthly 10-slide client deck");
|
|
@@ -474,7 +493,7 @@ export function run(argv) {
|
|
|
474
493
|
.option("--agent <id>", "target a specific agent instead of the org's active one")
|
|
475
494
|
.option("--use-settings-prompt [bool]", "settings.use_settings_prompt (true if bare)")
|
|
476
495
|
.option("--model <model>", "settings.model (e.g. gpt-5.4-mini)")
|
|
477
|
-
.option("--reasoning-effort <level>", "settings.reasoning_effort — low|medium|high (\"\" clears it); pairs with reasoning models (e.g. gpt-5.6-luna + high)")
|
|
496
|
+
.option("--reasoning-effort <level>", "settings.reasoning_effort — low|medium|high|xhigh|max (\"\" clears it; max = Claude only); pairs with reasoning models (e.g. gpt-5.6-luna + high, claude-sonnet-5 + medium)")
|
|
478
497
|
.option("--rename <name>", "rename the agent")
|
|
479
498
|
.option("--tool <flag=bool>", "toggle a tool flag (repeatable): woo_order_build/woo_tip_field/woo_order_note_field/view_cart_tool/restock_tool/block_tool_status/postal_code_tool_status/shopify_products_web_chat/ticket_tool_status/product_lookup/email_request_tool/collapse_product_variants", agentConfigCmd.collectTool, [])
|
|
480
499
|
.option("--discount [bool]", "agent.discount.enabled (true if bare)")
|
|
@@ -487,7 +506,7 @@ export function run(argv) {
|
|
|
487
506
|
// agent-updates (pending client change-requests + chat context; pull + resolve)
|
|
488
507
|
const au = program.command("agent-updates")
|
|
489
508
|
.alias("au")
|
|
490
|
-
.description("
|
|
509
|
+
.description("Agent updates: `all` = every org's queue in one call; `pull` = one org with chat context + attachments; `resolve` with a client-facing note");
|
|
491
510
|
au.command("pull <organization_id>")
|
|
492
511
|
.description("Fetch updates + linked contact + trigger-message window; downloads attachments locally")
|
|
493
512
|
.option("--status <status>", "pending (default) | resolved | in_progress | all")
|
|
@@ -498,6 +517,29 @@ export function run(argv) {
|
|
|
498
517
|
.option("--no-files", "do NOT download attachments (default: download is ON)")
|
|
499
518
|
.option("--max-mb <n>", "skip attachments larger than N MB (default 25)")
|
|
500
519
|
.action((orgId, opts) => agentUpdatesCmd.pull(orgId, opts));
|
|
520
|
+
au.command("all")
|
|
521
|
+
.alias("queue")
|
|
522
|
+
.alias("list-remote")
|
|
523
|
+
.description("EVERY org's agent updates in one call (default: open = pending + in_progress), grouped by org; → ./.flowiq/agent-updates/_all.<status>.json")
|
|
524
|
+
.option("--status <list>", "open (default) | pending | in_progress | resolved | declined | all — comma-separated allowed")
|
|
525
|
+
.option("--since <when>", "raised on/after: 14d, 12h, 2w or YYYY-MM-DD (SAST day start)")
|
|
526
|
+
.option("--until <when>", "raised on/before (same formats)")
|
|
527
|
+
.option("--priority <list>", "urgent,high,medium,low (comma-separated)")
|
|
528
|
+
.option("--org <uuid>", "limit to these org(s) (repeatable or comma-separated)", (v, prev) => prev.concat(v.split(",")), [])
|
|
529
|
+
.option("--titles <list>", "title contains any of these (comma-separated, ci substring)")
|
|
530
|
+
.option("--search <text>", "title OR description contains this text (ci)")
|
|
531
|
+
.option("--limit <n>", "max updates returned (default 1000, max 5000)")
|
|
532
|
+
.option("--context", "also fetch the chat window around each trigger message (slower)")
|
|
533
|
+
.option("--before <n>", "with --context: messages before the trigger (default 6)")
|
|
534
|
+
.option("--after <n>", "with --context: messages after the trigger (default 4)")
|
|
535
|
+
.option("--recent <n>", "with --context: fallback window when no trigger message (default 10)")
|
|
536
|
+
.option("--files", "download attachments too (off by default across orgs)")
|
|
537
|
+
.option("--max-mb <n>", "with --files: skip attachments larger than N MB (default 25)")
|
|
538
|
+
.option("--summary", "print only the per-org counts")
|
|
539
|
+
.option("--no-desc", "hide the one-line description under each update")
|
|
540
|
+
.option("--out <path>", "write the JSON here instead of ./.flowiq/agent-updates/_all.<status>.json")
|
|
541
|
+
.option("--json", "print the JSON to stdout (no file, no table) — pipe into jq")
|
|
542
|
+
.action((opts) => agentUpdatesCmd.all(opts));
|
|
501
543
|
au.command("list")
|
|
502
544
|
.description("List local agent-updates snapshots")
|
|
503
545
|
.action(() => agentUpdatesCmd.list());
|
|
@@ -517,7 +559,9 @@ export function run(argv) {
|
|
|
517
559
|
plans.command("list [organization_id]")
|
|
518
560
|
.description("List plans, default open (draft, pending_review, approved, scheduled), across all active orgs or one org")
|
|
519
561
|
.option("--status <status>", "open (default) | all | pending_review | draft | approved | rejected | scheduled | sent | cancelled (comma-separated allowed)")
|
|
520
|
-
.option("--since <date>", "only plans
|
|
562
|
+
.option("--since <date>", "only plans whose SEND date is on or after YYYY-MM-DD")
|
|
563
|
+
.option("--created-since <date>", "only plans CREATED on or after YYYY-MM-DD (SAST) — what a client just submitted")
|
|
564
|
+
.option("--touched-since <date>", "only plans UPDATED on or after YYYY-MM-DD (SAST)")
|
|
521
565
|
.option("--include-inactive", "include plans from inactive orgs (cross-org listing)")
|
|
522
566
|
.option("--limit <n>", "max plans returned (default 100, max 500)")
|
|
523
567
|
.option("--json", "print the raw response")
|
|
@@ -736,6 +780,18 @@ export function run(argv) {
|
|
|
736
780
|
.option("--confirm", "actually cancel")
|
|
737
781
|
.action((orgId, queueId, opts) => broadcastCmd.scheduledUpdate(orgId, queueId, "cancel", opts));
|
|
738
782
|
|
|
783
|
+
const drafts = broadcast.command("drafts")
|
|
784
|
+
.description("Saved SEND DRAFTS (the send dialog's stored configurations) — read-only");
|
|
785
|
+
drafts.command("list <organization_id>", { isDefault: true })
|
|
786
|
+
.description("List the org's saved send drafts, newest first")
|
|
787
|
+
.option("--limit <n>", "max rows (default 25, max 200)")
|
|
788
|
+
.option("--json", "raw JSON")
|
|
789
|
+
.action((orgId, opts) => broadcastCmd.draftsList(orgId, opts));
|
|
790
|
+
drafts.command("show <organization_id> <draft_id>")
|
|
791
|
+
.description("One saved send draft in full (the whole stored config)")
|
|
792
|
+
.option("--json", "raw JSON")
|
|
793
|
+
.action((orgId, draftId, opts) => broadcastCmd.draftsShow(orgId, draftId, opts));
|
|
794
|
+
|
|
739
795
|
broadcast.command("list-remote <organization_id>")
|
|
740
796
|
.description("List the org's broadcasts newest-first (full broadcastId + template + status + recipients) — the discovery step for `bc status` / `bc retry`")
|
|
741
797
|
.option("--limit <n>", "how many to show (default 25, max 200)")
|