@velaro/cli 0.2.0 → 0.3.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/velaro.js CHANGED
@@ -1,56 +1,56 @@
1
- #!/usr/bin/env node
2
-
3
- import yargs from 'yargs';
4
- import { hideBin } from 'yargs/helpers';
5
- import { loginCommand, logoutCommand } from '../lib/commands/login.js';
6
- import { whoamiCommand } from '../lib/commands/whoami.js';
7
- import { teamCommand } from '../lib/commands/team.js';
8
- import { botCommand } from '../lib/commands/bot.js';
9
- import { kbCommand } from '../lib/commands/kb.js';
10
- import { workflowCommand } from '../lib/commands/workflow.js';
11
- import { ruleCommand } from '../lib/commands/rule.js';
12
- import { agentCommand } from '../lib/commands/agent.js';
13
- import { ingestCommand } from '../lib/commands/ingest.js';
14
- import { mcpKeyCommand } from '../lib/commands/mcp-key.js';
15
- import { statusCommand } from '../lib/commands/status.js';
16
- import { siteCommand } from '../lib/commands/site.js';
17
- import { checkCommand } from '../lib/commands/check.js';
18
- import { updateCommand } from '../lib/commands/update.js';
19
- import { startUpdateCheck } from '../lib/update-check.js';
20
-
21
- // Start background update check — doesn't block the command
22
- const printUpdateNotice = startUpdateCheck();
23
-
24
- await yargs(hideBin(process.argv))
25
- .scriptName('velaro')
26
- .usage('$0 <command> [options]')
27
- // Auth
28
- .command(loginCommand)
29
- .command(logoutCommand)
30
- .command(whoamiCommand)
31
- // Info
32
- .command(siteCommand)
33
- .command(checkCommand)
34
- .command(statusCommand)
35
- // Config
36
- .command(teamCommand)
37
- .command(botCommand)
38
- .command(kbCommand)
39
- .command(workflowCommand)
40
- .command(ruleCommand)
41
- .command(agentCommand)
42
- // Integrations / keys
43
- .command(ingestCommand)
44
- .command(mcpKeyCommand)
45
- // CLI maintenance
46
- .command(updateCommand)
47
- .demandCommand(1, 'Specify a command. Run velaro --help for a list.')
48
- .strict()
49
- .help()
50
- .alias('h', 'help')
51
- .alias('v', 'version')
52
- .wrap(Math.min(100, process.stdout.columns || 100))
53
- .parseAsync();
54
-
55
- // Print update notice after command output (non-intrusive)
56
- await printUpdateNotice();
1
+ #!/usr/bin/env node
2
+
3
+ import yargs from 'yargs';
4
+ import { hideBin } from 'yargs/helpers';
5
+ import { loginCommand, logoutCommand } from '../lib/commands/login.js';
6
+ import { whoamiCommand } from '../lib/commands/whoami.js';
7
+ import { teamCommand } from '../lib/commands/team.js';
8
+ import { botCommand } from '../lib/commands/bot.js';
9
+ import { kbCommand } from '../lib/commands/kb.js';
10
+ import { workflowCommand } from '../lib/commands/workflow.js';
11
+ import { ruleCommand } from '../lib/commands/rule.js';
12
+ import { agentCommand } from '../lib/commands/agent.js';
13
+ import { ingestCommand } from '../lib/commands/ingest.js';
14
+ import { mcpKeyCommand } from '../lib/commands/mcp-key.js';
15
+ import { statusCommand } from '../lib/commands/status.js';
16
+ import { siteCommand } from '../lib/commands/site.js';
17
+ import { checkCommand } from '../lib/commands/check.js';
18
+ import { updateCommand } from '../lib/commands/update.js';
19
+ import { startUpdateCheck } from '../lib/update-check.js';
20
+
21
+ // Start background update check — doesn't block the command
22
+ const printUpdateNotice = startUpdateCheck();
23
+
24
+ await yargs(hideBin(process.argv))
25
+ .scriptName('velaro')
26
+ .usage('$0 <command> [options]')
27
+ // Auth
28
+ .command(loginCommand)
29
+ .command(logoutCommand)
30
+ .command(whoamiCommand)
31
+ // Info
32
+ .command(siteCommand)
33
+ .command(checkCommand)
34
+ .command(statusCommand)
35
+ // Config
36
+ .command(teamCommand)
37
+ .command(botCommand)
38
+ .command(kbCommand)
39
+ .command(workflowCommand)
40
+ .command(ruleCommand)
41
+ .command(agentCommand)
42
+ // Integrations / keys
43
+ .command(ingestCommand)
44
+ .command(mcpKeyCommand)
45
+ // CLI maintenance
46
+ .command(updateCommand)
47
+ .demandCommand(1, 'Specify a command. Run velaro --help for a list.')
48
+ .strict()
49
+ .help()
50
+ .alias('h', 'help')
51
+ .alias('v', 'version')
52
+ .wrap(Math.min(100, process.stdout.columns || 100))
53
+ .parseAsync();
54
+
55
+ // Print update notice after command output (non-intrusive)
56
+ await printUpdateNotice();
@@ -0,0 +1,280 @@
1
+ /**
2
+ * velaro kb article — CRUD for help-center KB articles.
3
+ *
4
+ * Auth: uses the stored OAuth JWT (same as all other commands).
5
+ * API: velaro-admin (help.velaro.com), not the messaging API.
6
+ * Base URL read from VELARO_ADMIN_API_BASE env var or creds.adminApiBase,
7
+ * falling back to https://help.velaro.com.
8
+ */
9
+
10
+ import { readFileSync, existsSync } from 'fs';
11
+ import { extname } from 'path';
12
+ import { getCredentials } from '../api.js';
13
+ import { runCommand } from '../run.js';
14
+
15
+ const DEFAULT_ADMIN_API = process.env.VELARO_ADMIN_API_BASE || 'https://help.velaro.com';
16
+
17
+ async function adminRequest(method, path, body) {
18
+ const creds = await getCredentials();
19
+ const apiBase = creds.adminApiBase || DEFAULT_ADMIN_API;
20
+
21
+ const res = await fetch(`${apiBase}${path}`, {
22
+ method,
23
+ headers: {
24
+ Authorization: `Bearer ${creds.velaroToken}`,
25
+ 'Content-Type': 'application/json',
26
+ },
27
+ body: body !== undefined ? JSON.stringify(body) : undefined,
28
+ });
29
+
30
+ if (!res.ok) {
31
+ let msg = `${method} ${path} -> ${res.status}`;
32
+ try { const t = await res.text(); if (t) msg += `: ${t.slice(0, 300)}`; } catch { /* ignore */ }
33
+ throw new Error(msg);
34
+ }
35
+
36
+ const text = await res.text();
37
+ return text ? JSON.parse(text) : null;
38
+ }
39
+
40
+ // ── list ──────────────────────────────────────────────────────────────────────
41
+
42
+ const listCommand = {
43
+ command: 'list',
44
+ describe: 'List KB articles',
45
+ builder: (y) =>
46
+ y
47
+ .option('topic-id', { type: 'number', describe: 'Filter by topic ID' })
48
+ .option('search', { type: 'string', describe: 'Search term' })
49
+ .option('published', { type: 'boolean', describe: 'Show only published articles' }),
50
+ handler: runCommand(async (argv) => {
51
+ const payload = {};
52
+ if (argv['topic-id']) payload.topicId = argv['topic-id'];
53
+ if (argv.search) payload.searchTerm = argv.search;
54
+ if (argv.published) payload.publishStatus = 'Published';
55
+
56
+ const data = await adminRequest('POST', '/api/kb/articles/search', payload);
57
+ const articles = data?.articles ?? [];
58
+
59
+ if (!articles.length) { console.log('No articles found.'); return; }
60
+
61
+ console.log(`\nFound ${articles.length} article(s):\n`);
62
+ for (const a of articles) {
63
+ const status = a.isPublished ? 'published ' : 'draft ';
64
+ const views = `${a.hitCount ?? 0} views`;
65
+ console.log(` [${a.id}] ${status} ${truncate(a.title, 55)} (${views})`);
66
+ if (a.topicName) console.log(` topic: ${a.topicName}`);
67
+ }
68
+ }),
69
+ };
70
+
71
+ // ── get ───────────────────────────────────────────────────────────────────────
72
+
73
+ const getCommand = {
74
+ command: 'get <id>',
75
+ describe: 'Get a KB article by ID',
76
+ builder: (y) => y.positional('id', { type: 'number', describe: 'Article ID' }),
77
+ handler: runCommand(async (argv) => {
78
+ const a = await adminRequest('GET', `/api/kb/articles?id=${argv.id}`);
79
+ console.log(`\n[${a.id}] ${a.title}`);
80
+ console.log(` slug: ${a.slug}`);
81
+ console.log(` topic: ${a.topicId}`);
82
+ console.log(` published: ${a.isPublished}`);
83
+ console.log(` url: ${a.url ?? '(not public)'}`);
84
+ console.log(`\n--- content (HTML) ---\n${a.content ?? ''}\n`);
85
+ }),
86
+ };
87
+
88
+ // ── topics ────────────────────────────────────────────────────────────────────
89
+
90
+ const topicsCommand = {
91
+ command: 'topics',
92
+ describe: 'List available KB topics (you need a topic ID to push an article)',
93
+ handler: runCommand(async () => {
94
+ const data = await adminRequest('GET', '/api/kb/topics');
95
+ const topics = data?.topics ?? [];
96
+ if (!topics.length) { console.log('No topics found.'); return; }
97
+ console.log(`\nTopics:\n`);
98
+ for (const t of topics)
99
+ console.log(` [${t.id}] ${t.name} (${t.articleCount} articles)`);
100
+ }),
101
+ };
102
+
103
+ // ── push ──────────────────────────────────────────────────────────────────────
104
+
105
+ const pushCommand = {
106
+ command: 'push <file>',
107
+ describe: 'Create or update a KB article from a Markdown or HTML file',
108
+ builder: (y) =>
109
+ y
110
+ .positional('file', { type: 'string', describe: 'Path to .md or .html file' })
111
+ .option('title', { type: 'string', describe: 'Article title (override file frontmatter)' })
112
+ .option('slug', { type: 'string', describe: 'URL slug (override file frontmatter)' })
113
+ .option('topic-id', { type: 'number', describe: 'Topic ID (required if not in frontmatter)' })
114
+ .option('description', { type: 'string', describe: 'Short description' })
115
+ .option('publish', { type: 'boolean', default: false, describe: 'Publish immediately' })
116
+ .option('update-id', { type: 'number', describe: 'Article ID to update (if omitted, creates new)' }),
117
+ handler: runCommand(async (argv) => {
118
+ if (!existsSync(argv.file)) throw new Error(`File not found: ${argv.file}`);
119
+
120
+ const raw = readFileSync(argv.file, 'utf8');
121
+ const ext = extname(argv.file).toLowerCase();
122
+ const fm = parseFrontmatter(raw);
123
+ const body = fm.body;
124
+
125
+ const title = argv.title || fm.title || null;
126
+ const slug = argv.slug || fm.slug || null;
127
+ const topicId = argv['topic-id'] || fm.topicId || null;
128
+ const description = argv.description || fm.description || '';
129
+
130
+ if (!title) throw new Error('Title is required. Add "title:" to frontmatter or use --title.');
131
+ if (!slug) throw new Error('Slug is required. Add "slug:" to frontmatter or use --slug.');
132
+ if (!topicId) throw new Error('Topic ID is required. Run "velaro kb article topics" then use --topic-id.');
133
+
134
+ const content = ext === '.md' ? markdownToHtml(body) : body;
135
+
136
+ const payload = {
137
+ title,
138
+ slug,
139
+ topicId,
140
+ description,
141
+ content,
142
+ visibility: 'Public',
143
+ searchTags: fm.tags || '',
144
+ isPublished: argv.publish || fm.published || false,
145
+ displayPriority: 0,
146
+ enableBotResponse: fm.botResponse !== false,
147
+ botSummary: fm.botSummary || '',
148
+ isFeatured: false,
149
+ };
150
+
151
+ if (argv['update-id']) {
152
+ await adminRequest('PUT', `/api/kb/articles?id=${argv['update-id']}`, payload);
153
+ console.log(`Updated article [${argv['update-id']}]: "${title}"`);
154
+ } else {
155
+ await adminRequest('POST', '/api/kb/articles', payload);
156
+ // Look up the newly created article by slug to get its ID
157
+ const result = await adminRequest('POST', '/api/kb/articles/search', { searchTerm: slug, topicId });
158
+ const created = result?.articles?.find(a => a.slug === slug);
159
+ const id = created?.id ?? '?';
160
+ console.log(`Created article [${id}]: "${title}"`);
161
+ if (argv.publish)
162
+ console.log(` Published at: https://help.velaro.com/kb/article/${id}/${slug}`);
163
+ else
164
+ console.log(` Draft saved. Use --publish to make it public.`);
165
+ }
166
+ }),
167
+ };
168
+
169
+ // ── delete ────────────────────────────────────────────────────────────────────
170
+
171
+ const deleteCommand = {
172
+ command: 'delete <id>',
173
+ describe: 'Delete a KB article',
174
+ builder: (y) => y.positional('id', { type: 'number', describe: 'Article ID' }),
175
+ handler: runCommand(async (argv) => {
176
+ await adminRequest('DELETE', `/api/kb/articles?id=${argv.id}`);
177
+ console.log(`Article ${argv.id} deleted.`);
178
+ }),
179
+ };
180
+
181
+ // ── improve ───────────────────────────────────────────────────────────────────
182
+
183
+ const improveCommand = {
184
+ command: 'improve <id>',
185
+ describe: 'AI-improve a KB article using a file or instruction (requires VelaroCopilotApiKey)',
186
+ builder: (y) =>
187
+ y
188
+ .positional('id', { type: 'number', describe: 'Article ID to improve' })
189
+ .option('file', { type: 'string', describe: 'Path to a reference file (.md, .txt, .html) to incorporate' })
190
+ .option('instruction', { type: 'string', describe: 'Plain-language instruction, e.g. "add a troubleshooting section"' })
191
+ .option('publish', { type: 'boolean', default: false, describe: 'Publish after improving' })
192
+ .check((argv) => {
193
+ if (!argv.file && !argv.instruction) throw new Error('Provide --file and/or --instruction.');
194
+ return true;
195
+ }),
196
+ handler: runCommand(async (argv) => {
197
+ // 1. Fetch the existing article
198
+ const article = await adminRequest('GET', `/api/kb/articles?id=${argv.id}`);
199
+ console.log(`Improving: "${article.title}" [${article.id}]`);
200
+
201
+ // 2. Build prompt context
202
+ let reference = '';
203
+ if (argv.file) {
204
+ if (!existsSync(argv.file)) throw new Error(`File not found: ${argv.file}`);
205
+ reference = readFileSync(argv.file, 'utf8');
206
+ }
207
+
208
+ // 3. POST to the server-side AI improve endpoint
209
+ const result = await adminRequest('POST', `/api/kb/articles/${argv.id}/ai-improve`, {
210
+ instruction: argv.instruction || '',
211
+ referenceContent: reference,
212
+ publish: argv.publish,
213
+ });
214
+
215
+ console.log(`\nImprovement applied.`);
216
+ if (result?.changesSummary) console.log(`Changes: ${result.changesSummary}`);
217
+ if (argv.publish) console.log(`Published: https://help.velaro.com/kb/article/${article.id}/${article.slug}`);
218
+ }),
219
+ };
220
+
221
+ // ── export command ─────────────────────────────────────────────────────────────
222
+
223
+ export const articleCommand = {
224
+ command: 'article <subcommand>',
225
+ describe: 'Manage help-center KB articles',
226
+ builder: (yargs) =>
227
+ yargs
228
+ .command(listCommand)
229
+ .command(getCommand)
230
+ .command(topicsCommand)
231
+ .command(pushCommand)
232
+ .command(deleteCommand)
233
+ .command(improveCommand)
234
+ .demandCommand(1, 'Specify a subcommand: list, get, topics, push, delete, improve'),
235
+ handler: () => {},
236
+ };
237
+
238
+ // ── helpers ───────────────────────────────────────────────────────────────────
239
+
240
+ function parseFrontmatter(raw) {
241
+ const fm = {};
242
+ let body = raw;
243
+
244
+ if (raw.startsWith('---')) {
245
+ const end = raw.indexOf('\n---', 3);
246
+ if (end !== -1) {
247
+ const block = raw.slice(3, end).trim();
248
+ body = raw.slice(end + 4).trim();
249
+ for (const line of block.split('\n')) {
250
+ const sep = line.indexOf(':');
251
+ if (sep === -1) continue;
252
+ const key = line.slice(0, sep).trim();
253
+ const val = line.slice(sep + 1).trim().replace(/^["']|["']$/g, '');
254
+ if (key === 'topicId' || key === 'topic_id') fm.topicId = parseInt(val, 10);
255
+ else fm[key] = val === 'true' ? true : val === 'false' ? false : val;
256
+ }
257
+ }
258
+ }
259
+
260
+ return { ...fm, body };
261
+ }
262
+
263
+ function markdownToHtml(md) {
264
+ // Minimal markdown-to-HTML — headings, bold, inline code, paragraphs.
265
+ // For rich content use an .html file or the admin UI.
266
+ return md
267
+ .replace(/^### (.+)$/gm, '<h3>$1</h3>')
268
+ .replace(/^## (.+)$/gm, '<h2>$1</h2>')
269
+ .replace(/^# (.+)$/gm, '<h1>$1</h1>')
270
+ .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
271
+ .replace(/`([^`]+)`/g, '<code>$1</code>')
272
+ .replace(/\n\n+/g, '</p><p>')
273
+ .replace(/^(?!<[hup])/gm, '')
274
+ .replace(/^(.+)(?!>)$/gm, (m) => m.startsWith('<') ? m : `<p>${m}</p>`);
275
+ }
276
+
277
+ function truncate(str, max) {
278
+ if (!str) return '-';
279
+ return str.length <= max ? str : str.slice(0, max - 1) + '...';
280
+ }