@flowapt/flowiq-cli 0.1.6 → 0.1.8
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 +136 -3
- package/package.json +2 -2
- package/src/commands/agent-config.js +96 -0
- package/src/commands/agent-test.js +232 -0
- package/src/commands/agent-updates.js +186 -0
- package/src/commands/agents.js +76 -0
- package/src/commands/export.js +131 -0
- package/src/commands/fine-tuning.js +4 -3
- package/src/commands/org.js +33 -0
- package/src/commands/pinboard.js +174 -0
- package/src/commands/prompts.js +4 -3
- package/src/commands/questionnaires.js +4 -3
- package/src/commands/templates.js +156 -0
- package/src/data/scenarios.qa.json +41 -0
- package/src/index.js +165 -9
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// `flowiq export chats <organization_id> [--out <path>]`.
|
|
2
|
+
// Full chat export → TXT, byte-identical to the in-app "Export Settings TXT".
|
|
3
|
+
// The server (/cli/export-chats) supplies keyset pages (ordered, whatsapp_id
|
|
4
|
+
// resolved); ALL grouping + formatting happens here, verbatim from the legacy
|
|
5
|
+
// export-chats.mjs, so the output matches the deployed feature exactly.
|
|
6
|
+
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import fsp from "node:fs/promises";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { http } from "../http.js";
|
|
11
|
+
|
|
12
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
13
|
+
|
|
14
|
+
function slugify(name, fallback) {
|
|
15
|
+
const s = String(name || "")
|
|
16
|
+
.toLowerCase()
|
|
17
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
18
|
+
.replace(/-+/g, "-")
|
|
19
|
+
.replace(/^-|-$/g, "");
|
|
20
|
+
return s || fallback;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Verbatim from export-chats.mjs.
|
|
24
|
+
function getSenderLabel(senderType) {
|
|
25
|
+
if (senderType.startsWith("user-")) return "customer";
|
|
26
|
+
if (senderType.startsWith("bot-")) return "bot";
|
|
27
|
+
if (senderType.startsWith("human-")) return "agent";
|
|
28
|
+
if (senderType.startsWith("flowmod-")) return "flowmod";
|
|
29
|
+
if (senderType.startsWith("call-")) return "call";
|
|
30
|
+
if (senderType === "tool-call") return "tool";
|
|
31
|
+
return senderType;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function chats(orgId, opts = {}) {
|
|
35
|
+
if (!UUID_RE.test(orgId)) {
|
|
36
|
+
console.error(`Error: "${orgId}" is not a valid organization UUID.`);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const t0 = Date.now();
|
|
41
|
+
|
|
42
|
+
// --- Drive the keyset page loop, accumulating messages in global order ---
|
|
43
|
+
const messages = [];
|
|
44
|
+
let organizationName = null;
|
|
45
|
+
let cursor = null;
|
|
46
|
+
let pages = 0;
|
|
47
|
+
for (;;) {
|
|
48
|
+
const query = { organization_id: orgId };
|
|
49
|
+
if (cursor) {
|
|
50
|
+
query.cursor_created_at = cursor.created_at;
|
|
51
|
+
query.cursor_id = cursor.id;
|
|
52
|
+
}
|
|
53
|
+
let resp;
|
|
54
|
+
try {
|
|
55
|
+
resp = await http.get("export-chats", query);
|
|
56
|
+
} catch (e) {
|
|
57
|
+
console.error(`Export failed on page ${pages + 1}: ${e.message}`);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
if (resp.organization_name != null) organizationName = resp.organization_name;
|
|
61
|
+
const page = resp.page || [];
|
|
62
|
+
for (const r of page) messages.push(r);
|
|
63
|
+
pages++;
|
|
64
|
+
if (pages % 20 === 0) {
|
|
65
|
+
console.log(` ...${messages.length.toLocaleString("en-US")} messages (${pages} pages)`);
|
|
66
|
+
}
|
|
67
|
+
if (resp.done || !resp.next_cursor) break;
|
|
68
|
+
cursor = resp.next_cursor;
|
|
69
|
+
}
|
|
70
|
+
console.log(`Messages fetched: ${messages.length} in ${pages} page${pages === 1 ? "" : "s"}`);
|
|
71
|
+
|
|
72
|
+
// --- Group exactly like the deployed exportTXT: plain object, waId key ---
|
|
73
|
+
const grouped = {};
|
|
74
|
+
for (const row of messages) {
|
|
75
|
+
const whatsappId = row.whatsapp_id ?? null;
|
|
76
|
+
const waId = (whatsappId || "unknown").replace(/[+ ]/g, "");
|
|
77
|
+
if (!grouped[waId]) grouped[waId] = [];
|
|
78
|
+
grouped[waId].push(row);
|
|
79
|
+
}
|
|
80
|
+
const uniqueContacts = Object.keys(grouped).length;
|
|
81
|
+
|
|
82
|
+
// --- Resolve output path ---
|
|
83
|
+
let outPath = opts.out;
|
|
84
|
+
if (!outPath) {
|
|
85
|
+
const dir = path.resolve(process.cwd(), ".flowiq", "exports");
|
|
86
|
+
await fsp.mkdir(dir, { recursive: true });
|
|
87
|
+
outPath = path.join(dir, `${slugify(organizationName, orgId)}-chats.txt`);
|
|
88
|
+
} else {
|
|
89
|
+
outPath = path.resolve(outPath);
|
|
90
|
+
await fsp.mkdir(path.dirname(outPath), { recursive: true });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// --- Write the TXT (streamed; content identical to the deployed string build) ---
|
|
94
|
+
const out = fs.createWriteStream(outPath, { encoding: "utf8" });
|
|
95
|
+
const write = (s) =>
|
|
96
|
+
new Promise((resolve, reject) => {
|
|
97
|
+
if (out.write(s)) resolve();
|
|
98
|
+
else out.once("drain", resolve);
|
|
99
|
+
out.once("error", reject);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
let header = `${organizationName || "Organization"} — Chat Export\n`;
|
|
103
|
+
header += `Total messages: ${messages.length.toLocaleString()} | Contacts: ${uniqueContacts}\n`;
|
|
104
|
+
header += `Exported: ${new Date().toLocaleString("en-ZA", { timeZone: "Africa/Johannesburg" })}\n`;
|
|
105
|
+
header += `${"=".repeat(60)}\n\n`;
|
|
106
|
+
await write(header);
|
|
107
|
+
|
|
108
|
+
for (const [waId, msgs] of Object.entries(grouped)) {
|
|
109
|
+
let chunk = `--- ${waId} ---\n`;
|
|
110
|
+
for (const msg of msgs) {
|
|
111
|
+
const date = msg.created_at
|
|
112
|
+
? new Date(msg.created_at).toLocaleString("en-ZA", { timeZone: "Africa/Johannesburg" })
|
|
113
|
+
: "unknown";
|
|
114
|
+
const label = getSenderLabel(msg.sender_type || "");
|
|
115
|
+
let line = `[${date}] (${label}) ${msg.message || ""}`;
|
|
116
|
+
if (msg.media_url) line += ` [Media: ${msg.media_type || "file"} - ${msg.media_url}]`;
|
|
117
|
+
if (msg.voice_note_transcription) line += ` [Voice: ${msg.voice_note_transcription}]`;
|
|
118
|
+
chunk += `${line}\n`;
|
|
119
|
+
}
|
|
120
|
+
chunk += `\n`;
|
|
121
|
+
await write(chunk);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
await new Promise((resolve, reject) => out.end((err) => (err ? reject(err) : resolve())));
|
|
125
|
+
|
|
126
|
+
const secs = ((Date.now() - t0) / 1000).toFixed(1);
|
|
127
|
+
console.log(`\nDone in ${secs}s`);
|
|
128
|
+
console.log(` total messages: ${messages.length.toLocaleString()}`);
|
|
129
|
+
console.log(` groups: ${uniqueContacts}`);
|
|
130
|
+
console.log(` file: ${outPath}`);
|
|
131
|
+
}
|
|
@@ -53,14 +53,14 @@ 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);
|
|
@@ -75,7 +75,7 @@ export async function pull(orgId) {
|
|
|
75
75
|
const stats = countEntries(resp.fine_tuning_entries);
|
|
76
76
|
console.log(`${overwriting ? "Overwrote" : "Wrote"} ${filePath}`);
|
|
77
77
|
console.log(` org: ${resp.organization_name} (${resp.organization_id})`);
|
|
78
|
-
console.log(` agent: ${resp.agent_name} (${resp.agent_id})`);
|
|
78
|
+
console.log(` agent: ${resp.agent_name} (${resp.agent_id})${resp.is_active === false ? " [NON-active]" : ""}`);
|
|
79
79
|
console.log(` instructions: ${resp.fine_tuning.length} chars`);
|
|
80
80
|
console.log(` entries: ${stats.total} (${stats.pending} pending, ${stats.resolved} resolved)`);
|
|
81
81
|
if (stats.total > 0) {
|
|
@@ -118,6 +118,7 @@ export async function push(identifier) {
|
|
|
118
118
|
organization_id: data.organization_id,
|
|
119
119
|
fine_tuning: data.fine_tuning,
|
|
120
120
|
fine_tuning_entries: data.fine_tuning_entries ?? [],
|
|
121
|
+
agent_id: data.agent_id, // push back to the agent this file was pulled from
|
|
121
122
|
});
|
|
122
123
|
} catch (e) {
|
|
123
124
|
console.error(`Push failed: ${e.message}`);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// `flowiq org info <organization_id>` — read-only org summary (platform detect
|
|
2
|
+
// + active agent) for the prompt-builder skill's Step 0. Raw store/Meta creds
|
|
3
|
+
// are stripped server-side; this only prints derived booleans + public urls.
|
|
4
|
+
|
|
5
|
+
import { http } from "../http.js";
|
|
6
|
+
|
|
7
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
8
|
+
|
|
9
|
+
export async function info(orgId, opts = {}) {
|
|
10
|
+
if (!UUID_RE.test(orgId)) {
|
|
11
|
+
console.error(`Error: "${orgId}" is not a valid organization UUID.`);
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
let resp;
|
|
15
|
+
try {
|
|
16
|
+
resp = await http.get("org", { organization_id: orgId });
|
|
17
|
+
} catch (e) {
|
|
18
|
+
console.error(`Lookup failed: ${e.message}`);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (opts.json) {
|
|
23
|
+
console.log(JSON.stringify(resp, null, 2));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
console.log(`${resp.name} (${resp.id})`);
|
|
28
|
+
console.log(` platform: ${resp.platform || "(neither shopify nor woo)"}${resp.ip_whitelist ? " [ip_whitelist]" : ""}`);
|
|
29
|
+
if (resp.shopify_domain) console.log(` shopify: ${resp.shopify_domain}`);
|
|
30
|
+
if (resp.woo_store_url) console.log(` woo store: ${resp.woo_store_url}`);
|
|
31
|
+
console.log(` provider: ${resp.provider || "(unset → meta)"}`);
|
|
32
|
+
console.log(` active agent: ${resp.active_whatsapp_agent || "(NONE — set one before building a prompt)"}`);
|
|
33
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// `flowiq pinboard pull <task_id>` / `push <slug>` / `list` / `list-remote [status]`.
|
|
2
|
+
// Round-trips a single Pin Board task (flowapt_tasks row) via /cli/pinboard.
|
|
3
|
+
// No DB credentials on disk — all work happens server-side.
|
|
4
|
+
|
|
5
|
+
import fs from "node:fs/promises";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { http } from "../http.js";
|
|
8
|
+
|
|
9
|
+
const PIN_DIR = path.resolve(process.cwd(), ".flowiq", "pinboard");
|
|
10
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
11
|
+
const VALID_STATUS = ["open", "in_progress", "done"];
|
|
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).slice(0, 60);
|
|
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(PIN_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(taskId) {
|
|
44
|
+
if (!UUID_RE.test(taskId)) {
|
|
45
|
+
console.error(`Error: "${taskId}" is not a valid task UUID.`);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
let resp;
|
|
49
|
+
try {
|
|
50
|
+
resp = await http.get("pinboard", { task_id: taskId });
|
|
51
|
+
} catch (e) {
|
|
52
|
+
console.error(`Pull failed: ${e.message}`);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
await fs.mkdir(PIN_DIR, { recursive: true });
|
|
57
|
+
const slug = slugify(resp.name, resp.id);
|
|
58
|
+
const filePath = path.join(PIN_DIR, `${slug}.json`);
|
|
59
|
+
const overwriting = await fileExists(filePath);
|
|
60
|
+
await fs.writeFile(filePath, JSON.stringify(resp, null, 2) + "\n", "utf8");
|
|
61
|
+
|
|
62
|
+
const orgs = resp.organizations_resolved || [];
|
|
63
|
+
console.log(`${overwriting ? "Overwrote" : "Wrote"} ${filePath}`);
|
|
64
|
+
console.log(` id: ${resp.id}`);
|
|
65
|
+
console.log(` name: ${resp.name}`);
|
|
66
|
+
console.log(` status: ${resp.status} / ${resp.urgency}`);
|
|
67
|
+
console.log(` due: ${resp.preferred_due_date || "(none)"}`);
|
|
68
|
+
console.log(` orgs: ${(resp.organizations || []).length}${orgs.length ? " (" + orgs.map((o) => o.name || o.id.slice(0, 8)).join(", ") + ")" : ""}`);
|
|
69
|
+
console.log(` media: ${(resp.media || []).length}`);
|
|
70
|
+
console.log(` messages: ${(resp.messages || []).length}`);
|
|
71
|
+
console.log(` created: ${resp.created_by} @ ${resp.created_at}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function push(identifier) {
|
|
75
|
+
let filePath;
|
|
76
|
+
try {
|
|
77
|
+
filePath = await resolvePushPath(identifier);
|
|
78
|
+
} catch (e) {
|
|
79
|
+
console.error(`Error: ${e.message}`);
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
const raw = await fs.readFile(filePath, "utf8");
|
|
83
|
+
let data;
|
|
84
|
+
try { data = JSON.parse(raw); }
|
|
85
|
+
catch (e) {
|
|
86
|
+
console.error(`Invalid JSON in ${filePath}: ${e.message}`);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
if (!data.id || !UUID_RE.test(data.id)) {
|
|
90
|
+
console.error(`${filePath}: id missing or invalid`);
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let resp;
|
|
95
|
+
try {
|
|
96
|
+
resp = await http.post("pinboard", {
|
|
97
|
+
id: data.id,
|
|
98
|
+
name: data.name,
|
|
99
|
+
description: data.description ?? null,
|
|
100
|
+
status: data.status,
|
|
101
|
+
urgency: data.urgency,
|
|
102
|
+
preferred_due_date: data.preferred_due_date ?? null,
|
|
103
|
+
organizations: data.organizations ?? [],
|
|
104
|
+
media: data.media ?? [],
|
|
105
|
+
messages: data.messages ?? [],
|
|
106
|
+
created_by: data.created_by,
|
|
107
|
+
});
|
|
108
|
+
} catch (e) {
|
|
109
|
+
console.error(`Push failed: ${e.message}`);
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
console.log(`Pushed ${filePath}`);
|
|
114
|
+
console.log(` id: ${resp.id}`);
|
|
115
|
+
console.log(` name: ${resp.name}`);
|
|
116
|
+
console.log(` status: ${resp.status} / ${resp.urgency}`);
|
|
117
|
+
console.log(` due: ${resp.preferred_due_date || "(none)"}`);
|
|
118
|
+
console.log(` orgs: ${resp.orgs}`);
|
|
119
|
+
console.log(` media: ${resp.media}`);
|
|
120
|
+
console.log(` messages: ${resp.messages}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export async function list() {
|
|
124
|
+
let files;
|
|
125
|
+
try {
|
|
126
|
+
files = (await fs.readdir(PIN_DIR)).filter((f) => f.endsWith(".json"));
|
|
127
|
+
} catch {
|
|
128
|
+
console.log("(no local task files; run `pinboard pull <task_id>`, or `pinboard list-remote` to see ids)");
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (!files.length) {
|
|
132
|
+
console.log("(no local task files; run `pinboard pull <task_id>`, or `pinboard list-remote` to see ids)");
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
files.sort();
|
|
136
|
+
for (const f of files) {
|
|
137
|
+
const fp = path.join(PIN_DIR, f);
|
|
138
|
+
try {
|
|
139
|
+
const d = JSON.parse(await fs.readFile(fp, "utf8"));
|
|
140
|
+
console.log(f);
|
|
141
|
+
console.log(` id: ${d.id || "?"}`);
|
|
142
|
+
console.log(` name: ${d.name || "(unknown)"}`);
|
|
143
|
+
console.log(` status: ${d.status || "?"} / ${d.urgency || "?"}`);
|
|
144
|
+
console.log(` msgs: ${Array.isArray(d.messages) ? d.messages.length : "?"}`);
|
|
145
|
+
console.log(` pulled: ${d.pulled_at || "(unknown)"}`);
|
|
146
|
+
} catch (e) {
|
|
147
|
+
console.log(`${f} [invalid JSON: ${e.message}]`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export async function listRemote(statusFilter) {
|
|
153
|
+
if (statusFilter && !VALID_STATUS.includes(statusFilter)) {
|
|
154
|
+
console.error(`Invalid status filter "${statusFilter}". Use one of ${VALID_STATUS.join(" | ")}.`);
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
let resp;
|
|
158
|
+
try {
|
|
159
|
+
resp = await http.get("pinboard", { list: "1", status: statusFilter });
|
|
160
|
+
} catch (e) {
|
|
161
|
+
console.error(`List failed: ${e.message}`);
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
const tasks = resp.tasks || [];
|
|
165
|
+
if (!tasks.length) {
|
|
166
|
+
console.log(statusFilter ? `(no tasks with status=${statusFilter})` : "(no tasks in flowapt_tasks)");
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
console.log(`Found ${tasks.length} task${tasks.length === 1 ? "" : "s"}${statusFilter ? ` (status=${statusFilter})` : ""}:\n`);
|
|
170
|
+
for (const t of tasks) {
|
|
171
|
+
const updated = t.updated_at ? new Date(t.updated_at).toISOString().replace("T", " ").slice(0, 16) : "?";
|
|
172
|
+
console.log(` ${t.id} [${String(t.status).padEnd(11)}] [${String(t.urgency).padEnd(8)}] ${updated} ${t.created_by} — ${t.name}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
package/src/commands/prompts.js
CHANGED
|
@@ -40,14 +40,14 @@ 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);
|
|
@@ -62,7 +62,7 @@ export async function pull(orgId) {
|
|
|
62
62
|
const totalChars = resp.prompt_sections.reduce((a, s) => a + (s.content?.length || 0), 0);
|
|
63
63
|
console.log(`${overwriting ? "Overwrote" : "Wrote"} ${filePath}`);
|
|
64
64
|
console.log(` org: ${resp.organization_name} (${resp.organization_id})`);
|
|
65
|
-
console.log(` agent: ${resp.agent_name} (${resp.agent_id})`);
|
|
65
|
+
console.log(` agent: ${resp.agent_name} (${resp.agent_id})${resp.is_active === false ? " [NON-active]" : ""}`);
|
|
66
66
|
console.log(` sections: ${resp.prompt_sections.length}`);
|
|
67
67
|
console.log(` content: ${totalChars.toLocaleString()} chars`);
|
|
68
68
|
if (resp.bootstrapped) {
|
|
@@ -105,6 +105,7 @@ export async function push(identifier) {
|
|
|
105
105
|
resp = await http.post("prompts", {
|
|
106
106
|
organization_id: data.organization_id,
|
|
107
107
|
prompt_sections: data.prompt_sections,
|
|
108
|
+
agent_id: data.agent_id, // push back to the agent this file was pulled from
|
|
108
109
|
});
|
|
109
110
|
} catch (e) {
|
|
110
111
|
console.error(`Push failed: ${e.message}`);
|
|
@@ -53,14 +53,14 @@ 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);
|
|
@@ -75,7 +75,7 @@ export async function pull(orgId) {
|
|
|
75
75
|
const { total, answered } = countQuestions(resp.questionnaire?.sections);
|
|
76
76
|
console.log(`${overwriting ? "Overwrote" : "Wrote"} ${filePath}`);
|
|
77
77
|
console.log(` org: ${resp.organization_name} (${resp.organization_id})`);
|
|
78
|
-
console.log(` agent: ${resp.agent_name} (${resp.agent_id})`);
|
|
78
|
+
console.log(` agent: ${resp.agent_name} (${resp.agent_id})${resp.is_active === false ? " [NON-active]" : ""}`);
|
|
79
79
|
console.log(` sections: ${resp.questionnaire?.sections?.length ?? 0}`);
|
|
80
80
|
console.log(` questions: ${answered}/${total} answered`);
|
|
81
81
|
if (resp.default_used) {
|
|
@@ -114,6 +114,7 @@ export async function push(identifier) {
|
|
|
114
114
|
resp = await http.post("questionnaires", {
|
|
115
115
|
organization_id: data.organization_id,
|
|
116
116
|
questionnaire: data.questionnaire,
|
|
117
|
+
agent_id: data.agent_id, // push back to the agent this file was pulled from
|
|
117
118
|
});
|
|
118
119
|
} catch (e) {
|
|
119
120
|
console.error(`Push failed: ${e.message}`);
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// `flowiq templates pull <org_id>` / `list` — read WhatsApp templates from Meta.
|
|
2
|
+
// `flowiq templates create <org> --request-file f.json` / `status` — §12 (create).
|
|
3
|
+
// Pull is read-only via /cli/templates; create proxies /cli/meta-templates.
|
|
4
|
+
|
|
5
|
+
import fs from "node:fs/promises";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { http } from "../http.js";
|
|
8
|
+
|
|
9
|
+
const TPL_DIR = path.resolve(process.cwd(), ".flowiq", "templates");
|
|
10
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
11
|
+
|
|
12
|
+
function slugify(name, fallback) {
|
|
13
|
+
const s = String(name || "")
|
|
14
|
+
.toLowerCase()
|
|
15
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
16
|
+
.replace(/-+/g, "-")
|
|
17
|
+
.replace(/^-|-$/g, "");
|
|
18
|
+
return s || fallback;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function fileExists(p) {
|
|
22
|
+
try { await fs.access(p); return true; } catch { return false; }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function pull(orgId) {
|
|
26
|
+
if (!UUID_RE.test(orgId)) {
|
|
27
|
+
console.error(`Error: "${orgId}" is not a valid organization UUID.`);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
let resp;
|
|
31
|
+
try {
|
|
32
|
+
resp = await http.get("templates", { organization_id: orgId });
|
|
33
|
+
} catch (e) {
|
|
34
|
+
console.error(`Pull failed: ${e.message}`);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
await fs.mkdir(TPL_DIR, { recursive: true });
|
|
39
|
+
const slug = slugify(resp.organization_name, resp.organization_id);
|
|
40
|
+
const filePath = path.join(TPL_DIR, `${slug}.json`);
|
|
41
|
+
const overwriting = await fileExists(filePath);
|
|
42
|
+
await fs.writeFile(filePath, JSON.stringify(resp, null, 2) + "\n", "utf8");
|
|
43
|
+
|
|
44
|
+
console.log(`${overwriting ? "Overwrote" : "Wrote"} ${filePath}`);
|
|
45
|
+
console.log(` org: ${resp.organization_name} (${resp.organization_id})`);
|
|
46
|
+
console.log(` waba: ${resp.waba_id}`);
|
|
47
|
+
if (resp.meta_phone_number) console.log(` phone: ${resp.meta_phone_number}`);
|
|
48
|
+
console.log(` templates: ${resp.template_count}`);
|
|
49
|
+
console.log(` status: ${JSON.stringify(resp.summary?.byStatus || {})}`);
|
|
50
|
+
console.log(` category: ${JSON.stringify(resp.summary?.byCategory || {})}`);
|
|
51
|
+
console.log(` language: ${JSON.stringify(resp.summary?.byLanguage || {})}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function create(orgId, opts = {}) {
|
|
55
|
+
if (!opts.requestFile) {
|
|
56
|
+
console.error("Error: --request-file <path> is required (JSON with template_request, optional template_data/media_header/cards_media).");
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
let filePath = opts.requestFile;
|
|
60
|
+
if (!path.isAbsolute(filePath)) filePath = path.resolve(filePath);
|
|
61
|
+
let data;
|
|
62
|
+
try {
|
|
63
|
+
data = JSON.parse(await fs.readFile(filePath, "utf8"));
|
|
64
|
+
} catch (e) {
|
|
65
|
+
console.error(`Cannot read request file ${filePath}: ${e.message}`);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// The file may be the full edge-fn body, or just the inner template_request.
|
|
70
|
+
const organizationId = orgId || data.organization_id;
|
|
71
|
+
if (!organizationId || !UUID_RE.test(organizationId)) {
|
|
72
|
+
console.error("Error: organization_id missing/invalid (pass <org_id> or include it in the request file).");
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
const templateRequest = data.template_request || (data.name ? data : null);
|
|
76
|
+
if (!templateRequest) {
|
|
77
|
+
console.error("Error: request file must contain a `template_request` object (or be one, with a `name`).");
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let resp;
|
|
82
|
+
try {
|
|
83
|
+
resp = await http.post("meta-templates", {
|
|
84
|
+
organization_id: organizationId,
|
|
85
|
+
template_request: templateRequest,
|
|
86
|
+
template_data: data.template_data,
|
|
87
|
+
media_header: data.media_header,
|
|
88
|
+
cards_media: data.cards_media,
|
|
89
|
+
});
|
|
90
|
+
} catch (e) {
|
|
91
|
+
console.error(`Create failed: ${e.message}`);
|
|
92
|
+
if (e.body) console.error(JSON.stringify(e.body, null, 2));
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
console.log(`Submitted template "${templateRequest.name}" for org ${organizationId}`);
|
|
97
|
+
console.log(JSON.stringify(resp, null, 2));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function status(orgId, opts = {}) {
|
|
101
|
+
if (!UUID_RE.test(orgId)) {
|
|
102
|
+
console.error(`Error: "${orgId}" is not a valid organization UUID.`);
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
let resp;
|
|
106
|
+
try {
|
|
107
|
+
resp = await http.get("meta-templates", { organization_id: orgId, name: opts.name });
|
|
108
|
+
} catch (e) {
|
|
109
|
+
console.error(`Status lookup failed: ${e.message}`);
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
const rows = resp.templates || [];
|
|
113
|
+
if (!rows.length) {
|
|
114
|
+
console.log(opts.name ? `(no templates matching "${opts.name}")` : "(no templates on record for this org)");
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
console.log(`${rows.length} template row${rows.length === 1 ? "" : "s"}${opts.name ? ` matching "${opts.name}"` : ""}:`);
|
|
118
|
+
for (const t of rows) {
|
|
119
|
+
const name = t.template_name || t.name || "(unnamed)";
|
|
120
|
+
const st = t.status || "(no status)";
|
|
121
|
+
const cat = t.category ? ` [${t.category}]` : "";
|
|
122
|
+
console.log(` ${name.padEnd(40)} ${String(st).padEnd(16)}${cat}`);
|
|
123
|
+
}
|
|
124
|
+
console.log("\nNote: status is captured at submit time (older rows may be null). For the live");
|
|
125
|
+
console.log("Meta-side status + rejection reasons, run `templates pull <org_id>`.");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export async function list() {
|
|
129
|
+
let files;
|
|
130
|
+
try {
|
|
131
|
+
files = (await fs.readdir(TPL_DIR)).filter((f) => f.endsWith(".json"));
|
|
132
|
+
} catch {
|
|
133
|
+
console.log("(no local template files; run `templates pull <org_id>`)");
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (!files.length) {
|
|
137
|
+
console.log("(no local template files; run `templates pull <org_id>`)");
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
files.sort();
|
|
141
|
+
for (const f of files) {
|
|
142
|
+
const fp = path.join(TPL_DIR, f);
|
|
143
|
+
try {
|
|
144
|
+
const d = JSON.parse(await fs.readFile(fp, "utf8"));
|
|
145
|
+
console.log(f);
|
|
146
|
+
console.log(` org: ${d.organization_name ?? "(unknown)"} (${d.organization_id ?? "?"})`);
|
|
147
|
+
console.log(` waba: ${d.waba_id ?? "?"}`);
|
|
148
|
+
console.log(` phone: ${d.meta_phone_number ?? "(none)"}`);
|
|
149
|
+
console.log(` templates: ${d.template_count ?? (Array.isArray(d.templates) ? d.templates.length : "?")}`);
|
|
150
|
+
if (d.summary?.byStatus) console.log(` status: ${JSON.stringify(d.summary.byStatus)}`);
|
|
151
|
+
console.log(` pulled: ${d.pulled_at ?? "(unknown)"}`);
|
|
152
|
+
} catch (e) {
|
|
153
|
+
console.log(`${f} [invalid JSON: ${e.message}]`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|