@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 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
- Pull an org's client-raised "change the agent" requests (`agent_updates`),
1774
- each with the chat context around the triggering message + the linked contact
1775
- (attachments downloaded locally) — then, once the fix is verified, resolve the
1776
- ticket from the same terminal.
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 a client's pending change requests | `flowiq au pull <org_id>` |
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.2",
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
- const downloadFiles = !opts.noFiles;
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
- const filePath = path.join(AU_DIR, `${slug}.json`);
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("Pull pending agent_updates + the chat context around each; resolve a ticket with a client-facing note");
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());