@flowapt/flowiq-cli 0.2.1 → 0.2.4
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 -2
- package/TEAM-GUIDE.md +2 -0
- package/package.json +1 -1
- package/src/commands/custom-tools.js +11 -0
- package/src/commands/keywords.js +135 -0
- package/src/commands/org.js +36 -0
- package/src/index.js +27 -2
package/README.md
CHANGED
|
@@ -155,6 +155,7 @@ flowiq ct list
|
|
|
155
155
|
```
|
|
156
156
|
|
|
157
157
|
- **Always `--dry-run` first** on a production agent — it prints added/removed/changed tool names before you commit.
|
|
158
|
+
- **Destructive pushes are blocked by default**: a push that removes a tool, disables one, or sends an empty `tools[]` (wiping everything) writes nothing and shows you exactly what it would strip — re-run with `--confirm` if intentional. Additive/no-op pushes are unaffected.
|
|
158
159
|
- Server-side validation is strict: tool `name` (`^[a-zA-Z0-9_-]{1,64}$`, no duplicates), non-empty `description`, `http(s)` `endpoint`, method `GET/POST/PUT/PATCH/DELETE`, object-typed `parameters`/`headers`/`injected_parameters`, valid `auth_type`/`channels`. Bad payloads are rejected before anything writes.
|
|
159
160
|
- **Warnings (non-blocking):** unknown keys (likely typos the runtime would silently ignore) and unknown `{{placeholders}}` (they will NOT be substituted at runtime — known: `organization_id`, `contact_id`, `agent_id`, `contact_whatsapp_id`, `contact_full_name`, `supabase_anon_key`, `openai_api_key`, …).
|
|
160
161
|
- `--agent` + filenames behave like `prompts`/`knowledge`; the file carries `agent_id`, so `push` targets the agent it was pulled from.
|
|
@@ -244,6 +245,33 @@ flowiq seg untag <org_id> july-promo --commit --confirm # ROLLBACK (its own ta
|
|
|
244
245
|
- Point-in-time warning: the plan tags snapshot ids; re-derive the cohort SQL
|
|
245
246
|
if freshness matters (someone who ordered since won't auto-drop).
|
|
246
247
|
|
|
248
|
+
### Keywords — `flowiq keywords pull|push|list` (alias `kw`)
|
|
249
|
+
|
|
250
|
+
Round-trips an org's **keyword auto-reply engine** — the `keywords` +
|
|
251
|
+
`keyword_actions` rows that fire instant replies / contact updates when a
|
|
252
|
+
customer message matches a trigger word. This is the **safe editor** for the
|
|
253
|
+
attribute-write actions (competition entries) that the dashboard editor
|
|
254
|
+
corrupts on save.
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
flowiq kw pull <org_id> # → ./.flowiq/keywords/<slug>.json (all keywords + actions)
|
|
258
|
+
flowiq kw push <slug> --dry-run # plan: created/updated/deleted at both levels, no write
|
|
259
|
+
flowiq kw push <slug> # id-keyed upsert (never touches keywords absent from the file)
|
|
260
|
+
flowiq kw push <slug> --prune # ALSO deletes DB keywords missing from the file (dry-run first!)
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
- **Identity = the `id`s.** Keep an id to update that row; drop the id to
|
|
264
|
+
create a new one; remove an *action* from a kept keyword to delete it.
|
|
265
|
+
A file id that doesn't exist for the org aborts (stale file → re-pull).
|
|
266
|
+
- **Pull immediately before editing** — clients edit auto-reply copy live.
|
|
267
|
+
- `text` is stored **verbatim** (byte-identical — required when the reply
|
|
268
|
+
must match a prompt quote exactly). `position` must be ≥ 1 (0 silently
|
|
269
|
+
collapses to 1 at runtime — rejected).
|
|
270
|
+
- `field:"attributes"` actions are warned (full jsonb replace; constant
|
|
271
|
+
values only) but applied — this CLI is their only safe editing surface.
|
|
272
|
+
- Scheduled keywords: the keyword-scheduler cron reconciles `active` from
|
|
273
|
+
`start_date`/`end_date` within ~30 min of your push.
|
|
274
|
+
|
|
247
275
|
### Messages — `flowiq messages pull <contact_id>` (alias `m`)
|
|
248
276
|
|
|
249
277
|
Read-only pull of a contact's `helpdesk_messages` history. Anchors on the
|
|
@@ -388,15 +416,28 @@ Media headers: pass `media_header.file_url` (a public URL) — the edge function
|
|
|
388
416
|
uploads it to Meta server-side. Approval is async; re-`pull` for the
|
|
389
417
|
authoritative Meta status.
|
|
390
418
|
|
|
391
|
-
### Org
|
|
419
|
+
### Org — `flowiq org create` / `flowiq org info <organization_id>`
|
|
392
420
|
|
|
393
|
-
|
|
421
|
+
Create a brand-new organization, or look one up (read-only; raw store/Meta
|
|
394
422
|
credentials are stripped server-side).
|
|
395
423
|
|
|
396
424
|
```bash
|
|
425
|
+
flowiq org create --name "New Client" [--slug new-client] [--owner client@email.com] [--provider wati]
|
|
426
|
+
# → org row + admin membership. Owner defaults to YOU; --owner requires the
|
|
427
|
+
# person to already have an account (otherwise invite them later in the app).
|
|
428
|
+
# Provider defaults to meta. Prints the new org id + the next-step commands.
|
|
429
|
+
|
|
397
430
|
flowiq org info <organization_id> # platform, storefront url, active agent
|
|
398
431
|
```
|
|
399
432
|
|
|
433
|
+
Full new-client bootstrap from the terminal:
|
|
434
|
+
```bash
|
|
435
|
+
flowiq org create --name "New Client"
|
|
436
|
+
flowiq agent create <new_org_id> --name "Zara" --make-active
|
|
437
|
+
flowiq agent config <new_org_id> --model gpt-5.4-mini --use-settings-prompt
|
|
438
|
+
flowiq prompts pull <new_org_id> # edit → push
|
|
439
|
+
```
|
|
440
|
+
|
|
400
441
|
### Agents — `flowiq agent list|create <org>`
|
|
401
442
|
|
|
402
443
|
List an org's agents (to discover ids) and create a new one.
|
package/TEAM-GUIDE.md
CHANGED
|
@@ -67,6 +67,7 @@ flowiq auth whoami
|
|
|
67
67
|
| Edit the text knowledge sources (get_more_answers playbooks) | `flowiq kn pull <org_id>` → edit `sources[]` → `flowiq kn push <slug>` |
|
|
68
68
|
| Edit the custom tools (API-call tools) | `flowiq ct pull <org_id>` → edit `tools[]` → `flowiq ct push <slug> --dry-run` → `flowiq ct push <slug>` |
|
|
69
69
|
| Turn one custom tool on/off | `flowiq ct enable\|disable <org_id> <tool_name>` |
|
|
70
|
+
| Edit keyword auto-replies (incl. competition entry keywords) | `flowiq kw pull <org_id>` → edit → `flowiq kw push <slug>` |
|
|
70
71
|
| See an org's agents / create one | `flowiq agent list <org_id>` / `flowiq agent create <org_id> --name "…"` |
|
|
71
72
|
| Change agent model / tool flags | `flowiq agent config <org_id> --model … --tool view_cart_tool=true` |
|
|
72
73
|
| Talk to the live agent safely (no real WhatsApp ever sent) | `flowiq test send <org_id> "hi, do you sell X?"` |
|
|
@@ -78,6 +79,7 @@ flowiq auth whoami
|
|
|
78
79
|
| Check / create WhatsApp templates | `flowiq tpl pull <org_id>` / `flowiq tpl create <org_id> --request-file req.json` |
|
|
79
80
|
| Manage Shopify/Woo platform webhooks | `flowiq wh pull <org_id>` → edit → `flowiq wh push <slug>` |
|
|
80
81
|
| Manage outbound messaging webhooks | `flowiq mw pull <org_id>` → `flowiq mw push <slug> --dry-run` → push |
|
|
82
|
+
| Create a brand-new client org | `flowiq org create --name "Client Name"` → then `agent create` on the printed id |
|
|
81
83
|
| See a client's pending change requests | `flowiq au pull <org_id>` |
|
|
82
84
|
| Close a client's change request (after verifying the fix!) | `flowiq au resolve <update_id> --note "what changed"` — the client reads the note |
|
|
83
85
|
| Check an org's platform + active agent | `flowiq org info <org_id>` |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flowapt/flowiq-cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
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": {
|
|
@@ -129,6 +129,7 @@ export async function push(identifier, opts = {}) {
|
|
|
129
129
|
agent_id: data.agent_id, // push back to the agent this file was pulled from
|
|
130
130
|
tools: data.tools,
|
|
131
131
|
dry_run: !!opts.dryRun,
|
|
132
|
+
confirm: !!opts.confirm,
|
|
132
133
|
});
|
|
133
134
|
} catch (e) {
|
|
134
135
|
console.error(`Push failed: ${e.message}`);
|
|
@@ -142,6 +143,16 @@ export async function push(identifier, opts = {}) {
|
|
|
142
143
|
console.log(` tools: ${resp.tool_count}`);
|
|
143
144
|
printDiff(resp.diff);
|
|
144
145
|
printWarnings(resp.warnings);
|
|
146
|
+
if (resp.requires_confirm) {
|
|
147
|
+
const d = resp.destructive || {};
|
|
148
|
+
console.log("");
|
|
149
|
+
console.log(" ⛔ DESTRUCTIVE PUSH — nothing was written:");
|
|
150
|
+
if (d.wipe_all) console.log(" this file would WIPE EVERY custom tool off the agent");
|
|
151
|
+
if (d.removed?.length) console.log(` removes: ${d.removed.join(", ")}`);
|
|
152
|
+
if (d.disabled?.length) console.log(` disables: ${d.disabled.join(", ")}`);
|
|
153
|
+
console.log(" Re-run with --confirm if this is intentional.");
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
|
145
156
|
if (resp.dry_run) console.log(" re-run without --dry-run to apply.");
|
|
146
157
|
}
|
|
147
158
|
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// `flowiq keywords pull <org_id>` / `push <slug> [--prune] [--dry-run]` / `list`.
|
|
2
|
+
// Round-trips an org's keyword auto-replies (keywords + keyword_actions rows)
|
|
3
|
+
// via /cli/keywords — the SAFE editing path for the attribute-write actions
|
|
4
|
+
// the dashboard editor corrupts.
|
|
5
|
+
//
|
|
6
|
+
// Push is an ID-KEYED diff upsert: keep an `id` to update, drop the `id` to
|
|
7
|
+
// create, remove an ACTION from a kept keyword to delete it. A DB keyword
|
|
8
|
+
// absent from the file is left alone unless --prune. ALWAYS pull right before
|
|
9
|
+
// editing (clients edit auto-reply copy live) and --dry-run before --prune.
|
|
10
|
+
|
|
11
|
+
import fs from "node:fs/promises";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import { http } from "../http.js";
|
|
14
|
+
|
|
15
|
+
const KW_DIR = path.resolve(process.cwd(), ".flowiq", "keywords");
|
|
16
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
17
|
+
|
|
18
|
+
function slugify(name, fallback) {
|
|
19
|
+
const s = String(name || "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
20
|
+
return s || fallback;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function fileExists(p) {
|
|
24
|
+
try { await fs.access(p); return true; } catch { return false; }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function resolvePushPath(identifier) {
|
|
28
|
+
const candidates = [];
|
|
29
|
+
if (path.isAbsolute(identifier)) candidates.push(identifier);
|
|
30
|
+
else if (identifier.includes("/") || identifier.includes("\\")) candidates.push(path.resolve(identifier));
|
|
31
|
+
else {
|
|
32
|
+
const base = identifier.endsWith(".json") ? identifier : `${identifier}.json`;
|
|
33
|
+
candidates.push(path.join(KW_DIR, base));
|
|
34
|
+
candidates.push(path.resolve(base));
|
|
35
|
+
}
|
|
36
|
+
for (const c of candidates) if (await fileExists(c)) return c;
|
|
37
|
+
throw new Error(`File not found. Tried:\n ${candidates.join("\n ")}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function pull(orgId) {
|
|
41
|
+
if (!UUID_RE.test(orgId)) {
|
|
42
|
+
console.error(`Error: "${orgId}" is not a valid organization UUID.`);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
let resp;
|
|
46
|
+
try { resp = await http.get("keywords", { organization_id: orgId }); }
|
|
47
|
+
catch (e) {
|
|
48
|
+
console.error(`Pull failed: ${e.message}`);
|
|
49
|
+
if (e.body?.error) console.error(` ${e.body.error}`);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
await fs.mkdir(KW_DIR, { recursive: true });
|
|
54
|
+
const slug = slugify(resp.organization_name, resp.organization_id);
|
|
55
|
+
const filePath = path.join(KW_DIR, `${slug}.json`);
|
|
56
|
+
const overwriting = await fileExists(filePath);
|
|
57
|
+
await fs.writeFile(filePath, JSON.stringify(resp, null, 2) + "\n", "utf8");
|
|
58
|
+
|
|
59
|
+
console.log(`${overwriting ? "Overwrote" : "Wrote"} ${filePath}`);
|
|
60
|
+
console.log(` org: ${resp.organization_name} (${resp.organization_id})`);
|
|
61
|
+
console.log(` keywords: ${resp.keyword_count}`);
|
|
62
|
+
for (const k of resp.keywords || []) {
|
|
63
|
+
const acts = (k.keyword_actions || []).length;
|
|
64
|
+
console.log(` ${String(k.type).padEnd(28)} [${(k.keywords || []).join(", ")}] · ${acts} action(s)${k.active === false ? " · INACTIVE" : ""}${k.end_date ? ` · ends ${k.end_date.slice(0, 10)}` : ""}`);
|
|
65
|
+
}
|
|
66
|
+
if (resp.has_attribute_actions) {
|
|
67
|
+
console.log("");
|
|
68
|
+
console.log(" ⚠ this org has attribute-write keyword action(s) — the dashboard editor");
|
|
69
|
+
console.log(" CANNOT render field:\"attributes\" and corrupts it on save. Edit those");
|
|
70
|
+
console.log(" keywords via this CLI only.");
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function push(identifier, opts = {}) {
|
|
75
|
+
let filePath;
|
|
76
|
+
try { filePath = await resolvePushPath(identifier); }
|
|
77
|
+
catch (e) { console.error(`Error: ${e.message}`); process.exit(1); }
|
|
78
|
+
const raw = await fs.readFile(filePath, "utf8");
|
|
79
|
+
let data;
|
|
80
|
+
try { data = JSON.parse(raw); }
|
|
81
|
+
catch (e) { console.error(`Invalid JSON in ${filePath}: ${e.message}`); process.exit(1); }
|
|
82
|
+
if (!data.organization_id || !UUID_RE.test(data.organization_id)) {
|
|
83
|
+
console.error(`${filePath}: organization_id missing or invalid`);
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
if (!Array.isArray(data.keywords)) {
|
|
87
|
+
console.error(`${filePath}: keywords must be an array`);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let resp;
|
|
92
|
+
try {
|
|
93
|
+
resp = await http.post("keywords", {
|
|
94
|
+
organization_id: data.organization_id,
|
|
95
|
+
keywords: data.keywords,
|
|
96
|
+
prune: !!opts.prune,
|
|
97
|
+
dry_run: !!opts.dryRun,
|
|
98
|
+
});
|
|
99
|
+
} catch (e) {
|
|
100
|
+
console.error(`Push failed: ${e.message}`);
|
|
101
|
+
if (e.body?.error) console.error(` ${e.body.error}`);
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const p = resp.plan;
|
|
106
|
+
console.log(`${resp.dry_run ? "DRY RUN (nothing written) —" : "Pushed"} ${filePath}`);
|
|
107
|
+
console.log(` org: ${resp.organization_name} (${data.organization_id})`);
|
|
108
|
+
console.log(` keywords: +${p.keywords.create} created · ~${p.keywords.update} updated · ${p.keywords.unchanged} unchanged · -${p.keywords.delete} deleted · ${p.keywords.orphan_kept} orphan(s) kept`);
|
|
109
|
+
console.log(` actions: +${p.actions.create} created · ~${p.actions.update} updated · ${p.actions.unchanged} unchanged · -${p.actions.delete} deleted`);
|
|
110
|
+
for (const w of resp.warnings || []) console.log(` ⚠ ${w}`);
|
|
111
|
+
if (resp.dry_run) console.log(" re-run without --dry-run to apply.");
|
|
112
|
+
else if (resp.prune && p.keywords.delete) console.log(` pruned ${p.keywords.delete} keyword(s) — re-pull to confirm.`);
|
|
113
|
+
if (!resp.dry_run && p.keywords.orphan_kept) {
|
|
114
|
+
console.log(` note: ${p.keywords.orphan_kept} DB keyword(s) not in the file were left untouched (--prune to delete).`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function list() {
|
|
119
|
+
let files;
|
|
120
|
+
try { files = (await fs.readdir(KW_DIR)).filter((f) => f.endsWith(".json")); }
|
|
121
|
+
catch { console.log("(no local keyword snapshots; run `keywords pull <org_id>`)"); return; }
|
|
122
|
+
if (!files.length) { console.log("(no local keyword snapshots)"); return; }
|
|
123
|
+
files.sort();
|
|
124
|
+
for (const f of files) {
|
|
125
|
+
try {
|
|
126
|
+
const d = JSON.parse(await fs.readFile(path.join(KW_DIR, f), "utf8"));
|
|
127
|
+
console.log(f);
|
|
128
|
+
console.log(` org: ${d.organization_name ?? "?"} (${d.organization_id ?? "?"})`);
|
|
129
|
+
console.log(` keywords: ${d.keyword_count ?? (Array.isArray(d.keywords) ? d.keywords.length : "?")}`);
|
|
130
|
+
console.log(` pulled: ${d.pulled_at ?? "?"}`);
|
|
131
|
+
} catch (e) {
|
|
132
|
+
console.log(`${f} [invalid JSON: ${e.message}]`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
package/src/commands/org.js
CHANGED
|
@@ -31,3 +31,39 @@ export async function info(orgId, opts = {}) {
|
|
|
31
31
|
console.log(` provider: ${resp.provider || "(unset → meta)"}`);
|
|
32
32
|
console.log(` active agent: ${resp.active_whatsapp_agent || "(NONE — set one before building a prompt)"}`);
|
|
33
33
|
}
|
|
34
|
+
|
|
35
|
+
// `flowiq org create --name "…" [--slug …] [--owner email] [--provider meta|wati]`
|
|
36
|
+
// Creates a NEW organization (org row + admin membership), mirroring the app's
|
|
37
|
+
// Create Organization dialog. Owner defaults to YOU (the staff key's user) —
|
|
38
|
+
// the client gets invited later via the app's member invite flow.
|
|
39
|
+
export async function create(opts = {}) {
|
|
40
|
+
if (!opts.name || !opts.name.trim()) {
|
|
41
|
+
console.error("Error: --name is required.");
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
let resp;
|
|
45
|
+
try {
|
|
46
|
+
resp = await http.post("org", {
|
|
47
|
+
action: "create",
|
|
48
|
+
name: opts.name.trim(),
|
|
49
|
+
slug: opts.slug,
|
|
50
|
+
owner_email: opts.owner,
|
|
51
|
+
provider: opts.provider,
|
|
52
|
+
});
|
|
53
|
+
} catch (e) {
|
|
54
|
+
console.error(`Create failed: ${e.message}`);
|
|
55
|
+
if (e.body?.error) console.error(` ${e.body.error}`);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
console.log(`Created "${resp.name}"`);
|
|
60
|
+
console.log(` id: ${resp.id}`);
|
|
61
|
+
console.log(` slug: ${resp.slug}`);
|
|
62
|
+
console.log(` owner: ${resp.owner_email ?? resp.owner_id}${resp.member_added ? " (admin member)" : " ⚠ membership insert failed — add via the app"}`);
|
|
63
|
+
console.log(` provider: ${resp.provider}`);
|
|
64
|
+
console.log("");
|
|
65
|
+
console.log("Next steps:");
|
|
66
|
+
console.log(` flowiq agent create ${resp.id} --name "<AgentName>" --make-active`);
|
|
67
|
+
console.log(` flowiq agent config ${resp.id} --model gpt-5.4-mini --use-settings-prompt`);
|
|
68
|
+
console.log(` flowiq prompts pull ${resp.id} # then edit + push`);
|
|
69
|
+
}
|
package/src/index.js
CHANGED
|
@@ -26,6 +26,7 @@ import * as knowledgeCmd from "./commands/knowledge.js";
|
|
|
26
26
|
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
|
+
import * as keywordsCmd from "./commands/keywords.js";
|
|
29
30
|
|
|
30
31
|
// Read version from package.json so it stays in sync with the published npm
|
|
31
32
|
// version automatically (single source of truth — bumping package.json on each
|
|
@@ -190,7 +191,14 @@ export function run(argv) {
|
|
|
190
191
|
.action((orgId, opts) => templatesCmd.status(orgId, opts));
|
|
191
192
|
|
|
192
193
|
// org (read-only org summary for the prompt-builder skill, creds stripped)
|
|
193
|
-
const org = program.command("org").description("
|
|
194
|
+
const org = program.command("org").description("Org info (read) + create a new organization");
|
|
195
|
+
org.command("create")
|
|
196
|
+
.description("Create a NEW organization (org row + admin membership; owner defaults to you)")
|
|
197
|
+
.requiredOption("--name <name>", "organization name")
|
|
198
|
+
.option("--slug <slug>", "URL slug (default: name lowercased, spaces→dashes; must be unique)")
|
|
199
|
+
.option("--owner <email>", "owner's auth email (must already have an account; default: you)")
|
|
200
|
+
.option("--provider <p>", "meta (default) | wati")
|
|
201
|
+
.action((opts) => orgCmd.create(opts));
|
|
194
202
|
org.command("info <organization_id>")
|
|
195
203
|
.description("Show the org's platform, storefront url + active agent (raw creds stripped)")
|
|
196
204
|
.option("--json", "print the raw JSON payload")
|
|
@@ -330,8 +338,9 @@ export function run(argv) {
|
|
|
330
338
|
.option("--agent <id>", "target a specific agent instead of the org's active one")
|
|
331
339
|
.action((orgId, opts) => customToolsCmd.pull(orgId, opts));
|
|
332
340
|
customTools.command("push <slug-or-path>")
|
|
333
|
-
.description("FULL-REPLACE the agent's custom tools with the local JSON's tools[] (validated server-side)")
|
|
341
|
+
.description("FULL-REPLACE the agent's custom tools with the local JSON's tools[] (validated server-side). Destructive pushes (remove/disable/wipe) are BLOCKED unless --confirm.")
|
|
334
342
|
.option("--dry-run", "validate + print the per-tool diff WITHOUT writing")
|
|
343
|
+
.option("--confirm", "required when the push removes or disables tools, or sends an empty tools[]")
|
|
335
344
|
.action((id, opts) => customToolsCmd.push(id, opts));
|
|
336
345
|
customTools.command("enable <organization_id> <tool_name>")
|
|
337
346
|
.description("Enable one custom tool by name (no round-trip)")
|
|
@@ -423,5 +432,21 @@ export function run(argv) {
|
|
|
423
432
|
.option("--confirm", "required alongside --commit")
|
|
424
433
|
.action((orgId, id, opts) => segmentsCmd.untag(orgId, id, opts));
|
|
425
434
|
|
|
435
|
+
// keywords (org auto-reply engine: keywords + keyword_actions; id-keyed diff push)
|
|
436
|
+
const keywords = program.command("keywords")
|
|
437
|
+
.alias("kw")
|
|
438
|
+
.description("Round-trip an org's keyword auto-replies + actions (the safe editor for attribute-write actions)");
|
|
439
|
+
keywords.command("pull <organization_id>")
|
|
440
|
+
.description("Fetch every keyword + its actions into a local JSON file (ALWAYS pull right before editing)")
|
|
441
|
+
.action((orgId) => keywordsCmd.pull(orgId));
|
|
442
|
+
keywords.command("push <slug-or-path>")
|
|
443
|
+
.description("Apply the local JSON: id-keyed UPSERT (keep ids to update, drop ids to create). --prune also DELETES keywords absent from the file.")
|
|
444
|
+
.option("--prune", "also delete DB keywords whose id is not in the file (destructive; cascade-deletes their actions)")
|
|
445
|
+
.option("--dry-run", "compute + print the plan without writing (ALWAYS before --prune)")
|
|
446
|
+
.action((id, opts) => keywordsCmd.push(id, opts));
|
|
447
|
+
keywords.command("list")
|
|
448
|
+
.description("List local keyword snapshots")
|
|
449
|
+
.action(() => keywordsCmd.list());
|
|
450
|
+
|
|
426
451
|
program.parseAsync(argv);
|
|
427
452
|
}
|