@flowapt/flowiq-cli 0.6.7 → 0.7.0

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 CHANGED
@@ -555,6 +555,70 @@ flowiq tag remove <org_id> vip,old-promo --confirm # remove tag(s) from ALL con
555
555
  matches — re-run until `matched 0`. Idempotent and safe to re-run.
556
556
  - **Undo** any tag with `flowiq tag remove <org> <tag> --confirm`.
557
557
 
558
+ ### Staying current — `flowiq doctor` (v0.6.9)
559
+
560
+ ```bash
561
+ flowiq doctor # version vs npm, auth, and whether the server still supports this install
562
+ flowiq doctor --json # same, machine-readable; exits 1 when something needs fixing
563
+ ```
564
+
565
+ ```
566
+ ✓ version 0.6.9 (latest)
567
+ ✓ api https://api.flowiq.live
568
+ ✓ auth you@flowapt.com · super admin
569
+ ✓ server contract 0.6.8 or newer · build 1f0fd48c
570
+ ```
571
+
572
+ **Three ways this install tells you it is stale**, so nobody — person or AI
573
+ assistant — has to remember to check:
574
+
575
+ 1. **npm nudge.** Any command prints, on stderr, when a newer version is
576
+ published:
577
+ `⬆ FLOWIQ CLI OUT OF DATE — installed X, latest Y / Run this before continuing: npm i -g @flowapt/flowiq-cli@latest`.
578
+ The registry is polled at most once a day by a detached child, so the hint
579
+ costs nothing on the hot path and appears on the next run after a release.
580
+ 2. **Server contract.** Every `api/cli/*` response carries
581
+ `X-Flowiq-Min-Version` — the oldest client the deployed server still
582
+ supports — and the CLI warns once per process when this install is below it.
583
+ This is the half npm cannot tell you: the registry knows what is *published*,
584
+ not whether the **server** still speaks your dialect. It is instant and
585
+ offline-safe, riding on a request you were making anyway.
586
+ 3. **`flowiq doctor`**, when you want to ask deliberately.
587
+
588
+ ⚠️ Until 9 Sep 2026 the npm nudge was also gated on `process.stderr.isTTY`, so
589
+ it was invisible to exactly the audience that most needs it — an assistant
590
+ running `flowiq` through a tool call gets a pipe, not a TTY. Teammates' AI
591
+ sessions could run a months-old CLI forever without ever being told. Writing to
592
+ stderr was already the whole safety property; the TTY gate bought nothing.
593
+
594
+ **Bumping `MIN_CLI_VERSION`** (`api/cli/_auth.js`): only when a server change
595
+ would MISBEHAVE on an older client — a removed or renamed response field, a
596
+ changed request shape, a validation rule an old client cannot satisfy. Never for
597
+ additive work; a new endpoint or an opt-in query param leaves old clients
598
+ correct, and crying wolf trains people to ignore the warning.
599
+
600
+ ### Publishing (maintainers) — publish from a CLEAN checkout
601
+
602
+ `npm publish` packs the **working tree**, not your branch. This repo's local
603
+ tree is routinely dozens of commits behind `origin/main` with other sessions'
604
+ work in it, so `cd cli && npm publish` from it ships a stale CLI under a version
605
+ number you can never reuse. That has happened twice — **0.6.5** (8 Sep 2026) and
606
+ **0.6.7** (9 Sep 2026) both shipped a local tree while the real work sat on
607
+ `origin/main`.
608
+
609
+ A `prepublishOnly` hook now refuses when `cli/` differs from `origin/main`:
610
+
611
+ ```bash
612
+ # the safe recipe
613
+ git clone https://github.com/MattFlowapt/flow-v1.git /tmp/pub
614
+ cd /tmp/pub/cli && npm publish
615
+
616
+ # or bring the local tree level first
617
+ git checkout origin/main -- cli/ && cd cli && npm publish
618
+ ```
619
+
620
+ `FLOWIQ_ALLOW_DIRTY_PUBLISH=1` overrides it, for a genuine emergency only.
621
+
558
622
  ### Links — `flowiq links shorten|list` (v0.4.8)
559
623
 
560
624
  Short links with campaign UTM tags — the terminal half of the in-app **URL
@@ -564,16 +628,53 @@ minted here is identical to one minted in the dialog.
564
628
  ```bash
565
629
  # Dry run FIRST — shows the exact destination each short link will carry
566
630
  flowiq links shorten <org> --url "https://shop.co.za/product/x" \
567
- --campaign 13Aug_Seeds --content 13Aug_Seeds
631
+ --campaign "Spring Promotion" # → utm_campaign=9Sep_SpringPromotion
568
632
 
569
633
  flowiq links shorten <org> --url "https://shop.co.za/product/x" \
570
634
  --url "https://shop.co.za/product/y" \
571
- --campaign 13Aug_Seeds --content 13Aug_Seeds --domain linklnk.io --commit
635
+ --campaign "Spring Promotion" --content ViewMore --domain linklnk.io --commit
572
636
 
573
- flowiq links shorten <org> --file ./slide2.txt --campaign 13Aug_Seeds_VM --content 13Aug_Seeds_VM --commit
574
- flowiq links list <org> --campaign 13Aug_Seeds
637
+ # a send going out later — give the send date, not today's
638
+ flowiq links shorten <org> --file ./slide2.txt \
639
+ --campaign "Heritage Day" --date 2026-09-24 --commit
640
+
641
+ flowiq links list <org> --campaign 9Sep_SpringPromotion
575
642
  ```
576
643
 
644
+ #### Campaign naming — `Date_Campaign` (you do not have to type it) (v0.6.7)
645
+
646
+ The house convention is **`Date_Campaign`**: the date the broadcast **goes out**
647
+ (`9Sep` — no leading zero, three letters, `Sep` never `Sept`), an underscore,
648
+ then the campaign title in PascalCase. A broadcast going out today for the
649
+ Spring Promotion is **`9Sep_SpringPromotion`**.
650
+
651
+ `--campaign` is normalised to it, so pass the campaign in plain words:
652
+
653
+ | you type | you get |
654
+ |---|---|
655
+ | `--campaign "Spring Promotion"` | `9Sep_SpringPromotion` (today's date prefixed) |
656
+ | `--campaign spring-promotion` | `9Sep_SpringPromotion` |
657
+ | `--campaign 10JunFathersDay` | `10Jun_FathersDay` (underscore inserted) |
658
+ | `--campaign 5Sept_BraaiDayOffer` | `5Sep_BraaiDayOffer` (`Sept` → `Sep`) |
659
+ | `--campaign "Heritage Day" --date 24/9/2026` | `24Sep_HeritageDay` |
660
+
661
+ - The resolved tag is **printed before anything is minted**, and `shorten` is
662
+ dry-run by default, so you always see it first.
663
+ - `--date` takes `9Sep`, `2026-09-24` or `24/9/2026` — use it whenever the send
664
+ is not today.
665
+ - `--content` is the **per-link** tag (which button, which product), so it gets
666
+ no date: `--content ViewMore`. Omit it and it defaults to the campaign.
667
+ - `--raw-campaign` uses your string exactly as typed, for the rare tag that is
668
+ deliberately outside the convention.
669
+ - **Not applied** to the agent's own auto-shortened links (minted server-side as
670
+ `ai_<org>` — a different namespace) or to `flowiq bc --campaign`, which names
671
+ the local `.flowiq/campaigns/<x>.json` file and the `bc-<x>` contact tag
672
+ rather than a UTM.
673
+
674
+ Why it exists: of 1,042 distinct date-prefixed campaign tags minted in the 12
675
+ months to 9 Sep 2026, only **27 (2.6%)** matched the convention — 924 fused the
676
+ date onto the title (`10JunFathersDay`) and 5 wrote `Sept`.
677
+
577
678
  - **`utm_source=whatsapp` and `utm_medium=whatsapp_paid` are FIXED** (the dialog
578
679
  sets them too) — a link built any other way stops matching the attribution
579
680
  queries. You supply `--campaign` and `--content`, and they must be **supplied
@@ -982,17 +1083,92 @@ flowiq report deck pull <org_id> 2026-08 # → ./.flowiq/repor
982
1083
  - `send --client` needs an approved deck AND `report.email.recipients` in the org's reporting config (Control center → Report delivery); it marks the deck `sent`. `--test-to` never changes status. Fees on the slides are the org's actual Meta billing when the token can read it, otherwise the rate-card estimate, and the footnote says which.
983
1084
  - Non-store orgs need `config.deck.outcome` (`source: handover | ticket_status | tag | keyword`, labels) — the revenue slides become outcome slides. Every verb except `status` and `pull` is audited.
984
1085
 
985
- ### WhatsApp templates — `flowiq templates pull|list|create|status` (alias `tpl`)
1086
+ ### Insights — `flowiq insights status|enable|disable|run` (v0.7.0)
1087
+
1088
+ The **Customer Insights Report** (the scheduled AI email: exec summary, top
1089
+ questions / complaints / topics, sentiment, product mentions — cron 72, 09:00
1090
+ SAST) per org. Reads and writes `feature_flags.export_insights` exactly the
1091
+ way the Insights Studio does, and fires a report the way the Profile tab's
1092
+ Run Now does.
986
1093
 
987
- Read an org's live templates straight from Meta (read-only), and submit new
988
- ones through the `create-meta-template` edge function.
1094
+ ```bash
1095
+ flowiq insights status # every org: STATE · CHATS30D · KEY · LAST REPORT · RCPT · SCHEDULES
1096
+ flowiq insights status --off --min-chats 20 # who is missing it and has real chat volume
1097
+ flowiq insights status <organization_id> # one org + its last 10 reports
1098
+
1099
+ flowiq insights enable <organization_id> --weekly monday --recipient client@example.com --recipient matt@flowapt.com
1100
+ flowiq insights enable <organization_id> --daily --store-only --monthly last # two schedules; dailies stored, not emailed
1101
+ flowiq insights enable <organization_id> --monthly 1 --report advanced # advanced engine (PDF + Studio config)
1102
+ flowiq insights enable <organization_id> --add-recipient kiah@flowapt.com # edit recipients, keep the cadence
1103
+ flowiq insights enable <organization_id> --weekly friday --dry-run # show what would be written
1104
+
1105
+ flowiq insights disable <organization_id> # enabled=false; schedules + recipients kept
1106
+
1107
+ flowiq insights run <organization_id> --period weekly --recipient matt@flowapt.com # DRY RUN: engine, window, recipients, message count
1108
+ flowiq insights run <organization_id> --period weekly --recipient matt@flowapt.com --commit
1109
+ flowiq insights run <organization_id> --from 2026-09-01 --to 2026-09-07 --store-only --commit
1110
+ ```
1111
+
1112
+ - **`enable` composes the whole config** (`enabled` + `recipient_emails` +
1113
+ `schedules[]`). Cadence flags (`--daily` / `--weekly [day]` / `--monthly [day]`)
1114
+ REPLACE the schedules; none given keeps what exists, or applies the default
1115
+ (weekly Monday, standard, emailed). `--report` and `--store-only` apply to the
1116
+ cadence flags you pass. `--recipient` replaces the list; `--add-recipient` /
1117
+ `--remove-recipient` edit it.
1118
+ - **It refuses the three ways an enabled org silently produces nothing**
1119
+ (`--force` overrides): the org has **no OpenAI key** (both engines hard-require
1120
+ `organizations.openai_api_key`); an emailing schedule with **no recipients**
1121
+ (the engines would email a hardcoded fallback address); an **inactive** org.
1122
+ - **`status` shows schedules as the scheduler will actually run them.** An
1123
+ enabled org with no schedules stored runs *daily standard* by default — it is
1124
+ marked `(implicit)`; `enable` replaces that with an explicit schedule.
1125
+ - **`run` is a dry run by default** (it costs OpenAI tokens and can email a
1126
+ client): it prints the engine, the window, who would get it and how many
1127
+ customer messages are in the window, and refuses an empty window. `--commit`
1128
+ fires it through python-render (fire-and-forget) and prints the job id; the
1129
+ report shows under *Recent reports* in `status <org>` a few minutes later.
1130
+ - `--from` / `--to` are SAST calendar days. `--report` defaults to the org's
1131
+ schedule for that period, else standard.
1132
+ - `enable` / `disable` / `run` are audited (`flowiq audit --endpoint insights`).
1133
+
1134
+ ### WhatsApp templates — `flowiq templates pull|list|show|create|status` (alias `tpl`)
1135
+
1136
+ Read an org's live templates straight from Meta (read-only), render any single
1137
+ row **including an unsubmitted DRAFT**, and submit new ones through the
1138
+ `create-meta-template` edge function.
989
1139
 
990
1140
  ```bash
991
1141
  flowiq templates pull <organization_id> # → ./.flowiq/templates/<slug>.json (Meta-side truth)
992
- flowiq templates create <organization_id> --request-file req.json
993
1142
  flowiq templates status <organization_id> --name booking # poll approval
1143
+ flowiq templates show <organization_id> heritage_day_v2 # render ONE row, drafts included
1144
+ flowiq templates create <organization_id> --request-file req.json
994
1145
  ```
995
1146
 
1147
+ **`show` is the only way to read a DRAFT from the terminal (v0.6.7).** A draft
1148
+ never reaches Meta, so `templates pull` cannot see it and neither can the
1149
+ broadcast introspector — before this, reviewing one meant opening the dialog.
1150
+ `show` prints the body with its examples substituted (as the customer will read
1151
+ it), then every carousel card: media URL, source filename, whether the Meta
1152
+ asset handle is present, card body, and each button's resolved URL. `--raw`
1153
+ also prints the escaped body so invisible whitespace is visible; `--json` gives
1154
+ the row.
1155
+
1156
+ It closes with **Checks** — cheap structural rules that Meta would otherwise
1157
+ catch only at review, or not at all until send:
1158
+
1159
+ - body variables not sequential from `{{1}}`, or missing an example
1160
+ - a card URL button using anything but `{{1}}` (Meta numbers each card's
1161
+ parameters per card, so `{{2}}` on a card breaks that card's link at send)
1162
+ - more than one variable in a card URL (Meta supports exactly one)
1163
+ - a bold/italic/strikethrough marker sitting against a space, which WhatsApp
1164
+ does not render — the customer sees the literal `_` or `*`
1165
+ - carousel outside 2–10 cards, cards that do not share one shape, missing media,
1166
+ body/card text over Meta's 1024 / 160 limits
1167
+
1168
+ Checks are structural only. They cannot tell you a card names a product your
1169
+ store does not sell — `show` renders the text so you can read it against the
1170
+ shop.
1171
+
996
1172
  `req.json` holds `{ template_request, template_data?, media_header?, cards_media? }`.
997
1173
  Media headers: pass `media_header.file_url` (a public URL) — the edge function
998
1174
  uploads it to Meta server-side. Approval is async; re-`pull` for the
@@ -1146,7 +1322,7 @@ Notes:
1146
1322
  org, which endpoint or query, how many records. **The response body is never
1147
1323
  stored**; the log records what was asked, not the customer data that came back.
1148
1324
 
1149
- ### Org — `flowiq org create` / `flowiq org info <organization_id>`
1325
+ ### Org — `flowiq org create` / `flowiq org info <organization_id>` / `flowiq org flags …`
1150
1326
 
1151
1327
  Create a brand-new organization, or look one up (read-only; raw store/Meta
1152
1328
  credentials are stripped server-side).
@@ -1173,6 +1349,46 @@ flowiq prompts pull <new_org_id> # edit → push
1173
1349
  (No `agent config` step any more — since 29 Aug 2026 a new agent is born on the
1174
1350
  house default: `gpt-5.6-luna`, `reasoning_effort high`, `use_settings_prompt` ON.)
1175
1351
 
1352
+ #### Feature flags — `flowiq org flags show|list|set|unset|keys` (v0.7.0)
1353
+
1354
+ The Settings → Profile switches (`organizations.feature_flags`) from the
1355
+ terminal. `show` reads one org, `list --key` is the cross-org census, `set` /
1356
+ `unset` write. Only keys the Profile tab manages can be set (server-side
1357
+ allowlist + type check); anything that looks like a credential is redacted on
1358
+ read and refused on write.
1359
+
1360
+ ```bash
1361
+ flowiq org flags show <organization_id> # every flag, by Settings section
1362
+ flowiq org flags list --key export_insights.enabled --off # which orgs do NOT have it (--on / --present / --absent / --all)
1363
+ flowiq org flags list --key mcp_member_access.enabled --on
1364
+
1365
+ flowiq org flags set <organization_id> mcp_member_access.enabled true
1366
+ flowiq org flags set <organization_id> wait_for_more_messages 5 # integer, 0-60
1367
+ flowiq org flags set <organization_id> UTM.excluded_campaign_keywords "test,internal" # string[] (comma list or JSON)
1368
+ flowiq org flags set <organization_id> integrations_visible.instagram true # wildcard keys take one more segment
1369
+ flowiq org flags set <organization_id> vert.active true --yes # dangerous keys need --yes
1370
+ flowiq org flags set <organization_id> export_insights @insights.json # JSON from a file (prefer `flowiq insights enable`)
1371
+
1372
+ flowiq org flags unset <organization_id> wait_for_more_messages --yes # remove the key (= platform default); always --yes
1373
+ flowiq org flags keys # what is settable, with type + danger notes
1374
+ flowiq org flags keys --section "members"
1375
+ ```
1376
+
1377
+ - **Values:** `true`/`false`, a whole number, JSON (`'["a","b"]'` / `'{"enabled":true}'`),
1378
+ `@file.json`, or a comma list for list keys. The server validates against the
1379
+ registry — a boolean given `12` is a 400, not a corrupted flag.
1380
+ - **Dangerous keys** (the Profile tab's amber-warning switches: `vert.active`,
1381
+ `ip_whitelist`, `follow_up.enabled`, `subscriptions_test_mode`,
1382
+ `api_key_member_access.enabled`, `export_insights`, `wait_for_more_messages`, …)
1383
+ are refused without `--yes`, with the reason printed.
1384
+ - **Writes are atomic per path** (the same `_jsonb_deep_set` the Profile tab
1385
+ uses), so `set a.b` never clobbers `a.c` that someone else just changed.
1386
+ - **Every set/unset is audited** (`flowiq audit --endpoint org-flags`) and the
1387
+ response prints before → after plus the exact undo command.
1388
+ - A key that is on the org but not in the registry is shown by `show` (and
1389
+ `--json`) but cannot be set here — it is DB-only by design; add it to the
1390
+ Profile tab first.
1391
+
1176
1392
  ### Agents — `flowiq agent list|create <org>`
1177
1393
 
1178
1394
  List an org's agents (to discover ids) and create a new one.
@@ -1237,7 +1453,7 @@ pairs with reasoning models like `gpt-5.6-luna`), agent `--rename`,
1237
1453
  the tool-flag columns (`woo_order_build`, `woo_tip_field`, `woo_order_note_field`,
1238
1454
  `view_cart_tool`, `restock_tool`, `block_tool_status`, `postal_code_tool_status`,
1239
1455
  `shopify_products_web_chat`, `ticket_tool_status`, `product_lookup`,
1240
- `collapse_product_variants`),
1456
+ `collapse_product_variants`, `email_request_tool`),
1241
1457
  `discount.enabled`, and the `flowiq test` contact
1242
1458
  (`settings.test_contact_number` / `settings.test_contact_name`). Anything else is
1243
1459
  rejected; every change is reported before → after.
@@ -1258,6 +1474,15 @@ agent answers product questions with "I can't pull the live menu". Found on Ouma
1258
1474
  Bets Gebak (11 Aug 2026): 25 product rows, 0 embeddings, and an unfunded OpenAI
1259
1475
  account — enabling this flag restored product answers immediately.
1260
1476
 
1477
+ **`--tool email_request_tool=true` (added 9 Sep 2026).** Turns on the `email_request_to_team`
1478
+ tool: once the agent has a customer's name, email and/or phone, preferred contact
1479
+ method and what they need, it emails that (plus the last few messages) to the
1480
+ addresses in `agents.settings.email_request.to`, from flowiq@flowapt.com with Reply-To
1481
+ set to the customer, and leaves an internal note on the thread. No ticket, no bot-off,
1482
+ no staff WhatsApp — built for businesses that want requests in an inbox rather than a
1483
+ human escalation (GIB Financial Services). The recipient list is set in the Tool
1484
+ Library (Agents → Tool Library → Email Request to Team); the CLI cannot set it yet.
1485
+
1261
1486
  **`--tool collapse_product_variants=true` (added 4 Aug 2026).** Not a tool toggle —
1262
1487
  it changes what `get_product_info` RETURNS. OFF (the default) the result cap counts
1263
1488
  **variant rows**, so on a catalogue with several packaging/size variants per product
@@ -1333,22 +1558,6 @@ flowiq test cleanup <organization_id> --confirm # delete the stress contact
1333
1558
  `--agent <id>` targets a specific (non-active) agent. `test stress cleanup` is
1334
1559
  destructive and requires `--confirm`.
1335
1560
 
1336
- **Scenario pack shape.** `{scenarios:[{id, title, turns:[{text, expect?, expectNot?}]}]}`.
1337
- `expect` / `expectNot` are case-insensitive substring checks (informational —
1338
- reported and summed, never a hard fail) and accept **either a single string or an
1339
- array of strings**:
1340
-
1341
- ```json
1342
- { "text": "What investment options do I have?",
1343
- "expect": "depend on your fund rules",
1344
- "expectNot": ["Destiny offers", "guaranteed return"] }
1345
- ```
1346
-
1347
- Before v0.6.7 a bare string was iterated character by character, so
1348
- `"expect": "fund rules"` reported ten single-letter checks that all trivially
1349
- passed — a wrong pass/fail signal that still looked like a real result. Write
1350
- either form now.
1351
-
1352
1561
  ### Guide — `flowiq guide`
1353
1562
 
1354
1563
  Read the bundled docs in the terminal — no digging through node_modules.
package/TEAM-GUIDE.md CHANGED
@@ -101,11 +101,48 @@ When you see it, run `npm i -g @flowapt/flowiq-cli` — a stale version also mea
101
101
  | 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) |
102
102
  | 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 |
103
103
  | 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) |
104
+ | Client wants leads/requests emailed to their team instead of a human escalation | `flowiq agent config <org_id> --tool email_request_tool=true`, then set the recipient list in Agents → Tool Library → Email Request to Team (no CLI path for the addresses yet) |
104
105
  | Talk to the live agent safely (no real WhatsApp ever sent) | `flowiq test send <org_id> "hi, do you sell X?"` |
105
106
  | Set which contact `flowiq test` uses (use a FAKE number!) | `flowiq agent config <org_id> --test-contact-number 27000000001 --test-contact-name "QA Bot"` |
106
- | Prove a batch of fixes actually landed, in one run | `flowiq test scenario <org_id> ./pack.json` — pack is `{scenarios:[{id,title,turns:[{text,expect?,expectNot?}]}]}`. `expect`/`expectNot` are case-insensitive substring checks and take **a single string OR an array**. (Before v0.6.7 a bare string was checked letter by letter, so it always "passed" — if you wrote packs on an older version, re-run them.) |
107
- | **Make short links for a campaign** (with UTM tracking) | `flowiq links shorten <org_id> --url "https://shop.co.za/product/x" --campaign 13Aug_Seeds --content 13Aug_Seeds` (dry run) → `… --commit`. `utm_source`/`utm_medium` are set for you; campaign + content must be given together |
108
- | Short links for every URL in a message | `flowiq links shorten <org_id> --file ./message.txt --campaign X --content X --commit` |
107
+ > **Publishing the CLI (maintainers only):** publish from a clean clone, never
108
+ > from your working tree — `npm publish` packs whatever is on disk. A
109
+ > `prepublishOnly` check now stops you if `cli/` differs from `origin/main`.
110
+
111
+ ### Keeping the CLI current
112
+
113
+ Run **`flowiq doctor`** any time you are unsure — it checks your version against
114
+ npm, your login, and whether the server still supports your install, and tells
115
+ you the exact command to fix anything wrong.
116
+
117
+ You will usually not need to: the CLI now tells you itself. If a newer version
118
+ is out, or if your install is older than the server supports, every command
119
+ prints a line starting `⬆ FLOWIQ CLI …` with the fix:
120
+
121
+ ```bash
122
+ npm i -g @flowapt/flowiq-cli@latest
123
+ ```
124
+
125
+ That warning is printed for AI assistants too, not just people — so if you work
126
+ with Claude in this repo, it will see it and can update for you.
127
+
128
+ ### Campaign naming — `Date_Campaign`
129
+
130
+ Every campaign tag is **the date the broadcast goes out, then the campaign
131
+ title**: a Spring Promotion going out on 9 September is `9Sep_SpringPromotion`.
132
+
133
+ **You do not have to type it that way.** Pass `--campaign "Spring Promotion"`
134
+ and the CLI builds the tag, prints it, and only mints once you add `--commit`.
135
+ Use `--date` when the send is not today (`--date 24/9/2026`), and `--content`
136
+ for a second link in the same send (`--content ViewMore`).
137
+
138
+ Getting this right is what makes a campaign's clicks and revenue group together
139
+ in reporting — a tag typed a different way each time splits one campaign into
140
+ several.
141
+
142
+ | **Make short links for a campaign** (with UTM tracking) | `flowiq links shorten <org_id> --url "https://shop.co.za/product/x" --campaign "Spring Promotion"` (dry run) → `… --commit`. **Just type the campaign in plain words** — the CLI names it for you as `9Sep_SpringPromotion` and prints it before minting. `utm_source`/`utm_medium` are set for you |
143
+ | A campaign going out on a later date | Add `--date`: `--campaign "Heritage Day" --date 24/9/2026` → `24Sep_HeritageDay`. Without it you get today's date |
144
+ | A second link in the same send (e.g. a VIEW MORE button) | Same `--campaign`, different `--content`: `--content ViewMore`. Content is the per-link tag and gets no date |
145
+ | Short links for every URL in a message | `flowiq links shorten <org_id> --file ./message.txt --campaign "Spring Promotion" --commit` |
109
146
  | Check which links a campaign has, and their clicks | `flowiq links list <org_id> --campaign 13Aug_Seeds` |
110
147
  | A link already looks short (`linklnk.io/abc123`) — can I re-tag it? | No: paste the DESTINATION url instead. Re-shortening keeps the old campaign tag and splits the click count, so the CLI refuses it |
111
148
  | Send a template broadcast to a CSV of people | `flowiq bc map <org_id> --template … --csv …` → `flowiq bc send … ` (dry-run) → `… --commit`. **v0.6.0: the commit imports every row as a contact (tag `bc-<campaign>` + the row's values as attributes) and fires ONE python broadcast** — it returns in seconds with a `broadcastId`; python sends in the background. Check delivery with `bc status`, re-fire failures with `bc retry`. A campaign that already fired refuses a re-run (`--resend` deliberately resends to everyone tagged). |
@@ -135,12 +172,22 @@ When you see it, run `npm i -g @flowapt/flowiq-cli` — a stale version also mea
135
172
  | Read a contact's chat | `flowiq m pull <contact_id>` then open the JSON. Ask for more than exists (`--count 100`) and you get the entire history — it says "that is ALL of them" when there is nothing older |
136
173
  | Export an org's full chat history | `flowiq export chats <org_id>` |
137
174
  | Check / create WhatsApp templates | `flowiq tpl pull <org_id>` / `flowiq tpl create <org_id> --request-file req.json` |
175
+ | **Read a template you have not submitted yet (a DRAFT)** | `flowiq tpl show <org_id> <template_name>` — the ONLY way to see a draft from the terminal (`pull` reads Meta, and a draft never gets there). Renders the message as the customer will read it, every carousel card, and a **Checks** list of the things Meta would bounce it for |
176
+ | Check a carousel before submitting it | `flowiq tpl show <org_id> <name>` and read **Checks**. It catches a card link using the wrong `{{n}}`, a missing greeting example, a bold/italic marker against a space (WhatsApp shows the literal `_`), wrong card counts and over-length text. It cannot know a card names a product you do not stock — read the card text against the shop yourself |
138
177
  | Manage Shopify/Woo platform webhooks | `flowiq wh pull <org_id>` → edit → `flowiq wh push <slug>` |
139
178
  | Manage outbound messaging webhooks (incl. their auth) | `flowiq mw pull <org_id>` → `flowiq mw push <slug> --dry-run` → push |
140
179
  | Create a brand-new client org | `flowiq org create --name "Client Name"` → then `agent create` on the printed id |
141
180
  | See a client's pending change requests | `flowiq au pull <org_id>` |
142
181
  | Close a client's change request (after verifying the fix!) | `flowiq au resolve <update_id> --note "what changed"` — the client reads the note |
143
182
  | Check an org's platform + active agent | `flowiq org info <org_id>` |
183
+ | **See every Settings → Profile switch on an org** | `flowiq org flags show <org_id>` (credentials are never shown) |
184
+ | **Which orgs have a flag on / off** (insights, MCP member access, a debounce…) | `flowiq org flags list --key export_insights.enabled --off` · `--key mcp_member_access.enabled --on` |
185
+ | Flip a Settings → Profile switch from the terminal | `flowiq org flags set <org_id> mcp_member_access.enabled true` — `flowiq org flags keys` lists what you can set; the risky ones ask for `--yes`; the reply prints the undo |
186
+ | Set the agent's message debounce ("customer sends 3 fragments, agent replies 3 times") | `flowiq org flags set <org_id> wait_for_more_messages 5` (seconds, 0-60) |
187
+ | **Who is missing the Customer Insights report?** | `flowiq insights status --off --min-chats 20` — KEY `NO` means the org has no OpenAI key and the report cannot run |
188
+ | Switch the insights report on for a client | `flowiq insights enable <org_id> --weekly monday --recipient client@example.com --recipient you@flowapt.com` — it refuses if there is no OpenAI key or nobody to email |
189
+ | Store insights daily without emailing anyone (build history first) | `flowiq insights enable <org_id> --daily --store-only` |
190
+ | Send one insights report right now | `flowiq insights run <org_id> --period weekly --recipient you@flowapt.com` (dry run) → add `--commit` |
144
191
  | Work a Pin Board task | `flowiq pin list-remote open` → `pull` → edit → `push` |
145
192
  | Log hours you worked for a client (every package = 10h Flowapt work/month) | `flowiq hours log <org_id> --hours 1.5 --desc "what you did"` — plain language, the client sees it on their Client Console |
146
193
  | Check a client's package hours / the month across all clients | `flowiq hours list <org_id>` / `flowiq hours summary` |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowapt/flowiq-cli",
3
- "version": "0.6.7",
3
+ "version": "0.7.0",
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": {
@@ -16,7 +16,9 @@
16
16
  "node": ">=18"
17
17
  },
18
18
  "scripts": {
19
- "start": "node bin/flowiq.js"
19
+ "start": "node bin/flowiq.js",
20
+ "prepublishOnly": "node scripts/prepublish-check.mjs",
21
+ "test": "node --test src/*.test.mjs"
20
22
  },
21
23
  "dependencies": {
22
24
  "commander": "^12.1.0",
@@ -0,0 +1,164 @@
1
+ // Campaign / UTM naming convention — Date_Campaign (Matt, 9 Sep 2026).
2
+ // ====================================================================
3
+ // Date = the physical date the broadcast GOES OUT, as `9Sep` (no
4
+ // leading zero, three-letter month, "Sep" never "Sept").
5
+ // Campaign = the broadcast title / campaign, PascalCase, no spaces.
6
+ // → `9Sep_SpringPromotion`
7
+ //
8
+ // Staff should not have to remember this, so the CLI resolves it for them:
9
+ // `--campaign "Spring Promotion"` on a send going out today becomes
10
+ // `9Sep_SpringPromotion`, and the resolved value is always PRINTED before a
11
+ // --commit so it can be seen and overridden.
12
+ //
13
+ // Measured 9 Sep 2026, why this exists: of 1,042 distinct date-prefixed
14
+ // campaign tags minted in 12 months, only 27 (2.6%) matched the convention —
15
+ // 924 fused the date to the title (`10JunFathersDay`) and 5 wrote `Sept`.
16
+ //
17
+ // SCOPE: this is the UTM campaign tag on a STAFF-minted short link
18
+ // (`flowiq links shorten`). It is deliberately NOT applied to:
19
+ // • agent auto-shortened links, tagged `ai_<org>` by api/_url-shorten-engine.js
20
+ // (5,337 `ai_flw`, 3,649 `ai_gf`, … — a different namespace, not a campaign)
21
+ // • `flowiq bc --campaign`, which names the LOCAL .flowiq/campaigns/<x>.json
22
+ // file and the `bc-<x>` contact tag, not a UTM.
23
+ // `--raw-campaign` bypasses normalisation entirely when you genuinely need it.
24
+
25
+ export const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
26
+
27
+ const MONTH_ALIASES = new Map([
28
+ ["sept", "Sep"], ["january", "Jan"], ["february", "Feb"], ["march", "Mar"],
29
+ ["april", "Apr"], ["june", "Jun"], ["july", "Jul"], ["august", "Aug"],
30
+ ["september", "Sep"], ["october", "Oct"], ["november", "Nov"], ["december", "Dec"],
31
+ ]);
32
+
33
+ /** `9Sep` for the given Date (default: today, local time). */
34
+ export function dateToken(d = new Date()) {
35
+ return `${d.getDate()}${MONTHS[d.getMonth()]}`;
36
+ }
37
+
38
+ /**
39
+ * Accepts `9Sep`, `09Sep`, `9Sept`, `9 September`, `2026-09-09`, `09/09/2026`.
40
+ * Returns a canonical `9Sep`, or null if it is not a date at all.
41
+ */
42
+ export function parseDateToken(input) {
43
+ const raw = String(input ?? "").trim();
44
+ if (!raw) return null;
45
+ const iso = raw.match(/^(\d{4})-(\d{2})-(\d{2})$/);
46
+ if (iso) {
47
+ const m = Number(iso[2]);
48
+ if (m < 1 || m > 12) return null;
49
+ return `${Number(iso[3])}${MONTHS[m - 1]}`;
50
+ }
51
+ const slash = raw.match(/^(\d{1,2})[/](\d{1,2})[/](\d{2,4})$/); // D/M/YYYY (SA order)
52
+ if (slash) {
53
+ const m = Number(slash[2]);
54
+ if (m < 1 || m > 12) return null;
55
+ return `${Number(slash[1])}${MONTHS[m - 1]}`;
56
+ }
57
+ const dm = raw.match(/^(\d{1,2})\s*([A-Za-z]+)$/);
58
+ if (dm) {
59
+ const day = Number(dm[1]);
60
+ if (day < 1 || day > 31) return null;
61
+ const mon = canonicalMonth(dm[2]);
62
+ return mon ? `${day}${mon}` : null;
63
+ }
64
+ return null;
65
+ }
66
+
67
+ // Longest-first alternation, so `10JunFathersDay` splits as Jun|FathersDay and
68
+ // not as the 9-letter non-month "JunFathers" (which a greedy [A-Za-z]{3,9} does,
69
+ // silently leaving the whole tag title-side and prefixing TODAY's date on top).
70
+ const MONTH_ALT = [...MONTH_ALIASES.keys(), ...MONTHS.map((m) => m.toLowerCase())]
71
+ .sort((a, b) => b.length - a.length)
72
+ .join("|");
73
+ const LEAD_RE = new RegExp(`^(\\d{1,2})\\s*(${MONTH_ALT})([_\\-\\s]*)(.*)$`, "i");
74
+
75
+ function canonicalMonth(word) {
76
+ const w = String(word || "").toLowerCase();
77
+ if (MONTH_ALIASES.has(w)) return MONTH_ALIASES.get(w);
78
+ const hit = MONTHS.find((m) => m.toLowerCase() === w);
79
+ return hit || null;
80
+ }
81
+
82
+ /** "Spring Promotion" / "spring-promotion" / "spring_promotion" → "SpringPromotion". */
83
+ export function pascal(input) {
84
+ return String(input ?? "")
85
+ .replace(/[^A-Za-z0-9]+/g, " ")
86
+ .trim()
87
+ .split(/\s+/)
88
+ .filter(Boolean)
89
+ .map((w) => (/^[A-Z0-9]+$/.test(w) ? w : w[0].toUpperCase() + w.slice(1)))
90
+ .join("");
91
+ }
92
+
93
+ export const CAMPAIGN_RE = /^\d{1,2}(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)_[A-Za-z0-9]+$/;
94
+
95
+ export function isConforming(value) {
96
+ return CAMPAIGN_RE.test(String(value ?? ""));
97
+ }
98
+
99
+ /**
100
+ * Resolve whatever the operator typed into `Date_Campaign`.
101
+ *
102
+ * @param {string} input what they passed to --campaign
103
+ * @param {object} [opts]
104
+ * @param {string} [opts.date] --date override (any parseDateToken form)
105
+ * @param {Date} [opts.now] injectable clock (tests)
106
+ * @returns {{value:string, changed:boolean, notes:string[], error?:string}}
107
+ */
108
+ export function resolveCampaign(input, opts = {}) {
109
+ const notes = [];
110
+ const original = String(input ?? "").trim();
111
+ if (!original) return { value: "", changed: false, notes, error: "campaign is empty" };
112
+
113
+ let overrideDate = null;
114
+ if (opts.date) {
115
+ overrideDate = parseDateToken(opts.date);
116
+ if (!overrideDate) {
117
+ return { value: original, changed: false, notes, error: `--date "${opts.date}" is not a date I understand (try 9Sep, 2026-09-09 or 9/9/2026)` };
118
+ }
119
+ }
120
+
121
+ // Split a leading date off the front, however it was written / fused.
122
+ let datePart = null;
123
+ let rest = original;
124
+ const lead = original.match(LEAD_RE);
125
+ if (lead) {
126
+ const mon = canonicalMonth(lead[2]);
127
+ if (mon) {
128
+ datePart = `${Number(lead[1])}${mon}`;
129
+ rest = lead[4];
130
+ if (lead[2] !== mon) notes.push(`month "${lead[2]}" → "${mon}"`);
131
+ if (!lead[3].includes("_")) notes.push("inserted the missing underscore after the date");
132
+ }
133
+ }
134
+
135
+ if (overrideDate) {
136
+ if (datePart && datePart !== overrideDate) notes.push(`date ${datePart} → ${overrideDate} (--date)`);
137
+ datePart = overrideDate;
138
+ }
139
+ if (!datePart) {
140
+ datePart = dateToken(opts.now || new Date());
141
+ notes.push(`prefixed today's send date "${datePart}"`);
142
+ }
143
+
144
+ const titlePart = pascal(rest);
145
+ if (!titlePart) {
146
+ return { value: original, changed: false, notes, error: `no campaign title found in "${original}" — expected something like "Spring Promotion"` };
147
+ }
148
+ if (titlePart !== rest) notes.push(`title "${rest}" → "${titlePart}"`);
149
+
150
+ const value = `${datePart}_${titlePart}`;
151
+ return { value, changed: value !== original, notes };
152
+ }
153
+
154
+ /** utm_content is per-LINK (which button / which product), so no date is imposed. */
155
+ export function resolveContent(input) {
156
+ const original = String(input ?? "").trim();
157
+ if (!original) return { value: "", changed: false, notes: [] };
158
+ const value = pascal(original);
159
+ return {
160
+ value: value || original,
161
+ changed: !!value && value !== original,
162
+ notes: value && value !== original ? [`content "${original}" → "${value}"`] : [],
163
+ };
164
+ }