@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.
@@ -1,132 +1,241 @@
1
- import { get, post, del } from '../api.js';
2
- import { requireFeature } from '../subscription.js';
3
- import { runCommand } from '../run.js';
4
-
5
- export const kbCommand = {
6
- command: 'kb <subcommand>',
7
- describe: 'Manage knowledge base — Q&A pairs and bot overrides',
8
- builder: (yargs) =>
9
- yargs
10
- .command(qnaCommand)
11
- .command(overrideCommand)
12
- .demandCommand(1, 'Specify a subcommand: qna, override'),
13
- handler: () => {},
14
- };
15
-
16
- // ────────────────────────────────────────────────────────────────────────────
17
- // Q&A
18
- // ────────────────────────────────────────────────────────────────────────────
19
-
20
- const qnaCommand = {
21
- command: 'qna <subcommand>',
22
- describe: 'Manage Q&A knowledge pairs',
23
- builder: (yargs) =>
24
- yargs
25
- .command({
26
- command: 'list',
27
- describe: 'List Q&A pairs',
28
- builder: (y) => y.option('bot-id', { type: 'number', describe: 'Filter by bot ID' }),
29
- handler: runCommand(async (argv) => {
30
- await requireFeature('enableKnowledgeBase', 'Knowledge Base');
31
- const params = argv['bot-id'] ? `?aiConfigurationId=${argv['bot-id']}` : '';
32
- const items = await get(`/api/BotQnA${params}`);
33
- if (!items?.length) { console.log('No Q&A pairs found.'); return; }
34
-
35
- console.log(`\nFound ${items.length} Q&A pair(s):\n`);
36
- for (const item of items) {
37
- console.log(` [${item.id}] Q: ${truncate(item.question, 70)}`);
38
- console.log(` A: ${truncate(item.answer, 70)}\n`);
39
- }
40
- }),
41
- })
42
- .command({
43
- command: 'add',
44
- describe: 'Add a Q&A pair',
45
- builder: (y) =>
46
- y
47
- .option('bot-id', { type: 'number', demandOption: true, describe: 'Bot ID' })
48
- .option('question', { type: 'string', demandOption: true, describe: 'Question text' })
49
- .option('answer', { type: 'string', demandOption: true, describe: 'Answer text' }),
50
- handler: runCommand(async (argv) => {
51
- await requireFeature('enableKnowledgeBase', 'Knowledge Base');
52
- const result = await post('/api/BotQnA', {
53
- aiConfigurationId: argv['bot-id'],
54
- question: argv.question,
55
- answer: argv.answer,
56
- });
57
- console.log(`Q&A pair added: [${result.id}]`);
58
- }),
59
- })
60
- .command({
61
- command: 'delete <id>',
62
- describe: 'Delete a Q&A pair',
63
- builder: (y) => y.positional('id', { type: 'number', describe: 'Q&A pair ID' }),
64
- handler: runCommand(async (argv) => {
65
- await requireFeature('enableKnowledgeBase', 'Knowledge Base');
66
- await del(`/api/BotQnA/${argv.id}`);
67
- console.log(`Q&A pair ${argv.id} deleted.`);
68
- }),
69
- })
70
- .demandCommand(1, 'Specify a subcommand: list, add, delete'),
71
- handler: () => {},
72
- };
73
-
74
- // ────────────────────────────────────────────────────────────────────────────
75
- // Overrides authoritative facts that take priority over KB search
76
- // ────────────────────────────────────────────────────────────────────────────
77
-
78
- const overrideCommand = {
79
- command: 'override <subcommand>',
80
- describe: 'Manage bot knowledge overrides (authoritative facts)',
81
- builder: (yargs) =>
82
- yargs
83
- .command({
84
- command: 'list',
85
- describe: 'List knowledge overrides',
86
- handler: runCommand(async () => {
87
- await requireFeature('enableKnowledgeBase', 'Knowledge Base');
88
- const items = await get('/Ticket/knowledge-overrides');
89
- if (!items?.length) { console.log('No overrides found.'); return; }
90
-
91
- console.log(`\nFound ${items.length} override(s):\n`);
92
- for (const item of items) {
93
- const status = item.isActive ? 'active ' : 'inactive';
94
- const expiry = item.expiresAt ? ` expires ${new Date(item.expiresAt).toLocaleDateString()}` : '';
95
- console.log(` [${item.id}] ${status} ${truncate(item.title ?? item.content, 60)}${expiry}`);
96
- }
97
- }),
98
- })
99
- .command({
100
- command: 'add',
101
- describe: 'Add a knowledge override',
102
- builder: (y) =>
103
- y
104
- .option('title', { type: 'string', demandOption: true, describe: 'Override title' })
105
- .option('content', { type: 'string', demandOption: true, describe: 'Authoritative content' })
106
- .option('expires', { type: 'string', describe: 'Expiry date (ISO 8601, e.g. 2026-12-31)' }),
107
- handler: runCommand(async (argv) => {
108
- await requireFeature('enableKnowledgeBase', 'Knowledge Base');
109
- const payload = { title: argv.title, content: argv.content, isActive: true };
110
- if (argv.expires) payload.expiresAt = new Date(argv.expires).toISOString();
111
- const result = await post('/Ticket/knowledge-overrides', payload);
112
- console.log(`Override added: [${result.id}] "${argv.title}"`);
113
- }),
114
- })
115
- .command({
116
- command: 'delete <id>',
117
- describe: 'Delete a knowledge override',
118
- builder: (y) => y.positional('id', { type: 'number', describe: 'Override ID' }),
119
- handler: runCommand(async (argv) => {
120
- await requireFeature('enableKnowledgeBase', 'Knowledge Base');
121
- await del(`/Ticket/knowledge-overrides/${argv.id}`);
122
- console.log(`Override ${argv.id} deleted.`);
123
- }),
124
- })
125
- .demandCommand(1, 'Specify a subcommand: list, add, delete'),
126
- handler: () => {},
127
- };
128
-
129
- function truncate(str, max) {
130
- if (!str) return '—';
131
- return str.length <= max ? str : str.slice(0, max - 1) + '…';
132
- }
1
+ import { readFileSync } from 'fs';
2
+ import { basename } from 'path';
3
+ import { get, post, del } from '../api.js';
4
+ import { requireFeature } from '../subscription.js';
5
+ import { runCommand } from '../run.js';
6
+ import { articleCommand } from './article.js';
7
+
8
+ export const kbCommand = {
9
+ command: 'kb <subcommand>',
10
+ describe: 'Manage knowledge base — articles, Q&A pairs, bot overrides, and custom content',
11
+ builder: (yargs) =>
12
+ yargs
13
+ .command(articleCommand)
14
+ .command(qnaCommand)
15
+ .command(overrideCommand)
16
+ .command(contentCommand)
17
+ .demandCommand(1, 'Specify a subcommand: article, qna, override, content'),
18
+ handler: () => {},
19
+ };
20
+
21
+ // ────────────────────────────────────────────────────────────────────────────
22
+ // Q&A
23
+ // ────────────────────────────────────────────────────────────────────────────
24
+
25
+ const qnaCommand = {
26
+ command: 'qna <subcommand>',
27
+ describe: 'Manage Q&A knowledge pairs',
28
+ builder: (yargs) =>
29
+ yargs
30
+ .command({
31
+ command: 'list',
32
+ describe: 'List Q&A pairs',
33
+ builder: (y) => y.option('bot-id', { type: 'number', describe: 'Filter by bot ID' }),
34
+ handler: runCommand(async (argv) => {
35
+ await requireFeature('enableKnowledgeBase', 'Knowledge Base');
36
+ const params = argv['bot-id'] ? `?aiConfigurationId=${argv['bot-id']}` : '';
37
+ const items = await get(`/api/BotQnA${params}`);
38
+ if (!items?.length) { console.log('No Q&A pairs found.'); return; }
39
+
40
+ console.log(`\nFound ${items.length} Q&A pair(s):\n`);
41
+ for (const item of items) {
42
+ console.log(` [${item.id}] Q: ${truncate(item.question, 70)}`);
43
+ console.log(` A: ${truncate(item.answer, 70)}\n`);
44
+ }
45
+ }),
46
+ })
47
+ .command({
48
+ command: 'add',
49
+ describe: 'Add a Q&A pair',
50
+ builder: (y) =>
51
+ y
52
+ .option('bot-id', { type: 'number', demandOption: true, describe: 'Bot ID' })
53
+ .option('question', { type: 'string', demandOption: true, describe: 'Question text' })
54
+ .option('answer', { type: 'string', demandOption: true, describe: 'Answer text' }),
55
+ handler: runCommand(async (argv) => {
56
+ await requireFeature('enableKnowledgeBase', 'Knowledge Base');
57
+ const result = await post('/api/BotQnA', {
58
+ aiConfigurationId: argv['bot-id'],
59
+ question: argv.question,
60
+ answer: argv.answer,
61
+ });
62
+ console.log(`Q&A pair added: [${result.id}]`);
63
+ }),
64
+ })
65
+ .command({
66
+ command: 'delete <id>',
67
+ describe: 'Delete a Q&A pair',
68
+ builder: (y) => y.positional('id', { type: 'number', describe: 'Q&A pair ID' }),
69
+ handler: runCommand(async (argv) => {
70
+ await requireFeature('enableKnowledgeBase', 'Knowledge Base');
71
+ await del(`/api/BotQnA/${argv.id}`);
72
+ console.log(`Q&A pair ${argv.id} deleted.`);
73
+ }),
74
+ })
75
+ .demandCommand(1, 'Specify a subcommand: list, add, delete'),
76
+ handler: () => {},
77
+ };
78
+
79
+ // ────────────────────────────────────────────────────────────────────────────
80
+ // Overrides authoritative facts that take priority over KB search
81
+ // ────────────────────────────────────────────────────────────────────────────
82
+
83
+ const overrideCommand = {
84
+ command: 'override <subcommand>',
85
+ describe: 'Manage bot knowledge overrides (authoritative facts)',
86
+ builder: (yargs) =>
87
+ yargs
88
+ .command({
89
+ command: 'list',
90
+ describe: 'List knowledge overrides',
91
+ handler: runCommand(async () => {
92
+ await requireFeature('enableKnowledgeBase', 'Knowledge Base');
93
+ const items = await get('/Ticket/knowledge-overrides');
94
+ if (!items?.length) { console.log('No overrides found.'); return; }
95
+
96
+ console.log(`\nFound ${items.length} override(s):\n`);
97
+ for (const item of items) {
98
+ const status = item.isActive ? 'active ' : 'inactive';
99
+ const expiry = item.expiresAt ? ` expires ${new Date(item.expiresAt).toLocaleDateString()}` : '';
100
+ console.log(` [${item.id}] ${status} ${truncate(item.title ?? item.content, 60)}${expiry}`);
101
+ }
102
+ }),
103
+ })
104
+ .command({
105
+ command: 'add',
106
+ describe: 'Add a knowledge override',
107
+ builder: (y) =>
108
+ y
109
+ .option('title', { type: 'string', demandOption: true, describe: 'Override title' })
110
+ .option('content', { type: 'string', demandOption: true, describe: 'Authoritative content' })
111
+ .option('expires', { type: 'string', describe: 'Expiry date (ISO 8601, e.g. 2026-12-31)' }),
112
+ handler: runCommand(async (argv) => {
113
+ await requireFeature('enableKnowledgeBase', 'Knowledge Base');
114
+ const payload = { title: argv.title, content: argv.content, isActive: true };
115
+ if (argv.expires) payload.expiresAt = new Date(argv.expires).toISOString();
116
+ const result = await post('/Ticket/knowledge-overrides', payload);
117
+ console.log(`Override added: [${result.id}] "${argv.title}"`);
118
+ }),
119
+ })
120
+ .command({
121
+ command: 'delete <id>',
122
+ describe: 'Delete a knowledge override',
123
+ builder: (y) => y.positional('id', { type: 'number', describe: 'Override ID' }),
124
+ handler: runCommand(async (argv) => {
125
+ await requireFeature('enableKnowledgeBase', 'Knowledge Base');
126
+ await del(`/Ticket/knowledge-overrides/${argv.id}`);
127
+ console.log(`Override ${argv.id} deleted.`);
128
+ }),
129
+ })
130
+ .demandCommand(1, 'Specify a subcommand: list, add, delete'),
131
+ handler: () => {},
132
+ };
133
+
134
+ // ────────────────────────────────────────────────────────────────────────────
135
+ // Custom content — push arbitrary text/files into the site's KB index
136
+ // ────────────────────────────────────────────────────────────────────────────
137
+ // Note: content goes into the site's default index (determined server-side by
138
+ // SiteId). Index selection is not yet supported by this API endpoint.
139
+
140
+ const contentCommand = {
141
+ command: 'content <subcommand>',
142
+ describe: 'Push custom text or files into the knowledge base index',
143
+ builder: (yargs) =>
144
+ yargs
145
+ .command({
146
+ command: 'ingest',
147
+ describe: 'Push a file or text snippet into the KB index',
148
+ builder: (y) =>
149
+ y
150
+ .option('file', {
151
+ describe: 'Path to a .txt or .md file to ingest',
152
+ type: 'string',
153
+ })
154
+ .option('text', {
155
+ describe: 'Inline text content to ingest (alternative to --file)',
156
+ type: 'string',
157
+ })
158
+ .option('content-id', {
159
+ describe: 'Stable ID for this document (used for dedup/updates). Defaults to filename.',
160
+ type: 'string',
161
+ })
162
+ .option('title', {
163
+ describe: 'Document title shown when bot attributes this content. Defaults to filename.',
164
+ type: 'string',
165
+ })
166
+ .option('source-url', {
167
+ describe: 'Canonical URL to show when bot cites this content',
168
+ type: 'string',
169
+ })
170
+ .check((argv) => {
171
+ if (!argv.file && !argv.text) throw new Error('Provide either --file or --text');
172
+ if (argv.file && argv.text) throw new Error('Use --file or --text, not both');
173
+ return true;
174
+ }),
175
+ handler: runCommand(async (argv) => {
176
+ await requireFeature('enableKnowledgeBase', 'Knowledge Base');
177
+
178
+ let text, defaultId, defaultTitle;
179
+ if (argv.file) {
180
+ text = readFileSync(argv.file, 'utf8');
181
+ defaultId = basename(argv.file);
182
+ defaultTitle = basename(argv.file);
183
+ } else {
184
+ text = argv.text;
185
+ defaultId = `inline-${Date.now()}`;
186
+ defaultTitle = defaultId;
187
+ }
188
+
189
+ const contentId = argv['content-id'] || defaultId;
190
+ const title = argv.title || defaultTitle;
191
+
192
+ const result = await post('/AzureIndexes/IngestContent', {
193
+ contentId,
194
+ title,
195
+ text,
196
+ sourceUrl: argv['source-url'] || undefined,
197
+ });
198
+
199
+ if (result.unchanged) {
200
+ console.log(`Unchanged: "${title}" — content hash matches, no re-embedding needed.`);
201
+ } else {
202
+ console.log(`Indexed: "${title}" [${contentId}]`);
203
+ console.log(` ${result.chunks} chunk(s) embedded and searchable immediately.`);
204
+ }
205
+ }),
206
+ })
207
+ .command({
208
+ command: 'list',
209
+ describe: 'List all custom content documents in the KB index',
210
+ handler: runCommand(async () => {
211
+ await requireFeature('enableKnowledgeBase', 'Knowledge Base');
212
+ const items = await get('/AzureIndexes/IngestContent');
213
+ if (!items?.length) { console.log('No custom content found.'); return; }
214
+
215
+ console.log(`\nFound ${items.length} document(s):\n`);
216
+ for (const item of items) {
217
+ const indexed = item.lastIndexedAt ? new Date(item.lastIndexedAt).toLocaleDateString() : 'never';
218
+ const status = item.lastError ? ` error: ${item.lastError}` : '';
219
+ console.log(` [${item.contentId}] chunks=${item.chunkCount ?? '?'} indexed=${indexed}${status}`);
220
+ if (item.title && item.title !== item.contentId) console.log(` "${item.title}"`);
221
+ }
222
+ }),
223
+ })
224
+ .command({
225
+ command: 'remove <content-id>',
226
+ describe: 'Remove a custom content document from the KB index',
227
+ builder: (y) => y.positional('content-id', { type: 'string', describe: 'Content ID to remove' }),
228
+ handler: runCommand(async (argv) => {
229
+ await requireFeature('enableKnowledgeBase', 'Knowledge Base');
230
+ await del(`/AzureIndexes/IngestContent/${encodeURIComponent(argv['content-id'])}`);
231
+ console.log(`Removed "${argv['content-id']}" from the index.`);
232
+ }),
233
+ })
234
+ .demandCommand(1, 'Specify a subcommand: ingest, list, remove'),
235
+ handler: () => {},
236
+ };
237
+
238
+ function truncate(str, max) {
239
+ if (!str) return '—';
240
+ return str.length <= max ? str : str.slice(0, max - 1) + '…';
241
+ }
@@ -1,59 +1,60 @@
1
- import { requestDeviceCode, pollForToken, exchangeForVelaroToken } from '../oauth.js';
2
- import { readConfig, writeConfig, DEFAULT_API_BASE, STAGING_API_BASE } from '../config.js';
3
- import { runCommand } from '../run.js';
4
-
5
- export const loginCommand = {
6
- command: 'login',
7
- describe: 'Authenticate with Velaro using your browser',
8
- builder: (y) =>
9
- y
10
- .option('staging', {
11
- describe: 'Connect to the Velaro staging environment',
12
- type: 'boolean',
13
- default: false,
14
- })
15
- .option('api', {
16
- describe: 'Override the Velaro API base URL (advanced)',
17
- type: 'string',
18
- }),
19
-
20
- handler: runCommand(async (argv) => {
21
- const apiBase = argv.api ?? (argv.staging ? STAGING_API_BASE : DEFAULT_API_BASE);
22
-
23
- console.log('Starting Velaro login...\n');
24
-
25
- const deviceData = await requestDeviceCode();
26
-
27
- console.log(` Open: ${deviceData.verification_uri}`);
28
- console.log(` Enter: ${deviceData.user_code}\n`);
29
- console.log('Waiting for you to complete login in your browser...');
30
-
31
- const entraTokens = await pollForToken(deviceData.device_code, deviceData.interval ?? 5);
32
- const velaroResult = await exchangeForVelaroToken(entraTokens.access_token, apiBase);
33
-
34
- writeConfig({
35
- ...readConfig(),
36
- apiBase,
37
- velaroToken: velaroResult.token.token,
38
- velaroExpires: velaroResult.token.expires,
39
- entraRefreshToken: entraTokens.refresh_token,
40
- siteId: velaroResult.profile?.SiteId,
41
- userName: velaroResult.profile?.Name,
42
- });
43
-
44
- const envLabel = argv.staging || argv.api ? ` (${apiBase})` : '';
45
- console.log(`\nLogged in as ${velaroResult.profile?.Name ?? velaroResult.profile?.UserName}${envLabel}`);
46
- console.log(`Site ID: ${velaroResult.profile?.SiteId}`);
47
- console.log('Credentials saved to ~/.velaro/config.json');
48
- }),
49
- };
50
-
51
- export const logoutCommand = {
52
- command: 'logout',
53
- describe: 'Clear stored credentials',
54
- handler: runCommand(async () => {
55
- const { clearConfig } = await import('../config.js');
56
- clearConfig();
57
- console.log('Logged out. Credentials removed from ~/.velaro/config.json');
58
- }),
59
- };
1
+ import { requestDeviceCode, pollForToken, exchangeForVelaroToken } from '../oauth.js';
2
+ import { readConfig, writeConfig, DEFAULT_API_BASE, STAGING_API_BASE } from '../config.js';
3
+ import { runCommand } from '../run.js';
4
+
5
+ export const loginCommand = {
6
+ command: 'login',
7
+ describe: 'Authenticate with Velaro using your browser',
8
+ builder: (y) =>
9
+ y
10
+ .option('staging', {
11
+ describe: 'Connect to the Velaro staging environment',
12
+ type: 'boolean',
13
+ default: false,
14
+ })
15
+ .option('api', {
16
+ describe: 'Override the Velaro API base URL. Set VELARO_API_BASE env var to make it permanent. ' +
17
+ 'Production cutover: set this to your production API URL before going live.',
18
+ type: 'string',
19
+ }),
20
+
21
+ handler: runCommand(async (argv) => {
22
+ const apiBase = argv.api ?? (argv.staging ? STAGING_API_BASE : DEFAULT_API_BASE);
23
+
24
+ console.log('Starting Velaro login...\n');
25
+
26
+ const deviceData = await requestDeviceCode();
27
+
28
+ console.log(` Open: ${deviceData.verification_uri}`);
29
+ console.log(` Enter: ${deviceData.user_code}\n`);
30
+ console.log('Waiting for you to complete login in your browser...');
31
+
32
+ const entraTokens = await pollForToken(deviceData.device_code, deviceData.interval ?? 5);
33
+ const velaroResult = await exchangeForVelaroToken(entraTokens.access_token, apiBase);
34
+
35
+ writeConfig({
36
+ ...readConfig(),
37
+ apiBase,
38
+ velaroToken: velaroResult.token.token,
39
+ velaroExpires: velaroResult.token.expires,
40
+ entraRefreshToken: entraTokens.refresh_token,
41
+ siteId: velaroResult.profile?.SiteId,
42
+ userName: velaroResult.profile?.Name,
43
+ });
44
+
45
+ const envLabel = argv.staging || argv.api ? ` (${apiBase})` : '';
46
+ console.log(`\nLogged in as ${velaroResult.profile?.Name ?? velaroResult.profile?.UserName}${envLabel}`);
47
+ console.log(`Site ID: ${velaroResult.profile?.SiteId}`);
48
+ console.log('Credentials saved to ~/.velaro/config.json');
49
+ }),
50
+ };
51
+
52
+ export const logoutCommand = {
53
+ command: 'logout',
54
+ describe: 'Clear stored credentials',
55
+ handler: runCommand(async () => {
56
+ const { clearConfig } = await import('../config.js');
57
+ clearConfig();
58
+ console.log('Logged out. Credentials removed from ~/.velaro/config.json');
59
+ }),
60
+ };