@velaro/cli 0.5.0 → 0.7.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
@@ -13,6 +13,8 @@ import { ruleCommand } from '../lib/commands/rule.js';
13
13
  import { agentCommand } from '../lib/commands/agent.js';
14
14
  import { ingestCommand } from '../lib/commands/ingest.js';
15
15
  import { mcpKeyCommand } from '../lib/commands/mcp-key.js';
16
+ import { indexCommand } from '../lib/commands/index.js';
17
+ import { opsCommand } from '../lib/commands/ops.js';
16
18
  import { statusCommand } from '../lib/commands/status.js';
17
19
  import { siteCommand } from '../lib/commands/site.js';
18
20
  import { checkCommand } from '../lib/commands/check.js';
@@ -41,9 +43,11 @@ await yargs(hideBin(process.argv))
41
43
  .command(workflowCommand)
42
44
  .command(ruleCommand)
43
45
  .command(agentCommand)
44
- // Integrations / keys
46
+ // Integrations / keys / indexes
45
47
  .command(ingestCommand)
46
48
  .command(mcpKeyCommand)
49
+ .command(indexCommand)
50
+ .command(opsCommand)
47
51
  // CLI maintenance
48
52
  .command(updateCommand)
49
53
  .demandCommand(1, 'Specify a command. Run velaro --help for a list.')
package/lib/api.js CHANGED
@@ -1,52 +1,52 @@
1
- import { readConfig, writeConfig, getActiveEnv, setEnvCredentials, ENVS } from './config.js';
2
- import { refreshVelaroToken } from './oauth.js';
3
-
4
- const REFRESH_BUFFER_MS = 5 * 60 * 1000;
5
-
6
- export async function getCredentials(envOverride) {
7
- const cfg = readConfig();
8
- const env = envOverride || cfg.activeEnv || 'prod';
9
- let creds = cfg.envs?.[env];
10
-
11
- if (!creds?.velaroToken) {
12
- throw new Error(`Not logged in to ${env}. Run: velaro login${env === 'staging' ? ' --staging' : ''}`);
13
- }
14
-
15
- if (Date.now() + REFRESH_BUFFER_MS >= new Date(creds.velaroExpires).getTime()) {
16
- if (!creds.entraRefreshToken) {
17
- throw new Error(`Session expired for ${env}. Run: velaro login${env === 'staging' ? ' --staging' : ''}`);
18
- }
19
- const refreshed = await refreshVelaroToken({ ...creds, apiBase: creds.adminApiBase });
20
- const updated = { ...creds, ...refreshed };
21
- setEnvCredentials(env, updated);
22
- creds = updated;
23
- }
24
-
25
- return { ...creds, env };
26
- }
27
-
28
- export async function request(method, path, body, envOverride) {
29
- const creds = await getCredentials(envOverride);
30
-
31
- const res = await fetch(`${creds.adminApiBase}${path}`, {
32
- method,
33
- headers: {
34
- Authorization: `Bearer ${creds.velaroToken}`,
35
- 'Content-Type': 'application/json',
36
- },
37
- body: body !== undefined ? JSON.stringify(body) : undefined,
38
- });
39
-
40
- if (!res.ok) {
41
- let msg = `${method} ${path} → ${res.status}`;
42
- try { const t = await res.text(); if (t) msg += `: ${t}`; } catch { /* status already captured */ }
43
- throw new Error(msg);
44
- }
45
-
46
- const text = await res.text();
47
- return text ? JSON.parse(text) : null;
48
- }
49
-
50
- export const get = (path) => request('GET', path);
51
- export const post = (path, body) => request('POST', path, body);
52
- export const del = (path) => request('DELETE', path);
1
+ import { readConfig, writeConfig, getActiveEnv, setEnvCredentials, ENVS } from './config.js';
2
+ import { refreshVelaroToken } from './oauth.js';
3
+
4
+ const REFRESH_BUFFER_MS = 5 * 60 * 1000;
5
+
6
+ export async function getCredentials(envOverride) {
7
+ const cfg = readConfig();
8
+ const env = envOverride || cfg.activeEnv || 'prod';
9
+ let creds = cfg.envs?.[env];
10
+
11
+ if (!creds?.velaroToken) {
12
+ throw new Error(`Not logged in to ${env}. Run: velaro login${env === 'staging' ? ' --staging' : ''}`);
13
+ }
14
+
15
+ if (Date.now() + REFRESH_BUFFER_MS >= new Date(creds.velaroExpires).getTime()) {
16
+ if (!creds.entraRefreshToken) {
17
+ throw new Error(`Session expired for ${env}. Run: velaro login${env === 'staging' ? ' --staging' : ''}`);
18
+ }
19
+ const refreshed = await refreshVelaroToken({ ...creds, apiBase: creds.adminApiBase });
20
+ const updated = { ...creds, ...refreshed };
21
+ setEnvCredentials(env, updated);
22
+ creds = updated;
23
+ }
24
+
25
+ return { ...creds, env };
26
+ }
27
+
28
+ export async function request(method, path, body, envOverride) {
29
+ const creds = await getCredentials(envOverride);
30
+
31
+ const res = await fetch(`${creds.adminApiBase}${path}`, {
32
+ method,
33
+ headers: {
34
+ Authorization: `Bearer ${creds.velaroToken}`,
35
+ 'Content-Type': 'application/json',
36
+ },
37
+ body: body !== undefined ? JSON.stringify(body) : undefined,
38
+ });
39
+
40
+ if (!res.ok) {
41
+ let msg = `${method} ${path} → ${res.status}`;
42
+ try { const t = await res.text(); if (t) msg += `: ${t}`; } catch { /* status already captured */ }
43
+ throw new Error(msg);
44
+ }
45
+
46
+ const text = await res.text();
47
+ return text ? JSON.parse(text) : null;
48
+ }
49
+
50
+ export const get = (path) => request('GET', path);
51
+ export const post = (path, body) => request('POST', path, body);
52
+ export const del = (path) => request('DELETE', path);
@@ -1,50 +1,50 @@
1
- import { get, post } from '../api.js';
2
- import { runCommand } from '../run.js';
3
-
4
- const VALID_STATUSES = ['Available', 'Away', 'Offline'];
5
-
6
- export const agentCommand = {
7
- command: 'agent <subcommand>',
8
- describe: 'View and manage agents',
9
- builder: (yargs) => yargs
10
- .command({
11
- command: 'list',
12
- describe: 'List agents and their current availability',
13
- handler: runCommand(async () => {
14
- const [agents, teams] = await Promise.all([
15
- get('/Users/List'),
16
- get('/Teams/List'),
17
- ]);
18
-
19
- const teamMap = Object.fromEntries(teams.map(t => [t.id, t.name]));
20
-
21
- if (!agents.length) { console.log('No agents found.'); return; }
22
-
23
- const rows = agents.map(a => ({
24
- id: `[${a.id}]`,
25
- status: (a.status ?? 'Offline').padEnd(9),
26
- name: a.displayName || `${a.firstName} ${a.lastName}`.trim() || a.email,
27
- teams: (a.teamIds ?? []).map(id => teamMap[id] ?? `team ${id}`).join(', ') || '—',
28
- }));
29
-
30
- const idW = Math.max(...rows.map(r => r.id.length));
31
-
32
- for (const r of rows) {
33
- console.log(`${r.id.padStart(idW)} ${r.status} ${r.name.padEnd(30)} ${r.teams}`);
34
- }
35
- console.log(`\n${rows.length} agent(s)`);
36
- }),
37
- })
38
- .command({
39
- command: 'set-status',
40
- describe: 'Set an agent\'s availability status (admin only)',
41
- builder: (y) => y
42
- .option('id', { type: 'number', demandOption: true, describe: 'Agent user ID' })
43
- .option('status', { type: 'string', demandOption: true, describe: 'Available | Away | Offline', choices: VALID_STATUSES }),
44
- handler: runCommand(async (argv) => {
45
- await post('/UserStatus/admin', { userId: argv.id, status: argv.status });
46
- console.log(`Agent ${argv.id} set to ${argv.status}.`);
47
- }),
48
- })
49
- .demandCommand(1, 'Specify a subcommand: list | set-status'),
50
- };
1
+ import { get, post } from '../api.js';
2
+ import { runCommand } from '../run.js';
3
+
4
+ const VALID_STATUSES = ['Available', 'Away', 'Offline'];
5
+
6
+ export const agentCommand = {
7
+ command: 'agent <subcommand>',
8
+ describe: 'View and manage agents',
9
+ builder: (yargs) => yargs
10
+ .command({
11
+ command: 'list',
12
+ describe: 'List agents and their current availability',
13
+ handler: runCommand(async () => {
14
+ const [agents, teams] = await Promise.all([
15
+ get('/Users/List'),
16
+ get('/Teams/List'),
17
+ ]);
18
+
19
+ const teamMap = Object.fromEntries(teams.map(t => [t.id, t.name]));
20
+
21
+ if (!agents.length) { console.log('No agents found.'); return; }
22
+
23
+ const rows = agents.map(a => ({
24
+ id: `[${a.id}]`,
25
+ status: (a.status ?? 'Offline').padEnd(9),
26
+ name: a.displayName || `${a.firstName} ${a.lastName}`.trim() || a.email,
27
+ teams: (a.teamIds ?? []).map(id => teamMap[id] ?? `team ${id}`).join(', ') || '—',
28
+ }));
29
+
30
+ const idW = Math.max(...rows.map(r => r.id.length));
31
+
32
+ for (const r of rows) {
33
+ console.log(`${r.id.padStart(idW)} ${r.status} ${r.name.padEnd(30)} ${r.teams}`);
34
+ }
35
+ console.log(`\n${rows.length} agent(s)`);
36
+ }),
37
+ })
38
+ .command({
39
+ command: 'set-status',
40
+ describe: 'Set an agent\'s availability status (admin only)',
41
+ builder: (y) => y
42
+ .option('id', { type: 'number', demandOption: true, describe: 'Agent user ID' })
43
+ .option('status', { type: 'string', demandOption: true, describe: 'Available | Away | Offline', choices: VALID_STATUSES }),
44
+ handler: runCommand(async (argv) => {
45
+ await post('/UserStatus/admin', { userId: argv.id, status: argv.status });
46
+ console.log(`Agent ${argv.id} set to ${argv.status}.`);
47
+ }),
48
+ })
49
+ .demandCommand(1, 'Specify a subcommand: list | set-status'),
50
+ };
@@ -178,6 +178,30 @@ const deleteCommand = {
178
178
  }),
179
179
  };
180
180
 
181
+ // ── seed-views ────────────────────────────────────────────────────────────────
182
+
183
+ const seedViewsCommand = {
184
+ command: 'seed-views <id>',
185
+ describe: 'Seed an article with random view counts (for new article submissions)',
186
+ builder: (y) =>
187
+ y
188
+ .positional('id', { type: 'number', describe: 'Article ID to seed' })
189
+ .option('hits', { type: 'number', default: 0, describe: 'Hit count (0 = random 1000-1999)' }),
190
+ handler: runCommand(async (argv) => {
191
+ const hitCount = argv.hits === 0 ? (Math.floor(Math.random() * 1000) + 1000) : argv.hits;
192
+ const payload = { articleId: argv.id, hitCount };
193
+
194
+ try {
195
+ // Call API endpoint using stored OAuth token — no credentials in code
196
+ await adminRequest('POST', '/api/kb/articles/seed-views', payload);
197
+ console.log(`✅ Article [${argv.id}] seeded with ${hitCount} views.`);
198
+ } catch (e) {
199
+ console.error(`❌ Failed to seed article: ${e.message}`);
200
+ throw e;
201
+ }
202
+ }),
203
+ };
204
+
181
205
  // ── improve ───────────────────────────────────────────────────────────────────
182
206
 
183
207
  const improveCommand = {
@@ -230,8 +254,9 @@ export const articleCommand = {
230
254
  .command(topicsCommand)
231
255
  .command(pushCommand)
232
256
  .command(deleteCommand)
257
+ .command(seedViewsCommand)
233
258
  .command(improveCommand)
234
- .demandCommand(1, 'Specify a subcommand: list, get, topics, push, delete, improve'),
259
+ .demandCommand(1, 'Specify a subcommand: list, get, topics, push, delete, seed-views, improve'),
235
260
  handler: () => {},
236
261
  };
237
262
 
@@ -260,18 +285,101 @@ function parseFrontmatter(raw) {
260
285
  return { ...fm, body };
261
286
  }
262
287
 
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>')
288
+ // help.velaro.com kb.css styles .kb-article-body table/th/td natively (header #1e3a5f,
289
+ // rounded corners, shadow, horizontal-scroll wrapper). Admin editor preview uses the same class.
290
+ // Inline fallback styles cover any external renderer that doesn't ship our CSS.
291
+ const TABLE_STYLE = 'border-collapse:collapse;width:100%;margin:16px 0;font-size:14px;';
292
+ const TH_STYLE = 'border:1px solid #1e3a5f;padding:11px 16px;background:#1e3a5f;color:#fff;text-align:left;font-weight:600;';
293
+ const TD_STYLE = 'border:1px solid #e2e8f0;padding:10px 14px;vertical-align:top;color:#1e293b;';
294
+
295
+ function inlineMd(s) {
296
+ return s
270
297
  .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>`);
298
+ .replace(/(^|[^*])\*([^*\n]+)\*/g, '$1<em>$2</em>')
299
+ .replace(/`([^`]+)`/g, '<code>$1</code>')
300
+ .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
301
+ }
302
+
303
+ function markdownToHtml(md) {
304
+ const lines = md.replace(/\r\n/g, '\n').split('\n');
305
+ const out = [];
306
+ let i = 0;
307
+ let para = [];
308
+ let list = null; // { type: 'ul'|'ol', items: [] }
309
+
310
+ const flushPara = () => {
311
+ if (para.length) {
312
+ out.push(`<p>${inlineMd(para.join(' '))}</p>`);
313
+ para = [];
314
+ }
315
+ };
316
+ const flushList = () => {
317
+ if (list) {
318
+ out.push(`<${list.type}>` + list.items.map(x => `<li>${inlineMd(x)}</li>`).join('') + `</${list.type}>`);
319
+ list = null;
320
+ }
321
+ };
322
+ const flushAll = () => { flushPara(); flushList(); };
323
+
324
+ const isTableSep = (s) => /^\s*\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)+\|?\s*$/.test(s);
325
+ const splitRow = (s) => {
326
+ let t = s.trim();
327
+ if (t.startsWith('|')) t = t.slice(1);
328
+ if (t.endsWith('|')) t = t.slice(0, -1);
329
+ return t.split('|').map(c => c.trim());
330
+ };
331
+
332
+ while (i < lines.length) {
333
+ const line = lines[i];
334
+
335
+ if (/^\s*$/.test(line)) { flushAll(); i++; continue; }
336
+
337
+ const h = line.match(/^(#{1,6})\s+(.+)$/);
338
+ if (h) { flushAll(); out.push(`<h${h[1].length}>${inlineMd(h[2])}</h${h[1].length}>`); i++; continue; }
339
+
340
+ // Pipe table
341
+ if (line.includes('|') && i + 1 < lines.length && isTableSep(lines[i + 1])) {
342
+ flushAll();
343
+ const header = splitRow(line);
344
+ i += 2;
345
+ const rows = [];
346
+ while (i < lines.length && lines[i].includes('|') && lines[i].trim() !== '') {
347
+ rows.push(splitRow(lines[i]));
348
+ i++;
349
+ }
350
+ const thead = `<thead><tr>${header.map(c => `<th style="${TH_STYLE}">${inlineMd(c)}</th>`).join('')}</tr></thead>`;
351
+ const tbody = `<tbody>${rows.map(r => `<tr>${r.map(c => `<td style="${TD_STYLE}">${inlineMd(c)}</td>`).join('')}</tr>`).join('')}</tbody>`;
352
+ out.push(`<table class="velaro-kb-table" style="${TABLE_STYLE}">${thead}${tbody}</table>`);
353
+ continue;
354
+ }
355
+
356
+ const ul = line.match(/^\s*[-*]\s+(.+)$/);
357
+ if (ul) {
358
+ flushPara();
359
+ if (!list || list.type !== 'ul') { flushList(); list = { type: 'ul', items: [] }; }
360
+ list.items.push(ul[1]);
361
+ i++; continue;
362
+ }
363
+ const ol = line.match(/^\s*\d+\.\s+(.+)$/);
364
+ if (ol) {
365
+ flushPara();
366
+ if (!list || list.type !== 'ol') { flushList(); list = { type: 'ol', items: [] }; }
367
+ list.items.push(ol[1]);
368
+ i++; continue;
369
+ }
370
+
371
+ if (/^<(h[1-6]|table|ul|ol|p|div|pre|blockquote)/i.test(line.trim())) {
372
+ flushAll();
373
+ out.push(line);
374
+ i++; continue;
375
+ }
376
+
377
+ flushList();
378
+ para.push(line.trim());
379
+ i++;
380
+ }
381
+ flushAll();
382
+ return out.join('\n');
275
383
  }
276
384
 
277
385
  function truncate(str, max) {