@flowapt/flowiq-cli 0.1.0 → 0.1.1
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/package.json +1 -1
- package/src/commands/questionnaires.js +128 -0
- package/src/index.js +12 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flowapt/flowiq-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Command-line tool for FlowIQ staff: round-trip agent prompts, questionnaires, webhooks, and more without ever touching service-role credentials.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// `flowiq questionnaires pull <organization_id>` /
|
|
2
|
+
// `flowiq questionnaires push <slug>`.
|
|
3
|
+
// Round-trips an org's active WhatsApp agent's `agents.questionnaire`
|
|
4
|
+
// jsonb column through a local JSON file. No DB credentials 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 QS_DIR = path.resolve(process.cwd(), ".flowiq", "questionnaires");
|
|
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(QS_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
|
+
function countQuestions(sections) {
|
|
44
|
+
let total = 0, answered = 0;
|
|
45
|
+
if (!Array.isArray(sections)) return { total, answered };
|
|
46
|
+
for (const s of sections) {
|
|
47
|
+
if (!Array.isArray(s?.questions)) continue;
|
|
48
|
+
for (const q of s.questions) {
|
|
49
|
+
total += 1;
|
|
50
|
+
if ((q?.answer ?? "").trim().length > 0) answered += 1;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return { total, answered };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function pull(orgId) {
|
|
57
|
+
if (!UUID_RE.test(orgId)) {
|
|
58
|
+
console.error(`Error: "${orgId}" is not a valid organization UUID.`);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
let resp;
|
|
62
|
+
try {
|
|
63
|
+
resp = await http.get("questionnaires", { organization_id: orgId });
|
|
64
|
+
} catch (e) {
|
|
65
|
+
console.error(`Pull failed: ${e.message}`);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
await fs.mkdir(QS_DIR, { recursive: true });
|
|
70
|
+
const slug = slugify(resp.organization_name, resp.organization_id);
|
|
71
|
+
const filePath = path.join(QS_DIR, `${slug}.json`);
|
|
72
|
+
const overwriting = await fileExists(filePath);
|
|
73
|
+
await fs.writeFile(filePath, JSON.stringify(resp, null, 2) + "\n", "utf8");
|
|
74
|
+
|
|
75
|
+
const { total, answered } = countQuestions(resp.questionnaire?.sections);
|
|
76
|
+
console.log(`${overwriting ? "Overwrote" : "Wrote"} ${filePath}`);
|
|
77
|
+
console.log(` org: ${resp.organization_name} (${resp.organization_id})`);
|
|
78
|
+
console.log(` agent: ${resp.agent_name} (${resp.agent_id})`);
|
|
79
|
+
console.log(` sections: ${resp.questionnaire?.sections?.length ?? 0}`);
|
|
80
|
+
console.log(` questions: ${answered}/${total} answered`);
|
|
81
|
+
if (resp.default_used) {
|
|
82
|
+
console.log(` note: DB had no questionnaire — seeded from DEFAULT_QUESTIONNAIRE.`);
|
|
83
|
+
console.log(` Pushing the file unchanged will write the full default.`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function push(identifier) {
|
|
88
|
+
let filePath;
|
|
89
|
+
try {
|
|
90
|
+
filePath = await resolvePushPath(identifier);
|
|
91
|
+
} catch (e) {
|
|
92
|
+
console.error(`Error: ${e.message}`);
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
const raw = await fs.readFile(filePath, "utf8");
|
|
96
|
+
let data;
|
|
97
|
+
try { data = JSON.parse(raw); }
|
|
98
|
+
catch (e) {
|
|
99
|
+
console.error(`Invalid JSON in ${filePath}: ${e.message}`);
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!data.organization_id || !UUID_RE.test(data.organization_id)) {
|
|
104
|
+
console.error(`${filePath}: organization_id missing or invalid`);
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
if (!data.questionnaire || !Array.isArray(data.questionnaire.sections)) {
|
|
108
|
+
console.error(`${filePath}: questionnaire.sections must be an array`);
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let resp;
|
|
113
|
+
try {
|
|
114
|
+
resp = await http.post("questionnaires", {
|
|
115
|
+
organization_id: data.organization_id,
|
|
116
|
+
questionnaire: data.questionnaire,
|
|
117
|
+
});
|
|
118
|
+
} catch (e) {
|
|
119
|
+
console.error(`Push failed: ${e.message}`);
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
console.log(`Pushed ${filePath}`);
|
|
124
|
+
console.log(` org: ${resp.organization_name} (${resp.organization_id})`);
|
|
125
|
+
console.log(` agent: ${resp.agent_name} (${resp.agent_id})`);
|
|
126
|
+
console.log(` sections: ${resp.sections}`);
|
|
127
|
+
console.log(` questions: ${resp.answered_questions}/${resp.total_questions} answered`);
|
|
128
|
+
}
|
package/src/index.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
import * as authCmd from "./commands/auth.js";
|
|
6
6
|
import * as promptsCmd from "./commands/prompts.js";
|
|
7
|
+
import * as questionnairesCmd from "./commands/questionnaires.js";
|
|
7
8
|
|
|
8
9
|
export function run(argv) {
|
|
9
10
|
const program = new Command();
|
|
@@ -31,5 +32,16 @@ export function run(argv) {
|
|
|
31
32
|
.description("Apply a local JSON file's prompt_sections to the agent (regenerates system_prompt server-side)")
|
|
32
33
|
.action((id) => promptsCmd.push(id));
|
|
33
34
|
|
|
35
|
+
// questionnaires
|
|
36
|
+
const qs = program.command("questionnaires")
|
|
37
|
+
.alias("q")
|
|
38
|
+
.description("Round-trip an org's active WhatsApp agent's questionnaire answers (agents.questionnaire JSONB)");
|
|
39
|
+
qs.command("pull <organization_id>")
|
|
40
|
+
.description("Fetch the active agent's questionnaire into a local JSON file (seeds from DEFAULT_QUESTIONNAIRE if the DB column is empty)")
|
|
41
|
+
.action((orgId) => questionnairesCmd.pull(orgId));
|
|
42
|
+
qs.command("push <slug-or-path>")
|
|
43
|
+
.description("Apply a local JSON file's questionnaire to the agent")
|
|
44
|
+
.action((id) => questionnairesCmd.push(id));
|
|
45
|
+
|
|
34
46
|
program.parseAsync(argv);
|
|
35
47
|
}
|