@flowapt/flowiq-cli 0.1.7 → 0.1.9

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
@@ -252,6 +252,42 @@ credentials are stripped server-side).
252
252
  flowiq org info <organization_id> # platform, storefront url, active agent
253
253
  ```
254
254
 
255
+ ### Agents — `flowiq agent list|create <org>`
256
+
257
+ List an org's agents (to discover ids) and create a new one.
258
+
259
+ ```bash
260
+ flowiq agent list <organization_id> # ★ marks the active agent; shows each id
261
+ flowiq agent create <organization_id> --name "Zara" \
262
+ --system-prompt-file ./zara-prompt.txt --make-active
263
+ ```
264
+
265
+ `create` inserts a new agent (`created_by` = your staff-key identity), optionally
266
+ seeds its prompt into a single "Main" section, and — with `--make-active` — sets
267
+ it as the org's `active_whatsapp_agent`. New agents start on the app default
268
+ model; set the house model afterwards with `agent config … --model gpt-5.4-mini
269
+ --use-settings-prompt`.
270
+
271
+ ### Targeting a specific agent — `--agent <id>`
272
+
273
+ By default `prompts`, `questionnaires`, `fine-tuning`, and `agent config` all act
274
+ on the org's **active** agent. To target a **different** agent, pass `--agent <id>`
275
+ (get ids from `agent list`):
276
+
277
+ ```bash
278
+ flowiq prompts pull <org> --agent <agent_id> # pull a NON-active agent's prompt
279
+ flowiq prompts push <slug> # pushes back to that same agent (id is in the file)
280
+ flowiq agent config <org> --agent <agent_id> --model gpt-5.4-mini
281
+ ```
282
+
283
+ `pull` records the agent id in the JSON, so `push` always writes back to the exact
284
+ agent you pulled from (never "whatever is active now").
285
+
286
+ **Filenames:** the active agent saves to `<org-slug>.json`; a non-active agent
287
+ (pulled with `--agent`) saves to `<org-slug>-<agent-slug>.json`. So pulling
288
+ multiple agents from one org produces distinct files that don't overwrite each
289
+ other. (Fixed in 0.1.9 — before that, both used `<org-slug>.json`.)
290
+
255
291
  ### Agent config — `flowiq agent config <organization_id> [flags]`
256
292
 
257
293
  Set an allowlisted set of the active agent's config fields (the prompt-builder
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowapt/flowiq-cli",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
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": {
@@ -37,6 +37,7 @@ export async function config(orgId, opts = {}) {
37
37
  }
38
38
 
39
39
  const body = { organization_id: orgId };
40
+ if (opts.agent !== undefined) body.agent_id = opts.agent;
40
41
  const settings = {};
41
42
  if (opts.useSettingsPrompt !== undefined) settings.use_settings_prompt = parseBool(opts.useSettingsPrompt, "--use-settings-prompt");
42
43
  if (opts.model !== undefined) settings.model = opts.model;
@@ -65,12 +66,12 @@ export async function config(orgId, opts = {}) {
65
66
  if (!hasWrite) {
66
67
  let resp;
67
68
  try {
68
- resp = await http.get("agent-config", { organization_id: orgId });
69
+ resp = await http.get("agent-config", { organization_id: orgId, agent_id: opts.agent });
69
70
  } catch (e) {
70
71
  console.error(`Lookup failed: ${e.message}`);
71
72
  process.exit(1);
72
73
  }
73
- console.log(`${resp.organization_name} → agent ${resp.agent_id}`);
74
+ console.log(`${resp.organization_name} → agent ${resp.agent_id}${resp.is_active === false ? " [NON-active]" : ""}`);
74
75
  printSnapshot("current", resp.current);
75
76
  console.log("\n(pass --use-settings-prompt / --model / --rename / --tool / --discount to change)");
76
77
  return;
@@ -85,7 +86,7 @@ export async function config(orgId, opts = {}) {
85
86
  process.exit(1);
86
87
  }
87
88
 
88
- console.log(`Updated agent ${resp.agent_id} (${resp.organization_name})`);
89
+ console.log(`Updated agent ${resp.agent_id} (${resp.organization_name})${resp.is_active === false ? " [NON-active]" : ""}`);
89
90
  console.log(` changed: ${resp.changed.join(", ") || "(nothing)"}`);
90
91
  for (const k of resp.changed) {
91
92
  const b = resp.before?.[k];
@@ -0,0 +1,76 @@
1
+ // `flowiq agent list <org>` / `flowiq agent create <org> --name … [flags]`.
2
+ // List an org's agents (to discover ids for --agent targeting) and create a
3
+ // new agent via /cli/agents.
4
+
5
+ import fs from "node:fs/promises";
6
+ import path from "node:path";
7
+ import { http } from "../http.js";
8
+
9
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
10
+
11
+ export async function list(orgId, opts = {}) {
12
+ if (!UUID_RE.test(orgId)) {
13
+ console.error(`Error: "${orgId}" is not a valid organization UUID.`);
14
+ process.exit(1);
15
+ }
16
+ let resp;
17
+ try {
18
+ resp = await http.get("agents", { organization_id: orgId });
19
+ } catch (e) {
20
+ console.error(`List failed: ${e.message}`);
21
+ process.exit(1);
22
+ }
23
+ if (opts.json) { console.log(JSON.stringify(resp, null, 2)); return; }
24
+
25
+ const agents = resp.agents || [];
26
+ console.log(`${resp.organization_name} (${resp.organization_id}) — ${agents.length} agent${agents.length === 1 ? "" : "s"}:`);
27
+ if (!agents.length) { console.log(" (none — create one with `agent create`)"); return; }
28
+ for (const a of agents) {
29
+ const created = a.created_at ? new Date(a.created_at).toISOString().slice(0, 10) : "?";
30
+ console.log(` ${a.is_active ? "★" : " "} ${a.id} ${created} ${a.name}`);
31
+ }
32
+ console.log("\n★ = active agent. Pass any id as --agent <id> to prompts/questionnaires/fine-tuning/agent config.");
33
+ }
34
+
35
+ export async function create(orgId, opts = {}) {
36
+ if (!UUID_RE.test(orgId)) {
37
+ console.error(`Error: "${orgId}" is not a valid organization UUID.`);
38
+ process.exit(1);
39
+ }
40
+ if (!opts.name || !String(opts.name).trim()) {
41
+ console.error("Error: --name <name> is required.");
42
+ process.exit(1);
43
+ }
44
+
45
+ let systemPrompt;
46
+ if (opts.systemPromptFile) {
47
+ let fp = opts.systemPromptFile;
48
+ if (!path.isAbsolute(fp)) fp = path.resolve(fp);
49
+ try { systemPrompt = await fs.readFile(fp, "utf8"); }
50
+ catch (e) { console.error(`Cannot read --system-prompt-file ${fp}: ${e.message}`); process.exit(1); }
51
+ } else if (opts.systemPrompt !== undefined) {
52
+ systemPrompt = opts.systemPrompt;
53
+ }
54
+
55
+ let resp;
56
+ try {
57
+ resp = await http.post("agents", {
58
+ organization_id: orgId,
59
+ name: opts.name,
60
+ system_prompt: systemPrompt,
61
+ make_active: !!opts.makeActive,
62
+ });
63
+ } catch (e) {
64
+ console.error(`Create failed: ${e.message}`);
65
+ if (e.body?.error) console.error(` ${e.body.error}`);
66
+ process.exit(1);
67
+ }
68
+
69
+ console.log(`Created agent "${resp.agent_name}" (${resp.agent_id})`);
70
+ console.log(` org: ${resp.organization_name} (${resp.organization_id})`);
71
+ console.log(` active: ${resp.is_active ? "yes (set as the org's active agent)" : "no"}`);
72
+ console.log(` seeded prompt: ${resp.seeded_prompt ? "yes (single \"Main\" section)" : "no (default settings)"}`);
73
+ if (resp.warning) console.log(` ⚠ ${resp.warning}`);
74
+ console.log("\nNext: `agent config " + orgId + " --agent " + resp.agent_id + " --use-settings-prompt --model gpt-5.4-mini`");
75
+ console.log(" then `prompts pull " + orgId + " --agent " + resp.agent_id + "` to author the full prompt.");
76
+ }
@@ -53,21 +53,26 @@ function countEntries(entries) {
53
53
  return { total: entries.length, pending, resolved, byCategory };
54
54
  }
55
55
 
56
- export async function pull(orgId) {
56
+ export async function pull(orgId, opts = {}) {
57
57
  if (!UUID_RE.test(orgId)) {
58
58
  console.error(`Error: "${orgId}" is not a valid organization UUID.`);
59
59
  process.exit(1);
60
60
  }
61
61
  let resp;
62
62
  try {
63
- resp = await http.get("fine-tuning", { organization_id: orgId });
63
+ resp = await http.get("fine-tuning", { organization_id: orgId, agent_id: opts.agent });
64
64
  } catch (e) {
65
65
  console.error(`Pull failed: ${e.message}`);
66
66
  process.exit(1);
67
67
  }
68
68
 
69
69
  await fs.mkdir(FT_DIR, { recursive: true });
70
- const slug = slugify(resp.organization_name, resp.organization_id);
70
+ // Non-active agents get an agent-suffixed filename so pulling multiple agents
71
+ // from the same org doesn't clobber each other's working copy.
72
+ const orgSlug = slugify(resp.organization_name, resp.organization_id);
73
+ const slug = resp.is_active === false
74
+ ? `${orgSlug}-${slugify(resp.agent_name, resp.agent_id.slice(0, 8))}`
75
+ : orgSlug;
71
76
  const filePath = path.join(FT_DIR, `${slug}.json`);
72
77
  const overwriting = await fileExists(filePath);
73
78
  await fs.writeFile(filePath, JSON.stringify(resp, null, 2) + "\n", "utf8");
@@ -75,7 +80,7 @@ export async function pull(orgId) {
75
80
  const stats = countEntries(resp.fine_tuning_entries);
76
81
  console.log(`${overwriting ? "Overwrote" : "Wrote"} ${filePath}`);
77
82
  console.log(` org: ${resp.organization_name} (${resp.organization_id})`);
78
- console.log(` agent: ${resp.agent_name} (${resp.agent_id})`);
83
+ console.log(` agent: ${resp.agent_name} (${resp.agent_id})${resp.is_active === false ? " [NON-active]" : ""}`);
79
84
  console.log(` instructions: ${resp.fine_tuning.length} chars`);
80
85
  console.log(` entries: ${stats.total} (${stats.pending} pending, ${stats.resolved} resolved)`);
81
86
  if (stats.total > 0) {
@@ -118,6 +123,7 @@ export async function push(identifier) {
118
123
  organization_id: data.organization_id,
119
124
  fine_tuning: data.fine_tuning,
120
125
  fine_tuning_entries: data.fine_tuning_entries ?? [],
126
+ agent_id: data.agent_id, // push back to the agent this file was pulled from
121
127
  });
122
128
  } catch (e) {
123
129
  console.error(`Push failed: ${e.message}`);
@@ -40,21 +40,27 @@ async function resolvePushPath(identifier) {
40
40
  throw new Error(`File not found. Tried:\n ${candidates.join("\n ")}`);
41
41
  }
42
42
 
43
- export async function pull(orgId) {
43
+ export async function pull(orgId, opts = {}) {
44
44
  if (!UUID_RE.test(orgId)) {
45
45
  console.error(`Error: "${orgId}" is not a valid organization UUID.`);
46
46
  process.exit(1);
47
47
  }
48
48
  let resp;
49
49
  try {
50
- resp = await http.get("prompts", { organization_id: orgId });
50
+ resp = await http.get("prompts", { organization_id: orgId, agent_id: opts.agent });
51
51
  } catch (e) {
52
52
  console.error(`Pull failed: ${e.message}`);
53
53
  process.exit(1);
54
54
  }
55
55
 
56
56
  await fs.mkdir(PROMPTS_DIR, { recursive: true });
57
- const slug = slugify(resp.organization_name, resp.organization_id);
57
+ // Non-active agents get an agent-suffixed filename so pulling multiple agents
58
+ // from the same org doesn't clobber each other's working copy. The active
59
+ // agent keeps the plain <org>.json name (backwards-compatible).
60
+ const orgSlug = slugify(resp.organization_name, resp.organization_id);
61
+ const slug = resp.is_active === false
62
+ ? `${orgSlug}-${slugify(resp.agent_name, resp.agent_id.slice(0, 8))}`
63
+ : orgSlug;
58
64
  const filePath = path.join(PROMPTS_DIR, `${slug}.json`);
59
65
  const overwriting = await fileExists(filePath);
60
66
  await fs.writeFile(filePath, JSON.stringify(resp, null, 2) + "\n", "utf8");
@@ -62,7 +68,7 @@ export async function pull(orgId) {
62
68
  const totalChars = resp.prompt_sections.reduce((a, s) => a + (s.content?.length || 0), 0);
63
69
  console.log(`${overwriting ? "Overwrote" : "Wrote"} ${filePath}`);
64
70
  console.log(` org: ${resp.organization_name} (${resp.organization_id})`);
65
- console.log(` agent: ${resp.agent_name} (${resp.agent_id})`);
71
+ console.log(` agent: ${resp.agent_name} (${resp.agent_id})${resp.is_active === false ? " [NON-active]" : ""}`);
66
72
  console.log(` sections: ${resp.prompt_sections.length}`);
67
73
  console.log(` content: ${totalChars.toLocaleString()} chars`);
68
74
  if (resp.bootstrapped) {
@@ -105,6 +111,7 @@ export async function push(identifier) {
105
111
  resp = await http.post("prompts", {
106
112
  organization_id: data.organization_id,
107
113
  prompt_sections: data.prompt_sections,
114
+ agent_id: data.agent_id, // push back to the agent this file was pulled from
108
115
  });
109
116
  } catch (e) {
110
117
  console.error(`Push failed: ${e.message}`);
@@ -53,21 +53,26 @@ function countQuestions(sections) {
53
53
  return { total, answered };
54
54
  }
55
55
 
56
- export async function pull(orgId) {
56
+ export async function pull(orgId, opts = {}) {
57
57
  if (!UUID_RE.test(orgId)) {
58
58
  console.error(`Error: "${orgId}" is not a valid organization UUID.`);
59
59
  process.exit(1);
60
60
  }
61
61
  let resp;
62
62
  try {
63
- resp = await http.get("questionnaires", { organization_id: orgId });
63
+ resp = await http.get("questionnaires", { organization_id: orgId, agent_id: opts.agent });
64
64
  } catch (e) {
65
65
  console.error(`Pull failed: ${e.message}`);
66
66
  process.exit(1);
67
67
  }
68
68
 
69
69
  await fs.mkdir(QS_DIR, { recursive: true });
70
- const slug = slugify(resp.organization_name, resp.organization_id);
70
+ // Non-active agents get an agent-suffixed filename so pulling multiple agents
71
+ // from the same org doesn't clobber each other's working copy.
72
+ const orgSlug = slugify(resp.organization_name, resp.organization_id);
73
+ const slug = resp.is_active === false
74
+ ? `${orgSlug}-${slugify(resp.agent_name, resp.agent_id.slice(0, 8))}`
75
+ : orgSlug;
71
76
  const filePath = path.join(QS_DIR, `${slug}.json`);
72
77
  const overwriting = await fileExists(filePath);
73
78
  await fs.writeFile(filePath, JSON.stringify(resp, null, 2) + "\n", "utf8");
@@ -75,7 +80,7 @@ export async function pull(orgId) {
75
80
  const { total, answered } = countQuestions(resp.questionnaire?.sections);
76
81
  console.log(`${overwriting ? "Overwrote" : "Wrote"} ${filePath}`);
77
82
  console.log(` org: ${resp.organization_name} (${resp.organization_id})`);
78
- console.log(` agent: ${resp.agent_name} (${resp.agent_id})`);
83
+ console.log(` agent: ${resp.agent_name} (${resp.agent_id})${resp.is_active === false ? " [NON-active]" : ""}`);
79
84
  console.log(` sections: ${resp.questionnaire?.sections?.length ?? 0}`);
80
85
  console.log(` questions: ${answered}/${total} answered`);
81
86
  if (resp.default_used) {
@@ -114,6 +119,7 @@ export async function push(identifier) {
114
119
  resp = await http.post("questionnaires", {
115
120
  organization_id: data.organization_id,
116
121
  questionnaire: data.questionnaire,
122
+ agent_id: data.agent_id, // push back to the agent this file was pulled from
117
123
  });
118
124
  } catch (e) {
119
125
  console.error(`Push failed: ${e.message}`);
package/src/index.js CHANGED
@@ -18,6 +18,7 @@ import * as pinboardCmd from "./commands/pinboard.js";
18
18
  import * as templatesCmd from "./commands/templates.js";
19
19
  import * as orgCmd from "./commands/org.js";
20
20
  import * as agentConfigCmd from "./commands/agent-config.js";
21
+ import * as agentsCmd from "./commands/agents.js";
21
22
  import * as agentUpdatesCmd from "./commands/agent-updates.js";
22
23
  import * as exportCmd from "./commands/export.js";
23
24
  import * as testCmd from "./commands/agent-test.js";
@@ -48,10 +49,11 @@ export function run(argv) {
48
49
  // prompts
49
50
  const prompts = program.command("prompts").description("Round-trip an org's active WhatsApp agent system prompt");
50
51
  prompts.command("pull <organization_id>")
51
- .description("Fetch the active agent's prompt_sections into a local JSON file")
52
- .action((orgId) => promptsCmd.pull(orgId));
52
+ .description("Fetch an agent's prompt_sections into a local JSON file (default: active agent)")
53
+ .option("--agent <id>", "target a specific agent instead of the org's active one")
54
+ .action((orgId, opts) => promptsCmd.pull(orgId, opts));
53
55
  prompts.command("push <slug-or-path>")
54
- .description("Apply a local JSON file's prompt_sections to the agent (regenerates system_prompt server-side)")
56
+ .description("Apply a local JSON file's prompt_sections back to the agent it was pulled from (regenerates system_prompt server-side)")
55
57
  .action((id) => promptsCmd.push(id));
56
58
 
57
59
  // questionnaires
@@ -59,10 +61,11 @@ export function run(argv) {
59
61
  .alias("q")
60
62
  .description("Round-trip an org's active WhatsApp agent's questionnaire answers (agents.questionnaire JSONB)");
61
63
  qs.command("pull <organization_id>")
62
- .description("Fetch the active agent's questionnaire into a local JSON file (seeds from DEFAULT_QUESTIONNAIRE if the DB column is empty)")
63
- .action((orgId) => questionnairesCmd.pull(orgId));
64
+ .description("Fetch an agent's questionnaire into a local JSON file (default: active agent; seeds DEFAULT_QUESTIONNAIRE if empty)")
65
+ .option("--agent <id>", "target a specific agent instead of the org's active one")
66
+ .action((orgId, opts) => questionnairesCmd.pull(orgId, opts));
64
67
  qs.command("push <slug-or-path>")
65
- .description("Apply a local JSON file's questionnaire to the agent")
68
+ .description("Apply a local JSON file's questionnaire back to the agent it was pulled from")
66
69
  .action((id) => questionnairesCmd.push(id));
67
70
 
68
71
  // messages (read-only: pull a contact's helpdesk_messages history)
@@ -79,10 +82,11 @@ export function run(argv) {
79
82
  .alias("ft")
80
83
  .description("Round-trip the active agent's fine-tuning instructions + Q&A entries");
81
84
  ft.command("pull <organization_id>")
82
- .description("Fetch agents.fine_tuning + agents.fine_tuning_entries into a local JSON file")
83
- .action((orgId) => fineTuningCmd.pull(orgId));
85
+ .description("Fetch an agent's fine_tuning + fine_tuning_entries into a local JSON file (default: active agent)")
86
+ .option("--agent <id>", "target a specific agent instead of the org's active one")
87
+ .action((orgId, opts) => fineTuningCmd.pull(orgId, opts));
84
88
  ft.command("push <slug-or-path>")
85
- .description("Apply a local JSON file's fine_tuning + fine_tuning_entries to the agent")
89
+ .description("Apply a local JSON file's fine_tuning + fine_tuning_entries back to the agent it was pulled from")
86
90
  .action((id) => fineTuningCmd.push(id));
87
91
 
88
92
  // messaging-webhooks (outbound notifications via meta_webhooks table)
@@ -184,10 +188,22 @@ export function run(argv) {
184
188
  .option("--json", "print the raw JSON payload")
185
189
  .action((orgId, opts) => orgCmd.info(orgId, opts));
186
190
 
187
- // agent (agent-level config writes: the prompt-builder skill's Step 5)
188
- const agent = program.command("agent").description("Configure the org's active agent");
191
+ // agent (list / create / config — config defaults to the active agent, or --agent <id>)
192
+ const agent = program.command("agent").description("List, create, and configure an org's agents");
193
+ agent.command("list <organization_id>")
194
+ .description("List the org's agents (★ = active); use an id as --agent <id> elsewhere")
195
+ .option("--json", "print the raw JSON payload")
196
+ .action((orgId, opts) => agentsCmd.list(orgId, opts));
197
+ agent.command("create <organization_id>")
198
+ .description("Create a new agent (optionally seed its prompt + set it active)")
199
+ .requiredOption("--name <name>", "agent name")
200
+ .option("--system-prompt <text>", "seed the prompt (single \"Main\" section)")
201
+ .option("--system-prompt-file <path>", "seed the prompt from a file")
202
+ .option("--make-active", "set the new agent as the org's active_whatsapp_agent")
203
+ .action((orgId, opts) => agentsCmd.create(orgId, opts));
189
204
  agent.command("config <organization_id>")
190
- .description("Set allowlisted agent config (no flags = show current)")
205
+ .description("Set allowlisted agent config (no write flags = show current; defaults to the active agent)")
206
+ .option("--agent <id>", "target a specific agent instead of the org's active one")
191
207
  .option("--use-settings-prompt [bool]", "settings.use_settings_prompt (true if bare)")
192
208
  .option("--model <model>", "settings.model (e.g. gpt-5.4-mini)")
193
209
  .option("--rename <name>", "rename the agent")