@flowapt/flowiq-cli 0.1.9 → 0.1.11
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 +42 -4
- package/package.json +1 -1
- package/src/commands/auth.js +152 -17
- package/src/commands/knowledge.js +156 -0
- package/src/index.js +22 -2
package/README.md
CHANGED
|
@@ -16,12 +16,24 @@ flowiq --version # should print 0.1.x
|
|
|
16
16
|
|
|
17
17
|
## Authenticate
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
string). Then:
|
|
19
|
+
Default flow — browser login (like `npm login`):
|
|
21
20
|
|
|
22
21
|
```bash
|
|
23
22
|
flowiq auth login
|
|
24
|
-
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The CLI shows a short code (e.g. `FQ4X-9KTB`), opens
|
|
26
|
+
`https://app.flowiq.live/cli-auth` when you press ENTER, and waits. A
|
|
27
|
+
logged-in FlowIQ **super admin** confirms the code on the page matches the
|
|
28
|
+
one in your terminal and clicks Approve — the CLI then receives a
|
|
29
|
+
freshly-minted personal key automatically. **Never approve a code you
|
|
30
|
+
didn't just see in your own terminal.**
|
|
31
|
+
|
|
32
|
+
Fallbacks (CI / no browser):
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
flowiq auth login --token fiq_staff_… # supply a key directly
|
|
36
|
+
flowiq auth login --paste # prompt for a key on stdin
|
|
25
37
|
```
|
|
26
38
|
|
|
27
39
|
Your key is saved to `~/.config/flowiq/auth.json` with `chmod 600`. To
|
|
@@ -31,8 +43,16 @@ verify what you're authenticated as:
|
|
|
31
43
|
flowiq auth whoami
|
|
32
44
|
```
|
|
33
45
|
|
|
46
|
+
To rotate this device's token (mints a fresh key, revokes the old one
|
|
47
|
+
server-side — run whenever you want a new secret):
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
flowiq auth refresh
|
|
51
|
+
```
|
|
52
|
+
|
|
34
53
|
To log out (deletes the local key file, does NOT revoke the key
|
|
35
|
-
server-side —
|
|
54
|
+
server-side — use `flowiq auth refresh` first if you want the old key
|
|
55
|
+
dead, or ask another super admin to revoke):
|
|
36
56
|
|
|
37
57
|
```bash
|
|
38
58
|
flowiq auth logout
|
|
@@ -99,6 +119,24 @@ Shipping / Returns / Promotions / Technical Support / Tone Adjustment / Other),
|
|
|
99
119
|
`suggested_tone` (Friendly / Professional / Encouraging / Brief / Detailed),
|
|
100
120
|
`status` (Pending / Resolved), `messages: []`, `media: []`.
|
|
101
121
|
|
|
122
|
+
### Knowledge / text sources — `flowiq knowledge pull|push|list` (alias `kn`)
|
|
123
|
+
|
|
124
|
+
Round-trips an agent's **text knowledge sources** — the `{ code, text }` entries
|
|
125
|
+
in `agents.additional_config` that the agent's `get_more_answers` tool retrieves
|
|
126
|
+
on demand (brand references + tethered playbooks; the "Knowledge Sources → Text"
|
|
127
|
+
tab in the UI).
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
flowiq knowledge pull <org_id> # → ./.flowiq/knowledge/<slug>.json (sources[] = {code,text})
|
|
131
|
+
flowiq knowledge pull <org_id> --agent <id> # a specific (non-active) agent
|
|
132
|
+
flowiq knowledge push <slug> # FULL-REPLACE sources[] back onto the agent
|
|
133
|
+
flowiq knowledge list
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
- 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).
|
|
137
|
+
- **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).
|
|
138
|
+
- `--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.
|
|
139
|
+
|
|
102
140
|
### Messages — `flowiq messages pull <contact_id>` (alias `m`)
|
|
103
141
|
|
|
104
142
|
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.
|
|
3
|
+
"version": "0.1.11",
|
|
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": {
|
package/src/commands/auth.js
CHANGED
|
@@ -1,8 +1,23 @@
|
|
|
1
|
-
// `flowiq auth login` / `
|
|
2
|
-
//
|
|
1
|
+
// `flowiq auth login` / `logout` / `whoami` / `refresh`.
|
|
2
|
+
//
|
|
3
|
+
// Default login = browser device flow (like `npm login` / `gh auth login`):
|
|
4
|
+
// 1. POST /cli/login-start (unauthenticated) → device_code + user_code
|
|
5
|
+
// 2. Show the user code, press ENTER → open https://app.flowiq.live/cli-auth
|
|
6
|
+
// 3. A logged-in SUPER ADMIN confirms the codes match and approves
|
|
7
|
+
// 4. Poll /cli/login-poll until the key is minted server-side (mint-at-claim)
|
|
8
|
+
//
|
|
9
|
+
// Fallbacks: `--token <key>` (direct) or `--paste` (interactive paste) keep
|
|
10
|
+
// the original flow for CI or when no browser is handy.
|
|
11
|
+
//
|
|
12
|
+
// `flowiq auth refresh` rotates the saved key in place via /cli/token-rotate —
|
|
13
|
+
// run it on each device whenever you want a fresh secret; the old key is
|
|
14
|
+
// revoked server-side. If the saved key is already dead it falls back to the
|
|
15
|
+
// full browser login.
|
|
3
16
|
|
|
4
17
|
import readline from "node:readline";
|
|
5
|
-
import
|
|
18
|
+
import os from "node:os";
|
|
19
|
+
import { spawn } from "node:child_process";
|
|
20
|
+
import { loadConfig, saveConfig, clearConfig } from "../config.js";
|
|
6
21
|
import { http } from "../http.js";
|
|
7
22
|
|
|
8
23
|
function prompt(question, { silent = false } = {}) {
|
|
@@ -41,25 +56,42 @@ function prompt(question, { silent = false } = {}) {
|
|
|
41
56
|
});
|
|
42
57
|
}
|
|
43
58
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
59
|
+
// Unauthenticated POST — login-start / login-poll run BEFORE a token exists,
|
|
60
|
+
// so they can't go through http.js (which requires the saved token).
|
|
61
|
+
async function unauthedPost(apiUrl, endpoint, body) {
|
|
62
|
+
const url = `${apiUrl.replace(/\/$/, "")}/cli/${endpoint}`;
|
|
63
|
+
const resp = await fetch(url, {
|
|
64
|
+
method: "POST",
|
|
65
|
+
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
66
|
+
body: JSON.stringify(body ?? {}),
|
|
67
|
+
});
|
|
68
|
+
const text = await resp.text();
|
|
69
|
+
let parsed;
|
|
70
|
+
try { parsed = text ? JSON.parse(text) : {}; } catch { parsed = { _raw: text }; }
|
|
71
|
+
if (!resp.ok) {
|
|
72
|
+
throw new Error(parsed.message || parsed.error || `${endpoint} → HTTP ${resp.status}`);
|
|
53
73
|
}
|
|
74
|
+
return parsed;
|
|
75
|
+
}
|
|
54
76
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
77
|
+
function openBrowser(url) {
|
|
78
|
+
const platform = process.platform;
|
|
79
|
+
let cmd, args;
|
|
80
|
+
if (platform === "darwin") { cmd = "open"; args = [url]; }
|
|
81
|
+
else if (platform === "win32") { cmd = "cmd"; args = ["/c", "start", "", url]; }
|
|
82
|
+
else { cmd = "xdg-open"; args = [url]; }
|
|
83
|
+
try {
|
|
84
|
+
spawn(cmd, args, { stdio: "ignore", detached: true }).unref();
|
|
85
|
+
return true;
|
|
86
|
+
} catch {
|
|
87
|
+
return false;
|
|
58
88
|
}
|
|
89
|
+
}
|
|
59
90
|
|
|
60
|
-
|
|
61
|
-
const file = await saveConfig({ api_url: cfg.api_url, token });
|
|
91
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
62
92
|
|
|
93
|
+
async function saveAndVerify(apiUrl, token) {
|
|
94
|
+
const file = await saveConfig({ api_url: apiUrl, token });
|
|
63
95
|
try {
|
|
64
96
|
const me = await http.get("whoami");
|
|
65
97
|
console.log(`Logged in as ${me.user_email || me.user_id} (super_admin=${me.is_super_admin})`);
|
|
@@ -73,6 +105,109 @@ export async function login(opts = {}) {
|
|
|
73
105
|
}
|
|
74
106
|
}
|
|
75
107
|
|
|
108
|
+
async function browserLogin(cfg) {
|
|
109
|
+
const deviceName = `${os.hostname()} (${os.platform()})`;
|
|
110
|
+
let start;
|
|
111
|
+
try {
|
|
112
|
+
start = await unauthedPost(cfg.api_url, "login-start", { device_name: deviceName });
|
|
113
|
+
} catch (e) {
|
|
114
|
+
console.error(`Could not start browser login: ${e.message}`);
|
|
115
|
+
console.error("Fallback: flowiq auth login --paste");
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
console.log("");
|
|
120
|
+
console.log("First, confirm this code matches the one shown in your browser:");
|
|
121
|
+
console.log("");
|
|
122
|
+
console.log(` ${start.user_code}`);
|
|
123
|
+
console.log("");
|
|
124
|
+
console.log(`Approval page: ${start.verification_url}`);
|
|
125
|
+
await prompt("Press ENTER to open in the browser... ");
|
|
126
|
+
if (!openBrowser(start.verification_url)) {
|
|
127
|
+
console.log("(Could not open a browser — visit the URL above manually.)");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const intervalMs = Math.max(2, start.interval || 5) * 1000;
|
|
131
|
+
const deadline = Date.now() + (start.expires_in || 600) * 1000;
|
|
132
|
+
process.stdout.write("Waiting for a super admin to approve");
|
|
133
|
+
|
|
134
|
+
while (Date.now() < deadline) {
|
|
135
|
+
await sleep(intervalMs);
|
|
136
|
+
let poll;
|
|
137
|
+
try {
|
|
138
|
+
poll = await unauthedPost(cfg.api_url, "login-poll", { device_code: start.device_code });
|
|
139
|
+
} catch (e) {
|
|
140
|
+
console.error(`\nPolling failed: ${e.message}`);
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (poll.status === "pending") { process.stdout.write("."); continue; }
|
|
145
|
+
if (poll.status === "slow_down") { await sleep(intervalMs); continue; }
|
|
146
|
+
process.stdout.write("\n");
|
|
147
|
+
|
|
148
|
+
if (poll.status === "approved") {
|
|
149
|
+
console.log(`Approved${poll.user_email ? ` by ${poll.user_email}` : ""}.`);
|
|
150
|
+
await saveAndVerify(cfg.api_url, poll.token);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (poll.status === "denied") { console.error("Login was DENIED in the browser."); process.exit(1); }
|
|
154
|
+
if (poll.status === "expired") { console.error("Login request expired. Re-run `flowiq auth login`."); process.exit(1); }
|
|
155
|
+
if (poll.status === "claimed") { console.error("This login was already claimed. Re-run `flowiq auth login`."); process.exit(1); }
|
|
156
|
+
console.error(`Unexpected status: ${poll.status}`); process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
console.error("\nTimed out waiting for approval. Re-run `flowiq auth login`.");
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export async function login(opts = {}) {
|
|
164
|
+
const cfg = await loadConfig();
|
|
165
|
+
if (opts.apiUrl) cfg.api_url = opts.apiUrl;
|
|
166
|
+
|
|
167
|
+
// Fallback paths: --token (direct) or --paste (interactive paste).
|
|
168
|
+
let token = opts.token;
|
|
169
|
+
if (!token && opts.paste) {
|
|
170
|
+
process.stdout.write(`API URL: ${cfg.api_url}\n`);
|
|
171
|
+
token = (await prompt("Paste your staff API key (fiq_staff_…): ", { silent: true })).trim();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (token) {
|
|
175
|
+
if (!token.startsWith("fiq_staff_")) {
|
|
176
|
+
console.error(`Error: token must start with "fiq_staff_" (got ${token.slice(0, 12)}…).`);
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
179
|
+
await saveAndVerify(cfg.api_url, token);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Default: browser device flow.
|
|
184
|
+
await browserLogin(cfg);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export async function refresh() {
|
|
188
|
+
const cfg = await loadConfig();
|
|
189
|
+
if (!cfg.token) {
|
|
190
|
+
console.log("No saved token — starting browser login instead.");
|
|
191
|
+
await browserLogin(cfg);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
const rotated = await http.post("token-rotate", {});
|
|
197
|
+
await saveConfig({ api_url: cfg.api_url, token: rotated.token });
|
|
198
|
+
console.log(`Token rotated (${rotated.rotated_from} → ${rotated.key_prefix}).`);
|
|
199
|
+
console.log(rotated.old_key_revoked ? "Old key revoked." : "WARNING: old key could not be revoked — revoke it manually.");
|
|
200
|
+
} catch (e) {
|
|
201
|
+
if (e.status === 401) {
|
|
202
|
+
console.log("Saved token is no longer valid — starting browser login…");
|
|
203
|
+
await browserLogin(cfg);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
console.error(`Refresh failed: ${e.message}`);
|
|
207
|
+
process.exit(1);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
76
211
|
export async function logout() {
|
|
77
212
|
const removed = await clearConfig();
|
|
78
213
|
console.log(removed ? "Logged out." : "Already logged out.");
|
|
@@ -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
|
|
@@ -39,10 +40,14 @@ export function run(argv) {
|
|
|
39
40
|
// auth
|
|
40
41
|
const auth = program.command("auth").description("Manage CLI authentication");
|
|
41
42
|
auth.command("login")
|
|
42
|
-
.description("
|
|
43
|
-
.option("--token <token>", "supply
|
|
43
|
+
.description("Log in via the browser (super-admin approval); --token/--paste for the old key-paste flow")
|
|
44
|
+
.option("--token <token>", "supply a staff API key directly instead of the browser flow")
|
|
45
|
+
.option("--paste", "prompt for a staff API key on stdin instead of the browser flow")
|
|
44
46
|
.option("--api-url <url>", "override the API host (default https://api.flowiq.live)")
|
|
45
47
|
.action((opts) => authCmd.login(opts));
|
|
48
|
+
auth.command("refresh")
|
|
49
|
+
.description("Rotate this device's token in place (old key is revoked server-side)")
|
|
50
|
+
.action(() => authCmd.refresh());
|
|
46
51
|
auth.command("logout").description("Remove the saved token").action(() => authCmd.logout());
|
|
47
52
|
auth.command("whoami").description("Show the user behind the saved token").action(() => authCmd.whoami());
|
|
48
53
|
|
|
@@ -290,5 +295,20 @@ export function run(argv) {
|
|
|
290
295
|
.option("--out <path>", "output file (default .flowiq/agent-test/judge_input.json)")
|
|
291
296
|
.action((orgId, opts) => testCmd.extract(orgId, opts));
|
|
292
297
|
|
|
298
|
+
// knowledge (agent text knowledge sources: additional_config {code,text} → get_more_answers)
|
|
299
|
+
const knowledge = program.command("knowledge")
|
|
300
|
+
.alias("kn")
|
|
301
|
+
.description("Round-trip an agent's TEXT knowledge sources (additional_config {code,text} — the get_more_answers playbooks/references)");
|
|
302
|
+
knowledge.command("pull <organization_id>")
|
|
303
|
+
.description("Fetch the agent's {code,text} text sources into a local JSON file (default: active agent)")
|
|
304
|
+
.option("--agent <id>", "target a specific agent instead of the org's active one")
|
|
305
|
+
.action((orgId, opts) => knowledgeCmd.pull(orgId, opts));
|
|
306
|
+
knowledge.command("push <slug-or-path>")
|
|
307
|
+
.description("FULL-REPLACE the agent's text sources with the local JSON's sources[] (refused if additional_config is object-shaped/managed)")
|
|
308
|
+
.action((id) => knowledgeCmd.push(id));
|
|
309
|
+
knowledge.command("list")
|
|
310
|
+
.description("List local knowledge snapshots")
|
|
311
|
+
.action(() => knowledgeCmd.list());
|
|
312
|
+
|
|
293
313
|
program.parseAsync(argv);
|
|
294
314
|
}
|