@flowapt/flowiq-cli 0.2.4 → 0.2.6
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 +43 -8
- package/TEAM-GUIDE.md +7 -1
- package/package.json +1 -1
- package/src/commands/guide.js +32 -0
- package/src/commands/segments.js +50 -7
- package/src/index.js +12 -1
package/README.md
CHANGED
|
@@ -230,20 +230,32 @@ throttle *and* your brake. Tags only; the send is `broadcast send --tag` (one
|
|
|
230
230
|
batch at a time) or the dashboard broadcaster.
|
|
231
231
|
|
|
232
232
|
```bash
|
|
233
|
+
# Cohort by ORDER-COUNT CRITERIA (v0.2.6 — server-resolved, no id file needed):
|
|
234
|
+
flowiq seg plan <org_id> --tag-prefix repeat-60d --min-orders 2 --window 60d
|
|
235
|
+
# "everyone with 2+ orders in the last 60 days" — counted from the captured
|
|
236
|
+
# order stores (shopify_orders/woo_orders), NOT the lifetime orders_count column.
|
|
237
|
+
# --window takes 60d / 8w / 3m; omit it for all captured history. --platform auto|shopify|woo.
|
|
238
|
+
|
|
239
|
+
# Cohort by explicit id list (the original mode):
|
|
233
240
|
flowiq seg plan <org_id> --tag-prefix july-promo --ids-file ./cohort.txt # or --from-segment snapshot.json
|
|
241
|
+
|
|
234
242
|
# → exclusions (opt-out/archived/blocked) applied BEFORE slicing → .flowiq/segments/<slug>.json
|
|
235
|
-
flowiq seg apply <org_id>
|
|
236
|
-
flowiq seg apply <org_id>
|
|
237
|
-
flowiq seg list <org_id> --prefix
|
|
238
|
-
flowiq seg untag <org_id>
|
|
243
|
+
flowiq seg apply <org_id> repeat-60d # DRY-RUN (default — nothing writes)
|
|
244
|
+
flowiq seg apply <org_id> repeat-60d --commit # append the tags (idempotent — re-runs report already_had)
|
|
245
|
+
flowiq seg list <org_id> --prefix repeat-60d # VERIFY: tag → count
|
|
246
|
+
flowiq seg untag <org_id> repeat-60d --commit --confirm # ROLLBACK (its own tags only)
|
|
239
247
|
```
|
|
240
248
|
|
|
241
|
-
-
|
|
242
|
-
|
|
249
|
+
- **Two cohort sources:** `--min-orders [--window]` resolves the cohort
|
|
250
|
+
server-side (the windowed order count the app's Advanced Tagging UI cannot
|
|
251
|
+
express — its Min Orders filter is lifetime-only), or pass explicit
|
|
252
|
+
**contact UUIDs** (a snapshot JSON's `contact_ids[]` or a plain file) for
|
|
253
|
+
SQL-derived / bespoke cohorts.
|
|
243
254
|
- Apply is **append-only** — it never touches a contact's other tags, names,
|
|
244
255
|
or anything else, and never double-adds.
|
|
245
|
-
- Point-in-time warning: the plan
|
|
246
|
-
|
|
256
|
+
- Point-in-time warning: the plan snapshots the cohort at plan time; re-run
|
|
257
|
+
`plan` to refresh it (someone who ordered since won't auto-drop, criteria
|
|
258
|
+
mode included).
|
|
247
259
|
|
|
248
260
|
### Keywords — `flowiq keywords pull|push|list` (alias `kw`)
|
|
249
261
|
|
|
@@ -269,6 +281,9 @@ flowiq kw push <slug> --prune # ALSO deletes DB keywords missing from
|
|
|
269
281
|
collapses to 1 at runtime — rejected).
|
|
270
282
|
- `field:"attributes"` actions are warned (full jsonb replace; constant
|
|
271
283
|
values only) but applied — this CLI is their only safe editing surface.
|
|
284
|
+
- `action_config.link_preview: false` disables WhatsApp's link-preview card
|
|
285
|
+
on that action's text send (absent/`true` = preview on, the default).
|
|
286
|
+
Passed through verbatim; also toggleable per action in the dashboard.
|
|
272
287
|
- Scheduled keywords: the keyword-scheduler cron reconciles `active` from
|
|
273
288
|
`start_date`/`end_date` within ~30 min of your push.
|
|
274
289
|
|
|
@@ -416,6 +431,13 @@ Media headers: pass `media_header.file_url` (a public URL) — the edge function
|
|
|
416
431
|
uploads it to Meta server-side. Approval is async; re-`pull` for the
|
|
417
432
|
authoritative Meta status.
|
|
418
433
|
|
|
434
|
+
`template_data` is FlowIQ's own send-time mapping (slot labels, default header
|
|
435
|
+
image) stored alongside the template. Omit it and a default is synthesized
|
|
436
|
+
server-side (`auto_synthesized: true`; the response says
|
|
437
|
+
`template_data_synthesized: true`) so the row always exists for `status` and
|
|
438
|
+
broadcast mapping suggestions — pass your own only when you want real slot
|
|
439
|
+
labels (e.g. `"body_params": {"1": "first_name", "2": "order_number"}`).
|
|
440
|
+
|
|
419
441
|
### Org — `flowiq org create` / `flowiq org info <organization_id>`
|
|
420
442
|
|
|
421
443
|
Create a brand-new organization, or look one up (read-only; raw store/Meta
|
|
@@ -545,6 +567,19 @@ flowiq test cleanup <organization_id> --confirm # delete the stress contact
|
|
|
545
567
|
`--agent <id>` targets a specific (non-active) agent. `test stress cleanup` is
|
|
546
568
|
destructive and requires `--confirm`.
|
|
547
569
|
|
|
570
|
+
### Guide — `flowiq guide`
|
|
571
|
+
|
|
572
|
+
Read the bundled docs in the terminal — no digging through node_modules.
|
|
573
|
+
|
|
574
|
+
```bash
|
|
575
|
+
flowiq guide # the team guide (TEAM-GUIDE.md), paged; q to quit
|
|
576
|
+
flowiq guide --reference # this full command reference (README.md)
|
|
577
|
+
flowiq guide --no-pager # plain print (also automatic when piped)
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
Both files ship inside the npm package, so what you read always matches the
|
|
581
|
+
version you have installed.
|
|
582
|
+
|
|
548
583
|
## Environment variable overrides
|
|
549
584
|
|
|
550
585
|
| Var | Default | Use |
|
package/TEAM-GUIDE.md
CHANGED
|
@@ -34,6 +34,10 @@ Confirm with:
|
|
|
34
34
|
flowiq auth whoami
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
+
This guide travels with the CLI — read it any time with `flowiq guide`
|
|
38
|
+
(`--reference` for the full command reference). It always matches the version
|
|
39
|
+
you have installed.
|
|
40
|
+
|
|
37
41
|
> **Approving someone else's login:** when a teammate runs `auth login`, they
|
|
38
42
|
> read you their code (or you open the link they send). On `/cli-auth`, check
|
|
39
43
|
> the code AND the device name match what they told you, then Approve.
|
|
@@ -72,7 +76,8 @@ flowiq auth whoami
|
|
|
72
76
|
| Change agent model / tool flags | `flowiq agent config <org_id> --model … --tool view_cart_tool=true` |
|
|
73
77
|
| Talk to the live agent safely (no real WhatsApp ever sent) | `flowiq test send <org_id> "hi, do you sell X?"` |
|
|
74
78
|
| Send a template broadcast to a CSV of people | `flowiq bc map <org_id> --template … --csv …` → `flowiq bc send … ` (dry-run) → `… --commit` |
|
|
75
|
-
|
|
|
79
|
+
| Tag every repeat buyer (e.g. 2+ orders in the last 60 days) | `flowiq seg plan <org_id> --tag-prefix repeat-60d --min-orders 2 --window 60d` → `flowiq seg apply <org_id> repeat-60d` (dry run) → `… --commit` |
|
|
80
|
+
| Split a big id-list cohort into send-safe batch tags | `flowiq seg plan <org_id> --tag-prefix … --ids-file …` → `flowiq seg apply … --commit` |
|
|
76
81
|
| Send to one batch tag | `flowiq bc send <org_id> --tag <batch-tag> --template … --body param1="Hi {{first_name}}" --commit` |
|
|
77
82
|
| Read a contact's chat | `flowiq m pull <contact_id>` then open the JSON |
|
|
78
83
|
| Export an org's full chat history | `flowiq export chats <org_id>` |
|
|
@@ -84,6 +89,7 @@ flowiq auth whoami
|
|
|
84
89
|
| Close a client's change request (after verifying the fix!) | `flowiq au resolve <update_id> --note "what changed"` — the client reads the note |
|
|
85
90
|
| Check an org's platform + active agent | `flowiq org info <org_id>` |
|
|
86
91
|
| Work a Pin Board task | `flowiq pin list-remote open` → `pull` → edit → `push` |
|
|
92
|
+
| Re-read this guide / the full command reference | `flowiq guide` / `flowiq guide --reference` |
|
|
87
93
|
|
|
88
94
|
The flow is the same everywhere: **pull → edit the JSON → push**. Slugs are the
|
|
89
95
|
filename the pull printed (e.g. `phytoceutics`), or pass a file path.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flowapt/flowiq-cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
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": {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// flowiq guide — read the bundled team guide (TEAM-GUIDE.md) or the full
|
|
2
|
+
// command reference (README.md) right in the terminal. Both files ship inside
|
|
3
|
+
// the npm tarball, so what this prints always matches the installed CLI
|
|
4
|
+
// version — no drift between the tool and its docs.
|
|
5
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { spawnSync } from "node:child_process";
|
|
9
|
+
|
|
10
|
+
const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
11
|
+
|
|
12
|
+
export function show(opts = {}) {
|
|
13
|
+
const file = opts.reference ? "README.md" : "TEAM-GUIDE.md";
|
|
14
|
+
const fullPath = path.join(PKG_ROOT, file);
|
|
15
|
+
|
|
16
|
+
if (!existsSync(fullPath)) {
|
|
17
|
+
console.error(`Could not find ${file} in the installed package (${PKG_ROOT}).`);
|
|
18
|
+
console.error("Reinstall the CLI: npm i -g @flowapt/flowiq-cli");
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Interactive terminal → page it (quit with q); piped/redirected or
|
|
23
|
+
// --no-pager → plain print.
|
|
24
|
+
if (process.stdout.isTTY && opts.pager !== false) {
|
|
25
|
+
const pager = process.env.PAGER || "less";
|
|
26
|
+
const args = pager === "less" ? ["-R", fullPath] : [fullPath];
|
|
27
|
+
const res = spawnSync(pager, args, { stdio: "inherit" });
|
|
28
|
+
if (!res.error) return;
|
|
29
|
+
// pager missing → fall through to plain print
|
|
30
|
+
}
|
|
31
|
+
process.stdout.write(readFileSync(fullPath, "utf8"));
|
|
32
|
+
}
|
package/src/commands/segments.js
CHANGED
|
@@ -3,11 +3,17 @@
|
|
|
3
3
|
// runbook). This command TAGS ONLY — it never sends. The send per tag is
|
|
4
4
|
// `flowiq broadcast send --tag <batch-tag>` (or the dashboard broadcaster).
|
|
5
5
|
//
|
|
6
|
-
// plan <org> --tag-prefix X (--from-segment file | --ids-file file
|
|
6
|
+
// plan <org> --tag-prefix X (--from-segment file | --ids-file file
|
|
7
|
+
// | --min-orders N [--window 60d]) # exclusions → slice → plan file (no DB write)
|
|
7
8
|
// apply <org> <slug> [--commit] [--yes] # append the batch tags (dry-run default)
|
|
8
9
|
// list <org> [--prefix X] # VERIFY: tag → contact count
|
|
9
10
|
// untag <org> <slug> --commit --confirm # ROLLBACK: remove the plan's own tags
|
|
10
11
|
//
|
|
12
|
+
// --min-orders resolves the cohort SERVER-SIDE (segment_order_cohort RPC):
|
|
13
|
+
// contacts with >= N captured orders (shopify_orders/woo_orders) inside the
|
|
14
|
+
// --window (e.g. 60d / 8w / 3m; omit for all captured history) — the windowed
|
|
15
|
+
// count the Advanced Tagging UI cannot express (its Min Orders is lifetime).
|
|
16
|
+
//
|
|
11
17
|
// Golden rules encoded: exclusions (opt-out/archived/blocked) are applied
|
|
12
18
|
// BEFORE slicing; apply is append-only + idempotent (`already_had` reported,
|
|
13
19
|
// never double-added); untag only ever removes the plan's own tags.
|
|
@@ -52,10 +58,20 @@ async function resolvePlan(identifier) {
|
|
|
52
58
|
throw new Error(`Plan file not found. Tried:\n ${candidates.join("\n ")}`);
|
|
53
59
|
}
|
|
54
60
|
|
|
61
|
+
/** Parse --window "60d" / "8w" / "3m" / "45" → days. */
|
|
62
|
+
function parseWindowDays(spec) {
|
|
63
|
+
const m = String(spec).trim().match(/^(\d+)([dwm])?$/i);
|
|
64
|
+
if (!m) throw new Error(`--window must look like 60d / 8w / 3m (got "${spec}")`);
|
|
65
|
+
const n = Number(m[1]);
|
|
66
|
+
const days = n * ({ d: 1, w: 7, m: 30 }[(m[2] || "d").toLowerCase()]);
|
|
67
|
+
if (!Number.isInteger(days) || days < 1 || days > 3650) throw new Error("--window must resolve to 1..3650 days");
|
|
68
|
+
return days;
|
|
69
|
+
}
|
|
70
|
+
|
|
55
71
|
/** Read the cohort ids from a christiaan/segments snapshot or a plain file. */
|
|
56
72
|
async function readCohort(opts, orgId) {
|
|
57
73
|
if (!!opts.fromSegment === !!opts.idsFile) {
|
|
58
|
-
throw new Error("provide exactly one of --from-segment / --ids-file");
|
|
74
|
+
throw new Error("provide exactly one of --from-segment / --ids-file / --min-orders");
|
|
59
75
|
}
|
|
60
76
|
let ids = [], sourceMeta;
|
|
61
77
|
if (opts.fromSegment) {
|
|
@@ -86,23 +102,50 @@ export async function plan(orgId, opts = {}) {
|
|
|
86
102
|
if (!Number.isInteger(batchSize) || batchSize < 1) { console.error("Error: --batch-size must be ≥ 1."); process.exit(1); }
|
|
87
103
|
if (batchSize > 500) console.log(`⚠ batch size ${batchSize} is large — Meta tier risk; the runbook default is 75.`);
|
|
88
104
|
|
|
105
|
+
// Cohort source: a server-resolved order-count criterion (--min-orders
|
|
106
|
+
// [--window]) OR an explicit id list (--from-segment / --ids-file).
|
|
107
|
+
const criteriaMode = opts.minOrders !== undefined;
|
|
108
|
+
if (criteriaMode && (opts.fromSegment || opts.idsFile)) {
|
|
109
|
+
console.error("Error: --min-orders cannot be combined with --from-segment / --ids-file.");
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
|
|
89
113
|
let cohort;
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
114
|
+
let cohortSpec = null;
|
|
115
|
+
if (criteriaMode) {
|
|
116
|
+
const minOrders = Number(opts.minOrders);
|
|
117
|
+
if (!Number.isInteger(minOrders) || minOrders < 1) { console.error("Error: --min-orders must be an integer ≥ 1."); process.exit(1); }
|
|
118
|
+
let windowDays = null;
|
|
119
|
+
if (opts.window) {
|
|
120
|
+
try { windowDays = parseWindowDays(opts.window); }
|
|
121
|
+
catch (e) { console.error(`Error: ${e.message}`); process.exit(1); }
|
|
122
|
+
}
|
|
123
|
+
cohortSpec = { min_orders: minOrders, window_days: windowDays, platform: opts.platform || "auto" };
|
|
124
|
+
cohort = { ids: [], badUuids: [], sourceMeta: { type: "order_cohort", ...cohortSpec } };
|
|
125
|
+
} else {
|
|
126
|
+
try { cohort = await readCohort(opts, orgId); }
|
|
127
|
+
catch (e) { console.error(`Error: ${e.message}`); process.exit(1); }
|
|
128
|
+
if (cohort.badUuids.length) console.log(`⚠ skipped ${cohort.badUuids.length} non-UUID line(s) in the cohort source.`);
|
|
129
|
+
if (!cohort.ids.length) { console.error("Error: no valid contact ids in the cohort source."); process.exit(1); }
|
|
130
|
+
}
|
|
94
131
|
|
|
95
132
|
let resp;
|
|
96
133
|
try {
|
|
97
134
|
resp = await http.post("segments", {
|
|
98
135
|
mode: "plan", organization_id: orgId,
|
|
99
|
-
contact_ids: cohort.ids
|
|
136
|
+
...(criteriaMode ? { cohort: cohortSpec } : { contact_ids: cohort.ids }),
|
|
137
|
+
include_unsafe: !!opts.includeUnsafe,
|
|
100
138
|
});
|
|
101
139
|
} catch (e) {
|
|
102
140
|
console.error(`Plan failed: ${e.message}`);
|
|
103
141
|
if (e.body?.error) console.error(` ${e.body.error}`);
|
|
104
142
|
process.exit(1);
|
|
105
143
|
}
|
|
144
|
+
if (resp.cohort) {
|
|
145
|
+
const w = resp.cohort.window_days ? `in the last ${resp.cohort.window_days} days` : "across all captured history";
|
|
146
|
+
console.log(`Cohort: ${resp.cohort.min_orders}+ orders ${w} → ${resp.cohort.resolved_count} contact(s) (server-resolved from captured orders).`);
|
|
147
|
+
cohort.sourceMeta.resolved_count = resp.cohort.resolved_count;
|
|
148
|
+
}
|
|
106
149
|
if (!resp.safe_count) { console.error("Nothing to tag — 0 safe contacts after exclusions (V-13)."); process.exit(1); }
|
|
107
150
|
|
|
108
151
|
// Slice safe_ids into contiguous batches (client-side, per the spec split).
|
package/src/index.js
CHANGED
|
@@ -27,6 +27,7 @@ import * as customToolsCmd from "./commands/custom-tools.js";
|
|
|
27
27
|
import * as broadcastCmd from "./commands/broadcast.js";
|
|
28
28
|
import * as segmentsCmd from "./commands/segments.js";
|
|
29
29
|
import * as keywordsCmd from "./commands/keywords.js";
|
|
30
|
+
import * as guideCmd from "./commands/guide.js";
|
|
30
31
|
|
|
31
32
|
// Read version from package.json so it stays in sync with the published npm
|
|
32
33
|
// version automatically (single source of truth — bumping package.json on each
|
|
@@ -408,10 +409,13 @@ export function run(argv) {
|
|
|
408
409
|
.alias("seg")
|
|
409
410
|
.description("Slice a contact cohort into fixed-size batch tags and bulk-apply them (broadcast batching)");
|
|
410
411
|
segments.command("plan <organization_id>")
|
|
411
|
-
.description("
|
|
412
|
+
.description("Resolve a cohort (id list OR order-count criteria), apply broadcast-safety exclusions, slice into N-sized batch tags, write the plan (no DB write)")
|
|
412
413
|
.requiredOption("--tag-prefix <name>", "campaign tag stem; batches become <prefix>-batch-NN")
|
|
413
414
|
.option("--from-segment <path>", "a segments snapshot JSON (uses its contact_ids[])")
|
|
414
415
|
.option("--ids-file <path>", "a plain newline/CSV file of contact UUIDs")
|
|
416
|
+
.option("--min-orders <n>", "SERVER-RESOLVED cohort: contacts with ≥ n captured orders (instead of an id file)")
|
|
417
|
+
.option("--window <span>", "with --min-orders: only count orders in this window, e.g. 60d / 8w / 3m (omit = all captured history)")
|
|
418
|
+
.option("--platform <p>", "with --min-orders: auto | shopify | woo", "auto")
|
|
415
419
|
.option("--batch-size <n>", "contacts per batch tag", "75")
|
|
416
420
|
.option("--segment <name>", "plan file slug (default: the tag prefix)")
|
|
417
421
|
.option("--include-unsafe", "do NOT exclude non-broadcast-safe contacts (rare; default excludes)")
|
|
@@ -448,5 +452,12 @@ export function run(argv) {
|
|
|
448
452
|
.description("List local keyword snapshots")
|
|
449
453
|
.action(() => keywordsCmd.list());
|
|
450
454
|
|
|
455
|
+
// guide (bundled docs — always match the installed version)
|
|
456
|
+
program.command("guide")
|
|
457
|
+
.description("Read the team guide (how we use this CLI); --reference for the full command reference")
|
|
458
|
+
.option("--reference", "show the full command reference (README) instead of the team guide")
|
|
459
|
+
.option("--no-pager", "print straight to stdout instead of paging")
|
|
460
|
+
.action((opts) => guideCmd.show(opts));
|
|
461
|
+
|
|
451
462
|
program.parseAsync(argv);
|
|
452
463
|
}
|