@flowapt/flowiq-cli 0.9.7 → 0.9.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -77,9 +77,13 @@ Round-trips an org's active WhatsApp agent's system prompt
77
77
  flowiq prompts pull <organization_id>
78
78
  # edit ./.flowiq/prompts/<slug>.json — modify section titles/content
79
79
  flowiq prompts push <slug>
80
+ flowiq prompts pull <organization_id> --out /tmp/check.json # a scratch copy; the working file is untouched (0.9.8)
80
81
  ```
81
82
 
82
83
  Notes:
84
+ - **A new section needs no id (0.9.8).** Add `{ "title": …, "content": … }` and push:
85
+ the CLI writes a fresh uuid into the local file first, says which sections got
86
+ one, then pushes, so the next pull keeps the same id.
83
87
  - `system_prompt` is regenerated server-side from `prompt_sections` on
84
88
  push using the `## SECTION: <title>` format.
85
89
  - Sections support optional `hidden: boolean` + `channels: string[]` (`web`, `whatsapp`, `messenger`, `instagram`, `email`; empty = every channel)
@@ -348,12 +352,17 @@ flowiq bc route <org_id> <broadcastId> --clear --commit # switc
348
352
  the **full broadcastId** per row + template, status, recipient count and SAST
349
353
  created time — the discovery step `status` / `retry` need (previously the id
350
354
  existed only in the send output or the DB). `--limit <n>` (default 25, max 200),
351
- `--template <substr>` (case-insensitive contains), `--since <date>`, `--json`.
352
- Read-only. NOTE: plain `bc list` (no org) still lists your **local campaign
355
+ `--template <substr>` (case-insensitive contains), `--since <date>`,
356
+ `--min-recipients <n>` (0.9.8: `2` hides one-recipient traffic like popup welcomes
357
+ and cart reminders, which otherwise fill the cap and hide campaign sends), `--json`.
358
+ When more rows match than are shown it prints `N of M shown` and says so. Read-only. NOTE: plain `bc list` (no org) still lists your **local campaign
353
359
  files** — the remote verb is named after `pinboard list-remote`.
354
360
  - **`status <org> <broadcastId>` (v0.3.5)**: live delivery **funnel** for a
355
361
  broadcast by its id — `accepted` → `delivered` → `read`, plus `pending` and
356
- `failed`, with percentages, from `helpdesk_messages`. Built for the `--python`
362
+ `failed`, with percentages, from `helpdesk_messages`. Since 0.9.8 it also prints
363
+ what the send said (`sent with:` header / body / button slot values) and, for a
364
+ button short code, the link's destination with its UTM tags and click count
365
+ (`payload` in `--json`). Built for the `--python`
357
366
  fire-and-forget engine (which returns a `broadcastId` but has no CLI status
358
367
  log), but works for any broadcast id (dashboard sends included).
359
368
  **`--failures` (v0.3.7)** additionally lists each failed recipient + the Meta
@@ -654,6 +663,14 @@ flowiq doctor --json # same, machine-readable; exits 1 when something needs fi
654
663
  ✓ server contract 0.6.8 or newer · build 1f0fd48c
655
664
  ```
656
665
 
666
+ **A Vercel security checkpoint is named as one (0.9.8).** When Vercel's bot
667
+ protection challenges this network, every command (and `doctor`) now says so:
668
+ "Vercel is challenging this network… not your key". Wait a few minutes or switch
669
+ network; do not re-login and do not narrow the request. Only a JSON 401/403 from
670
+ the API means the key itself was refused. A timeout on a WRITE now says the server
671
+ may have finished it (check before retrying); `templates create` names the exact
672
+ `templates status --live` command to check with.
673
+
657
674
  **Three ways this install tells you it is stale**, so nobody — person or AI
658
675
  assistant — has to remember to check:
659
676
 
@@ -745,6 +762,12 @@ Spring Promotion is **`9Sep_SpringPromotion`**.
745
762
 
746
763
  - The resolved tag is **printed before anything is minted**, and `shorten` is
747
764
  dry-run by default, so you always see it first.
765
+ - **A running campaign never forks (0.9.8).** Before minting, the org's tags from
766
+ the last 120 days are checked: when one normalises to the same tag but is
767
+ spelled differently (`5Sept_BraaiDayOffer` running, you asked for
768
+ `5Sep_BraaiDayOffer`), the dry run prints `⚠ CAMPAIGN FORK` and `--commit` is
769
+ refused. Reuse the running tag with `--campaign 5Sept_BraaiDayOffer --raw-campaign`,
770
+ or pass `--new-campaign` when it really is a separate campaign.
748
771
  - `--date` takes `9Sep`, `2026-09-24` or `24/9/2026` — use it whenever the send
749
772
  is not today.
750
773
  - `--content` is the **per-link** tag (which button, which product), so it gets
@@ -797,6 +820,17 @@ flowiq kw push <slug> # id-keyed upsert (never touches keywor
797
820
  flowiq kw push <slug> --prune # ALSO deletes DB keywords missing from the file (dry-run first!)
798
821
  ```
799
822
 
823
+ - **Runtime order and collisions (0.9.8).** `kw pull` lists ACTIVE rows in the
824
+ order the runtime tries them (by `type`, first match answers), then the inactive
825
+ ones, and prints `⚠ TRIGGER COLLISION` when two active rows match the same
826
+ message: which row wins, and whether `allowed_tags` lets the other one answer
827
+ some contacts. `kw push` warns the same way (a dry run predicts the order).
828
+ - **Schedule labels.** The keyword-scheduler cron switches `active` on at
829
+ `start_date` and off at `end_date` (every 30 min); the runtime reads `active`
830
+ only. So `kw pull` shows `starts … SAST` on a scheduled inactive row, and a loud
831
+ `⚠ LIVE BEFORE ITS START` on an active row whose start is still ahead (it is
832
+ answering now), or `⚠ PAST ITS END` on one not yet switched off.
833
+
800
834
  - **Identity = the `id`s.** Keep an id to update that row; drop the id to
801
835
  create a new one; remove an *action* from a kept keyword to delete it.
802
836
  A file id that doesn't exist for the org aborts (stale file → re-pull).
@@ -915,7 +949,11 @@ flowiq audit show <audit_id> --out entry.json # dump the whole entry
915
949
  ```
916
950
 
917
951
  Filters: `--endpoint --action --status --agent --target --user --since --until
918
- --limit` (default 50, max 200) `--json`.
952
+ --before --limit` (default 50, max 200) `--json`. `--since` / `--until` take a date
953
+ (SAST day start), an ISO time, or a window like `2d` / `12h` / `2w` (0.9.8). A
954
+ `--limit` above 200 is clamped (it used to fall back to 50 silently); when more
955
+ entries match than are shown, the header reads `200 of 1,044 matching entries`
956
+ and the footer prints the `--before <created_at>` that pages further back.
919
957
 
920
958
  **What gets logged.** Every command that CHANGES something writes a row —
921
959
  including two that were missed until 4 Aug 2026 and are now covered:
@@ -1387,7 +1425,8 @@ create call, including the ones that failed and why.
1387
1425
 
1388
1426
  ```bash
1389
1427
  flowiq templates pull <organization_id> # → ./.flowiq/templates/<slug>.json (Meta-side truth)
1390
- flowiq templates status <organization_id> --name booking # poll approval
1428
+ flowiq templates status <organization_id> --name booking # FlowIQ's record (webhook-kept status)
1429
+ flowiq templates status <organization_id> --live --name booking --watch # straight from Meta, re-checked every 15 s until decided (0.9.8)
1391
1430
  flowiq templates show <organization_id> heritage_day_v2 # render ONE row, drafts included
1392
1431
  flowiq templates create <organization_id> --request-file req.json
1393
1432
  flowiq templates attempts <organization_id> --failed # why a submit was refused (v0.8.1)
@@ -1540,6 +1579,14 @@ Looking up stock for "1 litre Oil Pourer" — the calls being made:
1540
1579
  `stock` prints **"not tracked"** where Shopify returns `available: null` — that
1541
1580
  means no inventory record exists at that location, which is not the same as zero.
1542
1581
 
1582
+ **No exact title? It asks Shopify's product search (0.9.8).** The first call is an
1583
+ exact `products.json?title=`; when that finds nothing, the recipe runs Shopify's
1584
+ own product search over the WHOLE catalogue (it used to scan only the first 250
1585
+ products and then say "Nothing matched" on a 3,594-product store). Titles holding
1586
+ every word you typed are kept; if none do, Shopify's closest matches are shown and
1587
+ labelled as such. Search syntax passes through: `"sku:5702018068984"`,
1588
+ `"handle:up-scaled-darth-vader"`. Per-location levels cover the first 50 variants.
1589
+
1543
1590
  #### Counting things — read `precision` before you trust a number
1544
1591
 
1545
1592
  Shopify's `*Count` fields return `{count, precision}`. **`precision: "AT_LEAST"`
@@ -1712,6 +1759,18 @@ flowiq agent config <organization_id> --model gpt-6-luna --reasoning-effort high
1712
1759
  flowiq agent config <organization_id> --model claude-sonnet-5 --reasoning-effort medium # Claude (needs the org's Claude key; warns if missing)
1713
1760
  flowiq agent config <organization_id> --disable-base-tool get_product_info
1714
1761
  flowiq agent config <organization_id> --enable-base-tool get_product_info
1762
+ # 0.9.8: the fields that used to need SQL
1763
+ flowiq agent config <org> --tool-instruction send_whatsapp_message=@rules.txt # appended to that tool's description ("tool=" removes)
1764
+ flowiq agent config <org> --shorten-links web=on --shorten-links whatsapp=default
1765
+ flowiq agent config <org> --email-from-name "Litehouse Support" --email-signature-name "Laia (AI)" --email-signature-title "Customer care"
1766
+ flowiq agent config <org> --email-request-to a@shop.co.za,b@shop.co.za --tool email_request_tool=true
1767
+ flowiq agent config <org> --send-success-rule "image=Stay silent after your last image." --send-success-rule "document="
1768
+ flowiq agent config <org> --after-escalation "Tell the customer the team replies here; never repeat what you already sent."
1769
+ flowiq agent config <org> --knowledge enabled=true,match_count=8,min_similarity=0.3
1770
+ flowiq agent config <org> --human-notify @human-notify.json # merged into human_notify; null deletes a key
1771
+ flowiq agent config <org> --order-notify '{"enabled":false}'
1772
+ flowiq agent config <org> --tool silent_option=true
1773
+ flowiq agent config <org> --full # show mode without truncating long values (also --json)
1715
1774
  ```
1716
1775
 
1717
1776
  Settable: `settings.use_settings_prompt`, `settings.model`,
@@ -1720,10 +1779,26 @@ pairs with reasoning models like `gpt-5.6-luna`; `max` is GPT-6 and Claude only)
1720
1779
  the tool-flag columns (`woo_order_build`, `woo_tip_field`, `woo_order_note_field`,
1721
1780
  `view_cart_tool`, `restock_tool`, `block_tool_status`, `postal_code_tool_status`,
1722
1781
  `shopify_products_web_chat`, `ticket_tool_status`, `product_lookup`,
1723
- `collapse_product_variants`, `email_request_tool`),
1782
+ `collapse_product_variants`, `email_request_tool`, `silent_option`),
1724
1783
  `discount.enabled`, and the `flowiq test` contact
1725
- (`settings.test_contact_number` / `settings.test_contact_name`). Anything else is
1726
- rejected; every change is reported before → after.
1784
+ (`settings.test_contact_number` / `settings.test_contact_name`). Since 0.9.8 also:
1785
+
1786
+ | Flag | Stored in | Runtime reader |
1787
+ |---|---|---|
1788
+ | `--tool-instruction <tool>=<text\|@file>` (repeatable; `tool=` removes) | `settings.tool_instructions` | appended to that tool's description (`tool-instructions.ts`); a name with no such tool is skipped and logged |
1789
+ | `--shorten-links <channel>=on\|off\|default` (repeatable) | `disable_url_shortening` | channels whatsapp / web / messenger / instagram / tiktok / email / call; `default` = on for WhatsApp only |
1790
+ | `--email-from-name` / `--email-signature-name` / `--email-signature-title` (`""` clears) | `settings.email_*` | the From name and signature on the agent's email replies |
1791
+ | `--email-request-to a,b` (`""` clears) / `--email-request-subject-prefix` | `settings.email_request` | `email_request_to_team`; warns while `email_request_tool` is off |
1792
+ | `--send-success-rule <when>=<text\|@file>` (repeatable; `when=` removes) / `--send-success <bool>` | `send_message_success_instructions` | one rule per `when` (media / image / video / document / audio / text / always), appended to the send tool's "sent" reply |
1793
+ | `--after-escalation <text\|@file>` (`""` clears) | `human_notify.success_instructions` | the NEXT STEP appended to the handover tool's reply |
1794
+ | `--knowledge enabled=…,match_count=…,min_similarity=…` (`default` clears a key) | `knowledge_config` | `search_knowledge` (match_count 1-20, min_similarity 0-1) |
1795
+ | `--human-notify` / `--order-notify <json\|@file.json>` | `human_notify` / `order_notify` | top-level merge, `null` deletes a key; unknown keys and wrong types are refused |
1796
+
1797
+ Show mode (no write flags) prints every one of these, collapses long values to one
1798
+ line (`--full` prints them whole, `--json` gives the raw response) and lists what
1799
+ is settable. Anything else is rejected; every change is reported before → after.
1800
+ Not settable here: `additional_config` (use `flowiq knowledge`) and
1801
+ `additional_tools` (use `flowiq ct`).
1727
1802
 
1728
1803
  `--disable-base-tool` / `--enable-base-tool` can be repeated. They manage only the
1729
1804
  allowlisted discovery/commerce built-ins; opt-out, human handover and channel-send
@@ -1748,7 +1823,7 @@ addresses in `agents.settings.email_request.to`, from flowiq@flowapt.com with Re
1748
1823
  set to the customer, and leaves an internal note on the thread. No ticket, no bot-off,
1749
1824
  no staff WhatsApp — built for businesses that want requests in an inbox rather than a
1750
1825
  human escalation (GIB Financial Services). The recipient list is set in the Tool
1751
- Library (Agents → Tool Library → Email Request to Team); the CLI cannot set it yet.
1826
+ Library (Agents → Tool Library → Email Request to Team) or with `--email-request-to` (0.9.8).
1752
1827
 
1753
1828
  **`--tool collapse_product_variants=true` (added 4 Aug 2026).** Not a tool toggle —
1754
1829
  it changes what `get_product_info` RETURNS. OFF (the default) the result cap counts
@@ -1798,8 +1873,14 @@ flowiq au pull <organization_id> --before 20 --after 10 --no-files
1798
1873
 
1799
1874
  flowiq au resolve <update_id> --note "We updated the agent so it no longer …"
1800
1875
  flowiq au resolve <update_id> --status declined --internal "duplicate of …"
1876
+ flowiq au resolve <update_id> --status in_progress --note "Fixed; waiting to see it on a real order" # 0.9.8
1801
1877
  ```
1802
1878
 
1879
+ - **`--status in_progress`** puts a ticket in progress without closing it: no
1880
+ resolution stamp (an old one is cleared, as the app's reopen does), the note is
1881
+ optional and an earlier client note is kept when you pass none. Use it when the
1882
+ fix shipped but cannot be proven yet.
1883
+
1803
1884
  - **`--note` is what the client reads** (rendered as `FlowIQ: <note>` in their
1804
1885
  console) and is **required** when resolving; `--internal` is staff-only.
1805
1886
  - The typical flow is `prompts push → flowiq test → au resolve` — resolve is
@@ -1881,7 +1962,7 @@ flowiq gaps list --mine
1881
1962
  flowiq gaps show 12 # detail, verbatim error, every hit / note / status change
1882
1963
  flowiq gaps hit 12 --note "same on Barkyn IT while copying the escalation config"
1883
1964
  flowiq gaps note 12 "also needs --agent"
1884
- flowiq gaps status 12 --to done --version 0.9.8 --note "agent config --tool-instruction name=@file.txt" # maintainers
1965
+ flowiq gaps status 12 --to done --fixed-in 0.9.8 --note "agent config --tool-instruction name=@file.txt" # maintainers
1885
1966
  flowiq gaps status 14 --to duplicate --of 12 # hits move to #12
1886
1967
  ```
1887
1968
 
@@ -1905,6 +1986,7 @@ flowiq gaps status 14 --to duplicate --of 12
1905
1986
  to. `--no-notify` closes quietly.
1906
1987
  - **A command or flag that does not exist** now ends its error with the exact
1907
1988
  `flowiq gaps report …` line to file it, `--command` and `--error` filled in.
1989
+ - **In the app:** the Changelog dialog has a **CLI requests** tab (super admins) showing the same list, filterable by status and kind, with each request's detail and history. It is read only; reporting and closing stay in the CLI.
1908
1990
  - Every report / hit / note / status change is audited
1909
1991
  (`flowiq audit --endpoint gaps`). A report or hit from anyone but Matt also
1910
1992
  emails him through the standing CLI-write alert.
@@ -1953,6 +2035,16 @@ messages (`memory_cutoff`) AND sets aside the agent's remembered tool results
1953
2035
  (`more_data.tool_usage_history`, which the agent reads as TOOL USAGE HISTORY) under
1954
2036
  `more_data.tool_usage_history_cleared`, so a cleared test really starts fresh. Since 0.9.6;
1955
2037
  before that earlier tool results and escalation reasons leaked into "cleared" tests.
2038
+ `test clear` prints the PREVIOUS `memory_cutoff` so a clear can be undone (0.9.8).
2039
+
2040
+ **Real-customer guard (0.9.8).** The configured test contact can be a real person
2041
+ (a client's reviewer, a dormant customer). When it had real (non-web) inbound in
2042
+ the last 6 h, `send`, `scenario`, `qa` and `clear` are refused with the latest
2043
+ message time; `--force` overrides. A test contact that has ever messaged on a real
2044
+ channel gets a warning on every run. Point the agent at a fake number with
2045
+ `flowiq agent config <org> --test-contact-number 27000000001`. A test contact the
2046
+ CLI creates is born with `allow_broadcast=false` and the `test-contact` tag, so it
2047
+ never lands on a broadcast list (stress contacts too).
1956
2048
 
1957
2049
  ### Guide — `flowiq guide`
1958
2050
 
package/TEAM-GUIDE.md CHANGED
@@ -81,14 +81,14 @@ When you see it, run `npm i -g @flowapt/flowiq-cli` — a stale version also mea
81
81
 
82
82
  | I want to… | Run |
83
83
  |---|---|
84
- | Edit a client agent's system prompt | `flowiq prompts pull <org_id>` → edit the JSON → `flowiq prompts push <slug>` |
84
+ | Edit a client agent's system prompt | `flowiq prompts pull <org_id>` → edit the JSON → `flowiq prompts push <slug>` (a new section needs no id; the push adds one) |
85
85
  | Edit the questionnaire answers | `flowiq q pull <org_id>` → edit → `flowiq q push <slug>` |
86
86
  | Edit fine-tuning instructions / Q&A | `flowiq ft pull <org_id>` → edit → `flowiq ft push <slug>` |
87
87
  | Edit the text knowledge sources (get_more_answers playbooks) | `flowiq kn pull <org_id>` → edit `sources[]` → `flowiq kn push <slug>` |
88
88
  | Edit the custom tools (API-call tools) | `flowiq ct pull <org_id>` → edit `tools[]` → `flowiq ct push <slug> --dry-run` → `flowiq ct push <slug>` |
89
89
  | Turn one custom tool on/off | `flowiq ct enable\|disable <org_id> <tool_name>` |
90
90
  | Give a custom tool a friendly name on the inbox tool-activity card | add `"display_name": "Checked loyalty points"` to that tool in `tools[]` → `flowiq ct push` (≤80 chars, never shown to the AI; empty = derived from the tool name) |
91
- | Edit keyword auto-replies (incl. competition entry keywords, add/remove-tag, set-agent and delay actions) | `flowiq kw pull <org_id>` → edit → `flowiq kw push <slug> --dry-run` → `flowiq kw push <slug>` |
91
+ | Edit keyword auto-replies (incl. competition entry keywords, add/remove-tag, set-agent and delay actions) | `flowiq kw pull <org_id>` → edit → `flowiq kw push <slug> --dry-run` → `flowiq kw push <slug>`. `kw pull` flags two active keywords answering the same word (and which one wins) and a keyword that is live before its start date |
92
92
  | Send a **different auto-reply depending on the contact** (e.g. "we already have your email" vs "send us your email") | add `"when": {"field":"email","op":"is_not_empty"}` to one action and `is_empty` to the other — see *Keywords* in `flowiq guide --reference`. CLI-only, no dashboard field yet |
93
93
  | Make a follow-up button **close the customer's ticket** ("Query solved") or **re-alert the team** ("I still need help") | keyword action `{"type":"update_ticket_status","status":"resolved"}` / `{"type":"renotify_ticket"}` — see *Keywords* in `flowiq guide --reference`. CLI-only, no dashboard field yet |
94
94
  | Make a keyword/button **hand the chat to a team or person** (assign in the inbox + email/WhatsApp them) | keyword action `{"type":"assign_chat","team_id":"…","assignee_user_id":"…","notify_member":true}` — see *Keywords* in `flowiq guide --reference`. Also in the dashboard (action type "Assign Chat") |
@@ -104,7 +104,7 @@ When you see it, run `npm i -g @flowapt/flowiq-cli` — a stale version also mea
104
104
  | 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` |
105
105
  | "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 |
106
106
  | 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 |
107
- | 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 |
107
+ | 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. Part of the title is enough (it searches the whole catalogue); `"sku:…"` works too |
108
108
  | Has this order shipped? What's the tracking? | `flowiq shopify order <org_id> '#14728'` — status, courier, tracking number + link |
109
109
  | **Size a customer cohort** | Use `customers(first:250, query:…)` and paginate — **NOT `customersCount(query:…)`, which Shopify ignores the filter on** and answers 10,000 every time. Any count showing `precision: AT_LEAST` is a cap, not a total; the CLI warns you. |
110
110
  | A big `--all` failed with a timeout | It's transient, not a limit — retry, or narrow it with `--max-pages` / a smaller `--q limit`. The message now says so. |
@@ -120,16 +120,22 @@ When you see it, run `npm i -g @flowapt/flowiq-cli` — a stale version also mea
120
120
  | 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) |
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
- | 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) |
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
+ | 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
+ | Turn link shortening on/off per channel | `flowiq agent config <org_id> --shorten-links web=on` (`off`, or `default` = on for WhatsApp only) |
126
+ | 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"` |
127
+ | Tell the agent what to do right after it sends an image / after a handover | `flowiq agent config <org_id> --send-success-rule "image=…"` / `--after-escalation "…"` |
128
+ | Knowledge search settings, human_notify / order_notify keys | `flowiq agent config <org_id> --knowledge enabled=true,match_count=8` · `--human-notify @file.json` · `--order-notify '{"enabled":false}'` (merged; `null` deletes a key) |
129
+ | See EVERYTHING an agent is configured with | `flowiq agent config <org_id>` (`--full` for long values, `--json` raw) |
124
130
  | Talk to the live agent safely (no real WhatsApp ever sent) | `flowiq test send <org_id> "hi, do you sell X?"` |
125
- | Start a test chat from scratch (agent forgets the earlier test messages AND its earlier tool results) | `flowiq test clear <org_id>` (or `flowiq test send <org_id> "…" --clear`) |
131
+ | Start a test chat from scratch (agent forgets the earlier test messages AND its earlier tool results) | `flowiq test clear <org_id>` (or `flowiq test send <org_id> "…" --clear`). It prints the previous `memory_cutoff`, and refuses when the test contact is a real customer who wrote in the last 6 h (`--force` overrides; better: use a fake test number) |
126
132
  | **Tell the team what shipped** (the FlowIQ team update email) | `flowiq updates draft` → enrich the JSON (screenshots via `flowiq updates asset`, a *For the team* note per headline) → `flowiq updates preview <file> --open` → `flowiq updates send <file> --test` → `flowiq updates send <file> --yes` |
127
133
  | Can I use `flowiq updates`? | Yes: `status`, `uncovered`, `draft`, `preview` and `send --test` (a copy to yourself) work for everyone. Only the real `send --yes` to the whole team is Matt's; anyone else gets a 403 there |
128
134
  | Which changelog rows has nobody emailed the team about yet? | `flowiq updates status` / `flowiq updates uncovered` — anything left 20h+ goes out automatically at 08:30 SAST as a plain digest |
129
135
  | Set which contact `flowiq test` uses (use a FAKE number!) | `flowiq agent config <org_id> --test-contact-number 27000000001 --test-contact-name "QA Bot"` |
130
- | **The CLI can't do something I need** (or does it wrong, or should do more) | `flowiq gaps report "agent config cannot set tool_instructions" --command "agent config" --error "<exact error>" --workaround "<what you did instead>"`. If it is already on the list you are told its number: `flowiq gaps hit <number> --note "…"` |
131
- | See what the team has asked the CLI to do | `flowiq gaps list` (open, most-hit first) · `flowiq gaps list --mine` · `flowiq gaps show <number>` |
132
- | Close a request (Matt / Gidon) | `flowiq gaps status <number> --to done --version 0.9.8 --note "what shipped"` — emails everyone who reported or hit it |
136
+ | **The CLI can't do something I need** (or does it wrong, or should do more) | `flowiq gaps report "<command> cannot <thing>" --command "<command>" --error "<exact error>" --workaround "<what you did instead>"`. If it is already on the list you are told its number: `flowiq gaps hit <number> --note "…"` |
137
+ | See what the team has asked the CLI to do | `flowiq gaps list` (open, most-hit first) · `flowiq gaps list --mine` · `flowiq gaps show <number>` · in the app: Changelog → **CLI requests** |
138
+ | Close a request (Matt / Gidon) | `flowiq gaps status <number> --to done --fixed-in 0.9.8 --note "what shipped"` — emails everyone who reported or hit it |
133
139
  > **Publishing the CLI (maintainers only):** publish from a clean clone, never
134
140
  > from your working tree — `npm publish` packs whatever is on disk. A
135
141
  > `prepublishOnly` check now stops you if `cli/` differs from `origin/main`.
@@ -165,7 +171,7 @@ Getting this right is what makes a campaign's clicks and revenue group together
165
171
  in reporting — a tag typed a different way each time splits one campaign into
166
172
  several.
167
173
 
168
- | **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 |
174
+ | **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. If the campaign already runs under another spelling (`5Sept_…` vs `5Sep_…`) it refuses and tells you to reuse the running tag (`--raw-campaign`) or confirm with `--new-campaign` |
169
175
  | 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 |
170
176
  | 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 |
171
177
  | Short links for every URL in a message | `flowiq links shorten <org_id> --file ./message.txt --campaign "Spring Promotion" --commit` |
@@ -192,19 +198,21 @@ several.
192
198
  | Send a broadcast whose link button should open a page on the store, tracked | Paste the real link: `flowiq bc send <org_id> --tag <batch-tag> --template <name> --button param1="https://shop.co.za/collections/spring?ref=ig"` (dry run shows `→ linklnk.io/<code>`, the campaign tag and anything replaced) → `… --commit` mints the code and sends. `--link-campaign "Spring Promotion"` names the tag (default: the template name); `--keep-link` sends your link untouched with no FlowIQ tags (clicks counted, the sale will not show as WhatsApp) |
193
199
  | Send a template with TWO link buttons (e.g. one per product) and track clicks per batch | Mint two short links per batch with the same campaign and a different `--content` (e.g. `--content CycleHarmony_b01` and `--content NeuroShyft_b01`), then `flowiq bc send <org_id> --tag <batch-tag> --template <name> --button param1=<first-code> --button param2=<second-code>`. `param1` is the first link button on the template, `param2` the second. The dry run prints both final URLs and stops if either is missing. Tag mode only (v0.7.5). |
194
200
  | Send via the SAME engine as the dashboard's "Python" toggle | add `--python` to a `bc send --tag …` (fire-and-forget; python resolves the tag + sends + tracks; no CLI resume for this engine). **Any tag send over 10 recipients uses python automatically.** |
195
- | Find a broadcast's id (don't have the `broadcastId`?) | `flowiq bc list-remote <org_id>` — the org's broadcasts newest-first with full ids (`--template <substr>` / `--since <date>` / `--limit <n>` to narrow) |
196
- | Check how a broadcast is landing (accepted → delivered → read, plus failed/pending) | `flowiq bc status <org_id> <broadcastId>` (from the send output, or `bc list-remote`) |
201
+ | Find a broadcast's id (don't have the `broadcastId`?) | `flowiq bc list-remote <org_id>` — the org's broadcasts newest-first with full ids (`--template <substr>` / `--since <date>` / `--limit <n>` to narrow; `--min-recipients 2` hides welcomes and cart reminders) |
202
+ | Check how a broadcast is landing (accepted → delivered → read, plus failed/pending) and what it said | `flowiq bc status <org_id> <broadcastId>` — also prints the slot values, the button code and the link's destination (UTM tag) |
197
203
  | See WHO failed on a broadcast + why (Meta error) | `flowiq bc status <org_id> <broadcastId> --failures` |
198
204
  | Re-send a broadcast to only the ones that failed | `flowiq bc retry <org_id> <broadcastId>` (dry-run) → `… --commit` |
199
205
  | Make EVERY reply to a broadcast land with a team or a person (client asks "all replies to the Spa team") | Add `--route-team "Spa"` (and/or `--route-member kim@client.com`) to the `bc send`. The first reply from each recipient within 72 hours (`--route-window 48h` to change) assigns the chat and notifies them, typed replies and button taps alike. The AI agent and keywords still answer as normal, so nothing is muted. The dry run shows `Replies → team Spa …`; a wrong team name stops the run and lists the org's teams. Works with `--csv` and `--at` too. |
200
206
  | Add, check or remove that routing on a broadcast that already went out | `flowiq bc route <org_id> <broadcastId>` shows it and how many chats it has routed · `… --team "Spa" --commit` sets it · `… --clear --commit` removes it. Only replies from now on are affected. |
201
207
  | **Get an OLD version of a prompt back** | `flowiq prompts history <org_id>` (pick the version) → `flowiq prompts restore <org_id> <audit_id>` (dry-run) → `… --commit` |
202
- | **See who changed what, and when** | `flowiq audit <org_id>` — add `--endpoint prompts`, `--user <name>`, `--since 2026-07-01` to narrow |
208
+ | **See who changed what, and when** | `flowiq audit <org_id>` — add `--endpoint prompts`, `--user <name>`, `--since 2d` (or a date) to narrow; `N of M` in the header means there is more (`--before` pages back) |
203
209
  | See exactly what a change looked like (before → after) | `flowiq audit show <audit_id> --content` (or `--out entry.json`) |
204
210
  | Know what the log does and doesn't keep | Anything that CHANGES something is logged — including `flowiq test` (it creates the test contact and clears conversations) and `flowiq auth refresh`. Reads and dry-runs are not. The log never stores the content itself: chats, store data and agent replies are recorded as "who read what", never copied. |
205
211
  | 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 |
206
212
  | Export an org's full chat history | `flowiq export chats <org_id>`. If it stops mid-way it says which page failed and nothing is written, so just re-run it (a 502/503/504 page retries on its own) |
207
213
  | Check / create WhatsApp templates | `flowiq tpl pull <org_id>` / `flowiq tpl create <org_id> --request-file req.json` |
214
+ | Is my new template approved yet? | `flowiq tpl status <org_id> --live --name <name> --watch` — straight from Meta every 15 s until decided, with the rejection reason. If a create timed out, run this before retrying (it may already exist) |
215
+ | A command says "Vercel is challenging this network" | Not your key: wait a few minutes or switch network (hotspot). Don't log in again |
208
216
  | **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 |
209
217
  | 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 |
210
218
  | **A template would not submit and you want to know why** | `flowiq tpl attempts <org_id> --failed` — the submission ledger. Every create call since 21 Sep 2026, from the dashboard or the CLI, with the reason a refused one was refused (e.g. `HEADER format is DOCUMENT but the media at file_url is video/mp4`). A failed submit writes no template row and never reaches Meta, so this is the ONLY place it shows |
@@ -214,6 +222,7 @@ several.
214
222
  | 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`) |
215
223
  | See one client's pending change requests, with the chat around each | `flowiq au pull <org_id>` |
216
224
  | Close a client's change request (after verifying the fix!) | `flowiq au resolve <update_id> --note "what changed"` — the client reads the note |
225
+ | Fix shipped but not provable yet | `flowiq au resolve <update_id> --status in_progress --note "…"` (stays open, no resolution stamp) |
217
226
  | Check an org's platform + active agent | `flowiq org info <org_id>` |
218
227
  | **See every Settings → Profile switch on an org** | `flowiq org flags show <org_id>` (credentials are never shown) |
219
228
  | **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` |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowapt/flowiq-cli",
3
- "version": "0.9.7",
3
+ "version": "0.9.9",
4
4
  "description": "Command-line tool for FlowIQ staff: round-trip agent prompts, questionnaires, fine-tuning, pin-board tasks, webhooks, templates, agent-updates, chat exports, and live agent testing without ever touching service-role credentials.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,39 @@
1
+ // node --test src/agent-config.test.mjs
2
+ import test from "node:test";
3
+ import assert from "node:assert/strict";
4
+ import { writeFileSync, mkdtempSync } from "node:fs";
5
+ import { tmpdir } from "node:os";
6
+ import { join } from "node:path";
7
+ import { splitPair, parseShorten, parseKnowledge, parseJsonObject, readValue } from "./commands/agent-config.js";
8
+
9
+ test("splitPair keeps '=' inside the value", () => {
10
+ assert.deepEqual(splitPair("send_whatsapp_message=use a=b style", "x"), ["send_whatsapp_message", "use a=b style"]);
11
+ assert.deepEqual(splitPair("document=", "x"), ["document", ""]);
12
+ });
13
+
14
+ test("parseShorten maps on/off/default", () => {
15
+ assert.equal(parseShorten("on", "x"), true);
16
+ assert.equal(parseShorten("OFF", "x"), false);
17
+ assert.equal(parseShorten("default", "x"), null);
18
+ });
19
+
20
+ test("parseKnowledge types each pair and clears on default", () => {
21
+ assert.deepEqual(parseKnowledge("enabled=true, match_count=8,min_similarity=0.35"), { enabled: true, match_count: 8, min_similarity: 0.35 });
22
+ assert.deepEqual(parseKnowledge("min_similarity=default"), { min_similarity: null });
23
+ });
24
+
25
+ test("readValue reads @file and drops one trailing newline", () => {
26
+ const dir = mkdtempSync(join(tmpdir(), "flowiq-ac-"));
27
+ const f = join(dir, "t.txt");
28
+ writeFileSync(f, "line one\nline two\n");
29
+ assert.equal(readValue(`@${f}`, "x"), "line one\nline two");
30
+ assert.equal(readValue("plain text", "x"), "plain text");
31
+ });
32
+
33
+ test("parseJsonObject accepts inline JSON and @file", () => {
34
+ assert.deepEqual(parseJsonObject('{"enabled":false,"template_name":null}', "x"), { enabled: false, template_name: null });
35
+ const dir = mkdtempSync(join(tmpdir(), "flowiq-ac-"));
36
+ const f = join(dir, "hn.json");
37
+ writeFileSync(f, '{"whatsapp_ids":["27000000000"]}');
38
+ assert.deepEqual(parseJsonObject(`@${f}`, "x"), { whatsapp_ids: ["27000000000"] });
39
+ });
@@ -0,0 +1,36 @@
1
+ // node --test src/batch-a.test.mjs
2
+ import test from "node:test";
3
+ import assert from "node:assert/strict";
4
+ import { mintSectionIds } from "./commands/prompts.js";
5
+ import { scheduleLabel } from "./commands/keywords.js";
6
+ import { describeError, isVercelChallenge } from "./http.js";
7
+
8
+ test("mintSectionIds gives only id-less sections a uuid and names them", () => {
9
+ const secs = [{ id: "keep-me", title: "A", content: "" }, { title: "New one", content: "" }, { id: " ", title: "Blank", content: "" }];
10
+ const touched = mintSectionIds(secs);
11
+ assert.deepEqual(touched, ["New one", "Blank"]);
12
+ assert.equal(secs[0].id, "keep-me");
13
+ assert.match(secs[1].id, /^[0-9a-f-]{36}$/);
14
+ assert.notEqual(secs[1].id, secs[2].id);
15
+ });
16
+
17
+ test("scheduleLabel flags an active row before its start and past its end", () => {
18
+ const now = Date.parse("2026-09-30T20:00:00Z");
19
+ assert.match(scheduleLabel({ active: true, start_date: "2026-09-30T22:00:00Z" }, now), /LIVE BEFORE ITS START/);
20
+ assert.match(scheduleLabel({ active: false, start_date: "2026-09-30T22:00:00Z" }, now), /starts 2026-10-01 00:00 SAST/);
21
+ assert.match(scheduleLabel({ active: true, end_date: "2026-09-29T10:00:00Z" }, now), /PAST ITS END/);
22
+ assert.equal(scheduleLabel({ active: true }, now), "");
23
+ });
24
+
25
+ test("describeError: HTML 'timeout' text is not a timeout; a POST timeout says check first", () => {
26
+ assert.doesNotMatch(describeError({ _raw: "<html>setTimeout(…) timed out</html>" }, "GET", "hours", 403, { json: false }), /TIMEOUT/);
27
+ assert.match(describeError({ error: "x" }, "POST", "meta-templates", 504), /may still have finished the write/);
28
+ assert.match(describeError({ error: "x" }, "GET", "store-api", 504), /--max-pages/);
29
+ });
30
+
31
+ test("isVercelChallenge reads the header or the checkpoint page", () => {
32
+ const h = (v) => ({ get: (k) => (k === "x-vercel-mitigated" ? v : null) });
33
+ assert.equal(isVercelChallenge(403, h("challenge"), ""), true);
34
+ assert.equal(isVercelChallenge(403, h(null), "<title>Vercel Security Checkpoint</title>"), true);
35
+ assert.equal(isVercelChallenge(403, h(null), '{"error":"Forbidden"}'), false);
36
+ });
@@ -1,8 +1,11 @@
1
1
  // `flowiq agent config <organization_id> [flags]` — the prompt-builder skill's
2
2
  // Step 5 as a staff-key call. Writes an allowlisted set of agent fields
3
- // (use_settings_prompt, model, tool flags, discount.enabled, rename) via
4
- // /cli/agent-config. With no write flags, prints the current config.
3
+ // (use_settings_prompt, model, tool flags, discount.enabled, rename, tool
4
+ // instructions, link shortening, email identity / request recipients, send
5
+ // success rules, after-escalation text, knowledge search, human_notify /
6
+ // order_notify) via /cli/agent-config. With no write flags, prints the current config.
5
7
 
8
+ import { readFileSync } from "node:fs";
6
9
  import { http } from "../http.js";
7
10
 
8
11
  const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
@@ -29,10 +32,77 @@ export function collectToolName(val, acc) {
29
32
  return acc;
30
33
  }
31
34
 
32
- function printSnapshot(title, snap) {
35
+ // "@path" reads the file; anything else is the literal text.
36
+ export function readValue(v, label) {
37
+ if (typeof v !== "string" || !v.startsWith("@")) return v;
38
+ try {
39
+ return readFileSync(v.slice(1), "utf8").replace(/\r?\n$/, "");
40
+ } catch (e) {
41
+ console.error(`Error: ${label} could not read ${v.slice(1)}: ${e.message}`);
42
+ process.exit(1);
43
+ }
44
+ }
45
+
46
+ // "key=value" → [key, value]; the value may itself contain "=".
47
+ export function splitPair(pair, label) {
48
+ const eq = pair.indexOf("=");
49
+ if (eq < 1) {
50
+ console.error(`Error: ${label} expects key=value (got "${pair}").`);
51
+ process.exit(1);
52
+ }
53
+ return [pair.slice(0, eq).trim(), pair.slice(eq + 1)];
54
+ }
55
+
56
+ export function parseShorten(v, label) {
57
+ const s = String(v).trim().toLowerCase();
58
+ if (["on", "true", "yes", "1"].includes(s)) return true;
59
+ if (["off", "false", "no", "0"].includes(s)) return false;
60
+ if (s === "default") return null;
61
+ console.error(`Error: ${label} must be on, off or default (got "${v}").`);
62
+ process.exit(1);
63
+ }
64
+
65
+ // --knowledge enabled=true,match_count=8,min_similarity=0.3 ("default" clears a key)
66
+ export function parseKnowledge(v) {
67
+ const out = {};
68
+ for (const part of String(v).split(",").map((p) => p.trim()).filter(Boolean)) {
69
+ const [k, raw] = splitPair(part, "--knowledge");
70
+ const val = raw.trim();
71
+ if (val === "default" || val === "") out[k] = null;
72
+ else if (k === "enabled") out[k] = parseBool(val, "--knowledge enabled");
73
+ else if (!Number.isNaN(Number(val))) out[k] = Number(val);
74
+ else out[k] = val; // the server names the bad value
75
+ }
76
+ return out;
77
+ }
78
+
79
+ export function parseJsonObject(v, label) {
80
+ const text = readValue(v, label);
81
+ let obj;
82
+ try {
83
+ obj = JSON.parse(text);
84
+ } catch (e) {
85
+ console.error(`Error: ${label} must be a JSON object or @file.json (${e.message}).`);
86
+ process.exit(1);
87
+ }
88
+ if (!obj || typeof obj !== "object" || Array.isArray(obj)) {
89
+ console.error(`Error: ${label} must be a JSON object (keys are merged; null deletes a key).`);
90
+ process.exit(1);
91
+ }
92
+ return obj;
93
+ }
94
+
95
+ function short(v, full) {
96
+ if (v === null || v === undefined) return "(unset)";
97
+ const s = String(v).replace(/\s+/g, " ");
98
+ if (full) return String(v);
99
+ return s.length <= 140 ? s : `${s.slice(0, 137)}… (${String(v).length} chars; --full shows all)`;
100
+ }
101
+
102
+ function printSnapshot(title, snap, full = false) {
33
103
  console.log(` ${title}:`);
34
104
  for (const [k, v] of Object.entries(snap)) {
35
- console.log(` ${k.padEnd(30)} ${v === null ? "(unset)" : v}`);
105
+ console.log(` ${k.padEnd(38)} ${short(v, full)}`);
36
106
  }
37
107
  }
38
108
 
@@ -60,6 +130,47 @@ export async function config(orgId, opts = {}) {
60
130
  };
61
131
  }
62
132
 
133
+ if (opts.toolInstruction?.length) {
134
+ body.tool_instructions = {};
135
+ for (const pair of opts.toolInstruction) {
136
+ const [tool, text] = splitPair(pair, "--tool-instruction");
137
+ body.tool_instructions[tool] = readValue(text, `--tool-instruction ${tool}`);
138
+ }
139
+ }
140
+ if (opts.shortenLinks?.length) {
141
+ body.url_shortening = {};
142
+ for (const pair of opts.shortenLinks) {
143
+ const [ch, v] = splitPair(pair, "--shorten-links");
144
+ body.url_shortening[ch] = parseShorten(v, `--shorten-links ${ch}`);
145
+ }
146
+ }
147
+ const identity = {};
148
+ if (opts.emailFromName !== undefined) identity.from_name = opts.emailFromName;
149
+ if (opts.emailSignatureName !== undefined) identity.signature_name = opts.emailSignatureName;
150
+ if (opts.emailSignatureTitle !== undefined) identity.signature_title = opts.emailSignatureTitle;
151
+ if (Object.keys(identity).length) body.email_identity = identity;
152
+ const emailRequest = {};
153
+ if (opts.emailRequestTo !== undefined) {
154
+ emailRequest.to = String(opts.emailRequestTo).split(/[,;\s]+/).map((e) => e.trim()).filter(Boolean);
155
+ }
156
+ if (opts.emailRequestSubjectPrefix !== undefined) emailRequest.subject_prefix = opts.emailRequestSubjectPrefix;
157
+ if (Object.keys(emailRequest).length) body.email_request = emailRequest;
158
+ if (opts.sendSuccessRule?.length || opts.sendSuccess !== undefined) {
159
+ body.send_success = {};
160
+ if (opts.sendSuccess !== undefined) body.send_success.enabled = parseBool(opts.sendSuccess, "--send-success");
161
+ if (opts.sendSuccessRule?.length) {
162
+ body.send_success.rules = {};
163
+ for (const pair of opts.sendSuccessRule) {
164
+ const [when, text] = splitPair(pair, "--send-success-rule");
165
+ body.send_success.rules[when] = readValue(text, `--send-success-rule ${when}`);
166
+ }
167
+ }
168
+ }
169
+ if (opts.afterEscalation !== undefined) body.after_escalation = readValue(opts.afterEscalation, "--after-escalation");
170
+ if (opts.knowledge !== undefined) body.knowledge = parseKnowledge(opts.knowledge);
171
+ if (opts.humanNotify !== undefined) body.human_notify = parseJsonObject(opts.humanNotify, "--human-notify");
172
+ if (opts.orderNotify !== undefined) body.order_notify = parseJsonObject(opts.orderNotify, "--order-notify");
173
+
63
174
  if (opts.tool && opts.tool.length) {
64
175
  const flags = {};
65
176
  for (const pair of opts.tool) {
@@ -74,8 +185,12 @@ export async function config(orgId, opts = {}) {
74
185
  body.tool_flags = flags;
75
186
  }
76
187
 
77
- const hasWrite =
78
- body.settings || body.name !== undefined || body.tool_flags || body.base_tool_changes || body.discount_enabled !== undefined;
188
+ const WRITE_KEYS = [
189
+ "settings", "name", "tool_flags", "base_tool_changes", "discount_enabled", "tool_instructions",
190
+ "url_shortening", "email_identity", "email_request", "send_success", "after_escalation",
191
+ "knowledge", "human_notify", "order_notify",
192
+ ];
193
+ const hasWrite = WRITE_KEYS.some((k) => body[k] !== undefined);
79
194
 
80
195
  // No write flags → just show current config.
81
196
  if (!hasWrite) {
@@ -86,9 +201,26 @@ export async function config(orgId, opts = {}) {
86
201
  console.error(`Lookup failed: ${e.message}`);
87
202
  process.exit(1);
88
203
  }
204
+ if (opts.json) {
205
+ console.log(JSON.stringify(resp, null, 2));
206
+ return;
207
+ }
89
208
  console.log(`${resp.organization_name} → agent ${resp.agent_id}${resp.is_active === false ? " [NON-active]" : ""}`);
90
- printSnapshot("current", resp.current);
91
- console.log("\n(pass --use-settings-prompt / --model / --reasoning-effort / --rename / --tool / --disable-base-tool / --enable-base-tool / --discount / --test-contact-number / --test-contact-name to change)");
209
+ printSnapshot("current", resp.current, opts.full);
210
+ console.log(`
211
+ Settable (flowiq agent config --help for the value formats):
212
+ --use-settings-prompt --model --reasoning-effort --rename --discount
213
+ --tool <flag=bool> --disable-base-tool / --enable-base-tool <name>
214
+ --test-contact-number / --test-contact-name
215
+ --tool-instruction <tool=text|@file> settings.tool_instructions ("" removes)
216
+ --shorten-links <channel=on|off|default> disable_url_shortening
217
+ --email-from-name / --email-signature-name / --email-signature-title
218
+ --email-request-to <a,b> / --email-request-subject-prefix
219
+ --send-success-rule <when=text|@file> --send-success <bool>
220
+ --after-escalation <text|@file> human_notify.success_instructions
221
+ --knowledge enabled=…,match_count=…,min_similarity=…
222
+ --human-notify / --order-notify <json|@file.json> (merged; null deletes a key)
223
+ Not settable here yet: additional_config (use \`flowiq knowledge\`), additional_tools (\`flowiq ct\`).`);
92
224
  return;
93
225
  }
94
226
 
@@ -106,7 +238,7 @@ export async function config(orgId, opts = {}) {
106
238
  for (const k of resp.changed) {
107
239
  const b = resp.before?.[k];
108
240
  const a = resp.after?.[k];
109
- console.log(` ${k.padEnd(30)} ${b === null || b === undefined ? "(unset)" : b} → ${a === null || a === undefined ? "(unset)" : a}`);
241
+ console.log(` ${k.padEnd(38)} ${short(b, opts.full)} → ${short(a, opts.full)}`);
110
242
  }
111
243
  for (const w of resp.warnings || []) console.log(` ⚠ ${w}`);
112
244
  }