@flowapt/flowiq-cli 0.1.9 → 0.1.10

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
@@ -99,6 +99,24 @@ Shipping / Returns / Promotions / Technical Support / Tone Adjustment / Other),
99
99
  `suggested_tone` (Friendly / Professional / Encouraging / Brief / Detailed),
100
100
  `status` (Pending / Resolved), `messages: []`, `media: []`.
101
101
 
102
+ ### Knowledge / text sources — `flowiq knowledge pull|push|list` (alias `kn`)
103
+
104
+ Round-trips an agent's **text knowledge sources** — the `{ code, text }` entries
105
+ in `agents.additional_config` that the agent's `get_more_answers` tool retrieves
106
+ on demand (brand references + tethered playbooks; the "Knowledge Sources → Text"
107
+ tab in the UI).
108
+
109
+ ```bash
110
+ flowiq knowledge pull <org_id> # → ./.flowiq/knowledge/<slug>.json (sources[] = {code,text})
111
+ flowiq knowledge pull <org_id> --agent <id> # a specific (non-active) agent
112
+ flowiq knowledge push <slug> # FULL-REPLACE sources[] back onto the agent
113
+ flowiq knowledge list
114
+ ```
115
+
116
+ - Codes are `UPPER_SNAKE` (`^[A-Z0-9][A-Z0-9_]{2,63}$`), validated server-side. Edit / add / remove entries in `sources[]`, then push (full-replace of the array).
117
+ - **Object-shape guard:** some agents use `additional_config` for Flowapt-managed config (`external_woo_build`, …) rather than text sources — a pull flags that, and a push is **refused** (never clobbers managed config).
118
+ - `--agent` + filenames behave like `prompts` (a non-active agent saves to `<org-slug>-<agent-slug>.json`). The file carries `agent_id`, so `push` targets the agent it was pulled from.
119
+
102
120
  ### Messages — `flowiq messages pull <contact_id>` (alias `m`)
103
121
 
104
122
  Read-only pull of a contact's `helpdesk_messages` history. Anchors on the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowapt/flowiq-cli",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
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,156 @@
1
+ // `flowiq knowledge pull <org_id> [--agent <id>]` / `push <slug>` / `list`.
2
+ // Round-trips an agent's TEXT knowledge sources — the `{ code, text }` entries
3
+ // in `agents.additional_config` that the get_more_answers tool serves (brand
4
+ // references + tethered playbooks). Via /cli/knowledge; no DB creds on disk.
5
+
6
+ import fs from "node:fs/promises";
7
+ import path from "node:path";
8
+ import { http } from "../http.js";
9
+
10
+ const KN_DIR = path.resolve(process.cwd(), ".flowiq", "knowledge");
11
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
12
+
13
+ function slugify(name, fallback) {
14
+ const s = String(name || "")
15
+ .toLowerCase()
16
+ .replace(/[^a-z0-9]+/g, "-")
17
+ .replace(/-+/g, "-")
18
+ .replace(/^-|-$/g, "");
19
+ return s || fallback;
20
+ }
21
+
22
+ async function fileExists(p) {
23
+ try { await fs.access(p); return true; } catch { return false; }
24
+ }
25
+
26
+ async function resolvePushPath(identifier) {
27
+ const candidates = [];
28
+ if (path.isAbsolute(identifier)) {
29
+ candidates.push(identifier);
30
+ } else if (identifier.includes("/") || identifier.includes("\\")) {
31
+ candidates.push(path.resolve(identifier));
32
+ } else {
33
+ const base = identifier.endsWith(".json") ? identifier : `${identifier}.json`;
34
+ candidates.push(path.join(KN_DIR, base));
35
+ candidates.push(path.resolve(base));
36
+ }
37
+ for (const c of candidates) {
38
+ if (await fileExists(c)) return c;
39
+ }
40
+ throw new Error(`File not found. Tried:\n ${candidates.join("\n ")}`);
41
+ }
42
+
43
+ export async function pull(orgId, opts = {}) {
44
+ if (!UUID_RE.test(orgId)) {
45
+ console.error(`Error: "${orgId}" is not a valid organization UUID.`);
46
+ process.exit(1);
47
+ }
48
+ let resp;
49
+ try {
50
+ resp = await http.get("knowledge", { organization_id: orgId, agent_id: opts.agent });
51
+ } catch (e) {
52
+ console.error(`Pull failed: ${e.message}`);
53
+ process.exit(1);
54
+ }
55
+
56
+ await fs.mkdir(KN_DIR, { recursive: true });
57
+ // Non-active agents get an agent-suffixed filename (same as prompts) so
58
+ // pulling multiple agents from one org doesn't clobber each other.
59
+ const orgSlug = slugify(resp.organization_name, resp.organization_id);
60
+ const slug = resp.is_active === false
61
+ ? `${orgSlug}-${slugify(resp.agent_name, resp.agent_id.slice(0, 8))}`
62
+ : orgSlug;
63
+ const filePath = path.join(KN_DIR, `${slug}.json`);
64
+ const overwriting = await fileExists(filePath);
65
+ await fs.writeFile(filePath, JSON.stringify(resp, null, 2) + "\n", "utf8");
66
+
67
+ console.log(`${overwriting ? "Overwrote" : "Wrote"} ${filePath}`);
68
+ console.log(` org: ${resp.organization_name} (${resp.organization_id})`);
69
+ console.log(` agent: ${resp.agent_name} (${resp.agent_id})${resp.is_active === false ? " [NON-active]" : ""}`);
70
+ console.log(` sources: ${resp.source_count}`);
71
+ console.log(` content: ${(resp.total_chars || 0).toLocaleString()} chars`);
72
+ for (const s of resp.sources || []) {
73
+ console.log(` ${s.code.padEnd(28)} ${(s.text?.length || 0).toLocaleString()} chars`);
74
+ }
75
+ if (resp.shape === "object") {
76
+ console.log("");
77
+ console.log(" ⚠ this agent's additional_config is object-shaped (Flowapt-managed config,");
78
+ console.log(" e.g. external_woo_build) — it holds NO text sources and CANNOT be pushed to.");
79
+ } else if (resp.shape === "empty") {
80
+ console.log("");
81
+ console.log(" note: no text sources yet — add {code, text} entries to the JSON and push to create them.");
82
+ }
83
+ }
84
+
85
+ export async function push(identifier) {
86
+ let filePath;
87
+ try {
88
+ filePath = await resolvePushPath(identifier);
89
+ } catch (e) {
90
+ console.error(`Error: ${e.message}`);
91
+ process.exit(1);
92
+ }
93
+ const raw = await fs.readFile(filePath, "utf8");
94
+ let data;
95
+ try { data = JSON.parse(raw); }
96
+ catch (e) {
97
+ console.error(`Invalid JSON in ${filePath}: ${e.message}`);
98
+ process.exit(1);
99
+ }
100
+ if (!data.organization_id || !UUID_RE.test(data.organization_id)) {
101
+ console.error(`${filePath}: organization_id missing or invalid`);
102
+ process.exit(1);
103
+ }
104
+ if (!Array.isArray(data.sources)) {
105
+ console.error(`${filePath}: sources must be an array of { code, text }`);
106
+ process.exit(1);
107
+ }
108
+
109
+ let resp;
110
+ try {
111
+ resp = await http.post("knowledge", {
112
+ organization_id: data.organization_id,
113
+ agent_id: data.agent_id, // push back to the agent this file was pulled from
114
+ sources: data.sources.map((s) => ({ code: s.code, text: s.text })),
115
+ });
116
+ } catch (e) {
117
+ console.error(`Push failed: ${e.message}`);
118
+ if (e.body?.error) console.error(` ${e.body.error}`);
119
+ process.exit(1);
120
+ }
121
+
122
+ console.log(`Pushed ${filePath}`);
123
+ console.log(` org: ${resp.organization_name} (${resp.organization_id})`);
124
+ console.log(` agent: ${resp.agent_name} (${resp.agent_id})${resp.is_active === false ? " [NON-active]" : ""}`);
125
+ console.log(` sources: ${resp.source_count}`);
126
+ console.log(` content: ${(resp.total_chars || 0).toLocaleString()} chars`);
127
+ console.log(` codes: ${(resp.codes || []).join(", ")}`);
128
+ }
129
+
130
+ export async function list() {
131
+ let files;
132
+ try {
133
+ files = (await fs.readdir(KN_DIR)).filter((f) => f.endsWith(".json"));
134
+ } catch {
135
+ console.log("(no local knowledge files; run `knowledge pull <org_id>`)");
136
+ return;
137
+ }
138
+ if (!files.length) {
139
+ console.log("(no local knowledge files; run `knowledge pull <org_id>`)");
140
+ return;
141
+ }
142
+ files.sort();
143
+ for (const f of files) {
144
+ const fp = path.join(KN_DIR, f);
145
+ try {
146
+ const d = JSON.parse(await fs.readFile(fp, "utf8"));
147
+ console.log(f);
148
+ console.log(` org: ${d.organization_name ?? "(unknown)"} (${d.organization_id ?? "?"})`);
149
+ console.log(` agent: ${d.agent_name ?? "?"} (${d.agent_id ?? "?"})`);
150
+ console.log(` sources: ${d.source_count ?? (Array.isArray(d.sources) ? d.sources.length : "?")}`);
151
+ console.log(` pulled: ${d.pulled_at ?? "(unknown)"}`);
152
+ } catch (e) {
153
+ console.log(`${f} [invalid JSON: ${e.message}]`);
154
+ }
155
+ }
156
+ }
package/src/index.js CHANGED
@@ -22,6 +22,7 @@ import * as agentsCmd from "./commands/agents.js";
22
22
  import * as agentUpdatesCmd from "./commands/agent-updates.js";
23
23
  import * as exportCmd from "./commands/export.js";
24
24
  import * as testCmd from "./commands/agent-test.js";
25
+ import * as knowledgeCmd from "./commands/knowledge.js";
25
26
 
26
27
  // Read version from package.json so it stays in sync with the published npm
27
28
  // version automatically (single source of truth — bumping package.json on each
@@ -290,5 +291,20 @@ export function run(argv) {
290
291
  .option("--out <path>", "output file (default .flowiq/agent-test/judge_input.json)")
291
292
  .action((orgId, opts) => testCmd.extract(orgId, opts));
292
293
 
294
+ // knowledge (agent text knowledge sources: additional_config {code,text} → get_more_answers)
295
+ const knowledge = program.command("knowledge")
296
+ .alias("kn")
297
+ .description("Round-trip an agent's TEXT knowledge sources (additional_config {code,text} — the get_more_answers playbooks/references)");
298
+ knowledge.command("pull <organization_id>")
299
+ .description("Fetch the agent's {code,text} text sources into a local JSON file (default: active agent)")
300
+ .option("--agent <id>", "target a specific agent instead of the org's active one")
301
+ .action((orgId, opts) => knowledgeCmd.pull(orgId, opts));
302
+ knowledge.command("push <slug-or-path>")
303
+ .description("FULL-REPLACE the agent's text sources with the local JSON's sources[] (refused if additional_config is object-shaped/managed)")
304
+ .action((id) => knowledgeCmd.push(id));
305
+ knowledge.command("list")
306
+ .description("List local knowledge snapshots")
307
+ .action(() => knowledgeCmd.list());
308
+
293
309
  program.parseAsync(argv);
294
310
  }