@flowapt/flowiq-cli 0.9.6 → 0.9.8
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 +152 -10
- package/TEAM-GUIDE.md +28 -9
- 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 +346 -0
- 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/gaps.test.mjs +48 -0
- package/src/http.js +34 -8
- package/src/index.js +107 -9
|
@@ -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
|
}
|
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
// expect/expectNot checks client-side, and prints the transcript.
|
|
5
5
|
//
|
|
6
6
|
// test resolve <org_id> [--agent <id>]
|
|
7
|
-
// test send <org_id> <message...> [--agent] [--clear] [--sender] [--image] [--timeout] [--json]
|
|
7
|
+
// test send <org_id> <message...> [--agent] [--clear] [--sender] [--image] [--timeout] [--json] [--force]
|
|
8
8
|
// test scenario <org_id> <file.json> [--agent] [--no-clear-between] [--json] [--sender] [--timeout]
|
|
9
9
|
// test qa <org_id> [--agent] ... (bundled generic smoke pack)
|
|
10
|
-
// test clear <org_id> [--agent]
|
|
10
|
+
// test clear <org_id> [--agent] [--force]
|
|
11
11
|
// test stress <org_id> [pack.json] [--agent] [--concurrency 6] [--timeout] [--out] [--contact]
|
|
12
12
|
// test cleanup <org_id> [--agent] --confirm (deletes stress contacts)
|
|
13
13
|
// test extract <org_id> [--out] (rebuild transcripts from DB)
|
|
@@ -28,7 +28,9 @@ function requireOrg(orgId) {
|
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
// force: the server refuses send / clear on a test contact that had real
|
|
32
|
+
// (non-web) inbound in the last 6 h, i.e. a live customer conversation.
|
|
33
|
+
async function runTurn(orgId, { agent, message, image, clear, sender, timeout, force }) {
|
|
32
34
|
return http.post("agent-test", {
|
|
33
35
|
action: "send",
|
|
34
36
|
organization_id: orgId,
|
|
@@ -38,11 +40,18 @@ async function runTurn(orgId, { agent, message, image, clear, sender, timeout })
|
|
|
38
40
|
clear: !!clear,
|
|
39
41
|
sender,
|
|
40
42
|
timeout_ms: timeout,
|
|
43
|
+
force: !!force,
|
|
41
44
|
});
|
|
42
45
|
}
|
|
43
46
|
|
|
44
|
-
async function clearConv(orgId, agent) {
|
|
45
|
-
return http.post("agent-test", { action: "clear", organization_id: orgId, agent_id: agent });
|
|
47
|
+
async function clearConv(orgId, agent, force) {
|
|
48
|
+
return http.post("agent-test", { action: "clear", organization_id: orgId, agent_id: agent, force: !!force });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let warned = false;
|
|
52
|
+
function printWarnings(r) {
|
|
53
|
+
if (warned) return;
|
|
54
|
+
for (const w of r?.warnings || []) { console.error(`⚠ ${w}`); warned = true; }
|
|
46
55
|
}
|
|
47
56
|
|
|
48
57
|
// A bare string is ONE needle, not a list of characters. Iterating a string
|
|
@@ -88,9 +97,11 @@ export async function send(orgId, messageParts, opts = {}) {
|
|
|
88
97
|
if (!message) { console.error("usage: test send <org_id> <message...>"); process.exit(1); }
|
|
89
98
|
let r;
|
|
90
99
|
try {
|
|
91
|
-
r = await runTurn(orgId, { agent: opts.agent, message, image: opts.image, clear: opts.clear, sender: opts.sender, timeout: opts.timeout });
|
|
100
|
+
r = await runTurn(orgId, { agent: opts.agent, message, image: opts.image, clear: opts.clear, sender: opts.sender, timeout: opts.timeout, force: opts.force });
|
|
92
101
|
} catch (e) { console.error(`Send failed: ${e.message}`); process.exit(1); }
|
|
93
102
|
if (opts.json) { console.log(JSON.stringify(r, null, 2)); return; }
|
|
103
|
+
printWarnings(r);
|
|
104
|
+
if (opts.clear && r.previous_memory_cutoff !== undefined) console.log(`# cleared first; previous memory_cutoff: ${r.previous_memory_cutoff ?? "(none)"}`);
|
|
94
105
|
console.log(`# org=${r.org?.name} agent=${r.agent?.name}${r.agent?.is_active ? " (active)" : " (override)"} contact=${r.contact?.id}`);
|
|
95
106
|
printTurn({ title: "send", tq: "" }, 0, message, r, []);
|
|
96
107
|
}
|
|
@@ -103,14 +114,15 @@ async function runPack(orgId, pack, opts) {
|
|
|
103
114
|
for (let s = 0; s < scenarios.length; s++) {
|
|
104
115
|
const sc = scenarios[s];
|
|
105
116
|
if (clearBetween) {
|
|
106
|
-
try { await clearConv(orgId, opts.agent); } catch (e) { console.error(` (clear failed: ${e.message})`); }
|
|
117
|
+
try { printWarnings(await clearConv(orgId, opts.agent, opts.force)); } catch (e) { console.error(` (clear failed: ${e.message})`); }
|
|
107
118
|
}
|
|
108
119
|
const scResult = { id: sc.id, tq: sc.tq, title: sc.title, turns: [] };
|
|
109
120
|
for (let t = 0; t < sc.turns.length; t++) {
|
|
110
121
|
const turn = sc.turns[t];
|
|
111
122
|
let r;
|
|
112
123
|
try {
|
|
113
|
-
r = await runTurn(orgId, { agent: opts.agent, message: turn.text, sender: opts.sender, timeout: opts.timeout });
|
|
124
|
+
r = await runTurn(orgId, { agent: opts.agent, message: turn.text, sender: opts.sender, timeout: opts.timeout, force: opts.force });
|
|
125
|
+
printWarnings(r);
|
|
114
126
|
} catch (e) {
|
|
115
127
|
r = { status: "error", reply: null, tool_calls: [], raw: { error: e.message } };
|
|
116
128
|
}
|
|
@@ -144,10 +156,12 @@ export async function qa(orgId, opts = {}) {
|
|
|
144
156
|
export async function clear(orgId, opts = {}) {
|
|
145
157
|
requireOrg(orgId);
|
|
146
158
|
let resp;
|
|
147
|
-
try { resp = await clearConv(orgId, opts.agent); }
|
|
159
|
+
try { resp = await clearConv(orgId, opts.agent, opts.force); }
|
|
148
160
|
catch (e) { console.error(`Clear failed: ${e.message}`); process.exit(1); }
|
|
161
|
+
printWarnings(resp);
|
|
149
162
|
const tools = Number(resp.tools_cleared || 0);
|
|
150
163
|
console.log(`Cleared web conversation for contact ${resp.contact_id} (history hidden via memory_cutoff; ${tools} remembered tool result(s) set aside in more_data.tool_usage_history_cleared; onboarding/disclaimer state NOT reset).`);
|
|
164
|
+
console.log(`Previous memory_cutoff: ${resp.previous_memory_cutoff ?? "(none)"}${resp.previous_memory_cutoff ? " (to undo, set contacts.memory_cutoff back to it)" : ""}`);
|
|
151
165
|
}
|
|
152
166
|
|
|
153
167
|
// ---------- phase 2: stress + extract ----------
|
|
@@ -320,8 +320,8 @@ export async function resolve(updateId, opts = {}) {
|
|
|
320
320
|
process.exit(1);
|
|
321
321
|
}
|
|
322
322
|
const status = opts.status || "resolved";
|
|
323
|
-
if (!["resolved", "declined"].includes(status)) {
|
|
324
|
-
console.error(`Error: --status must be resolved or
|
|
323
|
+
if (!["resolved", "declined", "in_progress"].includes(status)) {
|
|
324
|
+
console.error(`Error: --status must be resolved, declined or in_progress (got "${status}").`);
|
|
325
325
|
process.exit(1);
|
|
326
326
|
}
|
|
327
327
|
// R-3: a client-facing note is mandatory on resolve (--yes does NOT waive it).
|
|
@@ -332,7 +332,7 @@ export async function resolve(updateId, opts = {}) {
|
|
|
332
332
|
}
|
|
333
333
|
|
|
334
334
|
if (!opts.yes) {
|
|
335
|
-
console.log(`About to mark update ${updateId} as ${status.toUpperCase()}.`);
|
|
335
|
+
console.log(`About to mark update ${updateId} as ${status.toUpperCase()}${status === "in_progress" ? " (not closed: no resolution stamp)" : ""}.`);
|
|
336
336
|
if (opts.note) console.log(`The client will read: FlowIQ: ${opts.note}`);
|
|
337
337
|
if (opts.internal) console.log(`Staff-only note: ${opts.internal}`);
|
|
338
338
|
const answer = await confirmPrompt("Type y to confirm: ");
|
|
@@ -363,6 +363,7 @@ export async function resolve(updateId, opts = {}) {
|
|
|
363
363
|
console.log(` title: ${resp.title}`);
|
|
364
364
|
console.log(` was: ${resp.previous_status}`);
|
|
365
365
|
console.log(` by: ${resp.resolved_by_email ?? resp.resolved_by}`);
|
|
366
|
+
if (resp.status === "in_progress") console.log(` still open: resolve it later with --note once the fix is proven.`);
|
|
366
367
|
if (resp.superadmin_comment) console.log(` client: FlowIQ: ${resp.superadmin_comment}`);
|
|
367
368
|
if (resp.internal_notes) console.log(` internal: ${resp.internal_notes}`);
|
|
368
369
|
if (resp.already_resolved) console.log(` ⚠ was already terminal (${resp.previous_status}) — note/status overwritten.`);
|
package/src/commands/audit.js
CHANGED
|
@@ -61,6 +61,7 @@ export async function list(orgId, opts = {}) {
|
|
|
61
61
|
user: opts.user,
|
|
62
62
|
since: opts.since,
|
|
63
63
|
until: opts.until,
|
|
64
|
+
before: opts.before,
|
|
64
65
|
limit: opts.limit,
|
|
65
66
|
});
|
|
66
67
|
} catch (e) {
|
|
@@ -80,7 +81,10 @@ export async function list(orgId, opts = {}) {
|
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
console.log("");
|
|
83
|
-
|
|
84
|
+
const total = resp.total ?? entries.length;
|
|
85
|
+
console.log(resp.capped
|
|
86
|
+
? `${entries.length} of ${total.toLocaleString()} matching entries (newest first; limit ${resp.limit}${resp.requested_limit > resp.limit ? `, the most one call returns — you asked for ${resp.requested_limit}` : ""})`
|
|
87
|
+
: `${entries.length} entr${entries.length === 1 ? "y" : "ies"} (newest first)`);
|
|
84
88
|
console.log("");
|
|
85
89
|
console.log(` ${pad("WHEN (SAST)", 17)} ${pad("WHO", 24)} ${pad("WHAT", 26)} SUMMARY`);
|
|
86
90
|
console.log(` ${"-".repeat(17)} ${"-".repeat(24)} ${"-".repeat(26)} ${"-".repeat(40)}`);
|
|
@@ -101,6 +105,9 @@ export async function list(orgId, opts = {}) {
|
|
|
101
105
|
for (const e of entries) {
|
|
102
106
|
console.log(` ${e.id} ${e.endpoint} ${e.action}`);
|
|
103
107
|
}
|
|
108
|
+
if (resp.capped && resp.next_before) {
|
|
109
|
+
console.log(`\n ${(total - entries.length).toLocaleString()} older match(es) not shown: add --before ${resp.next_before}`);
|
|
110
|
+
}
|
|
104
111
|
console.log("");
|
|
105
112
|
}
|
|
106
113
|
|
|
@@ -1935,15 +1935,19 @@ export async function listRemote(orgId, opts = {}) {
|
|
|
1935
1935
|
...(opts.limit ? { limit: Number(opts.limit) } : {}),
|
|
1936
1936
|
...(opts.template ? { template: opts.template } : {}),
|
|
1937
1937
|
...(opts.since ? { since: opts.since } : {}),
|
|
1938
|
+
...(opts.minRecipients ? { min_recipients: Number(opts.minRecipients) } : {}),
|
|
1938
1939
|
});
|
|
1939
1940
|
} catch (e) { console.error(`Listing failed: ${e.message}`); if (e.body?.error) console.error(` ${e.body.error}`); process.exit(1); }
|
|
1940
1941
|
if (opts.json) { console.log(JSON.stringify(resp, null, 2)); return; }
|
|
1941
1942
|
const rows = resp.broadcasts || [];
|
|
1942
|
-
if (!rows.length) { console.log(`No broadcasts found for ${resp.organization_name}${opts.template || opts.since ? " matching the filters" : ""}.`); return; }
|
|
1943
|
+
if (!rows.length) { console.log(`No broadcasts found for ${resp.organization_name}${opts.template || opts.since || opts.minRecipients ? " matching the filters" : ""}.`); return; }
|
|
1943
1944
|
// Full ids one per row, deliberately — an id you can't paste into `bc status`
|
|
1944
1945
|
// is useless (the same lesson the audit listing learned).
|
|
1945
1946
|
const sast = (iso) => { try { return new Date(iso).toLocaleString("sv-SE", { timeZone: "Africa/Johannesburg" }).slice(0, 16); } catch { return iso; } };
|
|
1946
|
-
|
|
1947
|
+
const shown = resp.capped
|
|
1948
|
+
? `${rows.length} of ${resp.total.toLocaleString()} shown`
|
|
1949
|
+
: `${rows.length} shown`;
|
|
1950
|
+
console.log(`Broadcasts — ${resp.organization_name} (${shown}${resp.min_recipients ? `, ${resp.min_recipients}+ recipients` : ""}, newest first; times SAST)`);
|
|
1947
1951
|
for (const b of rows) {
|
|
1948
1952
|
console.log("");
|
|
1949
1953
|
console.log(` ${b.id}`);
|
|
@@ -1951,6 +1955,9 @@ export async function listRemote(orgId, opts = {}) {
|
|
|
1951
1955
|
console.log(` status ${b.status ?? "?"} · ${b.total_recipients ?? "?"} recipient(s)`);
|
|
1952
1956
|
}
|
|
1953
1957
|
console.log("");
|
|
1958
|
+
if (resp.capped) {
|
|
1959
|
+
console.log(`⚠ ${(resp.total - rows.length).toLocaleString()} older broadcast(s) not shown (limit ${resp.limit}). Narrow with --since, --template${resp.min_recipients ? "" : " or --min-recipients 2 (hides one-recipient sends like welcomes and cart reminders)"}.`);
|
|
1960
|
+
}
|
|
1954
1961
|
console.log(`Delivery detail: flowiq bc status ${orgId} <broadcast_id> [--failures]`);
|
|
1955
1962
|
}
|
|
1956
1963
|
|
|
@@ -1984,6 +1991,15 @@ export async function status(orgId, broadcastId, opts = {}) {
|
|
|
1984
1991
|
if (b.total_recipients) {
|
|
1985
1992
|
console.log(` ${((accepted / b.total_recipients) * 100).toFixed(1)}% of ${b.total_recipients} recipients accepted${d.linked < b.total_recipients ? " (still sending?)" : ""}`);
|
|
1986
1993
|
}
|
|
1994
|
+
const p = resp.payload;
|
|
1995
|
+
if (p && (Object.keys(p.body_parameters || {}).length || Object.keys(p.button_parameters || {}).length || Object.keys(p.header_parameters || {}).length)) {
|
|
1996
|
+
console.log(` sent with:`);
|
|
1997
|
+
const slot = (k) => String(k).replace(/^param/, "{{") + (String(k).startsWith("param") ? "}}" : "");
|
|
1998
|
+
for (const [k, v] of Object.entries(p.header_parameters || {})) console.log(` header ${slot(k)} ${String(v).replace(/\s+/g, " ")}`);
|
|
1999
|
+
for (const [k, v] of Object.entries(p.body_parameters || {})) console.log(` body ${slot(k)} ${String(v).replace(/\s+/g, " ")}`);
|
|
2000
|
+
for (const [k, v] of Object.entries(p.button_parameters || {})) console.log(` button ${slot(k)} ${v}`);
|
|
2001
|
+
for (const l of p.links || []) console.log(` link ${l.short_code} → ${l.destination} (${l.clicks} click${l.clicks === 1 ? "" : "s"})`);
|
|
2002
|
+
}
|
|
1987
2003
|
if (resp.reply_routing) {
|
|
1988
2004
|
const rr = resp.reply_routing;
|
|
1989
2005
|
console.log(` replies → ${describeRouting(rr)}${rr.enabled === false ? " (switched OFF)" : ""}`);
|
package/src/commands/doctor.js
CHANGED
|
@@ -101,10 +101,19 @@ export async function doctor(opts = {}) {
|
|
|
101
101
|
}
|
|
102
102
|
} catch (e) {
|
|
103
103
|
lines.push(` ${BAD} auth ${e.message.split("\n")[0]}`);
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
104
|
+
if (e.code === "EVERCELCHALLENGE") {
|
|
105
|
+
problems.push({
|
|
106
|
+
what: "Vercel's bot protection is challenging this network; the key was never checked",
|
|
107
|
+
fix: "wait a few minutes or switch network (hotspot), then flowiq doctor again; do NOT re-login",
|
|
108
|
+
});
|
|
109
|
+
} else {
|
|
110
|
+
// Only a JSON 401/403 from our API means the key itself was refused.
|
|
111
|
+
const keyRefused = (e.status === 401 || e.status === 403) && e.code !== "ENONJSON";
|
|
112
|
+
problems.push({
|
|
113
|
+
what: keyRefused ? "the API rejected this key" : "the API could not be reached properly",
|
|
114
|
+
fix: keyRefused ? "flowiq auth login" : "check api_url / network, then retry",
|
|
115
|
+
});
|
|
116
|
+
}
|
|
108
117
|
}
|
|
109
118
|
}
|
|
110
119
|
|