@flowapt/flowiq-cli 0.2.5 → 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 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> july-promo # DRY-RUN
236
- flowiq seg apply <org_id> july-promo --commit # append the tags (idempotent — re-runs report already_had)
237
- flowiq seg list <org_id> --prefix july-promo # VERIFY: tag → count
238
- flowiq seg untag <org_id> july-promo --commit --confirm # ROLLBACK (its own tags only)
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
- - The cohort comes in as **contact UUIDs** (a snapshot JSON's `contact_ids[]`
242
- or a plain file); defining the cohort with SQL stays upstream.
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 tags snapshot ids; re-derive the cohort SQL
246
- if freshness matters (someone who ordered since won't auto-drop).
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
 
package/TEAM-GUIDE.md CHANGED
@@ -76,7 +76,8 @@ you have installed.
76
76
  | Change agent model / tool flags | `flowiq agent config <org_id> --model … --tool view_cart_tool=true` |
77
77
  | Talk to the live agent safely (no real WhatsApp ever sent) | `flowiq test send <org_id> "hi, do you sell X?"` |
78
78
  | Send a template broadcast to a CSV of people | `flowiq bc map <org_id> --template … --csv …` → `flowiq bc send … ` (dry-run) → `… --commit` |
79
- | Split a big cohort into send-safe batch tags | `flowiq seg plan <org_id> --tag-prefix … --ids-file …` → `flowiq seg apply … --commit` |
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` |
80
81
  | Send to one batch tag | `flowiq bc send <org_id> --tag <batch-tag> --template … --body param1="Hi {{first_name}}" --commit` |
81
82
  | Read a contact's chat | `flowiq m pull <contact_id>` then open the JSON |
82
83
  | Export an org's full chat history | `flowiq export chats <org_id>` |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowapt/flowiq-cli",
3
- "version": "0.2.5",
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": {
@@ -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) # exclusions → slice → plan file (no DB write)
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
- try { cohort = await readCohort(opts, orgId); }
91
- catch (e) { console.error(`Error: ${e.message}`); process.exit(1); }
92
- if (cohort.badUuids.length) console.log(`⚠ skipped ${cohort.badUuids.length} non-UUID line(s) in the cohort source.`);
93
- if (!cohort.ids.length) { console.error("Error: no valid contact ids in the cohort source."); process.exit(1); }
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, include_unsafe: !!opts.includeUnsafe,
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
@@ -409,10 +409,13 @@ export function run(argv) {
409
409
  .alias("seg")
410
410
  .description("Slice a contact cohort into fixed-size batch tags and bulk-apply them (broadcast batching)");
411
411
  segments.command("plan <organization_id>")
412
- .description("Ingest a cohort id list, apply broadcast-safety exclusions, slice into N-sized batch tags, write the plan (no DB write)")
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)")
413
413
  .requiredOption("--tag-prefix <name>", "campaign tag stem; batches become <prefix>-batch-NN")
414
414
  .option("--from-segment <path>", "a segments snapshot JSON (uses its contact_ids[])")
415
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")
416
419
  .option("--batch-size <n>", "contacts per batch tag", "75")
417
420
  .option("--segment <name>", "plan file slug (default: the tag prefix)")
418
421
  .option("--include-unsafe", "do NOT exclude non-broadcast-safe contacts (rare; default excludes)")