@flowapt/flowiq-cli 0.1.12 → 0.2.1

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
@@ -159,6 +159,91 @@ flowiq ct list
159
159
  - **Warnings (non-blocking):** unknown keys (likely typos the runtime would silently ignore) and unknown `{{placeholders}}` (they will NOT be substituted at runtime — known: `organization_id`, `contact_id`, `agent_id`, `contact_whatsapp_id`, `contact_full_name`, `supabase_anon_key`, `openai_api_key`, …).
160
160
  - `--agent` + filenames behave like `prompts`/`knowledge`; the file carries `agent_id`, so `push` targets the agent it was pulled from.
161
161
 
162
+ ### Broadcast — `flowiq broadcast map|preview|send|resume|list` (alias `bc`)
163
+
164
+ Send an **APPROVED** WhatsApp template to every row of a CSV, filling the
165
+ template's variables **per row** from the CSV's own columns. The
166
+ highest-stakes command in the CLI — built as a map → preview → commit
167
+ pipeline with several layers of deliberate friction.
168
+
169
+ ```bash
170
+ # 1) Build the column→param mapping interactively (saved per campaign, no send)
171
+ flowiq bc map <org_id> --template rewards_referral_v1 --csv ./people.csv --campaign july-referrals
172
+
173
+ # 2) Dry-run: validation + exact rendered messages for sample rows (default)
174
+ flowiq bc send <org_id> --template rewards_referral_v1 --csv ./people.csv --campaign july-referrals
175
+
176
+ # 3) Commit: upsert contacts, then send per row @ ≤8/s
177
+ flowiq bc send <org_id> --template … --csv … --campaign july-referrals --commit
178
+ # → prompts: type the campaign name to confirm
179
+
180
+ # 4) Interrupted? Continue only unsent rows:
181
+ flowiq bc resume <org_id> --campaign july-referrals --commit [--retry-failed]
182
+ ```
183
+
184
+ - **Per-row values**: each template slot maps to a CSV **column**, a
185
+ **literal**, or a **concat transform** (e.g. `Country Code` + `Phone`,
186
+ leading-zero handled). The mapper auto-suggests from the template's
187
+ semantic labels + body text and you confirm each slot once; the mapping is
188
+ saved to `.flowiq/campaigns/<campaign>.json` and reused.
189
+ - **v1 scope**: Meta orgs, POSITIONAL templates, text/no header. NAMED,
190
+ carousel, media-header templates and WATI orgs are refused with a clear
191
+ message.
192
+ - **Validation before anything sends**: APPROVED-only, every slot mapped,
193
+ contiguous params (the Meta `#132000` guard — a stray key is structurally
194
+ impossible), phone validity, illegal characters (Meta `#100`; `reject` by
195
+ default), duplicates, and **live broadcast-safety** — opted-out / archived /
196
+ blocked contacts are skipped, always.
197
+ - **Dry-run is the default**; `--commit` + typing the campaign name is the
198
+ only way to send (`--yes` for CI skips the typing, never the dry-run).
199
+ - **Write-ahead status log** (`.flowiq/campaigns/<campaign>.status.jsonl`):
200
+ every row logs `sending` *before* the POST and `sent`/`failed` after. A
201
+ crash mid-row leaves an *ambiguous* row that is **never auto-resent** —
202
+ it's surfaced for manual review. `resume` sends only never-attempted rows
203
+ (`--retry-failed` adds confirmed failures).
204
+ - **Error handling**: `#132000` aborts the run (config bug — every row would
205
+ fail); rate errors back off exponentially and halve the send rate; a Meta
206
+ daily-cap soft-stops cleanly with a resume hint.
207
+ - Contacts are **pre-upserted** (`/cli/contacts-upsert`, ≤4000/batch, retries
208
+ then aborts) so the send payload never needs a `name` key — the classic
209
+ `#132000` footgun.
210
+
211
+ **Tag mode** — send to everyone carrying a tag (e.g. a `segments` batch tag)
212
+ instead of a CSV. Same guards, same status log, same resume:
213
+
214
+ ```bash
215
+ flowiq bc send <org_id> --tag july-promo-batch-01 --template fresh_drop_v1 \
216
+ --body param1="Hi {{first_name}}" [--button param1=<short-code>] --commit
217
+ ```
218
+
219
+ Values are shared across the tag (use `{{first_name}}` etc. for per-contact
220
+ personalization — resolved server-side). Opted-out / archived / blocked
221
+ contacts under the tag are excluded automatically. Tags with >2000 contacts
222
+ are refused — slice them with `flowiq segments` first.
223
+
224
+ ### Segments — `flowiq segments plan|apply|list|untag` (alias `seg`)
225
+
226
+ Slice a big contact cohort into fixed-size **batch tags** (default 75) so
227
+ sends respect Meta tier limits and stay controllable — the batch tag is your
228
+ throttle *and* your brake. Tags only; the send is `broadcast send --tag` (one
229
+ batch at a time) or the dashboard broadcaster.
230
+
231
+ ```bash
232
+ flowiq seg plan <org_id> --tag-prefix july-promo --ids-file ./cohort.txt # or --from-segment snapshot.json
233
+ # → exclusions (opt-out/archived/blocked) applied BEFORE slicing → .flowiq/segments/<slug>.json
234
+ flowiq seg apply <org_id> july-promo # DRY-RUN
235
+ flowiq seg apply <org_id> july-promo --commit # append the tags (idempotent — re-runs report already_had)
236
+ flowiq seg list <org_id> --prefix july-promo # VERIFY: tag → count
237
+ flowiq seg untag <org_id> july-promo --commit --confirm # ROLLBACK (its own tags only)
238
+ ```
239
+
240
+ - The cohort comes in as **contact UUIDs** (a snapshot JSON's `contact_ids[]`
241
+ or a plain file); defining the cohort with SQL stays upstream.
242
+ - Apply is **append-only** — it never touches a contact's other tags, names,
243
+ or anything else, and never double-adds.
244
+ - Point-in-time warning: the plan tags snapshot ids; re-derive the cohort SQL
245
+ if freshness matters (someone who ordered since won't auto-drop).
246
+
162
247
  ### Messages — `flowiq messages pull <contact_id>` (alias `m`)
163
248
 
164
249
  Read-only pull of a contact's `helpdesk_messages` history. Anchors on the
@@ -365,19 +450,30 @@ the tool-flag columns (`woo_order_build`, `woo_tip_field`, `woo_order_note_field
365
450
  `shopify_products_web_chat`), and `discount.enabled`. Anything else is rejected;
366
451
  every change is reported before → after.
367
452
 
368
- ### Agent updates — `flowiq agent-updates pull|list` (alias `au`)
453
+ ### Agent updates — `flowiq agent-updates pull|list|resolve` (alias `au`)
369
454
 
370
- Read-only pull of an org's client-raised "change the agent" requests
371
- (`agent_updates`), each with the chat context around the triggering message +
372
- the linked contact. Attachments are downloaded locally.
455
+ Pull an org's client-raised "change the agent" requests (`agent_updates`),
456
+ each with the chat context around the triggering message + the linked contact
457
+ (attachments downloaded locally) — then, once the fix is verified, resolve the
458
+ ticket from the same terminal.
373
459
 
374
460
  ```bash
375
461
  flowiq au pull <organization_id> # pending, with context
376
462
  flowiq au pull <organization_id> --status all --titles "EFT, bank"
377
463
  flowiq au pull <organization_id> --before 20 --after 10 --no-files
378
464
  # → ./.flowiq/agent-updates/<slug>.json + files/<update-id>/<attachment>
465
+
466
+ flowiq au resolve <update_id> --note "We updated the agent so it no longer …"
467
+ flowiq au resolve <update_id> --status declined --internal "duplicate of …"
379
468
  ```
380
469
 
470
+ - **`--note` is what the client reads** (rendered as `FlowIQ: <note>` in their
471
+ console) and is **required** when resolving; `--internal` is staff-only.
472
+ - The typical flow is `prompts push → flowiq test → au resolve` — resolve is
473
+ the LAST step, after a live test proves the fix. Don't resolve on hope.
474
+ - An interactive confirm shows exactly what the client will read; `--yes`
475
+ skips the confirm (but never the `--note` requirement).
476
+
381
477
  ### Chat export — `flowiq export chats <organization_id> [--out <path>]`
382
478
 
383
479
  Full chat history → TXT, byte-identical to the in-app "Export Settings TXT"
package/TEAM-GUIDE.md ADDED
@@ -0,0 +1,134 @@
1
+ # FlowIQ CLI — Team Guide
2
+
3
+ The `flowiq` CLI is how Flowapt staff work on client agents and org data from
4
+ the terminal: prompts, questionnaires, fine-tuning, knowledge sources, custom
5
+ tools, webhooks, templates, chat reads, live agent testing, and more — all
6
+ **without a database credential ever touching your laptop**. Every command goes
7
+ to `api.flowiq.live` with your personal staff key, is validated server-side,
8
+ and is logged under your name.
9
+
10
+ This guide is the "how we work" version. The full command reference with every
11
+ flag is [README.md](./README.md).
12
+
13
+ ---
14
+
15
+ ## Get set up (2 minutes)
16
+
17
+ ```bash
18
+ npm i -g @flowapt/flowiq-cli
19
+ flowiq auth login
20
+ ```
21
+
22
+ `auth login` works like `npm login`:
23
+
24
+ 1. Your terminal shows a short code, e.g. `FQ4X-9KTB`.
25
+ 2. Press ENTER — your browser opens `app.flowiq.live/cli-auth`.
26
+ 3. A logged-in **super admin** (you, if you are one) checks the code on the
27
+ page matches the one in your terminal and clicks **Approve**.
28
+ 4. Back in the terminal: you're logged in. A personal key was minted for this
29
+ device — you never see it, never paste it.
30
+
31
+ Confirm with:
32
+
33
+ ```bash
34
+ flowiq auth whoami
35
+ ```
36
+
37
+ > **Approving someone else's login:** when a teammate runs `auth login`, they
38
+ > read you their code (or you open the link they send). On `/cli-auth`, check
39
+ > the code AND the device name match what they told you, then Approve.
40
+ > **Never approve a code you can't verify came from a teammate's terminal
41
+ > right now** — that's the one way this flow can be abused.
42
+
43
+ ## The golden rules
44
+
45
+ 1. **Pull before you edit.** Local files under `./.flowiq/` are working
46
+ copies, not the source of truth. Someone may have edited in the UI since
47
+ your last pull — always re-pull first.
48
+ 2. **`--dry-run` before any push to a production agent** (where the command
49
+ supports it: `custom-tools`, `messaging-webhooks`). It shows exactly what
50
+ would change before anything writes.
51
+ 3. **Pushes are full-replace.** What's in your file becomes the whole surface
52
+ (all prompt sections, all tools, all webhooks). Deleting an entry from the
53
+ file deletes it from the platform.
54
+ 4. **`--agent <id>` targets a specific agent**; without it you get the org's
55
+ *active* agent. Files remember which agent they came from, so a push always
56
+ goes back to the agent you pulled — never "whatever is active now".
57
+ 5. Run the CLI from the repo root when you can — `./.flowiq/` is gitignored
58
+ there.
59
+
60
+ ## Everyday tasks
61
+
62
+ | I want to… | Run |
63
+ |---|---|
64
+ | Edit a client agent's system prompt | `flowiq prompts pull <org_id>` → edit the JSON → `flowiq prompts push <slug>` |
65
+ | Edit the questionnaire answers | `flowiq q pull <org_id>` → edit → `flowiq q push <slug>` |
66
+ | Edit fine-tuning instructions / Q&A | `flowiq ft pull <org_id>` → edit → `flowiq ft push <slug>` |
67
+ | Edit the text knowledge sources (get_more_answers playbooks) | `flowiq kn pull <org_id>` → edit `sources[]` → `flowiq kn push <slug>` |
68
+ | Edit the custom tools (API-call tools) | `flowiq ct pull <org_id>` → edit `tools[]` → `flowiq ct push <slug> --dry-run` → `flowiq ct push <slug>` |
69
+ | Turn one custom tool on/off | `flowiq ct enable\|disable <org_id> <tool_name>` |
70
+ | See an org's agents / create one | `flowiq agent list <org_id>` / `flowiq agent create <org_id> --name "…"` |
71
+ | Change agent model / tool flags | `flowiq agent config <org_id> --model … --tool view_cart_tool=true` |
72
+ | Talk to the live agent safely (no real WhatsApp ever sent) | `flowiq test send <org_id> "hi, do you sell X?"` |
73
+ | Send a template broadcast to a CSV of people | `flowiq bc map <org_id> --template … --csv …` → `flowiq bc send … ` (dry-run) → `… --commit` |
74
+ | Split a big cohort into send-safe batch tags | `flowiq seg plan <org_id> --tag-prefix … --ids-file …` → `flowiq seg apply … --commit` |
75
+ | Send to one batch tag | `flowiq bc send <org_id> --tag <batch-tag> --template … --body param1="Hi {{first_name}}" --commit` |
76
+ | Read a contact's chat | `flowiq m pull <contact_id>` then open the JSON |
77
+ | Export an org's full chat history | `flowiq export chats <org_id>` |
78
+ | Check / create WhatsApp templates | `flowiq tpl pull <org_id>` / `flowiq tpl create <org_id> --request-file req.json` |
79
+ | Manage Shopify/Woo platform webhooks | `flowiq wh pull <org_id>` → edit → `flowiq wh push <slug>` |
80
+ | Manage outbound messaging webhooks | `flowiq mw pull <org_id>` → `flowiq mw push <slug> --dry-run` → push |
81
+ | See a client's pending change requests | `flowiq au pull <org_id>` |
82
+ | Close a client's change request (after verifying the fix!) | `flowiq au resolve <update_id> --note "what changed"` — the client reads the note |
83
+ | Check an org's platform + active agent | `flowiq org info <org_id>` |
84
+ | Work a Pin Board task | `flowiq pin list-remote open` → `pull` → edit → `push` |
85
+
86
+ The flow is the same everywhere: **pull → edit the JSON → push**. Slugs are the
87
+ filename the pull printed (e.g. `phytoceutics`), or pass a file path.
88
+
89
+ ### Example: fix a line in a client's prompt
90
+
91
+ ```bash
92
+ flowiq prompts pull 08becb51-9734-432e-a1e8-3a2115c0f3eb
93
+ # → wrote ./.flowiq/prompts/phytoceutics.json
94
+ # edit the section's "content" in that file (keep section "id"s intact)
95
+ flowiq prompts push phytoceutics
96
+ # server regenerates the full system prompt from the sections
97
+ flowiq test send 08becb51-… "question that exercises your change"
98
+ ```
99
+
100
+ ### Example: safely change custom tools
101
+
102
+ ```bash
103
+ flowiq ct pull <org_id>
104
+ # edit tools[] in the JSON
105
+ flowiq ct push <slug> --dry-run
106
+ # → "diff: +1 added (…); ~1 changed (…); 8 unchanged" + any warnings
107
+ flowiq ct push <slug>
108
+ ```
109
+
110
+ Custom tools define real HTTP calls the agent can execute, so the server
111
+ validates hard (names, URLs, methods, parameter shapes) and warns about typo'd
112
+ keys or `{{placeholders}}` it doesn't recognise. Take the warnings seriously.
113
+
114
+ ## Keys, rotation, logging out
115
+
116
+ - **`flowiq auth refresh`** — rotates this device's key in place (a fresh key
117
+ is minted, the old one is revoked server-side). Run it whenever you want a
118
+ new secret, e.g. after using the CLI on a machine you don't fully trust.
119
+ - **`flowiq auth logout`** — deletes the key file from this machine only. The
120
+ key itself stays valid server-side, so if the machine is lost/compromised,
121
+ run `refresh` first (kills the old key) or ask a super admin to revoke it.
122
+ - Keys live at `~/.config/flowiq/auth.json` (chmod 600). CI can use
123
+ `FLOWIQ_TOKEN=…` instead.
124
+
125
+ ## Troubleshooting
126
+
127
+ | Symptom | Fix |
128
+ |---|---|
129
+ | `npm i -g …` says the new version doesn't exist | `npm i -g @flowapt/flowiq-cli@<version> --prefer-online` (npm's cache is stale for a few minutes after a publish) |
130
+ | `401 Unauthorized` on every command | `flowiq auth refresh` (falls back to browser login if the key is dead) |
131
+ | `403 … doesn't cover "<scope>"` | Your admin access has been scoped down — ask another super admin to widen your `allowed_scopes` |
132
+ | "Organization has no active_whatsapp_agent" | Target the agent directly: `--agent <id>` (ids from `flowiq agent list <org_id>`) |
133
+ | Login browser page says the code expired | Codes live 10 minutes — just re-run `flowiq auth login` |
134
+ | Pushed the wrong thing | Everything is pull→push, so re-pull an older copy if you have one, or check with Matt — server logs record every push with who/what/when |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowapt/flowiq-cli",
3
- "version": "0.1.12",
3
+ "version": "0.2.1",
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": {
@@ -9,7 +9,8 @@
9
9
  "files": [
10
10
  "bin",
11
11
  "src",
12
- "README.md"
12
+ "README.md",
13
+ "TEAM-GUIDE.md"
13
14
  ],
14
15
  "engines": {
15
16
  "node": ">=18"
@@ -4,6 +4,7 @@
4
4
 
5
5
  import fs from "node:fs/promises";
6
6
  import path from "node:path";
7
+ import readline from "node:readline";
7
8
  import { http } from "../http.js";
8
9
 
9
10
  const AU_DIR = path.resolve(process.cwd(), ".flowiq", "agent-updates");
@@ -184,3 +185,72 @@ export async function list() {
184
185
  }
185
186
  }
186
187
  }
188
+
189
+ // --- resolve (write-back) ---------------------------------------------------
190
+ // Flips a client-raised ticket to resolved/declined and writes the note the
191
+ // CLIENT reads ("FlowIQ: <note>"). --internal is staff-only. Deliberately a
192
+ // separate explicit step after `flowiq test` proves the fix — never automate
193
+ // it off the back of a push ("don't resolve on hope").
194
+
195
+ const UUID_RE_RESOLVE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
196
+
197
+ function confirmPrompt(question) {
198
+ return new Promise((resolve) => {
199
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
200
+ rl.question(question, (answer) => { rl.close(); resolve(answer.trim().toLowerCase()); });
201
+ });
202
+ }
203
+
204
+ export async function resolve(updateId, opts = {}) {
205
+ if (!UUID_RE_RESOLVE.test(updateId)) {
206
+ console.error(`Error: "${updateId}" is not a valid update UUID (get it from agent-updates pull).`);
207
+ process.exit(1);
208
+ }
209
+ const status = opts.status || "resolved";
210
+ if (!["resolved", "declined"].includes(status)) {
211
+ console.error(`Error: --status must be resolved or declined (got "${status}").`);
212
+ process.exit(1);
213
+ }
214
+ // R-3: a client-facing note is mandatory on resolve (--yes does NOT waive it).
215
+ if (status === "resolved" && (!opts.note || !opts.note.trim())) {
216
+ console.error('Refusing to resolve without --note (the client reads this as "FlowIQ: …").');
217
+ console.error('Pass --note "…", or use --status declined.');
218
+ process.exit(1);
219
+ }
220
+
221
+ if (!opts.yes) {
222
+ console.log(`About to mark update ${updateId} as ${status.toUpperCase()}.`);
223
+ if (opts.note) console.log(`The client will read: FlowIQ: ${opts.note}`);
224
+ if (opts.internal) console.log(`Staff-only note: ${opts.internal}`);
225
+ const answer = await confirmPrompt("Type y to confirm: ");
226
+ if (answer !== "y" && answer !== "yes") {
227
+ console.log("Aborted — nothing changed.");
228
+ process.exit(0);
229
+ }
230
+ }
231
+
232
+ let resp;
233
+ try {
234
+ resp = await http.post("agent-updates", {
235
+ action: "resolve",
236
+ update_id: updateId,
237
+ status,
238
+ superadmin_comment: opts.note ?? null,
239
+ internal_notes: opts.internal ?? null,
240
+ superadmin_response_image: opts.image ?? null,
241
+ });
242
+ } catch (e) {
243
+ console.error(`Resolve failed: ${e.message}`);
244
+ if (e.body?.error) console.error(` ${e.body.error}`);
245
+ process.exit(1);
246
+ }
247
+
248
+ console.log(`Update ${resp.update_id} → ${resp.status.toUpperCase()}`);
249
+ console.log(` org: ${resp.organization_name ?? resp.organization_id}`);
250
+ console.log(` title: ${resp.title}`);
251
+ console.log(` was: ${resp.previous_status}`);
252
+ console.log(` by: ${resp.resolved_by_email ?? resp.resolved_by}`);
253
+ if (resp.superadmin_comment) console.log(` client: FlowIQ: ${resp.superadmin_comment}`);
254
+ if (resp.internal_notes) console.log(` internal: ${resp.internal_notes}`);
255
+ if (resp.already_resolved) console.log(` ⚠ was already terminal (${resp.previous_status}) — note/status overwritten.`);
256
+ }