@flowapt/flowiq-cli 0.12.1 → 0.12.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 CHANGED
@@ -1292,9 +1292,9 @@ flowiq hours package clear <org_id> # back to "no package on file"
1292
1292
 
1293
1293
  ### Client deck — `flowiq report deck status|build|narrate|generate|inputs|approve|unapprove|send|print|pull` (v0.6.5)
1294
1294
 
1295
- The monthly client deck (`/reporting/deck` in the app; 10 slides, 11 for a store
1296
- org — slide 8 is store health: cart abandonment, returning buyers and opt-in
1297
- growth, online store only), driven from the
1295
+ The monthly client deck (`/reporting/deck` in the app; a cover and five slides
1296
+ since 5 Oct 2026: month at a glance, where WhatsApp made money, broadcasts and
1297
+ always-on, your customers, what we did and what's next), driven from the
1298
1298
  terminal. Everything runs server-side through the `flowiq-reporting-deck` edge
1299
1299
  function: the metrics are built from the FROZEN month snapshot, the copy is
1300
1300
  written by the model under the report rules and a numeric guard (every figure
@@ -1308,6 +1308,8 @@ flowiq report deck generate <org_id> 2026-08 # build the metrics (
1308
1308
  flowiq report deck generate <org_id> 2026-08 --refresh --force # rebuild everything
1309
1309
  flowiq report deck build <org_id> 2026-08 --refresh # metrics only; status then says "Copy is older than the numbers" until you narrate
1310
1310
  flowiq report deck narrate <org_id> 2026-08 # copy only (hand edits are kept)
1311
+ flowiq report deck narrate <org_id> 2026-08 --only changes,action_points # rewrite just these copy fields, keep the rest as written
1312
+ flowiq report deck check <org_id> 2026-08 # fact-check the copy as shown (hand edits too) against the figures; rewrites nothing
1311
1313
  flowiq report deck inputs <org_id> 2026-08 --file inputs.json # the super-admin input form
1312
1314
  flowiq report deck approve <org_id> 2026-08 # refused until every required input is present
1313
1315
  flowiq report deck send <org_id> 2026-08 --test-to me@flowapt.com # one test copy, status untouched
@@ -1326,9 +1328,9 @@ flowiq report deck pull <org_id> 2026-08 # → ./.flowiq/repor
1326
1328
  Free text (milestone, the four plan fields) is edited in the app; it is stored as overrides that survive `narrate`.
1327
1329
  - `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.
1328
1330
  - 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.
1329
- - A month with a Customer Insights report gets the "What your customers asked" slide after slide 7 (29 Sep 2026): `build` takes the one `org_insights` export-insights row whose window covers the most of the month (top questions, trending topics, complaints or product mentions, sentiment, each as "N customers"), stores it under `deck.insights`, and the copy carries `insights_title` + `insights_read`. `status --json` shows the window under `insights` (null = no report, no slide). The timing slide's next-step card is now written by the narrator (`next_step_title` / `next_step_body`) against next month's South African retail calendar (public holidays, Mother's / Father's Day, Black Friday, payday, month-end) and edited in place like every other block.
1331
+ - A month with a Customer Insights report gets its top questions and topics on the "Your customers" slide (a slide of its own from 29 Sep to 5 Oct 2026): `build` takes the one `org_insights` export-insights row whose window covers the most of the month (top questions, trending topics, complaints or product mentions, sentiment, each as "N customers"), stores it under `deck.insights`, and the copy carries `insights_title` + `insights_read`. `status --json` shows the window under `insights` (null = no report, no slide). The timing slide's next-step card is now written by the narrator (`next_step_title` / `next_step_body`) against next month's South African retail calendar (public holidays, Mother's / Father's Day, Black Friday, payday, month-end) and edited in place like every other block.
1330
1332
  - Cart funnel and buckets (29 Sep 2026): "messaged" counts reminders WhatsApp reports delivered (the old send-request count rides along as `messaged_requested`); a Woo cart reminder now matches its order, so Woo recoveries are no longer zero; our own cart link is a recovery, another tool's abandoned-cart email is not; on Woo an agent-built order is a conversation, not a campaign; chat-to-order counts buyers who chatted, any bucket. `status` refuses approval when the agent replied to nobody in the month (no live channel).
1331
- - Store orgs get slide 8, "Store health": cart abandonment (online checkouts started, bought, abandoned), first-time vs returning buyers and opt-in growth, each with a twelve-month strip and measured on the online store checkout only (till and other channels are named in the footer). `status --json` carries the headline figures under `store`; `config.deck.channel_labels {"<source_name>": "Label"}` renames a sales channel on the slide.
1333
+ - Store orgs: the deck still builds store health (its own slide until 5 Oct 2026; the five-slide deck does not show it as a slide): cart abandonment (online checkouts started, bought, abandoned), first-time vs returning buyers and opt-in growth, each with a twelve-month strip and measured on the online store checkout only (till and other channels are named in the footer). `status --json` carries the headline figures under `store`; `config.deck.channel_labels {"<source_name>": "Label"}` renames a sales channel on the slide.
1332
1334
 
1333
1335
  ### Insights — `flowiq insights status|enable|disable|run` (v0.7.0)
1334
1336
 
@@ -1789,7 +1791,7 @@ pairs with reasoning models like `gpt-5.6-luna`; `max` is GPT-6 and Claude only)
1789
1791
  the tool-flag columns (`woo_order_build`, `woo_tip_field`, `woo_order_note_field`,
1790
1792
  `view_cart_tool`, `restock_tool`, `block_tool_status`, `postal_code_tool_status`,
1791
1793
  `shopify_products_web_chat`, `ticket_tool_status`, `product_lookup`,
1792
- `collapse_product_variants`, `email_request_tool`, `silent_option`),
1794
+ `collapse_product_variants`, `email_request_tool`, `silent_option`, `reaction_tool`),
1793
1795
  `discount.enabled`, and the `flowiq test` contact
1794
1796
  (`settings.test_contact_number` / `settings.test_contact_name`). Since 0.9.8 also:
1795
1797
 
@@ -1835,6 +1837,13 @@ no staff WhatsApp — built for businesses that want requests in an inbox rather
1835
1837
  human escalation (GIB Financial Services). The recipient list is set in the Tool
1836
1838
  Library (Agents → Tool Library → Email Request to Team) or with `--email-request-to` (0.9.8).
1837
1839
 
1840
+ **`--tool reaction_tool=true` (added 6 Oct 2026).** Turns on the `react_to_message` tool:
1841
+ the agent reacts to the customer's latest WhatsApp message with one emoji, like a person
1842
+ does (🔍 when it starts a lookup, ✅ on the same message when done, 👍 on a thank-you that
1843
+ needs no reply; with `silent_option` on, the reaction is the whole response). One reaction
1844
+ per message, a new emoji replaces the old one. Meta Cloud API only (nothing on WATI, FlowMod
1845
+ or the other channels). Toggle in Agents → Tool Library → Emoji Reactions.
1846
+
1838
1847
  **`--tool collapse_product_variants=true` (added 4 Aug 2026).** Not a tool toggle —
1839
1848
  it changes what `get_product_info` RETURNS. OFF (the default) the result cap counts
1840
1849
  **variant rows**, so on a catalogue with several packaging/size variants per product
@@ -2299,7 +2308,9 @@ flowiq woo get <org> products --all --fields id,name,permalink,status # past t
2299
2308
  run lists subjects and contacts; `--confirm` asks you to type the org id.
2300
2309
  - **`plans create`** files the same row the MCP's `submit_plan` writes and
2301
2310
  alerts the team the same way; staff are exempt from the client lead time
2302
- (a sub-24 h send is noted, not refused).
2311
+ (a sub-24 h send is noted, not refused). On an org with
2312
+ `broadcast_planning.utility_only` (Lean Living) a `--type marketing` plan is
2313
+ refused, the same as the app and the MCP (server-side, 5 Oct 2026).
2303
2314
  - Not a CLI fix: #186 (Shopify store analytics need the `read_reports` scope
2304
2315
  on the FlowIQ app).
2305
2316
 
package/TEAM-GUIDE.md CHANGED
@@ -121,6 +121,7 @@ When you see it, run `npm i -g @flowapt/flowiq-cli` — a stale version also mea
121
121
  | 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 |
122
122
  | 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) |
123
123
  | Client wants leads/requests emailed to their team instead of a human escalation | `flowiq agent config <org_id> --tool email_request_tool=true --email-request-to a@shop.co.za,b@shop.co.za` |
124
+ | Let the agent react with an emoji (🔍 while it looks something up, ✅ when done, 👍 instead of a reply) | `flowiq agent config <org_id> --tool reaction_tool=true` (WhatsApp on the Meta API only; pair with `--tool silent_option=true` for reaction-only replies) |
124
125
  | Add extra rules to one tool (appended to its description) | `flowiq agent config <org_id> --tool-instruction send_whatsapp_message=@rules.txt` (`tool=` removes it) |
125
126
  | Turn link shortening on/off per channel | `flowiq agent config <org_id> --shorten-links web=on` (`off`, or `default` = on for WhatsApp only) |
126
127
  | Set the agent's email From name / signature | `flowiq agent config <org_id> --email-from-name "Shop Support" --email-signature-name "Laia (AI)" --email-signature-title "Customer care"` |
@@ -168,7 +169,7 @@ When you see it, run `npm i -g @flowapt/flowiq-cli` — a stale version also mea
168
169
  | Remove imported junk emails (auto-replies) across the whole inbox | `flowiq messages purge <org_id> --subject-regex '^Automatic reply' --sender-type user-email` (dry run) → `--confirm` |
169
170
  | Point an order message at a new template version, or fix its params | `flowiq org automations <org_id> --set <type> --template <name> --body param1=full_name,param2=order_number --buttons param1=short_code --commit` |
170
171
  | Reproduce what the agent or the Coworker said to a real customer | `flowiq test seed <org_id> --from <contact_id> --until "YYYY-MM-DD HH:MM" --commit`, open the seeded contact in the inbox, then `flowiq test unseed <org_id> <id> --confirm` |
171
- | File a broadcast plan for a client from the terminal | `flowiq plans create <org_id> --topic … --type utility\|marketing --date YYYY-MM-DD …` (dry run) → `--commit` |
172
+ | File a broadcast plan for a client from the terminal | `flowiq plans create <org_id> --topic … --type utility\|marketing --date YYYY-MM-DD …` (dry run) → `--commit` (utility only on orgs with `broadcast_planning.utility_only`, e.g. Lean Living) |
172
173
  | What did a tool actually return? | `flowiq messages search <org_id> "<text>" --tool-calls` |
173
174
  > **Publishing the CLI (maintainers only):** publish from a clean clone, never
174
175
  > from your working tree — `npm publish` packs whatever is on disk. A
@@ -272,6 +273,8 @@ several.
272
273
  | 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` |
273
274
  | 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 |
274
275
  | See whether a client deck can be approved, and what is still missing | `flowiq report deck status <org_id> 2026-08` |
276
+ | Rewrite one part of a deck's copy without touching the rest (e.g. the changes column and action points) | `flowiq report deck narrate <org_id> 2026-08 --only changes,suggested_updates,action_points` |
277
+ | Check a deck's wording against its figures before it goes out (e.g. a heading that says a group "leads" when it does not) | `flowiq report deck check <org_id> 2026-08` — lists each unsupported statement with a fix; rewrites nothing |
275
278
  | 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 |
276
279
  | Take a tile, pill or card off a client deck (and its PDF) for one month | On `/reporting/deck` open **Inputs** (the rail beside the slides) → Show or hide; or `flowiq report deck inputs <org_id> 2026-08 --file inputs.json` with `{ "hidden": { "tile:tail": true } }` |
277
280
  | Approve a client deck / send it to the client | `flowiq report deck approve <org_id> 2026-08` then `… send … --client` (or let the 09:00 SAST schedule send it on the 5th) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowapt/flowiq-cli",
3
- "version": "0.12.1",
3
+ "version": "0.12.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": {
@@ -48,7 +48,7 @@ export async function flags(opts = {}) {
48
48
  if (opts.model) rows = rows.filter((r) => String(r.model || "").includes(opts.model));
49
49
  if (opts.json) { console.log(JSON.stringify({ ...resp, count: rows.length, agents: rows }, null, 2)); return; }
50
50
  const pad = (s, n) => String(s ?? "").padEnd(n);
51
- const short = { product_lookup: "lookup", ticket_tool_status: "tickets", postal_code_tool_status: "postal", restock_tool: "restock", view_cart_tool: "cart", block_tool_status: "block", email_request_tool: "email-req", shopify_products_web_chat: "web-products", woo_order_build: "woo-order", woo_tip_field: "woo-tip", woo_order_note_field: "woo-note", collapse_product_variants: "collapse", silent_option: "silent" };
51
+ const short = { product_lookup: "lookup", ticket_tool_status: "tickets", postal_code_tool_status: "postal", restock_tool: "restock", view_cart_tool: "cart", block_tool_status: "block", email_request_tool: "email-req", shopify_products_web_chat: "web-products", woo_order_build: "woo-order", woo_tip_field: "woo-tip", woo_order_note_field: "woo-note", collapse_product_variants: "collapse", silent_option: "silent", reaction_tool: "reactions" };
52
52
  console.log(`${rows.length} active agent(s)${opts.tool ? ` with ${opts.tool}` : ""}${opts.model ? ` on model ~${opts.model}` : ""}`);
53
53
  console.log("");
54
54
  console.log(` ${pad("ORG", 26)} ${pad("AGENT", 18)} ${pad("MODEL", 20)} ${pad("REASON", 7)} ${pad("PROMPT", 9)} ${pad("SW", 3)} TOOLS ON`);
@@ -73,11 +73,28 @@ export async function build(orgId, month, opts) {
73
73
 
74
74
  export async function narrate(orgId, month, opts) {
75
75
  requireArgs(orgId, month);
76
- const r = await post("narrate", orgId, month, {}, "Narrate");
76
+ const only = opts.only ? String(opts.only).split(",").map((x) => x.trim()).filter(Boolean) : null;
77
+ const r = await post("narrate", orgId, month, only ? { only } : {}, "Narrate");
77
78
  if (opts.json) { console.log(JSON.stringify(r, null, 2)); return; }
79
+ if (only) console.log(`Rewrote ${only.join(", ")} for ${r.organization?.name ?? orgId} (${month}); every other field kept as written`);
78
80
  reportCopy(r, month);
79
81
  }
80
82
 
83
+ export async function check(orgId, month, opts) {
84
+ requireArgs(orgId, month);
85
+ const r = await post("check", orgId, month, {}, "Check");
86
+ if (opts.json) { console.log(JSON.stringify(r, null, 2)); return; }
87
+ printClaims(r.issues);
88
+ if (r.status) console.log(` status: ${r.status}${r.missing?.length ? ` · needed: ${r.missing.join(" · ")}` : ""}`);
89
+ }
90
+
91
+ function printClaims(issues) {
92
+ if (!Array.isArray(issues)) return;
93
+ if (!issues.length) { console.log(" fact check: passed (every statement is supported by the figures)"); return; }
94
+ console.log(` ⚠ fact check: ${issues.length} statement${issues.length === 1 ? "" : "s"} the figures do not support:`);
95
+ for (const i of issues) console.log(` ${i.path}: "${i.quote}" (${i.why}) → ${i.fix}`);
96
+ }
97
+
81
98
  export async function generate(orgId, month, opts) {
82
99
  requireArgs(orgId, month);
83
100
  const r = await post("generate", orgId, month, { refresh: !!opts.refresh, force: !!opts.force }, "Generate");
@@ -92,6 +109,7 @@ function reportCopy(r, month) {
92
109
  const n = Object.keys(r.guard.offenders || {}).length;
93
110
  console.log(` ⚠ numeric guard: ${n} field${n === 1 ? "" : "s"} carry figures that are not in the data — flagged on the slides: ${Object.keys(r.guard.offenders).join(", ")}`);
94
111
  } else if (r.guard) console.log(" numeric guard: passed (every figure in the copy exists in the data)");
112
+ if (r.guard?.claim_check?.ran) printClaims(r.guard.claim_check.issues);
95
113
  if (Array.isArray(r.length_issues) && r.length_issues.length) console.log(` trimmed to limit: ${r.length_issues.map((i) => `${i.path} (${i.length}→${i.limit})`).join(", ")}`);
96
114
  }
97
115
 
package/src/index.js CHANGED
@@ -377,9 +377,14 @@ export function run(argv) {
377
377
  .option("--json", "raw JSON output")
378
378
  .action((orgId, month, opts) => reportCmd.build(orgId, month, opts));
379
379
  deck.command("narrate <organization_id> <month>")
380
- .description("Rewrite the ten slides' copy (LLM, under the report rules + numeric guard); hand edits are kept")
380
+ .description("Rewrite the report's copy (LLM, under the report rules + numeric guard); hand edits are kept")
381
+ .option("--only <fields>", "rewrite only these copy fields, comma-separated (e.g. changes,suggested_updates,action_points); the rest stays as written")
381
382
  .option("--json", "raw JSON output")
382
383
  .action((orgId, month, opts) => reportCmd.narrate(orgId, month, opts));
384
+ deck.command("check <organization_id> <month>")
385
+ .description("Fact-check the copy as the slides show it (hand edits included) against the month's figures; rewrites nothing")
386
+ .option("--json", "raw JSON output")
387
+ .action((orgId, month, opts) => reportCmd.check(orgId, month, opts));
383
388
  deck.command("generate <organization_id> <month>")
384
389
  .description("Build the metrics (if missing, or --refresh) and write the copy")
385
390
  .option("--refresh", "rebuild the metrics first")
@@ -618,7 +623,7 @@ export function run(argv) {
618
623
  .option("--model <model>", "settings.model (e.g. gpt-6-luna, the house default)")
619
624
  .option("--reasoning-effort <level>", "settings.reasoning_effort — low|medium|high|xhigh|max (\"\" clears it; max = GPT-6 and Claude only); pairs with reasoning models (e.g. gpt-6-luna + high, gpt-6-luna + high, claude-sonnet-5 + medium)")
620
625
  .option("--rename <name>", "rename the agent")
621
- .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/silent_option", agentConfigCmd.collectTool, [])
626
+ .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/silent_option/reaction_tool", agentConfigCmd.collectTool, [])
622
627
  .option("--discount [bool]", "agent.discount.enabled (true if bare)")
623
628
  .option("--test-contact-number <number>", "settings.test_contact_number — the contact `flowiq test` uses (use a FAKE number; \"\" clears it → synthetic fallback)")
624
629
  .option("--test-contact-name <name>", "settings.test_contact_name — display name for the test contact (\"\" clears it)")