@flowapt/flowiq-cli 0.1.11 → 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 +122 -4
- package/TEAM-GUIDE.md +134 -0
- package/package.json +3 -2
- package/src/commands/agent-updates.js +70 -0
- package/src/commands/broadcast.js +879 -0
- package/src/commands/custom-tools.js +197 -0
- package/src/commands/segments.js +252 -0
- package/src/index.js +115 -2
package/README.md
CHANGED
|
@@ -137,6 +137,113 @@ flowiq knowledge list
|
|
|
137
137
|
- **Object-shape guard:** some agents use `additional_config` for Flowapt-managed config (`external_woo_build`, …) rather than text sources — a pull flags that, and a push is **refused** (never clobbers managed config).
|
|
138
138
|
- `--agent` + filenames behave like `prompts` (a non-active agent saves to `<org-slug>-<agent-slug>.json`). The file carries `agent_id`, so `push` targets the agent it was pulled from.
|
|
139
139
|
|
|
140
|
+
### Custom tools — `flowiq custom-tools pull|push|enable|disable|list` (alias `ct`)
|
|
141
|
+
|
|
142
|
+
Round-trips an agent's **custom tools** — the `agents.additional_tools` array
|
|
143
|
+
of "any API call → tool" definitions (the "Custom Tools" panel in the UI:
|
|
144
|
+
`get_more_answers`, CRM wrappers, loyalty tools, …). These define real HTTP
|
|
145
|
+
calls the agent can execute, so treat pushes with care.
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
flowiq custom-tools pull <org_id> # → ./.flowiq/custom-tools/<slug>.json (tools[])
|
|
149
|
+
flowiq ct pull <org_id> --agent <id> # a specific (non-active) agent
|
|
150
|
+
flowiq ct push <slug> --dry-run # validate + per-tool diff, writes NOTHING
|
|
151
|
+
flowiq ct push <slug> # FULL-REPLACE tools[] onto the agent
|
|
152
|
+
flowiq ct enable <org_id> <tool_name> # flip one tool on (no round-trip)
|
|
153
|
+
flowiq ct disable <org_id> <tool_name> # flip one tool off
|
|
154
|
+
flowiq ct list
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
- **Always `--dry-run` first** on a production agent — it prints added/removed/changed tool names before you commit.
|
|
158
|
+
- Server-side validation is strict: tool `name` (`^[a-zA-Z0-9_-]{1,64}$`, no duplicates), non-empty `description`, `http(s)` `endpoint`, method `GET/POST/PUT/PATCH/DELETE`, object-typed `parameters`/`headers`/`injected_parameters`, valid `auth_type`/`channels`. Bad payloads are rejected before anything writes.
|
|
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
|
+
- `--agent` + filenames behave like `prompts`/`knowledge`; the file carries `agent_id`, so `push` targets the agent it was pulled from.
|
|
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
|
+
|
|
140
247
|
### Messages — `flowiq messages pull <contact_id>` (alias `m`)
|
|
141
248
|
|
|
142
249
|
Read-only pull of a contact's `helpdesk_messages` history. Anchors on the
|
|
@@ -343,19 +450,30 @@ the tool-flag columns (`woo_order_build`, `woo_tip_field`, `woo_order_note_field
|
|
|
343
450
|
`shopify_products_web_chat`), and `discount.enabled`. Anything else is rejected;
|
|
344
451
|
every change is reported before → after.
|
|
345
452
|
|
|
346
|
-
### Agent updates — `flowiq agent-updates pull|list` (alias `au`)
|
|
453
|
+
### Agent updates — `flowiq agent-updates pull|list|resolve` (alias `au`)
|
|
347
454
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
the
|
|
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.
|
|
351
459
|
|
|
352
460
|
```bash
|
|
353
461
|
flowiq au pull <organization_id> # pending, with context
|
|
354
462
|
flowiq au pull <organization_id> --status all --titles "EFT, bank"
|
|
355
463
|
flowiq au pull <organization_id> --before 20 --after 10 --no-files
|
|
356
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 …"
|
|
357
468
|
```
|
|
358
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
|
+
|
|
359
477
|
### Chat export — `flowiq export chats <organization_id> [--out <path>]`
|
|
360
478
|
|
|
361
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
|
|
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
|
+
}
|