@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 +102 -10
- package/TEAM-GUIDE.md +21 -12
- package/package.json +1 -1
- package/src/agent-config.test.mjs +39 -0
- package/src/batch-a.test.mjs +36 -0
- package/src/commands/agent-config.js +141 -9
- package/src/commands/agent-test.js +23 -9
- package/src/commands/agent-updates.js +4 -3
- package/src/commands/audit.js +8 -1
- package/src/commands/broadcast.js +18 -2
- package/src/commands/doctor.js +13 -4
- package/src/commands/gaps.js +2 -2
- package/src/commands/keywords.js +43 -2
- package/src/commands/links.js +8 -0
- package/src/commands/prompts.js +25 -3
- package/src/commands/store-api.js +53 -6
- package/src/commands/templates.js +48 -5
- package/src/http.js +34 -8
- package/src/index.js +36 -9
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>`,
|
|
352
|
-
|
|
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`.
|
|
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 #
|
|
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`).
|
|
1726
|
-
|
|
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)
|
|
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 --
|
|
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
|
|
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 "
|
|
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 --
|
|
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>`
|
|
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
|
|
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.
|
|
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
|
|
4
|
-
//
|
|
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
|
-
|
|
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(
|
|
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
|
|
78
|
-
|
|
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(
|
|
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(
|
|
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
|
}
|