@vatio-ai/cli 0.37.2 → 0.43.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/bin/vatio.mjs +3 -0
- package/lib/api-client.mjs +36 -19
- package/lib/commands/chat.mjs +17 -8
- package/lib/commands/identity-key.mjs +95 -0
- package/lib/commands/knowledge.mjs +114 -140
- package/lib/commands/workspace-admin.mjs +0 -4
- package/lib/help.mjs +7 -4
- package/lib/workspace.mjs +3 -2
- package/package.json +1 -1
package/bin/vatio.mjs
CHANGED
|
@@ -18,6 +18,7 @@ import { login, logout } from "../lib/commands/auth.mjs";
|
|
|
18
18
|
import { configCommand, doctor, init, version } from "../lib/commands/misc.mjs";
|
|
19
19
|
import { diff, publish, push, rollback, status, toolsCheck } from "../lib/commands/deploy.mjs";
|
|
20
20
|
import { secrets, tokens, widget } from "../lib/commands/workspace-admin.mjs";
|
|
21
|
+
import { auth } from "../lib/commands/identity-key.mjs";
|
|
21
22
|
import { chat } from "../lib/commands/chat.mjs";
|
|
22
23
|
import { kb } from "../lib/commands/knowledge.mjs";
|
|
23
24
|
import { instagram, whatsapp } from "../lib/commands/channels.mjs";
|
|
@@ -70,6 +71,8 @@ async function main(argv) {
|
|
|
70
71
|
return await tokens(config, args);
|
|
71
72
|
case "widget":
|
|
72
73
|
return await widget(config, args);
|
|
74
|
+
case "auth":
|
|
75
|
+
return await auth(config, args);
|
|
73
76
|
case "chat":
|
|
74
77
|
return await chat(config, args);
|
|
75
78
|
case "kb":
|
package/lib/api-client.mjs
CHANGED
|
@@ -73,6 +73,9 @@ export class ApiClient extends HttpClient {
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
// --- knowledge bases ------------------------------------------------
|
|
76
|
+
//
|
|
77
|
+
// A base holds entries (markdown) and reads sites (a URL or a pattern). Two
|
|
78
|
+
// nouns, and every call below is one of them.
|
|
76
79
|
|
|
77
80
|
knowledgeBases() {
|
|
78
81
|
return this.get("/knowledge_bases");
|
|
@@ -90,27 +93,38 @@ export class ApiClient extends HttpClient {
|
|
|
90
93
|
return this.delete(`/knowledge_bases/${encode(name)}`);
|
|
91
94
|
}
|
|
92
95
|
|
|
93
|
-
|
|
94
|
-
return this.post(`/knowledge_bases/${encode(
|
|
95
|
-
name,
|
|
96
|
-
site_url: siteUrl,
|
|
97
|
-
include: includes,
|
|
98
|
-
exclude: excludes
|
|
99
|
-
});
|
|
96
|
+
refreshKnowledgeBase(name) {
|
|
97
|
+
return this.post(`/knowledge_bases/${encode(name)}/refresh`, {});
|
|
100
98
|
}
|
|
101
99
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
uploadKnowledgeDocument({ base, filename, content }) {
|
|
105
|
-
return this.post(`/knowledge_bases/${encode(base)}/sources`, { filename, content });
|
|
100
|
+
knowledgeEntry({ base, name }) {
|
|
101
|
+
return this.get(`/knowledge_bases/${encode(base)}/entries/${encode(name)}`);
|
|
106
102
|
}
|
|
107
103
|
|
|
108
|
-
|
|
109
|
-
|
|
104
|
+
// Put this markdown under this name, whether or not the entry exists yet --
|
|
105
|
+
// one call for "write" and "rewrite", because from the terminal they are the
|
|
106
|
+
// same sentence.
|
|
107
|
+
writeKnowledgeEntry({ base, name, content }) {
|
|
108
|
+
return this.patch(`/knowledge_bases/${encode(base)}/entries/${encode(name)}`, { content });
|
|
110
109
|
}
|
|
111
110
|
|
|
112
|
-
|
|
113
|
-
return this.
|
|
111
|
+
deleteKnowledgeEntry({ base, name }) {
|
|
112
|
+
return this.delete(`/knowledge_bases/${encode(base)}/entries/${encode(name)}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
followKnowledgeSite({ base, url }) {
|
|
116
|
+
return this.post(`/knowledge_bases/${encode(base)}/sites`, { url });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Addressed by the URL you typed rather than by an id you would have to look
|
|
120
|
+
// up first. It travels as a query parameter because a URL does not survive
|
|
121
|
+
// being a path segment.
|
|
122
|
+
unfollowKnowledgeSite({ base, url }) {
|
|
123
|
+
return this.delete(`/knowledge_bases/${encode(base)}/sites?url=${encode(url)}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
refreshKnowledgeSite({ base, url }) {
|
|
127
|
+
return this.post(`/knowledge_bases/${encode(base)}/sites/refresh`, { url });
|
|
114
128
|
}
|
|
115
129
|
|
|
116
130
|
// --- publishable tokens and the widget ------------------------------
|
|
@@ -240,12 +254,15 @@ export class ApiClient extends HttpClient {
|
|
|
240
254
|
return this.post(`/chats/${encode(chatId)}/reset`, { environment });
|
|
241
255
|
}
|
|
242
256
|
|
|
243
|
-
|
|
244
|
-
|
|
257
|
+
// No `view` parameter: the developer token is the developer view, so the
|
|
258
|
+
// API returns one shape and the caller decides how much of it to print.
|
|
259
|
+
// `transcript` prints the visitor's half of it; `debug` prints all of it.
|
|
260
|
+
showChat({ chatId }) {
|
|
261
|
+
return this.get(`/chats/${encode(chatId)}`);
|
|
245
262
|
}
|
|
246
263
|
|
|
247
|
-
chatMessages({ chatId,
|
|
248
|
-
const params = {
|
|
264
|
+
chatMessages({ chatId, after = null, limit = null }) {
|
|
265
|
+
const params = {};
|
|
249
266
|
if (after) params.after = after;
|
|
250
267
|
if (limit) params.limit = limit;
|
|
251
268
|
return this.get(`/chats/${encode(chatId)}/messages`, params);
|
package/lib/commands/chat.mjs
CHANGED
|
@@ -148,15 +148,25 @@ async function converse(config, options, message) {
|
|
|
148
148
|
console.log(formatVisitor(assistant));
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
+
// What a visitor would have seen in a row the developer API always sends in
|
|
152
|
+
// full -- the same three questions the server used to answer behind
|
|
153
|
+
// `view=visitor`: not deleted, said by a person or the agent, and actually
|
|
154
|
+
// carrying words. A tool call with no text answers the last one and is skipped,
|
|
155
|
+
// which is what makes this safe to run over the developer payload.
|
|
156
|
+
const visitorVisible = (entry) =>
|
|
157
|
+
!entry.discarded &&
|
|
158
|
+
["user", "assistant"].includes(entry.role) &&
|
|
159
|
+
String(entry.content ?? "").trim() !== "";
|
|
160
|
+
|
|
151
161
|
// Conversations are asynchronous: post a message, then poll for the reply.
|
|
152
162
|
async function waitForAssistant(api, { chatId, after, timeout, interval = 2 }) {
|
|
153
163
|
const deadline = Date.now() + timeout * 1000;
|
|
154
164
|
for (;;) {
|
|
155
|
-
const payload = await api.chatMessages({ chatId,
|
|
165
|
+
const payload = await api.chatMessages({ chatId, after });
|
|
156
166
|
const messages = Array.isArray(payload.data) ? payload.data : [];
|
|
157
|
-
const assistant = [...messages]
|
|
158
|
-
|
|
159
|
-
|
|
167
|
+
const assistant = [...messages]
|
|
168
|
+
.reverse()
|
|
169
|
+
.find((entry) => entry.role === "assistant" && visitorVisible(entry));
|
|
160
170
|
if (assistant) return assistant;
|
|
161
171
|
if (Date.now() >= deadline) fail(`Timed out waiting for assistant reply after ${timeout}s`);
|
|
162
172
|
await sleep(interval * 1000);
|
|
@@ -168,11 +178,10 @@ async function transcript(config, options) {
|
|
|
168
178
|
const chatId = await ensureChat(config, options);
|
|
169
179
|
const payload = await api.chatMessages({
|
|
170
180
|
chatId,
|
|
171
|
-
view: "visitor",
|
|
172
181
|
limit: Math.min(options.last * 6, 200)
|
|
173
182
|
});
|
|
174
183
|
const messages = (Array.isArray(payload.data) ? payload.data : [])
|
|
175
|
-
.filter(
|
|
184
|
+
.filter(visitorVisible)
|
|
176
185
|
.slice(-options.last);
|
|
177
186
|
for (const message of messages) console.log(formatVisitor(message));
|
|
178
187
|
}
|
|
@@ -182,10 +191,10 @@ async function debugChat(config, options) {
|
|
|
182
191
|
const chatId = await ensureChat(config, options);
|
|
183
192
|
|
|
184
193
|
console.log("## Chat status (developer)");
|
|
185
|
-
console.log(JSON.stringify(await api.showChat({ chatId
|
|
194
|
+
console.log(JSON.stringify(await api.showChat({ chatId }), null, 2));
|
|
186
195
|
console.log("");
|
|
187
196
|
console.log(`## Transcript (developer/debug, last ${options.last})`);
|
|
188
|
-
const payload = await api.chatMessages({ chatId,
|
|
197
|
+
const payload = await api.chatMessages({ chatId, limit: options.last });
|
|
189
198
|
for (const message of Array.isArray(payload.data) ? payload.data : []) {
|
|
190
199
|
console.log(`[${message.role}] ${String(message.content ?? "").trim()}`);
|
|
191
200
|
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// `vatio auth --new-key` — the keypair that signs the JWT saying who a visitor is.
|
|
2
|
+
//
|
|
3
|
+
// This exists because asking a developer to authenticate users and then leaving
|
|
4
|
+
// them to work out `openssl genpkey` flags, which half of the pair goes in the
|
|
5
|
+
// workspace, and what the token has to contain is most of the reason the old
|
|
6
|
+
// design grew a JavaScript escape hatch. Everything the platform can settle for
|
|
7
|
+
// them is settled here: the algorithm, the file names, the audience, and the
|
|
8
|
+
// exact claims, printed as code they can paste.
|
|
9
|
+
|
|
10
|
+
import { execFileSync } from "node:child_process";
|
|
11
|
+
import { existsSync, writeFileSync, readFileSync, unlinkSync } from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { fail } from "../support.mjs";
|
|
14
|
+
|
|
15
|
+
const PUBLIC_FILE = "identity.pub";
|
|
16
|
+
const PRIVATE_FILE = "identity.pem";
|
|
17
|
+
|
|
18
|
+
export async function auth(config, args) {
|
|
19
|
+
if (!args.includes("--new-key")) {
|
|
20
|
+
fail("Usage: vatio auth --new-key (generates the keypair that signs your JWT)");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const root = config.workspaceRootRequired();
|
|
24
|
+
const publicPath = join(root, PUBLIC_FILE);
|
|
25
|
+
const privatePath = join(root, PRIVATE_FILE);
|
|
26
|
+
const slug = config.resolveWorkspaceRequired();
|
|
27
|
+
|
|
28
|
+
if (existsSync(publicPath) && !args.includes("--force")) {
|
|
29
|
+
fail(
|
|
30
|
+
`${PUBLIC_FILE} already exists. Rotating a key signs out every visitor holding a token ` +
|
|
31
|
+
`from the old one — re-run with --force if that is what you want.`
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
execFileSync("openssl", ["genpkey", "-algorithm", "RSA", "-pkeyopt", "rsa_keygen_bits:2048", "-out", privatePath], {
|
|
37
|
+
stdio: "ignore"
|
|
38
|
+
});
|
|
39
|
+
execFileSync("openssl", ["rsa", "-in", privatePath, "-pubout", "-out", publicPath], { stdio: "ignore" });
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (existsSync(privatePath)) unlinkSync(privatePath);
|
|
42
|
+
fail(`Could not run openssl: ${error.message}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// The private half must never be pushed, and the surest way to keep it out is
|
|
46
|
+
// to keep it out of git — the deploy refuses it too, but by then it has
|
|
47
|
+
// already been read off disk.
|
|
48
|
+
ignorePrivateKey(root);
|
|
49
|
+
|
|
50
|
+
console.log(`Wrote ${PUBLIC_FILE} and ${PRIVATE_FILE}. They go to different places.`);
|
|
51
|
+
console.log("");
|
|
52
|
+
console.log(`1. ${PUBLIC_FILE} stays here. Commit it, and name it in vatio.yml:`);
|
|
53
|
+
console.log("");
|
|
54
|
+
console.log(" auth:");
|
|
55
|
+
console.log(` public_key: ${PUBLIC_FILE}`);
|
|
56
|
+
console.log("");
|
|
57
|
+
console.log(`2. ${PRIVATE_FILE} goes into your backend as a secret — an env var, or`);
|
|
58
|
+
console.log(" whatever credential store you use. Your backend is what signs, so it");
|
|
59
|
+
console.log(" is the only thing that needs it. Then delete it from this directory:");
|
|
60
|
+
console.log("");
|
|
61
|
+
console.log(` VATIO_IDENTITY_PRIVATE_KEY="$(cat ${PRIVATE_FILE})"`);
|
|
62
|
+
console.log("");
|
|
63
|
+
console.log(" Not `vatio secrets set` — that store is read by Vatio, and the whole");
|
|
64
|
+
console.log(" point is that Vatio can check your tokens but never mint one.");
|
|
65
|
+
console.log(` (${PRIVATE_FILE} was added to .gitignore in the meantime.)`);
|
|
66
|
+
console.log("");
|
|
67
|
+
console.log("Sign a JWT with it for the person who is signed in:");
|
|
68
|
+
console.log("");
|
|
69
|
+
const claims = [
|
|
70
|
+
[ '"sub": "<your user id>",', "who this is" ],
|
|
71
|
+
[ `"aud": ${JSON.stringify(slug)},`, "this workspace, always" ],
|
|
72
|
+
[ '"exp": <unix seconds>,', "required; keep it short" ],
|
|
73
|
+
[ '"name": "Ada Lovelace",', "optional, fills the CRM" ],
|
|
74
|
+
[ '"email": "ada@example.com"', "optional, fills the CRM" ]
|
|
75
|
+
];
|
|
76
|
+
const width = Math.max(...claims.map(([ claim ]) => claim.length));
|
|
77
|
+
console.log(" {");
|
|
78
|
+
for (const [ claim, note ] of claims) {
|
|
79
|
+
console.log(` ${claim.padEnd(width)} // ${note}`);
|
|
80
|
+
}
|
|
81
|
+
console.log(" }");
|
|
82
|
+
console.log("");
|
|
83
|
+
console.log("Put it on the widget as data-visitor-token, and push.");
|
|
84
|
+
console.log("Full examples: https://vatio.ai/docs#auth");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function ignorePrivateKey(root) {
|
|
88
|
+
const path = join(root, ".gitignore");
|
|
89
|
+
const line = PRIVATE_FILE;
|
|
90
|
+
let body = existsSync(path) ? readFileSync(path, "utf8") : "";
|
|
91
|
+
if (body.split(/\r?\n/).some((entry) => entry.trim() === line)) return;
|
|
92
|
+
|
|
93
|
+
if (body.length > 0 && !body.endsWith("\n")) body += "\n";
|
|
94
|
+
writeFileSync(path, `${body}${line}\n`);
|
|
95
|
+
}
|
|
@@ -4,12 +4,15 @@
|
|
|
4
4
|
// vatio.yml is a list of *names*, and a push neither fills nor empties one.
|
|
5
5
|
// That is why this is a command and not part of the manifest, and why live and
|
|
6
6
|
// preview read the same base.
|
|
7
|
+
//
|
|
8
|
+
// A base holds entries, and an entry is markdown. Either you wrote it, or a
|
|
9
|
+
// site wrote it from a page it read — and a site is one URL or one pattern,
|
|
10
|
+
// re-read every night. Everything below is one of those two nouns.
|
|
7
11
|
|
|
8
|
-
import { basename } from "node:path";
|
|
9
12
|
import { readFileSync, statSync } from "node:fs";
|
|
10
13
|
|
|
11
14
|
import { deployClient } from "./deploy.mjs";
|
|
12
|
-
import { fail
|
|
15
|
+
import { fail } from "../support.mjs";
|
|
13
16
|
|
|
14
17
|
export async function kb(config, args) {
|
|
15
18
|
const workspace = config.resolveWorkspaceRequired();
|
|
@@ -21,10 +24,12 @@ export async function kb(config, args) {
|
|
|
21
24
|
case "show": return await show(client, workspace, args.shift());
|
|
22
25
|
case "create": return await create(client, workspace, args.shift());
|
|
23
26
|
case "rm": return await remove(client, workspace, args.shift());
|
|
24
|
-
case "
|
|
25
|
-
case "
|
|
26
|
-
case "rm-
|
|
27
|
-
case "
|
|
27
|
+
case "write": return await write(client, workspace, args);
|
|
28
|
+
case "cat": return await cat(client, args.shift(), args.shift());
|
|
29
|
+
case "rm-entry": return await removeEntry(client, workspace, args.shift(), args.shift());
|
|
30
|
+
case "follow": return await follow(client, workspace, args.shift(), args.shift());
|
|
31
|
+
case "unfollow": return await unfollow(client, workspace, args.shift(), args.shift());
|
|
32
|
+
case "refresh": return await refresh(client, workspace, args.shift(), args.shift());
|
|
28
33
|
default: fail(`Unknown kb subcommand: ${sub}\n\n${usage()}`);
|
|
29
34
|
}
|
|
30
35
|
}
|
|
@@ -32,13 +37,15 @@ export async function kb(config, args) {
|
|
|
32
37
|
function usage() {
|
|
33
38
|
return `usage:
|
|
34
39
|
vatio kb List knowledge bases
|
|
35
|
-
vatio kb show NAME One base and its
|
|
40
|
+
vatio kb show NAME One base: its sites and its entries
|
|
36
41
|
vatio kb create NAME Create a base
|
|
37
42
|
vatio kb rm NAME Delete a base (must be unreferenced)
|
|
38
|
-
vatio kb
|
|
39
|
-
vatio kb
|
|
40
|
-
vatio kb rm-
|
|
41
|
-
vatio kb
|
|
43
|
+
vatio kb write BASE ENTRY [FILE] Write an entry, from FILE or stdin
|
|
44
|
+
vatio kb cat BASE ENTRY Print an entry's markdown
|
|
45
|
+
vatio kb rm-entry BASE ENTRY Delete one entry
|
|
46
|
+
vatio kb follow BASE URL Read a site into the base, nightly
|
|
47
|
+
vatio kb unfollow BASE URL Stop reading it, and drop its entries
|
|
48
|
+
vatio kb refresh BASE [URL] Read the sites again now`;
|
|
42
49
|
}
|
|
43
50
|
|
|
44
51
|
async function list(client, workspace) {
|
|
@@ -51,7 +58,10 @@ async function list(client, workspace) {
|
|
|
51
58
|
|
|
52
59
|
console.log(`Knowledge bases on workspace ${workspace} (${bases.length}):`);
|
|
53
60
|
for (const row of bases) {
|
|
54
|
-
console.log(
|
|
61
|
+
console.log(
|
|
62
|
+
` ${row.name} entries=${row.entries_count} ready=${row.answerable_count} ` +
|
|
63
|
+
`sites=${row.sites_count}${referenceNote(row)}`
|
|
64
|
+
);
|
|
55
65
|
}
|
|
56
66
|
}
|
|
57
67
|
|
|
@@ -66,24 +76,37 @@ async function show(client, workspace, name) {
|
|
|
66
76
|
if (!name) fail("usage: vatio kb show NAME");
|
|
67
77
|
|
|
68
78
|
const base = await client.knowledgeBase(name);
|
|
69
|
-
console.log(
|
|
79
|
+
console.log(
|
|
80
|
+
`${base.name} on workspace ${workspace}: entries=${base.entries_count} ` +
|
|
81
|
+
`ready=${base.answerable_count}${referenceNote(base)}`
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const sites = asArray(base.sites);
|
|
85
|
+
if (sites.length > 0) {
|
|
86
|
+
console.log("\n sites:");
|
|
87
|
+
for (const site of sites) {
|
|
88
|
+
const when = site.last_run_at ?? "never";
|
|
89
|
+
console.log(
|
|
90
|
+
` ${site.url} ${site.status} pages=${site.pages_found_count} ` +
|
|
91
|
+
`entries=${site.entries_written_count} last_read=${when}`
|
|
92
|
+
);
|
|
93
|
+
if (site.last_error) console.log(` error: ${site.last_error}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
70
96
|
|
|
71
|
-
const
|
|
72
|
-
if (
|
|
97
|
+
const entries = asArray(base.entries);
|
|
98
|
+
if (entries.length === 0) {
|
|
73
99
|
console.log(
|
|
74
|
-
|
|
100
|
+
`\n no entries yet — write one with \`vatio kb write ${name} ENTRY FILE\`, ` +
|
|
101
|
+
`or read a site with \`vatio kb follow ${name} acme.com/help/**\``
|
|
75
102
|
);
|
|
76
103
|
return;
|
|
77
104
|
}
|
|
78
105
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
`entries=${row.entries_count} last_indexed_at=${lastCrawled}`
|
|
84
|
-
);
|
|
85
|
-
console.log(row.kind === "upload" ? ` ${row.filename}` : ` ${row.site_url} ${patternsDisplay(row)}`);
|
|
86
|
-
if (row.last_error) console.log(` error: ${row.last_error}`);
|
|
106
|
+
console.log("\n entries:");
|
|
107
|
+
for (const entry of entries) {
|
|
108
|
+
const origin = entry.written ? "written here" : entry.url;
|
|
109
|
+
console.log(` ${entry.name} ${entry.state ?? "?"} ${origin}`);
|
|
87
110
|
}
|
|
88
111
|
}
|
|
89
112
|
|
|
@@ -92,7 +115,7 @@ async function create(client, workspace, name) {
|
|
|
92
115
|
|
|
93
116
|
const base = await client.createKnowledgeBase(name);
|
|
94
117
|
console.log(`Created knowledge base ${base.name} on workspace ${workspace}`);
|
|
95
|
-
console.log("It is empty and nothing reads it yet. Fill it with `vatio kb
|
|
118
|
+
console.log("It is empty and nothing reads it yet. Fill it with `vatio kb write` or `vatio kb follow`,");
|
|
96
119
|
console.log(`then add \`knowledge: [${base.name}]\` to vatio.yml and push.`);
|
|
97
120
|
}
|
|
98
121
|
|
|
@@ -103,143 +126,94 @@ async function remove(client, workspace, name) {
|
|
|
103
126
|
console.log(`Deleted knowledge base ${name} on workspace ${workspace}`);
|
|
104
127
|
}
|
|
105
128
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
129
|
+
// Put this markdown under this name. One shape, positional, no flags: the
|
|
130
|
+
// entry's name is the only thing the command cannot work out for itself — the
|
|
131
|
+
// title comes from the document's own `# heading`, and the body comes from the
|
|
132
|
+
// file or from stdin.
|
|
133
|
+
//
|
|
134
|
+
// Creates the entry if the name is free and replaces it if it is taken, so the
|
|
135
|
+
// round trip is the obvious one:
|
|
136
|
+
//
|
|
137
|
+
// vatio kb cat docs horarios > horarios.md
|
|
138
|
+
// $EDITOR horarios.md
|
|
139
|
+
// vatio kb write docs horarios horarios.md
|
|
140
|
+
async function write(client, workspace, args) {
|
|
141
|
+
const [base, name, file] = args;
|
|
142
|
+
if (!base || !name) fail("usage: vatio kb write BASE ENTRY [FILE] (or pipe the markdown in)");
|
|
143
|
+
if (file && !isFile(file)) fail(`No such file: ${file}`);
|
|
144
|
+
|
|
145
|
+
const content = file ? readFileSync(file, "utf8") : readStdin();
|
|
146
|
+
if (!content.trim()) {
|
|
147
|
+
fail("Nothing to write: pass a FILE, or pipe the markdown in on stdin.");
|
|
113
148
|
}
|
|
114
149
|
|
|
115
|
-
const row = await client.
|
|
116
|
-
console.log(`
|
|
117
|
-
console.log(`Crawling runs in the background — check progress with \`vatio kb show ${base}\``);
|
|
150
|
+
const row = await client.writeKnowledgeEntry({ base, name, content });
|
|
151
|
+
console.log(`Wrote ${row.name} in ${base} on workspace ${workspace}`);
|
|
118
152
|
}
|
|
119
153
|
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if (!base || paths.length === 0) fail("usage: vatio kb upload BASE FILE...");
|
|
154
|
+
// Prints exactly what the platform holds, so a diff against the file you
|
|
155
|
+
// pushed is a real answer to "is what I sent what it has".
|
|
156
|
+
async function cat(client, base, name) {
|
|
157
|
+
if (!base || !name) fail("usage: vatio kb cat BASE ENTRY");
|
|
125
158
|
|
|
126
|
-
const
|
|
127
|
-
|
|
159
|
+
const row = await client.knowledgeEntry({ base, name });
|
|
160
|
+
const content = row.content ?? "";
|
|
161
|
+
process.stdout.write(content.endsWith("\n") ? content : `${content}\n`);
|
|
162
|
+
}
|
|
128
163
|
|
|
129
|
-
|
|
164
|
+
async function removeEntry(client, workspace, base, name) {
|
|
165
|
+
if (!base || !name) fail("usage: vatio kb rm-entry BASE ENTRY");
|
|
130
166
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
base,
|
|
134
|
-
filename: basename(path),
|
|
135
|
-
content: readFileSync(path, "utf8")
|
|
136
|
-
});
|
|
137
|
-
console.log(`Uploaded ${basename(path)} to ${base} as ${row.name} (entries=${row.entries_count})`);
|
|
138
|
-
}
|
|
139
|
-
console.log(`Indexing runs in the background — check progress with \`vatio kb show ${base}\``);
|
|
167
|
+
await client.deleteKnowledgeEntry({ base, name });
|
|
168
|
+
console.log(`Deleted entry ${name} from ${base} on workspace ${workspace}`);
|
|
140
169
|
}
|
|
141
170
|
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
// on its own, but one file at a time means a collision on file twenty lands
|
|
145
|
-
// after nineteen are already in -- half an upload nobody asked for.
|
|
171
|
+
// One URL, and the shape of it is the whole configuration. A URL is that URL;
|
|
172
|
+
// a pattern is every page that matches it:
|
|
146
173
|
//
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
async function
|
|
152
|
-
|
|
153
|
-
const crawls = new Map(sources.filter((row) => row.kind === "crawl").map((row) => [row.name, row]));
|
|
154
|
-
if (crawls.size === 0) return;
|
|
155
|
-
|
|
156
|
-
const collisions = paths
|
|
157
|
-
.map((path) => [path, crawls.get(sourceNameFor(path))])
|
|
158
|
-
.filter(([, crawl]) => crawl);
|
|
159
|
-
if (collisions.length === 0) return;
|
|
160
|
-
|
|
161
|
-
const lines = collisions.map(([path, crawl]) => {
|
|
162
|
-
const declaration = [crawl.site_url, ...asArray(crawl.include).map((p) => `include=${p}`)]
|
|
163
|
-
.filter(Boolean)
|
|
164
|
-
.join(" ");
|
|
165
|
-
return ` ${basename(path)} would land on "${crawl.name}", a crawl of ${declaration}`;
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
fail(
|
|
169
|
-
`Refusing to upload: ${collisions.length} of ${paths.length} file(s) collide with a crawl in ${base}.\n\n` +
|
|
170
|
-
`${lines.join("\n")}\n\n` +
|
|
171
|
-
"An upload cannot take over a crawl — its pages would keep answering lookups\n" +
|
|
172
|
-
"under a source that no longer says where they came from. Rename the file(s),\n" +
|
|
173
|
-
`or drop the crawl first with \`vatio kb rm-source ${base} NAME\`.\n\n` +
|
|
174
|
-
"Nothing was uploaded."
|
|
175
|
-
);
|
|
176
|
-
}
|
|
174
|
+
// acme.com the home page, and only it
|
|
175
|
+
// acme.com/help that page
|
|
176
|
+
// acme.com/** every page of the site
|
|
177
|
+
// acme.com/help/** that section, however deep
|
|
178
|
+
async function follow(client, workspace, base, url) {
|
|
179
|
+
if (!base || !url) fail("usage: vatio kb follow BASE URL");
|
|
177
180
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const stem = basename(String(path)).replace(/\.[^.]*$/, "").toLowerCase();
|
|
182
|
-
const slug = stem.replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
183
|
-
return slug.slice(0, 60).replace(/-+$/, "");
|
|
181
|
+
const site = await client.followKnowledgeSite({ base, url });
|
|
182
|
+
console.log(`Reading ${site.url} into ${base} on workspace ${workspace}`);
|
|
183
|
+
console.log(`It runs in the background, and again every night — check it with \`vatio kb show ${base}\``);
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
-
async function
|
|
187
|
-
if (!base || !
|
|
186
|
+
async function unfollow(client, workspace, base, url) {
|
|
187
|
+
if (!base || !url) fail("usage: vatio kb unfollow BASE URL");
|
|
188
188
|
|
|
189
|
-
await client.
|
|
190
|
-
console.log(`
|
|
189
|
+
await client.unfollowKnowledgeSite({ base, url });
|
|
190
|
+
console.log(`Stopped reading ${url} into ${base} on workspace ${workspace}`);
|
|
191
|
+
console.log("Its entries went with it: nothing was left that nothing would refresh.");
|
|
191
192
|
}
|
|
192
193
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
// re-read, so "reindex everything" cannot mean them.
|
|
196
|
-
async function reindex(client, workspace, base, name) {
|
|
197
|
-
if (!base) fail("usage: vatio kb reindex BASE [NAME]");
|
|
194
|
+
async function refresh(client, workspace, base, url) {
|
|
195
|
+
if (!base) fail("usage: vatio kb refresh BASE [URL]");
|
|
198
196
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
197
|
+
if (url) {
|
|
198
|
+
const site = await client.refreshKnowledgeSite({ base, url });
|
|
199
|
+
console.log(`Reading ${site.url} again in ${base} on workspace ${workspace}`);
|
|
202
200
|
} else {
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
targets = crawls.map((row) => row.name);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
for (const target of targets) {
|
|
209
|
-
const row = await client.reindexKnowledgeSource({ base, name: target });
|
|
210
|
-
console.log(`Reindexing ${row.name} in ${base} on workspace ${workspace} (status=${row.status})`);
|
|
201
|
+
const row = await client.refreshKnowledgeBase(base);
|
|
202
|
+
console.log(`Reading every site in ${base} again on workspace ${workspace} (${asArray(row.sites).length} of them)`);
|
|
211
203
|
}
|
|
212
|
-
console.log(`
|
|
204
|
+
console.log(`Runs in the background — check progress with \`vatio kb show ${base}\``);
|
|
213
205
|
}
|
|
214
206
|
|
|
215
|
-
//
|
|
216
|
-
//
|
|
217
|
-
//
|
|
218
|
-
function
|
|
219
|
-
|
|
220
|
-
const indexed = row.pages_indexed_count;
|
|
221
|
-
return discovered ? `${indexed}/${discovered}` : String(indexed ?? "");
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// No patterns at all means the source covers the whole site, which is the
|
|
225
|
-
// default and needs saying rather than printing blank.
|
|
226
|
-
function patternsDisplay(row) {
|
|
227
|
-
const includes = asArray(row.include);
|
|
228
|
-
const excludes = asArray(row.exclude);
|
|
229
|
-
if (includes.length === 0 && excludes.length === 0) return "whole site";
|
|
230
|
-
|
|
231
|
-
const parts = [];
|
|
232
|
-
if (includes.length > 0) parts.push(`include=${includes.join(",")}`);
|
|
233
|
-
if (excludes.length > 0) parts.push(`exclude=${excludes.join(",")}`);
|
|
234
|
-
return parts.join(" ");
|
|
235
|
-
}
|
|
207
|
+
// Blocking on purpose: `vatio kb write docs refunds < refunds.md` should behave
|
|
208
|
+
// like every other filter, and a terminal with nobody piping into it returns ""
|
|
209
|
+
// rather than hanging on a read that will never end.
|
|
210
|
+
function readStdin() {
|
|
211
|
+
if (process.stdin.isTTY) return "";
|
|
236
212
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
if (value === null) return values;
|
|
242
|
-
values.push(value);
|
|
213
|
+
try {
|
|
214
|
+
return readFileSync(0, "utf8");
|
|
215
|
+
} catch {
|
|
216
|
+
return "";
|
|
243
217
|
}
|
|
244
218
|
}
|
|
245
219
|
|
|
@@ -108,10 +108,6 @@ export async function tokens(config, args) {
|
|
|
108
108
|
fail("Usage: vatio tokens list|create [--env live|preview|NAME] [--label NAME]|revoke PREFIX");
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
// Everything needed to put the bubble on a page, in one screen: what the
|
|
112
|
-
// platform will actually enforce, the tokens that exist, and the snippet to
|
|
113
|
-
// paste. Read-only -- vatio.yml owns every field, so `vatio push` is what
|
|
114
|
-
// changes it.
|
|
115
111
|
export async function widget(config, args) {
|
|
116
112
|
const environment = environmentOption(args, "live");
|
|
117
113
|
const workspace = config.resolveWorkspaceRequired();
|
package/lib/help.mjs
CHANGED
|
@@ -39,16 +39,19 @@ flag, the directory decides.
|
|
|
39
39
|
|
|
40
40
|
Knowledge:
|
|
41
41
|
kb [list] Bases, and whether a deployed agent reads each
|
|
42
|
-
kb show NAME One base and its
|
|
42
|
+
kb show NAME One base: its sites and its entries
|
|
43
43
|
kb create NAME | rm NAME
|
|
44
|
-
kb
|
|
45
|
-
kb
|
|
46
|
-
kb rm-
|
|
44
|
+
kb write BASE ENTRY [FILE] Write an entry, from FILE or stdin
|
|
45
|
+
kb cat BASE ENTRY Print an entry's markdown
|
|
46
|
+
kb rm-entry BASE ENTRY Delete one entry
|
|
47
|
+
kb follow BASE URL Read a site into the base, nightly
|
|
48
|
+
kb unfollow BASE URL | refresh BASE [URL]
|
|
47
49
|
|
|
48
50
|
Credentials and the widget:
|
|
49
51
|
secrets list|set KEY VALUE|rm KEY
|
|
50
52
|
tokens list|create [--env NAME] [--label NAME]|revoke PREFIX
|
|
51
53
|
widget [--env NAME] What the platform enforces, and the tokens there are
|
|
54
|
+
auth --new-key Keypair that signs the JWT saying who a visitor is
|
|
52
55
|
|
|
53
56
|
Channels (your own account serves live; the shared preview always serves preview):
|
|
54
57
|
whatsapp [status]|connect|check|activate|deactivate|disconnect
|
package/lib/workspace.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// contract -- the port of cli/lib/workspace_root.rb, and deliberately just as
|
|
3
3
|
// small.
|
|
4
4
|
//
|
|
5
|
-
// What a `tools/*.
|
|
5
|
+
// What a `tools/*.yml` declares, which blocks `vatio.yml` accepts and whether
|
|
6
6
|
// any of it is valid are answered by POST /api/v1/:slug/deploy/check. The one
|
|
7
7
|
// thing a client cannot ask the platform for is the address of that call: the
|
|
8
8
|
// slug is in its URL. So this reads `workspace:` and stops.
|
|
@@ -16,7 +16,8 @@ export const MANIFEST_FILE = "vatio.yml";
|
|
|
16
16
|
// past this is still the platform's to reject; matching here only turns a typo
|
|
17
17
|
// into a sentence instead of a 404.
|
|
18
18
|
export const SLUG_FORMAT = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
19
|
-
//
|
|
19
|
+
// The key `agent:` stores its single agent under, and the default an `entry:`
|
|
20
|
+
// table falls back to. A workspace may declare several under `agents:`.
|
|
20
21
|
export const ENTRY_AGENT = "main";
|
|
21
22
|
|
|
22
23
|
// The manifest in a directory, or null when the directory is not a workspace
|