@flowapt/flowiq-cli 0.9.2 → 0.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -6
- package/TEAM-GUIDE.md +2 -1
- package/package.json +1 -1
- package/src/commands/agent-updates.js +116 -3
- package/src/index.js +24 -1
package/README.md
CHANGED
|
@@ -1768,18 +1768,30 @@ number already maps to an existing contact, the command prints a `⚠` warning
|
|
|
1768
1768
|
naming it. Pass `--test-contact-number ""` (empty) to clear the override — the
|
|
1769
1769
|
harness then falls back to a synthetic per-agent `test_<agent_id>` contact.
|
|
1770
1770
|
|
|
1771
|
-
### Agent updates — `flowiq agent-updates pull|list|resolve` (alias `au`)
|
|
1771
|
+
### Agent updates — `flowiq agent-updates all|pull|list|resolve` (alias `au`)
|
|
1772
1772
|
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1773
|
+
Client-raised "change the agent" requests (`agent_updates`). **`all`** is the
|
|
1774
|
+
cross-org queue: every org in ONE call (about a second), grouped by org, no
|
|
1775
|
+
UUID needed. **`pull`** is the deep read for one org: each update with the chat
|
|
1776
|
+
context around the triggering message + the linked contact (attachments
|
|
1777
|
+
downloaded locally). Once a fix is verified, resolve the ticket from the same
|
|
1778
|
+
terminal.
|
|
1777
1779
|
|
|
1778
1780
|
```bash
|
|
1781
|
+
# every org at once (v0.9.3)
|
|
1782
|
+
flowiq au all # open = pending + in_progress, grouped by org
|
|
1783
|
+
flowiq au all --status all --since 14d # everything raised in the last 14 days
|
|
1784
|
+
flowiq au all --priority urgent,high --summary # just the per-org counts
|
|
1785
|
+
flowiq au all --search wallet --status all # title OR description contains "wallet"
|
|
1786
|
+
flowiq au all --org <uuid>,<uuid> --context # a few orgs, with the chat window around each trigger
|
|
1787
|
+
flowiq au all --json | jq '.updates[] | {org: .organization_name, title}'
|
|
1788
|
+
# → ./.flowiq/agent-updates/_all.<status>.json (aliases: `au queue`, `au list-remote`)
|
|
1789
|
+
|
|
1790
|
+
# one org in depth
|
|
1779
1791
|
flowiq au pull <organization_id> # pending, with context
|
|
1780
1792
|
flowiq au pull <organization_id> --status all --titles "EFT, bank"
|
|
1781
1793
|
flowiq au pull <organization_id> --before 20 --after 10 --no-files
|
|
1782
|
-
# → ./.flowiq/agent-updates/<slug>.json + files/<update-id>/<attachment>
|
|
1794
|
+
# → ./.flowiq/agent-updates/<slug>.json (pending) or <slug>.<status>.json + files/<update-id>/<attachment>
|
|
1783
1795
|
|
|
1784
1796
|
flowiq au resolve <update_id> --note "We updated the agent so it no longer …"
|
|
1785
1797
|
flowiq au resolve <update_id> --status declined --internal "duplicate of …"
|
|
@@ -1791,6 +1803,20 @@ flowiq au resolve <update_id> --status declined --internal "duplicate of …"
|
|
|
1791
1803
|
the LAST step, after a live test proves the fix. Don't resolve on hope.
|
|
1792
1804
|
- An interactive confirm shows exactly what the client will read; `--yes`
|
|
1793
1805
|
skips the confirm (but never the `--note` requirement).
|
|
1806
|
+
- `all` flags: `--status` (`open` default, or any of `pending,in_progress,
|
|
1807
|
+
resolved,declined`, or `all`), `--since` / `--until` (`14d`, `12h`, `2w` or
|
|
1808
|
+
`YYYY-MM-DD`, SAST day start), `--priority`, `--org` (repeatable),
|
|
1809
|
+
`--titles`, `--search`, `--limit` (default 1000, max 5000; a ⚠ TRUNCATED
|
|
1810
|
+
line says when more matched), `--context` (+ `--before/--after/--recent`),
|
|
1811
|
+
`--files`, `--summary`, `--no-desc`, `--out`, `--json`. No context window and
|
|
1812
|
+
no attachment download unless asked, which is what keeps it fast.
|
|
1813
|
+
- Every update (in `all` and `pull`) names the **agent that answers that
|
|
1814
|
+
conversation**: the contact's binding, else the agent on the update, else the
|
|
1815
|
+
org's active agent, with a ⚠ when it is not the active one, so a fix goes to
|
|
1816
|
+
the right agent.
|
|
1817
|
+
- `pull` builds the context windows in parallel (v0.9.3), so `--status all` no
|
|
1818
|
+
longer times out on orgs with long ticket histories, and `--no-files` really
|
|
1819
|
+
skips downloads.
|
|
1794
1820
|
|
|
1795
1821
|
### Broadcast planning — `flowiq plans list|show|status` (alias `planning`) (v0.7.3)
|
|
1796
1822
|
|
package/TEAM-GUIDE.md
CHANGED
|
@@ -201,7 +201,8 @@ several.
|
|
|
201
201
|
| Manage Shopify/Woo platform webhooks | `flowiq wh pull <org_id>` → edit → `flowiq wh push <slug>` |
|
|
202
202
|
| Manage outbound messaging webhooks (incl. their auth) | `flowiq mw pull <org_id>` → `flowiq mw push <slug> --dry-run` → push |
|
|
203
203
|
| Create a brand-new client org | `flowiq org create --name "Client Name"` → then `agent create` on the printed id |
|
|
204
|
-
| See
|
|
204
|
+
| 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`) |
|
|
205
|
+
| See one client's pending change requests, with the chat around each | `flowiq au pull <org_id>` |
|
|
205
206
|
| Close a client's change request (after verifying the fix!) | `flowiq au resolve <update_id> --note "what changed"` — the client reads the note |
|
|
206
207
|
| Check an org's platform + active agent | `flowiq org info <org_id>` |
|
|
207
208
|
| **See every Settings → Profile switch on an org** | `flowiq org flags show <org_id>` (credentials are never shown) |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flowapt/flowiq-cli",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.3",
|
|
4
4
|
"description": "Command-line tool for FlowIQ staff: round-trip agent prompts, questionnaires, fine-tuning, pin-board tasks, webhooks, templates, agent-updates, chat exports, and live agent testing without ever touching service-role credentials.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
// `flowiq agent-updates pull <org_id> [flags]` / `list`.
|
|
1
|
+
// `flowiq agent-updates pull <org_id> [flags]` / `all` / `list` / `resolve`.
|
|
2
2
|
// Read-only: fetches an org's agent_updates + chat context via /cli/agent-updates,
|
|
3
3
|
// then downloads any attachments client-side into ./.flowiq/agent-updates/files/.
|
|
4
|
+
// `all` (v0.9.3) is the cross-org queue: one server query over every org.
|
|
4
5
|
|
|
5
6
|
import fs from "node:fs/promises";
|
|
6
7
|
import path from "node:path";
|
|
@@ -81,7 +82,9 @@ export async function pull(orgId, opts = {}) {
|
|
|
81
82
|
process.exit(1);
|
|
82
83
|
}
|
|
83
84
|
|
|
84
|
-
|
|
85
|
+
// Commander turns --no-files into `files: false` (the old `noFiles` read was
|
|
86
|
+
// never set, so attachments downloaded regardless — gap row 14 Sep 2026).
|
|
87
|
+
const downloadFiles = opts.files !== false && !opts.noFiles;
|
|
85
88
|
const maxMb = opts.maxMb != null ? Number.parseFloat(opts.maxMb) : DEFAULT_MAX_MB;
|
|
86
89
|
if (!Number.isFinite(maxMb) || maxMb <= 0) {
|
|
87
90
|
console.error(`Invalid --max-mb: ${opts.maxMb}`);
|
|
@@ -116,7 +119,10 @@ export async function pull(orgId, opts = {}) {
|
|
|
116
119
|
|
|
117
120
|
await fs.mkdir(AU_DIR, { recursive: true });
|
|
118
121
|
const slug = slugify(resp.organization_name, resp.organization_id);
|
|
119
|
-
|
|
122
|
+
// A non-default status gets its own file, so pulling in_progress no longer
|
|
123
|
+
// overwrites the pending snapshot (gap rows 18 + 21 Sep 2026).
|
|
124
|
+
const statusKey = String(resp.status_filter || "pending");
|
|
125
|
+
const filePath = path.join(AU_DIR, statusKey === "pending" ? `${slug}.json` : `${slug}.${slugify(statusKey, "status")}.json`);
|
|
120
126
|
const overwriting = await fileExists(filePath);
|
|
121
127
|
await fs.writeFile(filePath, JSON.stringify(resp, null, 2) + "\n", "utf8");
|
|
122
128
|
|
|
@@ -138,6 +144,7 @@ export async function pull(orgId, opts = {}) {
|
|
|
138
144
|
console.log(` priority=${u.priority} status=${u.status} raised=${date}${u.created_by_name ? ` by ${u.created_by_name}` : ""}`);
|
|
139
145
|
console.log(` desc: ${oneLine(u.description, 140)}`);
|
|
140
146
|
if (u.contact) console.log(` contact: ${u.contact.full_name ?? "(no name)"} (${u.contact.whatsapp_id ?? u.contact.id})`);
|
|
147
|
+
if (u.agent) console.log(` agent: ${agentLine(u.agent)}`);
|
|
141
148
|
if (u.attachments && u.attachments.length) {
|
|
142
149
|
console.log(` attachments: ${u.attachments.length} (${u.attachments.map((a) => a.filename || a.type || "file").join(", ")})`);
|
|
143
150
|
for (const a of u.attachments) {
|
|
@@ -158,6 +165,112 @@ export async function pull(orgId, opts = {}) {
|
|
|
158
165
|
console.log(`${path.join(AU_DIR, FILES_DIRNAME)}/<update-id>/ — Read those files for visual context, then act.`);
|
|
159
166
|
}
|
|
160
167
|
|
|
168
|
+
function agentLine(a) {
|
|
169
|
+
const src = { update: "set on the update", contact_binding: "contact is bound to it", org_active: "org's active agent" }[a.source] || a.source;
|
|
170
|
+
const warn = a.is_active ? "" : ` ⚠ NOT the active agent (${a.active_agent_name ?? a.active_agent_id ?? "none"}) — fix THIS agent`;
|
|
171
|
+
return `${a.name ?? a.id} (${src})${warn}`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function sastStamp(iso) {
|
|
175
|
+
if (!iso) return "?";
|
|
176
|
+
const d = new Date(new Date(iso).getTime() + 2 * 3600e3);
|
|
177
|
+
return d.toISOString().slice(0, 16).replace("T", " ");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const PRIORITY_MARK = { urgent: "‼", high: "!", medium: "·", low: " " };
|
|
181
|
+
|
|
182
|
+
// --- all (cross-org queue) --------------------------------------------------
|
|
183
|
+
// One server query across every org: no per-org UUIDs, no context window and
|
|
184
|
+
// no attachment downloads unless asked, so the whole open queue is one call.
|
|
185
|
+
export async function all(opts = {}) {
|
|
186
|
+
const query = { scope: "all", status: opts.status || "open" };
|
|
187
|
+
if (opts.since) query.since = opts.since;
|
|
188
|
+
if (opts.until) query.until = opts.until;
|
|
189
|
+
if (opts.priority) query.priority = opts.priority;
|
|
190
|
+
if (opts.org && opts.org.length) query.org = [].concat(opts.org).join(",");
|
|
191
|
+
if (opts.titles) query.titles = opts.titles;
|
|
192
|
+
if (opts.search) query.search = opts.search;
|
|
193
|
+
if (opts.limit) query.limit = String(opts.limit);
|
|
194
|
+
if (opts.context) {
|
|
195
|
+
query.context = "1";
|
|
196
|
+
if (opts.before != null) query.before = String(opts.before);
|
|
197
|
+
if (opts.after != null) query.after = String(opts.after);
|
|
198
|
+
if (opts.recent != null) query.recent = String(opts.recent);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
let resp;
|
|
202
|
+
try {
|
|
203
|
+
resp = await http.get("agent-updates", query);
|
|
204
|
+
} catch (e) {
|
|
205
|
+
console.error(`Pull failed: ${e.message}`);
|
|
206
|
+
process.exit(1);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (opts.files) {
|
|
210
|
+
const maxMb = opts.maxMb != null ? Number.parseFloat(opts.maxMb) : DEFAULT_MAX_MB;
|
|
211
|
+
const maxBytes = Math.round(maxMb * 1024 * 1024);
|
|
212
|
+
for (const u of resp.updates || []) {
|
|
213
|
+
if (!(u.attachments || []).length && !u.superadmin_response_image) continue;
|
|
214
|
+
try { await downloadUpdateFiles(u, maxBytes); } catch (e) { u._file_download_error = e.message; }
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (opts.json) {
|
|
219
|
+
process.stdout.write(JSON.stringify(resp, null, 2) + "\n");
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
await fs.mkdir(AU_DIR, { recursive: true });
|
|
224
|
+
const statusKey = Array.isArray(resp.filters?.status) ? resp.filters.status.join("-") : String(resp.filters?.status ?? "all");
|
|
225
|
+
const filePath = opts.out ? path.resolve(opts.out) : path.join(AU_DIR, `_all.${slugify(statusKey, "all")}.json`);
|
|
226
|
+
await fs.writeFile(filePath, JSON.stringify(resp, null, 2) + "\n", "utf8");
|
|
227
|
+
|
|
228
|
+
const updates = resp.updates || [];
|
|
229
|
+
const f = resp.filters || {};
|
|
230
|
+
console.log(`Wrote ${filePath} (${resp.elapsed_ms ?? "?"} ms server)`);
|
|
231
|
+
console.log(` status: ${Array.isArray(f.status) ? f.status.join(", ") : f.status}` +
|
|
232
|
+
`${f.since ? ` · since ${sastStamp(f.since)} SAST` : ""}${f.until ? ` · until ${sastStamp(f.until)}` : ""}` +
|
|
233
|
+
`${f.priority ? ` · priority ${f.priority.join(",")}` : ""}${f.search ? ` · search "${f.search}"` : ""}` +
|
|
234
|
+
`${f.titles ? ` · titles ${f.titles.join("|")}` : ""}${f.org ? ` · ${f.org.length} org(s)` : ""}`);
|
|
235
|
+
console.log(` updates: ${resp.returned} across ${(resp.by_org || []).length} org(s)` +
|
|
236
|
+
(resp.truncated ? ` ⚠ TRUNCATED: ${resp.total_matched} matched, raise --limit` : ""));
|
|
237
|
+
if (!updates.length) { console.log(" (nothing matches)"); return; }
|
|
238
|
+
|
|
239
|
+
console.log("");
|
|
240
|
+
console.log(" By org (count · pending · in progress · urgent/high · oldest):");
|
|
241
|
+
for (const b of resp.by_org || []) {
|
|
242
|
+
const age = Math.floor((Date.now() - new Date(b.oldest_created_at).getTime()) / 86400e3);
|
|
243
|
+
console.log(` ${String(b.count).padStart(4)} ${String(b.pending).padStart(3)} ${String(b.in_progress).padStart(3)} ${String(b.urgent_or_high).padStart(3)} ${String(age + "d").padStart(5)} ${b.organization_name ?? b.organization_id}`);
|
|
244
|
+
}
|
|
245
|
+
if (opts.summary) return;
|
|
246
|
+
|
|
247
|
+
const byOrg = new Map();
|
|
248
|
+
for (const u of updates) {
|
|
249
|
+
if (!byOrg.has(u.organization_id)) byOrg.set(u.organization_id, []);
|
|
250
|
+
byOrg.get(u.organization_id).push(u);
|
|
251
|
+
}
|
|
252
|
+
for (const b of resp.by_org || []) {
|
|
253
|
+
const list = byOrg.get(b.organization_id) || [];
|
|
254
|
+
console.log("");
|
|
255
|
+
console.log(` ── ${b.organization_name ?? b.organization_id} (${b.organization_id})`);
|
|
256
|
+
for (const u of list) {
|
|
257
|
+
const mark = PRIORITY_MARK[u.priority] ?? "?";
|
|
258
|
+
const st = u.status === "pending" ? "" : ` [${u.status}]`;
|
|
259
|
+
console.log(` ${mark} ${u.id.slice(0, 8)} ${String(u.age_days + "d").padStart(4)} ${oneLine(u.title, 70)}${st}`);
|
|
260
|
+
const bits = [];
|
|
261
|
+
if (u.created_by_name) bits.push(`by ${u.created_by_name}`);
|
|
262
|
+
if (u.contact) bits.push(`contact ${u.contact.full_name ?? u.contact.whatsapp_id ?? u.contact.id}`);
|
|
263
|
+
if ((u.attachments || []).length) bits.push(`${u.attachments.length} attachment(s)`);
|
|
264
|
+
if (u.agent && !u.agent.is_active) bits.push(`⚠ agent ${u.agent.name ?? u.agent.id} is NOT active`);
|
|
265
|
+
if (bits.length) console.log(` ${bits.join(" · ")}`);
|
|
266
|
+
if (opts.desc !== false && u.description) console.log(` ${oneLine(u.description, 150)}`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
console.log("");
|
|
270
|
+
console.log("Full text, contacts, agents" + (opts.context ? ", chat context" : "") + " are in the JSON. Drill into one org with");
|
|
271
|
+
console.log("`flowiq au pull <org_id>` (chat context + attachments), then `flowiq au resolve <id> --note …`.");
|
|
272
|
+
}
|
|
273
|
+
|
|
161
274
|
export async function list() {
|
|
162
275
|
let files;
|
|
163
276
|
try {
|
package/src/index.js
CHANGED
|
@@ -506,7 +506,7 @@ export function run(argv) {
|
|
|
506
506
|
// agent-updates (pending client change-requests + chat context; pull + resolve)
|
|
507
507
|
const au = program.command("agent-updates")
|
|
508
508
|
.alias("au")
|
|
509
|
-
.description("
|
|
509
|
+
.description("Agent updates: `all` = every org's queue in one call; `pull` = one org with chat context + attachments; `resolve` with a client-facing note");
|
|
510
510
|
au.command("pull <organization_id>")
|
|
511
511
|
.description("Fetch updates + linked contact + trigger-message window; downloads attachments locally")
|
|
512
512
|
.option("--status <status>", "pending (default) | resolved | in_progress | all")
|
|
@@ -517,6 +517,29 @@ export function run(argv) {
|
|
|
517
517
|
.option("--no-files", "do NOT download attachments (default: download is ON)")
|
|
518
518
|
.option("--max-mb <n>", "skip attachments larger than N MB (default 25)")
|
|
519
519
|
.action((orgId, opts) => agentUpdatesCmd.pull(orgId, opts));
|
|
520
|
+
au.command("all")
|
|
521
|
+
.alias("queue")
|
|
522
|
+
.alias("list-remote")
|
|
523
|
+
.description("EVERY org's agent updates in one call (default: open = pending + in_progress), grouped by org; → ./.flowiq/agent-updates/_all.<status>.json")
|
|
524
|
+
.option("--status <list>", "open (default) | pending | in_progress | resolved | declined | all — comma-separated allowed")
|
|
525
|
+
.option("--since <when>", "raised on/after: 14d, 12h, 2w or YYYY-MM-DD (SAST day start)")
|
|
526
|
+
.option("--until <when>", "raised on/before (same formats)")
|
|
527
|
+
.option("--priority <list>", "urgent,high,medium,low (comma-separated)")
|
|
528
|
+
.option("--org <uuid>", "limit to these org(s) (repeatable or comma-separated)", (v, prev) => prev.concat(v.split(",")), [])
|
|
529
|
+
.option("--titles <list>", "title contains any of these (comma-separated, ci substring)")
|
|
530
|
+
.option("--search <text>", "title OR description contains this text (ci)")
|
|
531
|
+
.option("--limit <n>", "max updates returned (default 1000, max 5000)")
|
|
532
|
+
.option("--context", "also fetch the chat window around each trigger message (slower)")
|
|
533
|
+
.option("--before <n>", "with --context: messages before the trigger (default 6)")
|
|
534
|
+
.option("--after <n>", "with --context: messages after the trigger (default 4)")
|
|
535
|
+
.option("--recent <n>", "with --context: fallback window when no trigger message (default 10)")
|
|
536
|
+
.option("--files", "download attachments too (off by default across orgs)")
|
|
537
|
+
.option("--max-mb <n>", "with --files: skip attachments larger than N MB (default 25)")
|
|
538
|
+
.option("--summary", "print only the per-org counts")
|
|
539
|
+
.option("--no-desc", "hide the one-line description under each update")
|
|
540
|
+
.option("--out <path>", "write the JSON here instead of ./.flowiq/agent-updates/_all.<status>.json")
|
|
541
|
+
.option("--json", "print the JSON to stdout (no file, no table) — pipe into jq")
|
|
542
|
+
.action((opts) => agentUpdatesCmd.all(opts));
|
|
520
543
|
au.command("list")
|
|
521
544
|
.description("List local agent-updates snapshots")
|
|
522
545
|
.action(() => agentUpdatesCmd.list());
|