@flowapt/flowiq-cli 0.4.7 → 0.4.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 +69 -3
- package/TEAM-GUIDE.md +6 -0
- package/package.json +1 -1
- package/src/commands/links.js +144 -0
- package/src/index.js +21 -0
package/README.md
CHANGED
|
@@ -480,6 +480,47 @@ flowiq tag remove <org_id> vip,old-promo --confirm # remove tag(s) from ALL con
|
|
|
480
480
|
matches — re-run until `matched 0`. Idempotent and safe to re-run.
|
|
481
481
|
- **Undo** any tag with `flowiq tag remove <org> <tag> --confirm`.
|
|
482
482
|
|
|
483
|
+
### Links — `flowiq links shorten|list` (v0.4.8)
|
|
484
|
+
|
|
485
|
+
Short links with campaign UTM tags — the terminal half of the in-app **URL
|
|
486
|
+
Shortener**. Same engine server-side (`api/_url-shorten-engine.js`), so a link
|
|
487
|
+
minted here is identical to one minted in the dialog.
|
|
488
|
+
|
|
489
|
+
```bash
|
|
490
|
+
# Dry run FIRST — shows the exact destination each short link will carry
|
|
491
|
+
flowiq links shorten <org> --url "https://shop.co.za/product/x" \
|
|
492
|
+
--campaign 13Aug_Seeds --content 13Aug_Seeds
|
|
493
|
+
|
|
494
|
+
flowiq links shorten <org> --url "https://shop.co.za/product/x" \
|
|
495
|
+
--url "https://shop.co.za/product/y" \
|
|
496
|
+
--campaign 13Aug_Seeds --content 13Aug_Seeds --domain linklnk.io --commit
|
|
497
|
+
|
|
498
|
+
flowiq links shorten <org> --file ./slide2.txt --campaign 13Aug_Seeds_VM --content 13Aug_Seeds_VM --commit
|
|
499
|
+
flowiq links list <org> --campaign 13Aug_Seeds
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
- **`utm_source=whatsapp` and `utm_medium=whatsapp_paid` are FIXED** (the dialog
|
|
503
|
+
sets them too) — a link built any other way stops matching the attribution
|
|
504
|
+
queries. You supply `--campaign` and `--content`, and they must be **supplied
|
|
505
|
+
together**: a campaign with no content silently under-tags every link.
|
|
506
|
+
- **Dry-run by default.** `--commit` mints. The dry run prints the full
|
|
507
|
+
destination URL per link and whether an identical row already exists.
|
|
508
|
+
- **A new UTM tag = a NEW row with its own code and its own click count.** Reuse
|
|
509
|
+
is keyed on the exact `(org, full_url)` pair, which is what makes per-campaign
|
|
510
|
+
attribution work. Re-running the same command is idempotent (`↻ reused`).
|
|
511
|
+
- **Pasting an existing short link is REFUSED** (`chatcart.io` / `linklnk.io` /
|
|
512
|
+
`yapi.store`) with "pass the destination URL". Shortening a short link would
|
|
513
|
+
add a second hop and split the click count — and would silently keep the OLD
|
|
514
|
+
campaign tag, which is the trap this guard exists for.
|
|
515
|
+
- **`--file`** shortens every URL found in a text file — the fast path for a
|
|
516
|
+
multi-link message body. It does NOT rewrite the file; it prints the mapping.
|
|
517
|
+
- `--domain` picks the short host: `chatcart.io` (default), `linklnk.io`,
|
|
518
|
+
`yapi.store`. All three resolve identically — match whatever that campaign's
|
|
519
|
+
other links already use.
|
|
520
|
+
- `--campaign` on **`list`** is an EXACT match, not a prefix: asking for
|
|
521
|
+
`13Aug_Seeds` does not return the `13Aug_Seeds_VM` rows.
|
|
522
|
+
- Scope group `broadcast`. Audited (`links` / `shorten`); dry runs are not.
|
|
523
|
+
|
|
483
524
|
### Keywords — `flowiq keywords pull|push|list` (alias `kw`)
|
|
484
525
|
|
|
485
526
|
Round-trips an org's **keyword auto-reply engine** — the `keywords` +
|
|
@@ -504,10 +545,10 @@ flowiq kw push <slug> --prune # ALSO deletes DB keywords missing from
|
|
|
504
545
|
collapses to 1 at runtime — rejected).
|
|
505
546
|
- `field:"attributes"` actions are warned (full jsonb replace; constant
|
|
506
547
|
values only) but applied — this CLI is their only safe editing surface.
|
|
507
|
-
- **Action types accepted** (all
|
|
548
|
+
- **Action types accepted** (all ten the runtime implements):
|
|
508
549
|
`send_message` · `update_contact_field` · `add_contact_tag` ·
|
|
509
550
|
`remove_contact_tag` · `set_agent` · `delay` · `combined` ·
|
|
510
|
-
`update_ticket_status` · `renotify_ticket`.
|
|
551
|
+
`update_ticket_status` · `renotify_ticket` · `assign_chat`.
|
|
511
552
|
Per-type rules: tag actions need a non-empty `tags[]` (or a single `tag`
|
|
512
553
|
string) or the runtime writes no tag at all; `set_agent.agent_id` must be
|
|
513
554
|
an agent UUID, or `null`/`""` to CLEAR the contact's binding (warned, since
|
|
@@ -530,6 +571,18 @@ flowiq kw push <slug> --prune # ALSO deletes DB keywords missing from
|
|
|
530
571
|
ticket-tool `mode:"renotify"` (60s server-side rate limit; a resolved/closed
|
|
531
572
|
ticket is reopened first) — canonical use: the **"I still need help"**
|
|
532
573
|
button. Both send the action's `text` (if any) after the ticket work.
|
|
574
|
+
- **Assign-chat action** (added 13 Aug 2026, for keyword-driven handovers —
|
|
575
|
+
e.g. a broadcast quick-reply button handing the conversation to a person):
|
|
576
|
+
`{ "type": "assign_chat", "team_id": "<team-uuid>", "assignee_user_id":
|
|
577
|
+
"<user-uuid>", "notify_team": true, "notify_member": true }` — writes the
|
|
578
|
+
contact's `team_id` and/or `assignee` (at least ONE of the two is required,
|
|
579
|
+
both UUID-shaped; `assignee_user_id` is the profiles/auth user id, not an
|
|
580
|
+
`organization_members` id), then invokes `team-assignment-notify` for each
|
|
581
|
+
target unless its `notify_*` flag is explicitly `false`. The notify fn
|
|
582
|
+
re-checks the org's team/member notification feature flags AND the target's
|
|
583
|
+
own notify config (Teams page) server-side, so a target with notifications
|
|
584
|
+
off is assigned silently. Also editable in the dashboard (Keyword Actions →
|
|
585
|
+
action type "Assign Chat").
|
|
533
586
|
- `action_config.link_preview: false` disables WhatsApp's link-preview card
|
|
534
587
|
on that action's text send (absent/`true` = preview on, the default).
|
|
535
588
|
Passed through verbatim; also toggleable per action in the dashboard.
|
|
@@ -990,11 +1043,24 @@ flowiq agent config <organization_id> --test-contact-number 27000000001 --test-c
|
|
|
990
1043
|
Settable: `settings.use_settings_prompt`, `settings.model`, agent `--rename`,
|
|
991
1044
|
the tool-flag columns (`woo_order_build`, `woo_tip_field`, `woo_order_note_field`,
|
|
992
1045
|
`view_cart_tool`, `restock_tool`, `block_tool_status`, `postal_code_tool_status`,
|
|
993
|
-
`shopify_products_web_chat`, `ticket_tool_status`, `
|
|
1046
|
+
`shopify_products_web_chat`, `ticket_tool_status`, `product_lookup`,
|
|
1047
|
+
`collapse_product_variants`),
|
|
994
1048
|
`discount.enabled`, and the `flowiq test` contact
|
|
995
1049
|
(`settings.test_contact_number` / `settings.test_contact_name`). Anything else is
|
|
996
1050
|
rejected; every change is reported before → after.
|
|
997
1051
|
|
|
1052
|
+
**`--tool product_lookup=true` (added 11 Aug 2026).** Turns on the `product_lookup`
|
|
1053
|
+
tool: a typo-tolerant **pg_trgm fuzzy match on `product_title`** (plus badge/tag
|
|
1054
|
+
search), as opposed to `get_product_info`, which is **semantic**. This matters far
|
|
1055
|
+
beyond typo tolerance: `get_product_info` needs BOTH a populated
|
|
1056
|
+
`products_all.embedding` column AND a working OpenAI key (it embeds the query at
|
|
1057
|
+
call time), so on an org missing either one, **every** product question fails and
|
|
1058
|
+
the agent cannot quote a price, confirm stock, or send a product link.
|
|
1059
|
+
`product_lookup` needs neither and reads the row directly. Reach for it whenever an
|
|
1060
|
+
agent answers product questions with "I can't pull the live menu". Found on Ouma
|
|
1061
|
+
Bets Gebak (11 Aug 2026): 25 product rows, 0 embeddings, and an unfunded OpenAI
|
|
1062
|
+
account — enabling this flag restored product answers immediately.
|
|
1063
|
+
|
|
998
1064
|
**`--tool collapse_product_variants=true` (added 4 Aug 2026).** Not a tool toggle —
|
|
999
1065
|
it changes what `get_product_info` RETURNS. OFF (the default) the result cap counts
|
|
1000
1066
|
**variant rows**, so on a catalogue with several packaging/size variants per product
|
package/TEAM-GUIDE.md
CHANGED
|
@@ -80,6 +80,7 @@ When you see it, run `npm i -g @flowapt/flowiq-cli` — a stale version also mea
|
|
|
80
80
|
| Edit keyword auto-replies (incl. competition entry keywords, add/remove-tag, set-agent and delay actions) | `flowiq kw pull <org_id>` → edit → `flowiq kw push <slug> --dry-run` → `flowiq kw push <slug>` |
|
|
81
81
|
| Send a **different auto-reply depending on the contact** (e.g. "we already have your email" vs "send us your email") | add `"when": {"field":"email","op":"is_not_empty"}` to one action and `is_empty` to the other — see *Keywords* in `flowiq guide --reference`. CLI-only, no dashboard field yet |
|
|
82
82
|
| Make a follow-up button **close the customer's ticket** ("Query solved") or **re-alert the team** ("I still need help") | keyword action `{"type":"update_ticket_status","status":"resolved"}` / `{"type":"renotify_ticket"}` — see *Keywords* in `flowiq guide --reference`. CLI-only, no dashboard field yet |
|
|
83
|
+
| Make a keyword/button **hand the chat to a team or person** (assign in the inbox + email/WhatsApp them) | keyword action `{"type":"assign_chat","team_id":"…","assignee_user_id":"…","notify_member":true}` — see *Keywords* in `flowiq guide --reference`. Also in the dashboard (action type "Assign Chat") |
|
|
83
84
|
| **Find an org's ID** (needed by nearly every command) | `flowiq org list` — or `flowiq org list african` to filter |
|
|
84
85
|
| What's the stock on a product, per branch? | `flowiq shopify stock <org_id> "olive oil"` — shows each location by NAME, and says "not tracked" rather than a confusing 0 |
|
|
85
86
|
| Has this order shipped? What's the tracking? | `flowiq shopify order <org_id> '#14728'` — status, courier, tracking number + link |
|
|
@@ -93,8 +94,13 @@ When you see it, run `npm i -g @flowapt/flowiq-cli` — a stale version also mea
|
|
|
93
94
|
| See an org's agents / create one | `flowiq agent list <org_id>` / `flowiq agent create <org_id> --name "…"` |
|
|
94
95
|
| Change agent model / tool flags | `flowiq agent config <org_id> --model … --tool view_cart_tool=true` |
|
|
95
96
|
| Agent says an in-stock product "isn't showing" | `flowiq agent config <org_id> --tool collapse_product_variants=true` — the search cap counts VARIANT rows until this is on |
|
|
97
|
+
| Agent can't quote ANY price / "I can't pull the live menu" | `flowiq agent config <org_id> --tool product_lookup=true` — name-based fuzzy lookup that works without embeddings or a live OpenAI key (semantic `get_product_info` needs both) |
|
|
96
98
|
| Talk to the live agent safely (no real WhatsApp ever sent) | `flowiq test send <org_id> "hi, do you sell X?"` |
|
|
97
99
|
| Set which contact `flowiq test` uses (use a FAKE number!) | `flowiq agent config <org_id> --test-contact-number 27000000001 --test-contact-name "QA Bot"` |
|
|
100
|
+
| **Make short links for a campaign** (with UTM tracking) | `flowiq links shorten <org_id> --url "https://shop.co.za/product/x" --campaign 13Aug_Seeds --content 13Aug_Seeds` (dry run) → `… --commit`. `utm_source`/`utm_medium` are set for you; campaign + content must be given together |
|
|
101
|
+
| Short links for every URL in a message | `flowiq links shorten <org_id> --file ./message.txt --campaign X --content X --commit` |
|
|
102
|
+
| Check which links a campaign has, and their clicks | `flowiq links list <org_id> --campaign 13Aug_Seeds` |
|
|
103
|
+
| A link already looks short (`linklnk.io/abc123`) — can I re-tag it? | No: paste the DESTINATION url instead. Re-shortening keeps the old campaign tag and splits the click count, so the CLI refuses it |
|
|
98
104
|
| Send a template broadcast to a CSV of people | `flowiq bc map <org_id> --template … --csv …` → `flowiq bc send … ` (dry-run) → `… --commit` |
|
|
99
105
|
| 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` |
|
|
100
106
|
| Tag everyone who bought a product (accurate, windowable) | `flowiq seg plan <org_id> --tag-prefix whey --bought "Whey" --window 90d` → `flowiq seg apply … --commit` |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flowapt/flowiq-cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.8",
|
|
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,144 @@
|
|
|
1
|
+
// `flowiq links` — short links + campaign UTM tags from the terminal.
|
|
2
|
+
//
|
|
3
|
+
// The terminal half of the in-app URL Shortener. Same engine server-side
|
|
4
|
+
// (api/_url-shorten-engine.js), so a link minted here is identical to one minted
|
|
5
|
+
// in the dialog: utm_source=whatsapp, utm_medium=whatsapp_paid fixed, campaign +
|
|
6
|
+
// content yours.
|
|
7
|
+
//
|
|
8
|
+
// flowiq links shorten <org> --url <u> [--url <u2>…] --campaign 13Aug_Seeds --content 13Aug_Seeds
|
|
9
|
+
// flowiq links shorten <org> --file ./message.txt --campaign X --content X --domain linklnk.io
|
|
10
|
+
// flowiq links list <org> [--campaign X] [--limit 25]
|
|
11
|
+
//
|
|
12
|
+
// Dry-run FIRST (`--dry-run`) on anything going into a real campaign: it shows
|
|
13
|
+
// the exact destination URL each short link will carry, and whether an identical
|
|
14
|
+
// row already exists (so you can see you are about to reuse an OLD campaign tag).
|
|
15
|
+
|
|
16
|
+
import fs from "node:fs";
|
|
17
|
+
import { http } from "../http.js";
|
|
18
|
+
|
|
19
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
20
|
+
const URL_PATTERN = /(https?:\/\/[^\s\)]+)/g;
|
|
21
|
+
|
|
22
|
+
function requireOrg(orgId) {
|
|
23
|
+
if (!UUID_RE.test(orgId)) {
|
|
24
|
+
console.error(`Error: "${orgId}" is not a valid organization UUID. Find it with: flowiq org list <name>`);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function shorten(orgId, opts = {}) {
|
|
30
|
+
requireOrg(orgId);
|
|
31
|
+
|
|
32
|
+
// Collect URLs: repeatable --url, and/or every URL found in --file.
|
|
33
|
+
const urls = [...(opts.url || [])];
|
|
34
|
+
if (opts.file) {
|
|
35
|
+
let text;
|
|
36
|
+
try {
|
|
37
|
+
text = fs.readFileSync(opts.file, "utf8");
|
|
38
|
+
} catch (e) {
|
|
39
|
+
console.error(`Error: could not read --file ${opts.file}: ${e.message}`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
const found = text.match(URL_PATTERN) || [];
|
|
43
|
+
if (!found.length) console.error(`⚠ No URLs found in ${opts.file}`);
|
|
44
|
+
urls.push(...found);
|
|
45
|
+
}
|
|
46
|
+
if (!urls.length) {
|
|
47
|
+
console.error("Error: pass at least one --url <url>, or --file <path> containing URLs.");
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Dedupe while preserving order — the same URL twice would otherwise print twice.
|
|
52
|
+
const seen = new Set();
|
|
53
|
+
const unique = urls.filter((u) => {
|
|
54
|
+
const k = u.trim();
|
|
55
|
+
if (seen.has(k)) return false;
|
|
56
|
+
seen.add(k);
|
|
57
|
+
return true;
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const dryRun = opts.dryRun !== false && opts.commit !== true;
|
|
61
|
+
|
|
62
|
+
let resp;
|
|
63
|
+
try {
|
|
64
|
+
resp = await http.post("links", {
|
|
65
|
+
organization_id: orgId,
|
|
66
|
+
action: "shorten",
|
|
67
|
+
urls: unique,
|
|
68
|
+
campaign: opts.campaign,
|
|
69
|
+
content: opts.content,
|
|
70
|
+
domain: opts.domain,
|
|
71
|
+
dry_run: dryRun,
|
|
72
|
+
});
|
|
73
|
+
} catch (e) {
|
|
74
|
+
console.error(`Shorten failed: ${e.message}`);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (opts.json) { console.log(JSON.stringify(resp, null, 2)); return; }
|
|
79
|
+
|
|
80
|
+
const utm = resp.utm
|
|
81
|
+
? `utm_source=${resp.utm.source} · utm_medium=${resp.utm.medium} · utm_campaign=${resp.utm.campaign} · utm_content=${resp.utm.content}`
|
|
82
|
+
: "no UTM tags (plain short link)";
|
|
83
|
+
console.log(`${resp.organization_name} · ${resp.domain}`);
|
|
84
|
+
console.log(`${utm}\n`);
|
|
85
|
+
|
|
86
|
+
for (const r of resp.results) {
|
|
87
|
+
const label = {
|
|
88
|
+
created: "✓ created",
|
|
89
|
+
reused: "↻ reused ",
|
|
90
|
+
would_create: "· would create",
|
|
91
|
+
would_reuse: "· would REUSE an existing row",
|
|
92
|
+
refused_already_short: "✗ refused",
|
|
93
|
+
invalid_url: "✗ invalid url",
|
|
94
|
+
failed: "✗ failed",
|
|
95
|
+
}[r.state] || r.state;
|
|
96
|
+
console.log(`${label} ${r.short_url || r.input}`);
|
|
97
|
+
if (r.full_url) console.log(` → ${r.full_url}`);
|
|
98
|
+
if (r.note) console.log(` ⚠ ${r.note}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
console.log("");
|
|
102
|
+
if (dryRun) {
|
|
103
|
+
console.log(`DRY RUN — nothing written. Re-run with --commit to mint these links.`);
|
|
104
|
+
} else {
|
|
105
|
+
console.log(`${resp.created} created · ${resp.reused} reused${resp.failed ? ` · ${resp.failed} failed` : ""}`);
|
|
106
|
+
}
|
|
107
|
+
// A reuse is not automatically wrong (idempotent re-run), but it means the code
|
|
108
|
+
// you are about to paste already existed — with whatever campaign tag it was
|
|
109
|
+
// born with. Worth a second look when you asked for a NEW campaign.
|
|
110
|
+
const reusedRows = resp.results.filter((r) => r.state === "reused" || r.state === "would_reuse");
|
|
111
|
+
if (reusedRows.length && resp.utm) {
|
|
112
|
+
console.log(`⚠ ${reusedRows.length} url(s) already had a row with this exact destination — those keep their existing click history.`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function list(orgId, opts = {}) {
|
|
117
|
+
requireOrg(orgId);
|
|
118
|
+
let resp;
|
|
119
|
+
try {
|
|
120
|
+
resp = await http.post("links", {
|
|
121
|
+
organization_id: orgId,
|
|
122
|
+
action: "list",
|
|
123
|
+
campaign: opts.campaign,
|
|
124
|
+
limit: opts.limit,
|
|
125
|
+
});
|
|
126
|
+
} catch (e) {
|
|
127
|
+
console.error(`List failed: ${e.message}`);
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (opts.json) { console.log(JSON.stringify(resp, null, 2)); return; }
|
|
132
|
+
|
|
133
|
+
if (!resp.links.length) {
|
|
134
|
+
console.log(opts.campaign ? `No links on ${resp.organization_name} for campaign "${opts.campaign}".` : `No links on ${resp.organization_name}.`);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const pad = (s, n) => String(s ?? "").padEnd(n);
|
|
138
|
+
console.log(`${pad("CODE", 10)}${pad("CLICKS", 8)}${pad("CREATED", 12)}URL`);
|
|
139
|
+
for (const l of resp.links) {
|
|
140
|
+
const day = String(l.created_at || "").slice(0, 10);
|
|
141
|
+
console.log(`${pad(l.code, 10)}${pad(l.clicks ?? 0, 8)}${pad(day, 12)}${l.full_url}`);
|
|
142
|
+
}
|
|
143
|
+
console.log(`\n${resp.count} link(s)${opts.campaign ? ` · campaign "${opts.campaign}"` : ""}`);
|
|
144
|
+
}
|
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 tagCmd from "./commands/tag.js";
|
|
30
|
+
import * as linksCmd from "./commands/links.js";
|
|
30
31
|
import * as keywordsCmd from "./commands/keywords.js";
|
|
31
32
|
import * as guideCmd from "./commands/guide.js";
|
|
32
33
|
import * as auditCmd from "./commands/audit.js";
|
|
@@ -534,6 +535,26 @@ export function run(argv) {
|
|
|
534
535
|
.option("--confirm", "required alongside --commit")
|
|
535
536
|
.action((orgId, id, opts) => segmentsCmd.untag(orgId, id, opts));
|
|
536
537
|
|
|
538
|
+
// links (short links + campaign UTM tags — the URL Shortener dialog in the terminal; dry-run default)
|
|
539
|
+
const links = program.command("links")
|
|
540
|
+
.description("Short links with campaign UTM tags (dry-run by default; --commit to mint)");
|
|
541
|
+
links.command("shorten <organization_id>")
|
|
542
|
+
.description("Shorten one or more URLs, tagging each with utm_campaign/utm_content")
|
|
543
|
+
.option("--url <url>", "URL to shorten (repeatable)", (v, acc) => (acc || []).concat([v]), [])
|
|
544
|
+
.option("--file <path>", "shorten every URL found in this text file")
|
|
545
|
+
.option("--campaign <value>", "utm_campaign (e.g. 13Aug_Seeds) — must be paired with --content")
|
|
546
|
+
.option("--content <value>", "utm_content — must be paired with --campaign")
|
|
547
|
+
.option("--domain <host>", "short domain: chatcart.io (default) | linklnk.io | yapi.store")
|
|
548
|
+
.option("--commit", "actually mint the links (omit = dry-run preview)")
|
|
549
|
+
.option("--json", "raw JSON output")
|
|
550
|
+
.action((orgId, opts) => linksCmd.shorten(orgId, opts));
|
|
551
|
+
links.command("list <organization_id>")
|
|
552
|
+
.description("List the org's short links, newest first")
|
|
553
|
+
.option("--campaign <value>", "only links carrying this exact utm_campaign")
|
|
554
|
+
.option("--limit <n>", "max rows (default 25, max 200)")
|
|
555
|
+
.option("--json", "raw JSON output")
|
|
556
|
+
.action((orgId, opts) => linksCmd.list(orgId, opts));
|
|
557
|
+
|
|
537
558
|
// tag (the Advanced Tagging dialog in the terminal — ONE named tag on a matched set; dry-run default)
|
|
538
559
|
const tag = program.command("tag")
|
|
539
560
|
.description("Advanced Tagging: apply one named tag to a matched set of contacts (dry-run by default; --commit to write)");
|