@flowapt/flowiq-cli 0.1.0
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 +69 -0
- package/bin/flowiq.js +4 -0
- package/package.json +43 -0
- package/src/commands/auth.js +89 -0
- package/src/commands/prompts.js +112 -0
- package/src/config.js +70 -0
- package/src/http.js +57 -0
- package/src/index.js +35 -0
package/README.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# flowiq
|
|
2
|
+
|
|
3
|
+
FlowIQ staff CLI. Round-trips agent prompts, questionnaires, webhooks, and
|
|
4
|
+
other internal config through `https://api.flowiq.live/cli/*` so the Supabase
|
|
5
|
+
service role never leaves the server.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm i -g @flowapt/flowiq-cli
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Authenticate
|
|
14
|
+
|
|
15
|
+
Get a personal staff API key from your FlowIQ admin (an `fiq_staff_…`
|
|
16
|
+
string). Then:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
flowiq auth login
|
|
20
|
+
# paste your key when prompted
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Your key is saved to `~/.config/flowiq/auth.json` with `chmod 600`. To
|
|
24
|
+
verify what you're authenticated as:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
flowiq auth whoami
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
To log out (deletes the local key file, does NOT revoke the key
|
|
31
|
+
server-side — ask your admin to revoke):
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
flowiq auth logout
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Commands
|
|
38
|
+
|
|
39
|
+
### `flowiq prompts pull <organization_id>`
|
|
40
|
+
|
|
41
|
+
Fetches the active WhatsApp agent's `prompt_sections` and writes them to
|
|
42
|
+
`./.flowiq/prompts/<org-slug>.json`.
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
flowiq prompts pull 0689d0be-ba76-43d9-a0d5-f4d59c1db537
|
|
46
|
+
# → wrote .flowiq/prompts/kickz-sa-african-oils.json (18 sections, 18,747 chars)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### `flowiq prompts push <slug-or-path>`
|
|
50
|
+
|
|
51
|
+
Reads the JSON file and sends the `prompt_sections` array back to the
|
|
52
|
+
server, which regenerates the agent's `system_prompt`.
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
flowiq prompts push kickz-sa-african-oils
|
|
56
|
+
# → pushed 18 sections, system_prompt now 17,337 chars
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Environment variables
|
|
60
|
+
|
|
61
|
+
| Var | Default | Use |
|
|
62
|
+
|---|---|---|
|
|
63
|
+
| `FLOWIQ_API_URL` | `https://api.flowiq.live` | Override the API host (e.g. local dev). |
|
|
64
|
+
| `FLOWIQ_TOKEN` | (saved in `~/.config/flowiq/auth.json`) | Override the auth token, useful for CI. |
|
|
65
|
+
|
|
66
|
+
## License
|
|
67
|
+
|
|
68
|
+
UNLICENSED. Internal staff tool — install requires a valid `fiq_staff_…`
|
|
69
|
+
key issued by a FlowIQ super-admin.
|
package/bin/flowiq.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@flowapt/flowiq-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Command-line tool for FlowIQ staff: round-trip agent prompts, questionnaires, webhooks, and more without ever touching service-role credentials.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"flowiq": "bin/flowiq.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"src",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"start": "node bin/flowiq.js"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"commander": "^12.1.0"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://github.com/MattFlowapt/flow-v1/tree/main/cli",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/MattFlowapt/flow-v1.git",
|
|
27
|
+
"directory": "cli"
|
|
28
|
+
},
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/MattFlowapt/flow-v1/issues"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"flowapt",
|
|
34
|
+
"flowiq",
|
|
35
|
+
"cli",
|
|
36
|
+
"whatsapp",
|
|
37
|
+
"internal-tool"
|
|
38
|
+
],
|
|
39
|
+
"license": "UNLICENSED",
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// `flowiq auth login` / `flowiq auth logout` / `flowiq auth whoami`.
|
|
2
|
+
// Stores a `fiq_staff_…` bearer token at ~/.config/flowiq/auth.json.
|
|
3
|
+
|
|
4
|
+
import readline from "node:readline";
|
|
5
|
+
import { loadConfig, saveConfig, clearConfig, configFilePath } from "../config.js";
|
|
6
|
+
import { http } from "../http.js";
|
|
7
|
+
|
|
8
|
+
function prompt(question, { silent = false } = {}) {
|
|
9
|
+
return new Promise((resolve) => {
|
|
10
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
11
|
+
if (silent) {
|
|
12
|
+
// Best-effort masking: turn off terminal echo for the duration of the read.
|
|
13
|
+
const stdin = process.stdin;
|
|
14
|
+
const wasRaw = stdin.isRaw;
|
|
15
|
+
stdin.setRawMode?.(true);
|
|
16
|
+
let buf = "";
|
|
17
|
+
stdin.resume();
|
|
18
|
+
stdin.setEncoding("utf8");
|
|
19
|
+
process.stdout.write(question);
|
|
20
|
+
const onData = (ch) => {
|
|
21
|
+
if (ch === "\r" || ch === "\n" || ch === "") {
|
|
22
|
+
stdin.setRawMode?.(wasRaw);
|
|
23
|
+
stdin.removeListener("data", onData);
|
|
24
|
+
process.stdout.write("\n");
|
|
25
|
+
rl.close();
|
|
26
|
+
resolve(buf);
|
|
27
|
+
} else if (ch === "") {
|
|
28
|
+
process.stdout.write("\n");
|
|
29
|
+
process.exit(130);
|
|
30
|
+
} else if (ch === "" || ch === "\b") {
|
|
31
|
+
if (buf.length) { buf = buf.slice(0, -1); process.stdout.write("\b \b"); }
|
|
32
|
+
} else {
|
|
33
|
+
buf += ch;
|
|
34
|
+
process.stdout.write("*");
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
stdin.on("data", onData);
|
|
38
|
+
} else {
|
|
39
|
+
rl.question(question, (answer) => { rl.close(); resolve(answer); });
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function login(opts = {}) {
|
|
45
|
+
const cfg = await loadConfig();
|
|
46
|
+
if (opts.apiUrl) cfg.api_url = opts.apiUrl;
|
|
47
|
+
|
|
48
|
+
let token = opts.token;
|
|
49
|
+
if (!token) {
|
|
50
|
+
process.stdout.write(`API URL: ${cfg.api_url}\n`);
|
|
51
|
+
token = await prompt("Paste your staff API key (fiq_staff_…): ", { silent: true });
|
|
52
|
+
token = token.trim();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!token.startsWith("fiq_staff_")) {
|
|
56
|
+
console.error(`Error: token must start with "fiq_staff_" (got ${token.slice(0, 12)}…).`);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Save first so http.js can use it, then verify with /cli/whoami.
|
|
61
|
+
const file = await saveConfig({ api_url: cfg.api_url, token });
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const me = await http.get("whoami");
|
|
65
|
+
console.log(`Logged in as ${me.user_email || me.user_id} (super_admin=${me.is_super_admin})`);
|
|
66
|
+
console.log(`Token saved to ${file}`);
|
|
67
|
+
} catch (e) {
|
|
68
|
+
// Roll back the save if verification failed — leaving a bad token on disk
|
|
69
|
+
// is worse than no token at all.
|
|
70
|
+
await clearConfig();
|
|
71
|
+
console.error(`Login failed: ${e.message}`);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function logout() {
|
|
77
|
+
const removed = await clearConfig();
|
|
78
|
+
console.log(removed ? "Logged out." : "Already logged out.");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function whoami() {
|
|
82
|
+
try {
|
|
83
|
+
const me = await http.get("whoami");
|
|
84
|
+
console.log(JSON.stringify(me, null, 2));
|
|
85
|
+
} catch (e) {
|
|
86
|
+
console.error(`whoami failed: ${e.message}`);
|
|
87
|
+
process.exit(e.code === "ENOAUTH" ? 0 : 1);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// `flowiq prompts pull <organization_id>` / `flowiq prompts push <slug>`.
|
|
2
|
+
// Round-trips an org's active WhatsApp agent's prompt_sections through a
|
|
3
|
+
// local JSON file. No DB credentials on disk — all work happens server-side
|
|
4
|
+
// via /cli/prompts on api.flowiq.live.
|
|
5
|
+
|
|
6
|
+
import fs from "node:fs/promises";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { http } from "../http.js";
|
|
9
|
+
|
|
10
|
+
const PROMPTS_DIR = path.resolve(process.cwd(), ".flowiq", "prompts");
|
|
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(PROMPTS_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) {
|
|
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("prompts", { organization_id: orgId });
|
|
51
|
+
} catch (e) {
|
|
52
|
+
console.error(`Pull failed: ${e.message}`);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
await fs.mkdir(PROMPTS_DIR, { recursive: true });
|
|
57
|
+
const slug = slugify(resp.organization_name, resp.organization_id);
|
|
58
|
+
const filePath = path.join(PROMPTS_DIR, `${slug}.json`);
|
|
59
|
+
const overwriting = await fileExists(filePath);
|
|
60
|
+
await fs.writeFile(filePath, JSON.stringify(resp, null, 2) + "\n", "utf8");
|
|
61
|
+
|
|
62
|
+
const totalChars = resp.prompt_sections.reduce((a, s) => a + (s.content?.length || 0), 0);
|
|
63
|
+
console.log(`${overwriting ? "Overwrote" : "Wrote"} ${filePath}`);
|
|
64
|
+
console.log(` org: ${resp.organization_name} (${resp.organization_id})`);
|
|
65
|
+
console.log(` agent: ${resp.agent_name} (${resp.agent_id})`);
|
|
66
|
+
console.log(` sections: ${resp.prompt_sections.length}`);
|
|
67
|
+
console.log(` content: ${totalChars.toLocaleString()} chars`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function push(identifier) {
|
|
71
|
+
let filePath;
|
|
72
|
+
try {
|
|
73
|
+
filePath = await resolvePushPath(identifier);
|
|
74
|
+
} catch (e) {
|
|
75
|
+
console.error(`Error: ${e.message}`);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
const raw = await fs.readFile(filePath, "utf8");
|
|
79
|
+
let data;
|
|
80
|
+
try { data = JSON.parse(raw); }
|
|
81
|
+
catch (e) {
|
|
82
|
+
console.error(`Invalid JSON in ${filePath}: ${e.message}`);
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!data.organization_id || !UUID_RE.test(data.organization_id)) {
|
|
87
|
+
console.error(`${filePath}: organization_id missing or invalid`);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
if (!Array.isArray(data.prompt_sections) || data.prompt_sections.length === 0) {
|
|
91
|
+
console.error(`${filePath}: prompt_sections must be a non-empty array`);
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let resp;
|
|
96
|
+
try {
|
|
97
|
+
resp = await http.post("prompts", {
|
|
98
|
+
organization_id: data.organization_id,
|
|
99
|
+
prompt_sections: data.prompt_sections,
|
|
100
|
+
});
|
|
101
|
+
} catch (e) {
|
|
102
|
+
console.error(`Push failed: ${e.message}`);
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
console.log(`Pushed ${filePath}`);
|
|
107
|
+
console.log(` org: ${resp.organization_name} (${resp.organization_id})`);
|
|
108
|
+
console.log(` agent: ${resp.agent_name} (${resp.agent_id})`);
|
|
109
|
+
console.log(` sections: ${resp.sections}`);
|
|
110
|
+
console.log(` content: ${resp.total_chars.toLocaleString()} chars`);
|
|
111
|
+
console.log(` prompt now: ${resp.system_prompt_chars.toLocaleString()} chars`);
|
|
112
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Token + base-URL config for the flowiq CLI.
|
|
2
|
+
//
|
|
3
|
+
// Storage: ~/.config/flowiq/auth.json, chmod 600 (mode 0600). One file holds:
|
|
4
|
+
// { api_url: "https://api.flowiq.live", token: "fiq_staff_..." }
|
|
5
|
+
//
|
|
6
|
+
// Env-var overrides (handy for CI or local dev):
|
|
7
|
+
// FLOWIQ_API_URL → base URL (default https://api.flowiq.live)
|
|
8
|
+
// FLOWIQ_TOKEN → bearer token (takes precedence over the saved file)
|
|
9
|
+
|
|
10
|
+
import fs from "node:fs/promises";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import os from "node:os";
|
|
13
|
+
|
|
14
|
+
const DEFAULT_API_URL = "https://api.flowiq.live";
|
|
15
|
+
|
|
16
|
+
function configDir() {
|
|
17
|
+
// Honour XDG_CONFIG_HOME if set; else ~/.config (Linux/Mac default).
|
|
18
|
+
const xdg = process.env.XDG_CONFIG_HOME;
|
|
19
|
+
const base = xdg && xdg.trim() ? xdg : path.join(os.homedir(), ".config");
|
|
20
|
+
return path.join(base, "flowiq");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function configFile() {
|
|
24
|
+
return path.join(configDir(), "auth.json");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function readFile() {
|
|
28
|
+
try {
|
|
29
|
+
const raw = await fs.readFile(configFile(), "utf8");
|
|
30
|
+
return JSON.parse(raw);
|
|
31
|
+
} catch (e) {
|
|
32
|
+
if (e.code === "ENOENT") return null;
|
|
33
|
+
throw e;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function loadConfig() {
|
|
38
|
+
const saved = (await readFile()) || {};
|
|
39
|
+
const envToken = process.env.FLOWIQ_TOKEN?.trim();
|
|
40
|
+
const envApi = process.env.FLOWIQ_API_URL?.trim();
|
|
41
|
+
return {
|
|
42
|
+
api_url: envApi || saved.api_url || DEFAULT_API_URL,
|
|
43
|
+
token: envToken || saved.token || null,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function saveConfig({ api_url, token }) {
|
|
48
|
+
const dir = configDir();
|
|
49
|
+
await fs.mkdir(dir, { recursive: true, mode: 0o700 });
|
|
50
|
+
const file = configFile();
|
|
51
|
+
const payload = JSON.stringify({ api_url, token }, null, 2) + "\n";
|
|
52
|
+
await fs.writeFile(file, payload, { encoding: "utf8", mode: 0o600 });
|
|
53
|
+
// Defensive chmod for hosts where the open() mode was honoured loosely.
|
|
54
|
+
try { await fs.chmod(file, 0o600); } catch { /* best-effort */ }
|
|
55
|
+
return file;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function clearConfig() {
|
|
59
|
+
try {
|
|
60
|
+
await fs.unlink(configFile());
|
|
61
|
+
return true;
|
|
62
|
+
} catch (e) {
|
|
63
|
+
if (e.code === "ENOENT") return false;
|
|
64
|
+
throw e;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function configFilePath() {
|
|
69
|
+
return configFile();
|
|
70
|
+
}
|
package/src/http.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Tiny fetch wrapper for the flowiq CLI. All requests:
|
|
2
|
+
// - go to ${config.api_url}/cli/<endpoint>
|
|
3
|
+
// - carry Authorization: Bearer <token>
|
|
4
|
+
// - parse JSON, surfacing { error, message } shape with a readable error.
|
|
5
|
+
|
|
6
|
+
import { loadConfig } from "./config.js";
|
|
7
|
+
|
|
8
|
+
async function call(method, endpoint, { query, body } = {}) {
|
|
9
|
+
const cfg = await loadConfig();
|
|
10
|
+
if (!cfg.token) {
|
|
11
|
+
const err = new Error(
|
|
12
|
+
"Not authenticated. Run `flowiq auth login` and paste your staff API key."
|
|
13
|
+
);
|
|
14
|
+
err.code = "ENOAUTH";
|
|
15
|
+
throw err;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const qs = query
|
|
19
|
+
? "?" + new URLSearchParams(
|
|
20
|
+
Object.fromEntries(Object.entries(query).filter(([, v]) => v != null))
|
|
21
|
+
).toString()
|
|
22
|
+
: "";
|
|
23
|
+
const url = `${cfg.api_url.replace(/\/$/, "")}/cli/${endpoint}${qs}`;
|
|
24
|
+
|
|
25
|
+
const init = {
|
|
26
|
+
method,
|
|
27
|
+
headers: {
|
|
28
|
+
Authorization: `Bearer ${cfg.token}`,
|
|
29
|
+
Accept: "application/json",
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
if (body !== undefined) {
|
|
33
|
+
init.headers["Content-Type"] = "application/json";
|
|
34
|
+
init.body = JSON.stringify(body);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const resp = await fetch(url, init);
|
|
38
|
+
const text = await resp.text();
|
|
39
|
+
let parsed;
|
|
40
|
+
try { parsed = text ? JSON.parse(text) : {}; } catch { parsed = { _raw: text }; }
|
|
41
|
+
if (!resp.ok) {
|
|
42
|
+
const err = new Error(
|
|
43
|
+
parsed.message || parsed.error || `${method} ${endpoint} → HTTP ${resp.status}`
|
|
44
|
+
);
|
|
45
|
+
err.status = resp.status;
|
|
46
|
+
err.body = parsed;
|
|
47
|
+
throw err;
|
|
48
|
+
}
|
|
49
|
+
return parsed;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const http = {
|
|
53
|
+
get: (endpoint, query) => call("GET", endpoint, { query }),
|
|
54
|
+
post: (endpoint, body) => call("POST", endpoint, { body }),
|
|
55
|
+
put: (endpoint, body) => call("PUT", endpoint, { body }),
|
|
56
|
+
del: (endpoint, body) => call("DELETE", endpoint, { body }),
|
|
57
|
+
};
|
package/src/index.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// flowiq — staff CLI entry point.
|
|
2
|
+
// commander dispatches to src/commands/* based on argv.
|
|
3
|
+
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import * as authCmd from "./commands/auth.js";
|
|
6
|
+
import * as promptsCmd from "./commands/prompts.js";
|
|
7
|
+
|
|
8
|
+
export function run(argv) {
|
|
9
|
+
const program = new Command();
|
|
10
|
+
program
|
|
11
|
+
.name("flowiq")
|
|
12
|
+
.description("FlowIQ staff CLI — round-trip agent prompts, webhooks, and more, without service-role credentials.")
|
|
13
|
+
.version("0.1.0");
|
|
14
|
+
|
|
15
|
+
// auth
|
|
16
|
+
const auth = program.command("auth").description("Manage CLI authentication");
|
|
17
|
+
auth.command("login")
|
|
18
|
+
.description("Save a staff API key (fiq_staff_…)")
|
|
19
|
+
.option("--token <token>", "supply the token via flag instead of stdin")
|
|
20
|
+
.option("--api-url <url>", "override the API host (default https://api.flowiq.live)")
|
|
21
|
+
.action((opts) => authCmd.login(opts));
|
|
22
|
+
auth.command("logout").description("Remove the saved token").action(() => authCmd.logout());
|
|
23
|
+
auth.command("whoami").description("Show the user behind the saved token").action(() => authCmd.whoami());
|
|
24
|
+
|
|
25
|
+
// prompts
|
|
26
|
+
const prompts = program.command("prompts").description("Round-trip an org's active WhatsApp agent system prompt");
|
|
27
|
+
prompts.command("pull <organization_id>")
|
|
28
|
+
.description("Fetch the active agent's prompt_sections into a local JSON file")
|
|
29
|
+
.action((orgId) => promptsCmd.pull(orgId));
|
|
30
|
+
prompts.command("push <slug-or-path>")
|
|
31
|
+
.description("Apply a local JSON file's prompt_sections to the agent (regenerates system_prompt server-side)")
|
|
32
|
+
.action((id) => promptsCmd.push(id));
|
|
33
|
+
|
|
34
|
+
program.parseAsync(argv);
|
|
35
|
+
}
|