@flowapt/flowiq-cli 0.2.8 → 0.3.0
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 +27 -2
- package/TEAM-GUIDE.md +7 -0
- package/package.json +1 -1
- package/src/commands/agent-config.js +4 -1
- package/src/config.js +1 -1
- package/src/index.js +6 -0
- package/src/update-check.js +132 -0
package/README.md
CHANGED
|
@@ -552,13 +552,25 @@ Set an allowlisted set of the active agent's config fields (the prompt-builder
|
|
|
552
552
|
flowiq agent config <organization_id> # show current
|
|
553
553
|
flowiq agent config <organization_id> --use-settings-prompt --model gpt-5.4-mini \
|
|
554
554
|
--rename Zara --tool woo_order_build=true --tool view_cart_tool=true --discount true
|
|
555
|
+
flowiq agent config <organization_id> --test-contact-number 27000000001 --test-contact-name "QA Bot"
|
|
555
556
|
```
|
|
556
557
|
|
|
557
558
|
Settable: `settings.use_settings_prompt`, `settings.model`, agent `--rename`,
|
|
558
559
|
the tool-flag columns (`woo_order_build`, `woo_tip_field`, `woo_order_note_field`,
|
|
559
560
|
`view_cart_tool`, `restock_tool`, `block_tool_status`, `postal_code_tool_status`,
|
|
560
|
-
`shopify_products_web_chat`),
|
|
561
|
-
|
|
561
|
+
`shopify_products_web_chat`), `discount.enabled`, and the `flowiq test` contact
|
|
562
|
+
(`settings.test_contact_number` / `settings.test_contact_name`). Anything else is
|
|
563
|
+
rejected; every change is reported before → after.
|
|
564
|
+
|
|
565
|
+
**Test contact (`--test-contact-number` / `--test-contact-name`, v0.2.9).**
|
|
566
|
+
This is the contact `flowiq test` drives the live agent through (read by
|
|
567
|
+
`api/cli/agent-test.js` as `settings.test_contact_number`). ⚠ **Use a clearly
|
|
568
|
+
FAKE number.** Whatever you set becomes the `whatsapp_id` the test harness looks
|
|
569
|
+
up or creates in the org — point it at a real customer's number and your tests
|
|
570
|
+
route through *their* contact record and force `bot_status=true` on it. If that
|
|
571
|
+
number already maps to an existing contact, the command prints a `⚠` warning
|
|
572
|
+
naming it. Pass `--test-contact-number ""` (empty) to clear the override — the
|
|
573
|
+
harness then falls back to a synthetic per-agent `test_<agent_id>` contact.
|
|
562
574
|
|
|
563
575
|
### Agent updates — `flowiq agent-updates pull|list|resolve` (alias `au`)
|
|
564
576
|
|
|
@@ -633,6 +645,7 @@ version you have installed.
|
|
|
633
645
|
|---|---|---|
|
|
634
646
|
| `FLOWIQ_API_URL` | `https://api.flowiq.live` | Override the API host (local dev, staging). |
|
|
635
647
|
| `FLOWIQ_TOKEN` | (saved in `~/.config/flowiq/auth.json`) | Override the auth token, useful for CI. |
|
|
648
|
+
| `FLOWIQ_NO_UPDATE_CHECK` | (unset) | Set to `1` to silence the "you're behind" update hint (also honours `NO_UPDATE_NOTIFIER=1` and any `CI` env). |
|
|
636
649
|
| `FLOWMOD_EVO_DB_URL` | (none) | Full `postgres://` URL for `groups` (Evolution DB). Required for `groups`. |
|
|
637
650
|
| `FLOWMOD_DB_HOST/PORT/USER/PASS/NAME` | (none) | Alternative to `FLOWMOD_EVO_DB_URL` — assembled into a connection string. |
|
|
638
651
|
|
|
@@ -648,6 +661,18 @@ flowiq --version
|
|
|
648
661
|
Your saved token in `~/.config/flowiq/auth.json` is preserved across
|
|
649
662
|
upgrades.
|
|
650
663
|
|
|
664
|
+
**You'll be told when you're behind.** The CLI checks npm about once a day (in a
|
|
665
|
+
detached background process — it never slows a command) and prints a one-line
|
|
666
|
+
hint to stderr on your next run when a newer version exists:
|
|
667
|
+
|
|
668
|
+
```
|
|
669
|
+
⬆ flowiq 0.3.1 is available (you're on 0.3.0)
|
|
670
|
+
update: npm i -g @flowapt/flowiq-cli
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
It's stderr-only (never corrupts piped or `--json` output) and shows only in an
|
|
674
|
+
interactive terminal. Silence it with `FLOWIQ_NO_UPDATE_CHECK=1`.
|
|
675
|
+
|
|
651
676
|
## License
|
|
652
677
|
|
|
653
678
|
UNLICENSED. Internal staff tool — install requires a valid `fiq_staff_…`
|
package/TEAM-GUIDE.md
CHANGED
|
@@ -38,6 +38,12 @@ This guide travels with the CLI — read it any time with `flowiq guide`
|
|
|
38
38
|
(`--reference` for the full command reference). It always matches the version
|
|
39
39
|
you have installed.
|
|
40
40
|
|
|
41
|
+
**Keep it current.** The CLI checks npm about once a day and, on your next run,
|
|
42
|
+
prints a one-line hint when you're behind (e.g. `⬆ flowiq 0.3.1 is available…`).
|
|
43
|
+
When you see it, run `npm i -g @flowapt/flowiq-cli` — a stale version also means
|
|
44
|
+
`flowiq guide` shows you stale instructions. (Silence it with
|
|
45
|
+
`FLOWIQ_NO_UPDATE_CHECK=1` if you must.)
|
|
46
|
+
|
|
41
47
|
> **Approving someone else's login:** when a teammate runs `auth login`, they
|
|
42
48
|
> read you their code (or you open the link they send). On `/cli-auth`, check
|
|
43
49
|
> the code AND the device name match what they told you, then Approve.
|
|
@@ -75,6 +81,7 @@ you have installed.
|
|
|
75
81
|
| See an org's agents / create one | `flowiq agent list <org_id>` / `flowiq agent create <org_id> --name "…"` |
|
|
76
82
|
| Change agent model / tool flags | `flowiq agent config <org_id> --model … --tool view_cart_tool=true` |
|
|
77
83
|
| Talk to the live agent safely (no real WhatsApp ever sent) | `flowiq test send <org_id> "hi, do you sell X?"` |
|
|
84
|
+
| Set which contact `flowiq test` uses (use a FAKE number!) | `flowiq agent config <org_id> --test-contact-number 27000000001 --test-contact-name "QA Bot"` |
|
|
78
85
|
| Send a template broadcast to a CSV of people | `flowiq bc map <org_id> --template … --csv …` → `flowiq bc send … ` (dry-run) → `… --commit` |
|
|
79
86
|
| 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
87
|
| 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.
|
|
3
|
+
"version": "0.3.0",
|
|
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": {
|
|
@@ -41,6 +41,8 @@ export async function config(orgId, opts = {}) {
|
|
|
41
41
|
const settings = {};
|
|
42
42
|
if (opts.useSettingsPrompt !== undefined) settings.use_settings_prompt = parseBool(opts.useSettingsPrompt, "--use-settings-prompt");
|
|
43
43
|
if (opts.model !== undefined) settings.model = opts.model;
|
|
44
|
+
if (opts.testContactNumber !== undefined) settings.test_contact_number = opts.testContactNumber;
|
|
45
|
+
if (opts.testContactName !== undefined) settings.test_contact_name = opts.testContactName;
|
|
44
46
|
if (Object.keys(settings).length) body.settings = settings;
|
|
45
47
|
if (opts.rename !== undefined) body.name = opts.rename;
|
|
46
48
|
if (opts.discount !== undefined) body.discount_enabled = parseBool(opts.discount, "--discount");
|
|
@@ -73,7 +75,7 @@ export async function config(orgId, opts = {}) {
|
|
|
73
75
|
}
|
|
74
76
|
console.log(`${resp.organization_name} → agent ${resp.agent_id}${resp.is_active === false ? " [NON-active]" : ""}`);
|
|
75
77
|
printSnapshot("current", resp.current);
|
|
76
|
-
console.log("\n(pass --use-settings-prompt / --model / --rename / --tool / --discount to change)");
|
|
78
|
+
console.log("\n(pass --use-settings-prompt / --model / --rename / --tool / --discount / --test-contact-number / --test-contact-name to change)");
|
|
77
79
|
return;
|
|
78
80
|
}
|
|
79
81
|
|
|
@@ -93,4 +95,5 @@ export async function config(orgId, opts = {}) {
|
|
|
93
95
|
const a = resp.after?.[k];
|
|
94
96
|
console.log(` ${k.padEnd(30)} ${b === null || b === undefined ? "(unset)" : b} → ${a === null || a === undefined ? "(unset)" : a}`);
|
|
95
97
|
}
|
|
98
|
+
for (const w of resp.warnings || []) console.log(` ⚠ ${w}`);
|
|
96
99
|
}
|
package/src/config.js
CHANGED
|
@@ -13,7 +13,7 @@ import os from "node:os";
|
|
|
13
13
|
|
|
14
14
|
const DEFAULT_API_URL = "https://api.flowiq.live";
|
|
15
15
|
|
|
16
|
-
function configDir() {
|
|
16
|
+
export function configDir() {
|
|
17
17
|
// Honour XDG_CONFIG_HOME if set; else ~/.config (Linux/Mac default).
|
|
18
18
|
const xdg = process.env.XDG_CONFIG_HOME;
|
|
19
19
|
const base = xdg && xdg.trim() ? xdg : path.join(os.homedir(), ".config");
|
package/src/index.js
CHANGED
|
@@ -29,6 +29,7 @@ import * as segmentsCmd from "./commands/segments.js";
|
|
|
29
29
|
import * as tagCmd from "./commands/tag.js";
|
|
30
30
|
import * as keywordsCmd from "./commands/keywords.js";
|
|
31
31
|
import * as guideCmd from "./commands/guide.js";
|
|
32
|
+
import { maybeNotifyUpdate } from "./update-check.js";
|
|
32
33
|
|
|
33
34
|
// Read version from package.json so it stays in sync with the published npm
|
|
34
35
|
// version automatically (single source of truth — bumping package.json on each
|
|
@@ -37,6 +38,9 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
37
38
|
const pkg = JSON.parse(readFileSync(path.join(__dirname, "..", "package.json"), "utf8"));
|
|
38
39
|
|
|
39
40
|
export function run(argv) {
|
|
41
|
+
// Non-blocking "you're behind" hint (stderr; cached; detached refresh).
|
|
42
|
+
maybeNotifyUpdate(pkg.version);
|
|
43
|
+
|
|
40
44
|
const program = new Command();
|
|
41
45
|
program
|
|
42
46
|
.name("flowiq")
|
|
@@ -227,6 +231,8 @@ export function run(argv) {
|
|
|
227
231
|
.option("--rename <name>", "rename the agent")
|
|
228
232
|
.option("--tool <flag=bool>", "toggle a tool flag (repeatable): woo_order_build/woo_tip_field/woo_order_note_field/view_cart_tool/restock_tool/block_tool_status/postal_code_tool_status/shopify_products_web_chat", agentConfigCmd.collectTool, [])
|
|
229
233
|
.option("--discount [bool]", "agent.discount.enabled (true if bare)")
|
|
234
|
+
.option("--test-contact-number <number>", "settings.test_contact_number — the contact `flowiq test` uses (use a FAKE number; \"\" clears it → synthetic fallback)")
|
|
235
|
+
.option("--test-contact-name <name>", "settings.test_contact_name — display name for the test contact (\"\" clears it)")
|
|
230
236
|
.action((orgId, opts) => agentConfigCmd.config(orgId, opts));
|
|
231
237
|
|
|
232
238
|
// agent-updates (pending client change-requests + chat context; pull + resolve)
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// Best-effort "you're behind" nudge for the flowiq CLI.
|
|
2
|
+
//
|
|
3
|
+
// On startup we print a one-line update hint to STDERR (never stdout — so it
|
|
4
|
+
// can never corrupt piped / --json output) when the installed version is older
|
|
5
|
+
// than the latest published on npm.
|
|
6
|
+
//
|
|
7
|
+
// The nudge is driven by a CACHED value read SYNCHRONOUSLY — zero network on the
|
|
8
|
+
// hot path, so no command is ever slowed. The cache is refreshed at most once a
|
|
9
|
+
// day by a DETACHED, unref'd child process, so even `flowiq --version` returns
|
|
10
|
+
// instantly (the parent never waits on the registry). The hint therefore
|
|
11
|
+
// appears on the NEXT run after a new release — exactly how npm / update-notifier
|
|
12
|
+
// behave.
|
|
13
|
+
//
|
|
14
|
+
// Fully fail-silent: offline, registry down, malformed cache → no output, no
|
|
15
|
+
// crash, never blocks. Opt out with FLOWIQ_NO_UPDATE_CHECK=1 (also honours
|
|
16
|
+
// NO_UPDATE_NOTIFIER=1 and any CI env).
|
|
17
|
+
|
|
18
|
+
import fs from "node:fs";
|
|
19
|
+
import fsp from "node:fs/promises";
|
|
20
|
+
import path from "node:path";
|
|
21
|
+
import { spawn } from "node:child_process";
|
|
22
|
+
import { fileURLToPath } from "node:url";
|
|
23
|
+
import { configDir } from "./config.js";
|
|
24
|
+
|
|
25
|
+
const PKG = "@flowapt/flowiq-cli";
|
|
26
|
+
const REGISTRY = "https://registry.npmjs.org/@flowapt%2Fflowiq-cli/latest";
|
|
27
|
+
const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // refresh the cache at most once/day
|
|
28
|
+
const FETCH_TIMEOUT_MS = 2000;
|
|
29
|
+
|
|
30
|
+
function cacheFile() {
|
|
31
|
+
return path.join(configDir(), "update-check.json");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function disabled() {
|
|
35
|
+
return (
|
|
36
|
+
process.env.FLOWIQ_NO_UPDATE_CHECK === "1" ||
|
|
37
|
+
process.env.NO_UPDATE_NOTIFIER === "1" ||
|
|
38
|
+
!!process.env.CI
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Compare dotted numeric versions (ignoring any -prerelease). a>b → 1, a<b → -1, = → 0.
|
|
43
|
+
export function cmpVersion(a, b) {
|
|
44
|
+
const parse = (v) => String(v).split("-")[0].split(".").map((n) => parseInt(n, 10) || 0);
|
|
45
|
+
const pa = parse(a);
|
|
46
|
+
const pb = parse(b);
|
|
47
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
48
|
+
const d = (pa[i] || 0) - (pb[i] || 0);
|
|
49
|
+
if (d !== 0) return d > 0 ? 1 : -1;
|
|
50
|
+
}
|
|
51
|
+
return 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function readCacheSync() {
|
|
55
|
+
try {
|
|
56
|
+
return JSON.parse(fs.readFileSync(cacheFile(), "utf8"));
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function writeCache(obj) {
|
|
63
|
+
try {
|
|
64
|
+
await fsp.mkdir(configDir(), { recursive: true, mode: 0o700 });
|
|
65
|
+
await fsp.writeFile(cacheFile(), JSON.stringify(obj) + "\n", "utf8");
|
|
66
|
+
} catch {
|
|
67
|
+
/* best-effort — a failed cache write just means we re-check next run */
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Hit the npm registry for dist-tags.latest and cache it. Runs ONLY in the
|
|
72
|
+
// detached child (never on the hot path). Backs off for the day even on failure
|
|
73
|
+
// so an offline machine doesn't spawn a refresher on every invocation.
|
|
74
|
+
async function refreshLatest(prevLatest) {
|
|
75
|
+
const ctrl = new AbortController();
|
|
76
|
+
const timer = setTimeout(() => ctrl.abort(), FETCH_TIMEOUT_MS);
|
|
77
|
+
try {
|
|
78
|
+
const res = await fetch(REGISTRY, { signal: ctrl.signal, headers: { Accept: "application/json" } });
|
|
79
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
80
|
+
const body = await res.json();
|
|
81
|
+
await writeCache({ checked_at: Date.now(), latest: body?.version || prevLatest || null });
|
|
82
|
+
} catch {
|
|
83
|
+
await writeCache({ checked_at: Date.now(), latest: prevLatest || null });
|
|
84
|
+
} finally {
|
|
85
|
+
clearTimeout(timer);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Print the nudge (from cache, synchronously) and, if the cache is stale, spawn
|
|
90
|
+
// a detached child to refresh it for the NEXT run. Never throws, never blocks.
|
|
91
|
+
export function maybeNotifyUpdate(currentVersion) {
|
|
92
|
+
try {
|
|
93
|
+
if (disabled()) return;
|
|
94
|
+
const cache = readCacheSync();
|
|
95
|
+
const latest = cache?.latest;
|
|
96
|
+
|
|
97
|
+
if (latest && process.stderr.isTTY && cmpVersion(latest, currentVersion) > 0) {
|
|
98
|
+
process.stderr.write(
|
|
99
|
+
`\n ⬆ flowiq ${latest} is available (you're on ${currentVersion})\n` +
|
|
100
|
+
` update: npm i -g ${PKG}\n\n`
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const stale = !cache || (Date.now() - (cache.checked_at || 0)) > CHECK_INTERVAL_MS;
|
|
105
|
+
if (stale) {
|
|
106
|
+
try {
|
|
107
|
+
const child = spawn(process.execPath, [fileURLToPath(import.meta.url), "--refresh"], {
|
|
108
|
+
detached: true,
|
|
109
|
+
stdio: "ignore",
|
|
110
|
+
windowsHide: true,
|
|
111
|
+
});
|
|
112
|
+
child.unref();
|
|
113
|
+
} catch {
|
|
114
|
+
/* if we can't spawn, we simply try again next run */
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
} catch {
|
|
118
|
+
/* fully silent — an update hint must never be the reason a command fails */
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// When this module is executed directly as `node update-check.js --refresh`
|
|
123
|
+
// (the detached child spawned above), do the network refresh and exit. Guarded
|
|
124
|
+
// so importing the module for maybeNotifyUpdate never triggers it.
|
|
125
|
+
const invokedDirectly =
|
|
126
|
+
process.argv[1] &&
|
|
127
|
+
path.resolve(process.argv[1]) === fileURLToPath(import.meta.url) &&
|
|
128
|
+
process.argv.includes("--refresh");
|
|
129
|
+
if (invokedDirectly) {
|
|
130
|
+
const cache = readCacheSync();
|
|
131
|
+
refreshLatest(cache?.latest).finally(() => process.exit(0));
|
|
132
|
+
}
|