@mintlify/cli 4.0.1083 → 4.0.1085
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/__test__/analytics/client.test.ts +158 -0
- package/__test__/analytics/format.test.ts +139 -0
- package/bin/analytics/client.js +73 -0
- package/bin/analytics/format.js +13 -0
- package/bin/analytics/index.js +524 -0
- package/bin/analytics/output.js +74 -0
- package/bin/analytics/types.js +1 -0
- package/bin/cli.js +56 -0
- package/bin/config.js +14 -0
- package/bin/keyring.js +5 -2
- package/bin/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +7 -7
- package/src/analytics/client.ts +164 -0
- package/src/analytics/format.ts +13 -0
- package/src/analytics/index.tsx +620 -0
- package/src/analytics/output.ts +97 -0
- package/src/analytics/types.ts +132 -0
- package/src/cli.tsx +88 -0
- package/src/config.ts +16 -0
- package/src/keyring.ts +3 -1
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
import { addLog, ErrorLog, SpinnerLog, removeLastLog } from '@mintlify/previewing';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { Text } from 'ink';
|
|
4
|
+
import type { Argv } from 'yargs';
|
|
5
|
+
|
|
6
|
+
import { getConfigValue } from '../config.js';
|
|
7
|
+
import { terminate } from '../helpers.js';
|
|
8
|
+
import {
|
|
9
|
+
getBucketThreads,
|
|
10
|
+
getBuckets,
|
|
11
|
+
getConversations,
|
|
12
|
+
getFeedback,
|
|
13
|
+
getFeedbackByPage,
|
|
14
|
+
getKpi,
|
|
15
|
+
getSearches,
|
|
16
|
+
} from './client.js';
|
|
17
|
+
import { num, truncate } from './format.js';
|
|
18
|
+
import { formatBarChart, formatOutput, resolveFormat, type OutputFormat } from './output.js';
|
|
19
|
+
import type { Conversation } from './types.js';
|
|
20
|
+
|
|
21
|
+
const withSubdomain = <T extends object>(yargs: Argv<T>) =>
|
|
22
|
+
yargs.option('subdomain', {
|
|
23
|
+
type: 'string' as const,
|
|
24
|
+
default: getConfigValue('subdomain'),
|
|
25
|
+
description: 'Documentation subdomain (default: mint config set subdomain)',
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
function defaultFrom(): string {
|
|
29
|
+
const configured = getConfigValue('dateFrom');
|
|
30
|
+
if (configured) return configured;
|
|
31
|
+
const d = new Date();
|
|
32
|
+
d.setDate(d.getDate() - 7);
|
|
33
|
+
return d.toISOString().slice(0, 10);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function defaultTo(): string {
|
|
37
|
+
return getConfigValue('dateTo') ?? new Date().toISOString().slice(0, 10);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const withDates = <T extends object>(yargs: Argv<T>) =>
|
|
41
|
+
yargs
|
|
42
|
+
.option('from', {
|
|
43
|
+
type: 'string' as const,
|
|
44
|
+
default: defaultFrom(),
|
|
45
|
+
description: 'Start date (YYYY-MM-DD)',
|
|
46
|
+
})
|
|
47
|
+
.option('to', {
|
|
48
|
+
type: 'string' as const,
|
|
49
|
+
default: defaultTo(),
|
|
50
|
+
description: 'End date (YYYY-MM-DD)',
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const withFormat = <T extends object>(yargs: Argv<T>) =>
|
|
54
|
+
yargs
|
|
55
|
+
.option('format', {
|
|
56
|
+
type: 'string' as const,
|
|
57
|
+
choices: ['table', 'plain', 'json', 'graph'] as const,
|
|
58
|
+
description: 'Output format (table=pretty, plain=pipeable, json=raw)',
|
|
59
|
+
})
|
|
60
|
+
.option('agent', {
|
|
61
|
+
type: 'boolean' as const,
|
|
62
|
+
description: 'Agent-friendly output (equivalent to --format json)',
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const withAll = <T extends object>(yargs: Argv<T>) => withFormat(withDates(withSubdomain(yargs)));
|
|
66
|
+
|
|
67
|
+
function output(format: OutputFormat, text: string) {
|
|
68
|
+
if (format === 'table') {
|
|
69
|
+
addLog(<Text>{text}</Text>);
|
|
70
|
+
} else {
|
|
71
|
+
process.stdout.write(text + '\n');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const analyticsBuilder = (yargs: Argv) =>
|
|
76
|
+
yargs
|
|
77
|
+
.command(
|
|
78
|
+
'stats',
|
|
79
|
+
'display KPI numbers (views, visitors, searches)',
|
|
80
|
+
(yargs) =>
|
|
81
|
+
withAll(yargs)
|
|
82
|
+
.option('agents', { type: 'boolean', description: 'Show only agent traffic' })
|
|
83
|
+
.option('humans', { type: 'boolean', description: 'Show only human traffic' })
|
|
84
|
+
.option('page', { type: 'string', description: 'Filter to a specific page path' }),
|
|
85
|
+
async (argv) => {
|
|
86
|
+
const format = resolveFormat(argv);
|
|
87
|
+
try {
|
|
88
|
+
if (format === 'table') addLog(<SpinnerLog message="Fetching analytics..." />);
|
|
89
|
+
const kpi = await getKpi(
|
|
90
|
+
{ dateFrom: argv.from, dateTo: argv.to, page: argv.page },
|
|
91
|
+
argv.subdomain
|
|
92
|
+
);
|
|
93
|
+
if (format === 'table') removeLastLog();
|
|
94
|
+
|
|
95
|
+
if (format === 'json') {
|
|
96
|
+
output(format, JSON.stringify(kpi, null, 2));
|
|
97
|
+
await terminate(0);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (format === 'plain') {
|
|
102
|
+
const lines = [
|
|
103
|
+
['METRIC', 'HUMAN', 'AGENT', 'TOTAL'].join('\t'),
|
|
104
|
+
['Views', kpi.humanViews, kpi.agentViews, kpi.humanViews + kpi.agentViews].join('\t'),
|
|
105
|
+
[
|
|
106
|
+
'Visitors',
|
|
107
|
+
kpi.humanVisitors,
|
|
108
|
+
kpi.agentVisitors,
|
|
109
|
+
kpi.humanVisitors + kpi.agentVisitors,
|
|
110
|
+
].join('\t'),
|
|
111
|
+
['Searches', kpi.humanSearches, '', kpi.humanSearches].join('\t'),
|
|
112
|
+
['Feedback', kpi.humanFeedback, '', kpi.humanFeedback].join('\t'),
|
|
113
|
+
[
|
|
114
|
+
'Assistant',
|
|
115
|
+
kpi.humanAssistant,
|
|
116
|
+
kpi.agentMcpSearches,
|
|
117
|
+
kpi.humanAssistant + kpi.agentMcpSearches,
|
|
118
|
+
].join('\t'),
|
|
119
|
+
];
|
|
120
|
+
output(format, lines.join('\n'));
|
|
121
|
+
await terminate(0);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (format === 'graph') {
|
|
126
|
+
const label = argv.subdomain ?? 'default';
|
|
127
|
+
const lines: string[] = [];
|
|
128
|
+
lines.push(chalk.bold(`\nAnalytics \u2014 ${label} (${argv.from} to ${argv.to})\n`));
|
|
129
|
+
lines.push(chalk.bold(' Human vs Agent\n'));
|
|
130
|
+
lines.push(
|
|
131
|
+
formatBarChart([
|
|
132
|
+
{ label: 'Human Views', value: kpi.humanViews, color: 'cyan' },
|
|
133
|
+
{ label: 'Agent Views', value: kpi.agentViews, color: 'magenta' },
|
|
134
|
+
{ label: 'Human Visitors', value: kpi.humanVisitors, color: 'cyan' },
|
|
135
|
+
{ label: 'Agent Visitors', value: kpi.agentVisitors, color: 'magenta' },
|
|
136
|
+
])
|
|
137
|
+
);
|
|
138
|
+
lines.push(chalk.bold('\n\n Engagement\n'));
|
|
139
|
+
lines.push(
|
|
140
|
+
formatBarChart([
|
|
141
|
+
{ label: 'Searches', value: kpi.humanSearches, color: 'yellow' },
|
|
142
|
+
{ label: 'Feedback', value: kpi.humanFeedback, color: 'green' },
|
|
143
|
+
{ label: 'Assistant (web)', value: kpi.humanAssistant, color: 'blue' },
|
|
144
|
+
{ label: 'Assistant (API)', value: kpi.agentMcpSearches, color: 'magenta' },
|
|
145
|
+
])
|
|
146
|
+
);
|
|
147
|
+
output('table', lines.join('\n'));
|
|
148
|
+
await terminate(0);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const agentOnly = argv.agents && !argv.humans;
|
|
153
|
+
const humanOnly = argv.humans && !argv.agents;
|
|
154
|
+
const showHuman = !agentOnly;
|
|
155
|
+
const showAgent = !humanOnly;
|
|
156
|
+
const showTotal = showHuman && showAgent;
|
|
157
|
+
|
|
158
|
+
const lines: string[] = [];
|
|
159
|
+
const label = argv.subdomain ?? 'default';
|
|
160
|
+
lines.push(chalk.bold(`\nAnalytics \u2014 ${label} (${argv.from} to ${argv.to})\n`));
|
|
161
|
+
|
|
162
|
+
if (argv.page) {
|
|
163
|
+
lines.push(` Page: ${argv.page}\n`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
lines.push(chalk.bold(' Views'));
|
|
167
|
+
if (showHuman) lines.push(` Human: ${chalk.cyan(num(kpi.humanViews).padStart(8))}`);
|
|
168
|
+
if (showAgent)
|
|
169
|
+
lines.push(` Agent: ${chalk.magenta(num(kpi.agentViews).padStart(8))}`);
|
|
170
|
+
if (showTotal)
|
|
171
|
+
lines.push(` Total: ${num(kpi.humanViews + kpi.agentViews).padStart(8)}`);
|
|
172
|
+
|
|
173
|
+
lines.push(chalk.bold('\n Visitors'));
|
|
174
|
+
if (showHuman)
|
|
175
|
+
lines.push(` Human: ${chalk.cyan(num(kpi.humanVisitors).padStart(8))}`);
|
|
176
|
+
if (showAgent)
|
|
177
|
+
lines.push(` Agent: ${chalk.magenta(num(kpi.agentVisitors).padStart(8))}`);
|
|
178
|
+
if (showTotal)
|
|
179
|
+
lines.push(` Total: ${num(kpi.humanVisitors + kpi.agentVisitors).padStart(8)}`);
|
|
180
|
+
|
|
181
|
+
lines.push(`\n Searches: ${chalk.bold(num(kpi.humanSearches))}`);
|
|
182
|
+
lines.push(` Feedback: ${chalk.bold(num(kpi.humanFeedback))}`);
|
|
183
|
+
lines.push(
|
|
184
|
+
` Assistant: ${chalk.bold(num(kpi.humanAssistant))} web, ${chalk.bold(num(kpi.agentMcpSearches))} API`
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
output(format, lines.join('\n'));
|
|
188
|
+
await terminate(0);
|
|
189
|
+
} catch (err) {
|
|
190
|
+
if (format === 'table') removeLastLog();
|
|
191
|
+
addLog(<ErrorLog message={err instanceof Error ? err.message : 'unknown error'} />);
|
|
192
|
+
await terminate(1);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
)
|
|
196
|
+
.command(
|
|
197
|
+
'search',
|
|
198
|
+
'display search analytics',
|
|
199
|
+
(yargs) =>
|
|
200
|
+
withAll(yargs)
|
|
201
|
+
.option('query', { type: 'string', description: 'Filter by search query substring' })
|
|
202
|
+
.option('page', { type: 'string', description: 'Filter by top clicked page' }),
|
|
203
|
+
async (argv) => {
|
|
204
|
+
const format = resolveFormat(argv);
|
|
205
|
+
try {
|
|
206
|
+
if (format === 'table') addLog(<SpinnerLog message="Fetching search analytics..." />);
|
|
207
|
+
const data = await getSearches({ dateFrom: argv.from, dateTo: argv.to }, argv.subdomain);
|
|
208
|
+
if (format === 'table') removeLastLog();
|
|
209
|
+
|
|
210
|
+
let rows = data.searches;
|
|
211
|
+
if (argv.query) {
|
|
212
|
+
const q = argv.query.toLowerCase();
|
|
213
|
+
rows = rows.filter((r) => r.searchQuery.toLowerCase().includes(q));
|
|
214
|
+
}
|
|
215
|
+
if (argv.page) {
|
|
216
|
+
rows = rows.filter((r) => r.topClickedPage?.includes(argv.page!));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const headers = ['Query', 'Hits', 'CTR', 'Top Clicked Page', 'Last Searched'];
|
|
220
|
+
const tableRows = rows.map((r) => [
|
|
221
|
+
truncate(r.searchQuery, 30),
|
|
222
|
+
num(r.hits),
|
|
223
|
+
r.ctr.toFixed(1) + '%',
|
|
224
|
+
truncate(r.topClickedPage || '\u2014', 30),
|
|
225
|
+
r.lastSearchedAt.slice(0, 10),
|
|
226
|
+
]);
|
|
227
|
+
|
|
228
|
+
if (format === 'json') {
|
|
229
|
+
output(format, JSON.stringify(data, null, 2));
|
|
230
|
+
} else if (format === 'graph') {
|
|
231
|
+
const label = argv.subdomain ?? 'default';
|
|
232
|
+
const lines: string[] = [];
|
|
233
|
+
lines.push(
|
|
234
|
+
chalk.bold(`\nSearch Queries \u2014 ${label} (${argv.from} to ${argv.to})\n`)
|
|
235
|
+
);
|
|
236
|
+
lines.push(
|
|
237
|
+
formatBarChart(
|
|
238
|
+
rows.slice(0, 20).map((r) => ({
|
|
239
|
+
label: truncate(r.searchQuery, 25),
|
|
240
|
+
value: r.hits,
|
|
241
|
+
color: 'yellow',
|
|
242
|
+
}))
|
|
243
|
+
)
|
|
244
|
+
);
|
|
245
|
+
output('table', lines.join('\n'));
|
|
246
|
+
} else if (format === 'plain') {
|
|
247
|
+
output(format, formatOutput(format, headers, tableRows, data));
|
|
248
|
+
} else {
|
|
249
|
+
const label = argv.subdomain ?? 'default';
|
|
250
|
+
const lines: string[] = [];
|
|
251
|
+
lines.push(
|
|
252
|
+
chalk.bold(`\nSearch Analytics \u2014 ${label} (${argv.from} to ${argv.to})`)
|
|
253
|
+
);
|
|
254
|
+
lines.push(`Total Searches: ${chalk.bold(num(data.totalSearches))}\n`);
|
|
255
|
+
lines.push(formatOutput(format, headers, tableRows, data));
|
|
256
|
+
output(format, lines.join('\n'));
|
|
257
|
+
}
|
|
258
|
+
await terminate(0);
|
|
259
|
+
} catch (err) {
|
|
260
|
+
if (format === 'table') removeLastLog();
|
|
261
|
+
addLog(<ErrorLog message={err instanceof Error ? err.message : 'unknown error'} />);
|
|
262
|
+
await terminate(1);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
)
|
|
266
|
+
.command(
|
|
267
|
+
'feedback',
|
|
268
|
+
'display feedback analytics',
|
|
269
|
+
(yargs) =>
|
|
270
|
+
withAll(yargs)
|
|
271
|
+
.option('type', {
|
|
272
|
+
type: 'string',
|
|
273
|
+
choices: ['code', 'page'] as const,
|
|
274
|
+
description: 'Feedback type: code snippets or page-level aggregation',
|
|
275
|
+
})
|
|
276
|
+
.option('page', { type: 'string', description: 'Filter to a specific page path' }),
|
|
277
|
+
async (argv) => {
|
|
278
|
+
const format = resolveFormat(argv);
|
|
279
|
+
try {
|
|
280
|
+
if (format === 'table') addLog(<SpinnerLog message="Fetching feedback..." />);
|
|
281
|
+
|
|
282
|
+
if (argv.type === 'page') {
|
|
283
|
+
const data = await getFeedbackByPage(
|
|
284
|
+
{ dateFrom: argv.from, dateTo: argv.to },
|
|
285
|
+
argv.subdomain
|
|
286
|
+
);
|
|
287
|
+
if (format === 'table') removeLastLog();
|
|
288
|
+
|
|
289
|
+
let rows = data.feedback;
|
|
290
|
+
if (argv.page) rows = rows.filter((r) => r.path.includes(argv.page!));
|
|
291
|
+
|
|
292
|
+
const headers = ['Path', 'Thumbs Up', 'Thumbs Down', 'Code', 'Total'];
|
|
293
|
+
const tableRows = rows.map((r) => [
|
|
294
|
+
truncate(r.path, 40),
|
|
295
|
+
num(r.thumbsUp),
|
|
296
|
+
num(r.thumbsDown),
|
|
297
|
+
num(r.code),
|
|
298
|
+
num(r.total),
|
|
299
|
+
]);
|
|
300
|
+
|
|
301
|
+
if (format === 'json') {
|
|
302
|
+
output(format, JSON.stringify(data, null, 2));
|
|
303
|
+
} else if (format === 'graph') {
|
|
304
|
+
const label = argv.subdomain ?? 'default';
|
|
305
|
+
const lines: string[] = [];
|
|
306
|
+
lines.push(
|
|
307
|
+
chalk.bold(`\nFeedback by Page \u2014 ${label} (${argv.from} to ${argv.to})\n`)
|
|
308
|
+
);
|
|
309
|
+
lines.push(
|
|
310
|
+
formatBarChart(
|
|
311
|
+
rows.slice(0, 20).map((r) => ({
|
|
312
|
+
label: truncate(r.path, 25),
|
|
313
|
+
value: r.total,
|
|
314
|
+
color: 'green',
|
|
315
|
+
}))
|
|
316
|
+
)
|
|
317
|
+
);
|
|
318
|
+
output('table', lines.join('\n'));
|
|
319
|
+
} else {
|
|
320
|
+
const label = argv.subdomain ?? 'default';
|
|
321
|
+
const lines: string[] = [];
|
|
322
|
+
if (format === 'table')
|
|
323
|
+
lines.push(chalk.bold(`\nFeedback \u2014 ${label} (${argv.from} to ${argv.to})\n`));
|
|
324
|
+
lines.push(formatOutput(format, headers, tableRows, data));
|
|
325
|
+
if (format === 'table' && data.hasMore)
|
|
326
|
+
lines.push(chalk.dim('\n (more results available)'));
|
|
327
|
+
output(format, lines.join('\n'));
|
|
328
|
+
}
|
|
329
|
+
} else {
|
|
330
|
+
const source = argv.type === 'code' ? 'code' : undefined;
|
|
331
|
+
const data = await getFeedback(
|
|
332
|
+
{ dateFrom: argv.from, dateTo: argv.to, source },
|
|
333
|
+
argv.subdomain
|
|
334
|
+
);
|
|
335
|
+
if (format === 'table') removeLastLog();
|
|
336
|
+
|
|
337
|
+
let rows = data.feedback;
|
|
338
|
+
if (argv.page) rows = rows.filter((r) => r.path.includes(argv.page!));
|
|
339
|
+
|
|
340
|
+
const headers = ['ID', 'Path', 'Status', 'Source', 'Comment', 'Created'];
|
|
341
|
+
const tableRows = rows.map((r) => [
|
|
342
|
+
r.id.slice(0, 8),
|
|
343
|
+
truncate(r.path, 30),
|
|
344
|
+
r.status,
|
|
345
|
+
r.source,
|
|
346
|
+
truncate(r.comment ?? '\u2014', 30),
|
|
347
|
+
r.createdAt?.slice(0, 10) ?? '\u2014',
|
|
348
|
+
]);
|
|
349
|
+
|
|
350
|
+
if (format === 'json') {
|
|
351
|
+
output(format, JSON.stringify(data, null, 2));
|
|
352
|
+
} else {
|
|
353
|
+
const label = argv.subdomain ?? 'default';
|
|
354
|
+
const lines: string[] = [];
|
|
355
|
+
if (format === 'table')
|
|
356
|
+
lines.push(chalk.bold(`\nFeedback \u2014 ${label} (${argv.from} to ${argv.to})\n`));
|
|
357
|
+
lines.push(formatOutput(format, headers, tableRows, data));
|
|
358
|
+
if (format === 'table' && data.hasMore)
|
|
359
|
+
lines.push(chalk.dim('\n (more results available)'));
|
|
360
|
+
output(format, lines.join('\n'));
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
await terminate(0);
|
|
364
|
+
} catch (err) {
|
|
365
|
+
if (format === 'table') removeLastLog();
|
|
366
|
+
addLog(<ErrorLog message={err instanceof Error ? err.message : 'unknown error'} />);
|
|
367
|
+
await terminate(1);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
)
|
|
371
|
+
.command('conversation', 'view assistant conversation analytics', (yargs) =>
|
|
372
|
+
yargs
|
|
373
|
+
.command(
|
|
374
|
+
'list',
|
|
375
|
+
'list assistant conversations',
|
|
376
|
+
(yargs) =>
|
|
377
|
+
withAll(yargs).option('page', {
|
|
378
|
+
type: 'string',
|
|
379
|
+
description: 'Filter conversations mentioning this page in sources',
|
|
380
|
+
}),
|
|
381
|
+
async (argv) => {
|
|
382
|
+
const format = resolveFormat(argv);
|
|
383
|
+
try {
|
|
384
|
+
if (format === 'table') addLog(<SpinnerLog message="Fetching conversations..." />);
|
|
385
|
+
const data = await getConversations(
|
|
386
|
+
{ dateFrom: argv.from, dateTo: argv.to },
|
|
387
|
+
argv.subdomain
|
|
388
|
+
);
|
|
389
|
+
if (format === 'table') removeLastLog();
|
|
390
|
+
|
|
391
|
+
let conversations = data.conversations;
|
|
392
|
+
if (argv.page) {
|
|
393
|
+
conversations = conversations.filter((c) =>
|
|
394
|
+
c.sources.some((s) => s.url.includes(argv.page!))
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const headers = ['ID', 'Timestamp', 'Query', 'Category'];
|
|
399
|
+
const tableRows = conversations.map((c) => [
|
|
400
|
+
c.id,
|
|
401
|
+
c.timestamp.slice(0, 19).replace('T', ' '),
|
|
402
|
+
truncate(c.query, 40),
|
|
403
|
+
c.queryCategory || '\u2014',
|
|
404
|
+
]);
|
|
405
|
+
|
|
406
|
+
if (format === 'json') {
|
|
407
|
+
output(format, JSON.stringify(data, null, 2));
|
|
408
|
+
} else {
|
|
409
|
+
const label = argv.subdomain ?? 'default';
|
|
410
|
+
const lines: string[] = [];
|
|
411
|
+
if (format === 'table')
|
|
412
|
+
lines.push(
|
|
413
|
+
chalk.bold(`\nConversations \u2014 ${label} (${argv.from} to ${argv.to})\n`)
|
|
414
|
+
);
|
|
415
|
+
lines.push(formatOutput(format, headers, tableRows, data));
|
|
416
|
+
if (format === 'table' && data.hasMore)
|
|
417
|
+
lines.push(chalk.dim('\n (more results available)'));
|
|
418
|
+
output(format, lines.join('\n'));
|
|
419
|
+
}
|
|
420
|
+
await terminate(0);
|
|
421
|
+
} catch (err) {
|
|
422
|
+
if (format === 'table') removeLastLog();
|
|
423
|
+
addLog(<ErrorLog message={err instanceof Error ? err.message : 'unknown error'} />);
|
|
424
|
+
await terminate(1);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
)
|
|
428
|
+
.command(
|
|
429
|
+
'view <id>',
|
|
430
|
+
'view a single conversation',
|
|
431
|
+
(yargs) =>
|
|
432
|
+
withFormat(withSubdomain(yargs)).positional('id', {
|
|
433
|
+
type: 'string',
|
|
434
|
+
demandOption: true,
|
|
435
|
+
description: 'Conversation ID',
|
|
436
|
+
}),
|
|
437
|
+
async (argv) => {
|
|
438
|
+
const format = resolveFormat(argv);
|
|
439
|
+
try {
|
|
440
|
+
if (format === 'table') addLog(<SpinnerLog message="Fetching conversation..." />);
|
|
441
|
+
|
|
442
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
443
|
+
let conversation: Conversation | undefined;
|
|
444
|
+
let cursor: string | undefined;
|
|
445
|
+
|
|
446
|
+
for (let i = 0; i < 10 && !conversation; i++) {
|
|
447
|
+
const data = await getConversations(
|
|
448
|
+
{
|
|
449
|
+
dateFrom: '2020-01-01',
|
|
450
|
+
dateTo: today,
|
|
451
|
+
limit: 100,
|
|
452
|
+
cursor,
|
|
453
|
+
},
|
|
454
|
+
argv.subdomain
|
|
455
|
+
);
|
|
456
|
+
conversation = data.conversations.find((c) => c.id === argv.id);
|
|
457
|
+
if (!data.nextCursor) break;
|
|
458
|
+
cursor = data.nextCursor;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (format === 'table') removeLastLog();
|
|
462
|
+
|
|
463
|
+
if (!conversation) {
|
|
464
|
+
addLog(<ErrorLog message={`Conversation ${argv.id} not found`} />);
|
|
465
|
+
await terminate(1);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (format === 'json' || format === 'plain') {
|
|
470
|
+
output(format, JSON.stringify(conversation, null, 2));
|
|
471
|
+
} else {
|
|
472
|
+
const lines: string[] = [];
|
|
473
|
+
lines.push(chalk.bold(`\nConversation ${conversation.id}\n`));
|
|
474
|
+
lines.push(` Timestamp: ${conversation.timestamp}`);
|
|
475
|
+
lines.push(` Category: ${conversation.queryCategory || '\u2014'}`);
|
|
476
|
+
lines.push(chalk.bold('\n Query:'));
|
|
477
|
+
lines.push(` ${conversation.query}`);
|
|
478
|
+
lines.push(chalk.bold('\n Response:'));
|
|
479
|
+
for (const line of conversation.response.split('\n')) {
|
|
480
|
+
lines.push(` ${line}`);
|
|
481
|
+
}
|
|
482
|
+
if (conversation.sources.length > 0) {
|
|
483
|
+
lines.push(chalk.bold('\n Sources:'));
|
|
484
|
+
for (const src of conversation.sources) {
|
|
485
|
+
lines.push(` ${src.title} \u2014 ${chalk.dim(src.url)}`);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
output(format, lines.join('\n'));
|
|
489
|
+
}
|
|
490
|
+
await terminate(0);
|
|
491
|
+
} catch (err) {
|
|
492
|
+
if (format === 'table') removeLastLog();
|
|
493
|
+
addLog(<ErrorLog message={err instanceof Error ? err.message : 'unknown error'} />);
|
|
494
|
+
await terminate(1);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
)
|
|
498
|
+
.command('buckets', 'view conversation category buckets', (yargs) =>
|
|
499
|
+
yargs
|
|
500
|
+
.command(
|
|
501
|
+
'list',
|
|
502
|
+
'list conversation buckets',
|
|
503
|
+
(yargs) => withAll(yargs),
|
|
504
|
+
async (argv) => {
|
|
505
|
+
const format = resolveFormat(argv);
|
|
506
|
+
try {
|
|
507
|
+
if (format === 'table')
|
|
508
|
+
addLog(<SpinnerLog message="Fetching conversation buckets..." />);
|
|
509
|
+
const data = await getBuckets(
|
|
510
|
+
{ dateFrom: argv.from, dateTo: argv.to },
|
|
511
|
+
argv.subdomain
|
|
512
|
+
);
|
|
513
|
+
if (format === 'table') removeLastLog();
|
|
514
|
+
|
|
515
|
+
const headers = ['ID', 'Label', 'Count', 'Last Asked'];
|
|
516
|
+
const tableRows = data.data.map((b) => [
|
|
517
|
+
b.id.slice(0, 12),
|
|
518
|
+
truncate(b.questionSummary, 50),
|
|
519
|
+
num(b.size),
|
|
520
|
+
b.lastAsked ? b.lastAsked.slice(0, 10) : '\u2014',
|
|
521
|
+
]);
|
|
522
|
+
|
|
523
|
+
if (format === 'json') {
|
|
524
|
+
output(format, JSON.stringify(data, null, 2));
|
|
525
|
+
} else if (format === 'graph') {
|
|
526
|
+
const label = argv.subdomain ?? 'default';
|
|
527
|
+
const lines: string[] = [];
|
|
528
|
+
lines.push(
|
|
529
|
+
chalk.bold(
|
|
530
|
+
`\nConversation Buckets \u2014 ${label} (${argv.from} to ${argv.to})\n`
|
|
531
|
+
)
|
|
532
|
+
);
|
|
533
|
+
lines.push(
|
|
534
|
+
formatBarChart(
|
|
535
|
+
data.data.slice(0, 20).map((b) => ({
|
|
536
|
+
label: truncate(b.questionSummary, 30),
|
|
537
|
+
value: b.size,
|
|
538
|
+
color: 'blue',
|
|
539
|
+
}))
|
|
540
|
+
)
|
|
541
|
+
);
|
|
542
|
+
output('table', lines.join('\n'));
|
|
543
|
+
} else {
|
|
544
|
+
const label = argv.subdomain ?? 'default';
|
|
545
|
+
const lines: string[] = [];
|
|
546
|
+
if (format === 'table')
|
|
547
|
+
lines.push(
|
|
548
|
+
chalk.bold(
|
|
549
|
+
`\nConversation Buckets \u2014 ${label} (${argv.from} to ${argv.to})\n`
|
|
550
|
+
)
|
|
551
|
+
);
|
|
552
|
+
lines.push(formatOutput(format, headers, tableRows, data));
|
|
553
|
+
if (format === 'table')
|
|
554
|
+
lines.push(chalk.dim(`\n Total: ${data.pagination.total}`));
|
|
555
|
+
output(format, lines.join('\n'));
|
|
556
|
+
}
|
|
557
|
+
await terminate(0);
|
|
558
|
+
} catch (err) {
|
|
559
|
+
if (format === 'table') removeLastLog();
|
|
560
|
+
addLog(
|
|
561
|
+
<ErrorLog message={err instanceof Error ? err.message : 'unknown error'} />
|
|
562
|
+
);
|
|
563
|
+
await terminate(1);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
)
|
|
567
|
+
.command(
|
|
568
|
+
'view <id>',
|
|
569
|
+
'view conversations in a bucket',
|
|
570
|
+
(yargs) =>
|
|
571
|
+
withAll(yargs).positional('id', {
|
|
572
|
+
type: 'string',
|
|
573
|
+
demandOption: true,
|
|
574
|
+
description: 'Bucket ID',
|
|
575
|
+
}),
|
|
576
|
+
async (argv) => {
|
|
577
|
+
const format = resolveFormat(argv);
|
|
578
|
+
try {
|
|
579
|
+
if (format === 'table')
|
|
580
|
+
addLog(<SpinnerLog message="Fetching bucket conversations..." />);
|
|
581
|
+
const data = await getBucketThreads(
|
|
582
|
+
argv.id,
|
|
583
|
+
{ dateFrom: argv.from, dateTo: argv.to },
|
|
584
|
+
argv.subdomain
|
|
585
|
+
);
|
|
586
|
+
if (format === 'table') removeLastLog();
|
|
587
|
+
|
|
588
|
+
if (format === 'json') {
|
|
589
|
+
output(format, JSON.stringify(data, null, 2));
|
|
590
|
+
} else {
|
|
591
|
+
const headers = ['Thread ID', 'Query', 'Length', 'Feedback', 'Created'];
|
|
592
|
+
const tableRows = data.data.map((t) => [
|
|
593
|
+
t.id.slice(0, 12),
|
|
594
|
+
truncate(t.firstUserMessage || '\u2014', 40),
|
|
595
|
+
num(t.length),
|
|
596
|
+
`+${t.feedback.up} -${t.feedback.down}`,
|
|
597
|
+
t.createdAt.slice(0, 10),
|
|
598
|
+
]);
|
|
599
|
+
const lines: string[] = [];
|
|
600
|
+
if (format === 'table') lines.push(chalk.bold(`\nBucket ${argv.id}\n`));
|
|
601
|
+
lines.push(formatOutput(format, headers, tableRows, data));
|
|
602
|
+
if (format === 'table' && data.pagination.hasMore)
|
|
603
|
+
lines.push(chalk.dim('\n (more results available)'));
|
|
604
|
+
output(format, lines.join('\n'));
|
|
605
|
+
}
|
|
606
|
+
await terminate(0);
|
|
607
|
+
} catch (err) {
|
|
608
|
+
if (format === 'table') removeLastLog();
|
|
609
|
+
addLog(
|
|
610
|
+
<ErrorLog message={err instanceof Error ? err.message : 'unknown error'} />
|
|
611
|
+
);
|
|
612
|
+
await terminate(1);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
)
|
|
616
|
+
.demandCommand(1, 'specify a subcommand: list or view')
|
|
617
|
+
)
|
|
618
|
+
.demandCommand(1, 'specify a subcommand: list, view, or buckets')
|
|
619
|
+
)
|
|
620
|
+
.demandCommand(1, 'specify a subcommand: stats, search, feedback, or conversation');
|