@operor/cli 0.1.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/README.md +76 -0
- package/dist/config-Bn2pbORi.js +34 -0
- package/dist/config-Bn2pbORi.js.map +1 -0
- package/dist/converse-C_PB7-JH.js +142 -0
- package/dist/converse-C_PB7-JH.js.map +1 -0
- package/dist/doctor-98gPl743.js +122 -0
- package/dist/doctor-98gPl743.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2268 -0
- package/dist/index.js.map +1 -0
- package/dist/llm-override-BIQl0V6H.js +445 -0
- package/dist/llm-override-BIQl0V6H.js.map +1 -0
- package/dist/reset-DT8SBgFS.js +87 -0
- package/dist/reset-DT8SBgFS.js.map +1 -0
- package/dist/simulate-BKv62GJc.js +144 -0
- package/dist/simulate-BKv62GJc.js.map +1 -0
- package/dist/status-D6LIZvQa.js +82 -0
- package/dist/status-D6LIZvQa.js.map +1 -0
- package/dist/test-DYjkxbtK.js +177 -0
- package/dist/test-DYjkxbtK.js.map +1 -0
- package/dist/test-suite-D8H_5uKs.js +209 -0
- package/dist/test-suite-D8H_5uKs.js.map +1 -0
- package/dist/utils-BuV4q7f6.js +11 -0
- package/dist/utils-BuV4q7f6.js.map +1 -0
- package/dist/vibe-Bl_js3Jo.js +395 -0
- package/dist/vibe-Bl_js3Jo.js.map +1 -0
- package/package.json +43 -0
- package/src/commands/analytics.ts +408 -0
- package/src/commands/chat.ts +310 -0
- package/src/commands/config.ts +34 -0
- package/src/commands/converse.ts +182 -0
- package/src/commands/doctor.ts +154 -0
- package/src/commands/history.ts +60 -0
- package/src/commands/init.ts +163 -0
- package/src/commands/kb.ts +429 -0
- package/src/commands/llm-override.ts +480 -0
- package/src/commands/reset.ts +72 -0
- package/src/commands/simulate.ts +187 -0
- package/src/commands/status.ts +112 -0
- package/src/commands/test-suite.ts +247 -0
- package/src/commands/test.ts +177 -0
- package/src/commands/vibe.ts +478 -0
- package/src/config.ts +127 -0
- package/src/index.ts +190 -0
- package/src/log-timestamps.ts +26 -0
- package/src/setup.ts +712 -0
- package/src/start.ts +573 -0
- package/src/utils.ts +6 -0
- package/templates/agents/_defaults/SOUL.md +20 -0
- package/templates/agents/_defaults/USER.md +16 -0
- package/templates/agents/customer-support/IDENTITY.md +6 -0
- package/templates/agents/customer-support/INSTRUCTIONS.md +79 -0
- package/templates/agents/customer-support/SOUL.md +26 -0
- package/templates/agents/faq-bot/IDENTITY.md +6 -0
- package/templates/agents/faq-bot/INSTRUCTIONS.md +53 -0
- package/templates/agents/faq-bot/SOUL.md +19 -0
- package/templates/agents/sales/IDENTITY.md +6 -0
- package/templates/agents/sales/INSTRUCTIONS.md +67 -0
- package/templates/agents/sales/SOUL.md +20 -0
- package/tsconfig.json +9 -0
- package/tsdown.config.ts +13 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import * as clack from '@clack/prompts';
|
|
3
|
+
import { readConfig, configExists } from '../config.js';
|
|
4
|
+
|
|
5
|
+
function getAnalyticsConfig() {
|
|
6
|
+
if (!configExists()) {
|
|
7
|
+
clack.log.error('.env file not found. Run "operor setup" first.');
|
|
8
|
+
process.exit(1);
|
|
9
|
+
}
|
|
10
|
+
const config = readConfig();
|
|
11
|
+
if (config.ANALYTICS_ENABLED === 'false') {
|
|
12
|
+
clack.log.error('Analytics is disabled. Set ANALYTICS_ENABLED=true in .env.');
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
return config;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function loadStore(config: ReturnType<typeof readConfig>) {
|
|
19
|
+
try {
|
|
20
|
+
const { SQLiteAnalyticsStore } = await import('@operor/analytics');
|
|
21
|
+
const store = new SQLiteAnalyticsStore(config.ANALYTICS_DB_PATH || './analytics.db');
|
|
22
|
+
await store.initialize();
|
|
23
|
+
return store;
|
|
24
|
+
} catch {
|
|
25
|
+
clack.log.error(
|
|
26
|
+
'@operor/analytics package not found. Install it first:\n pnpm add @operor/analytics',
|
|
27
|
+
);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function makeRange(days: number): { from: number; to: number } {
|
|
33
|
+
const to = Date.now();
|
|
34
|
+
const from = to - days * 24 * 60 * 60 * 1000;
|
|
35
|
+
return { from, to };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Simple sparkline from an array of numbers */
|
|
39
|
+
function sparkline(values: number[]): string {
|
|
40
|
+
if (values.length === 0) return '';
|
|
41
|
+
const chars = '▁▂▃▄▅▆▇█';
|
|
42
|
+
const max = Math.max(...values);
|
|
43
|
+
if (max === 0) return chars[0].repeat(values.length);
|
|
44
|
+
return values.map(v => chars[Math.min(Math.floor((v / max) * (chars.length - 1)), chars.length - 1)]).join('');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Simple horizontal bar */
|
|
48
|
+
function bar(value: number, max: number, width = 30): string {
|
|
49
|
+
if (max === 0) return '';
|
|
50
|
+
const filled = Math.round((value / max) * width);
|
|
51
|
+
return '█'.repeat(filled) + '░'.repeat(width - filled);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function maskPhone(phone: string): string {
|
|
55
|
+
if (phone.length <= 6) return '***' + phone.slice(-3);
|
|
56
|
+
return phone.slice(0, 3) + '***' + phone.slice(-3);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function fmtMs(ms: number): string {
|
|
60
|
+
if (ms < 1000) return `${Math.round(ms)}ms`;
|
|
61
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function fmtPct(pct: number): string {
|
|
65
|
+
return `${pct.toFixed(1)}%`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function registerAnalyticsCommand(program: Command): void {
|
|
69
|
+
const analytics = program.command('analytics').description('Usage analytics and insights');
|
|
70
|
+
|
|
71
|
+
// Default: summary dashboard
|
|
72
|
+
analytics
|
|
73
|
+
.option('--days <n>', 'Number of days to include', '7')
|
|
74
|
+
.action(async (opts: { days: string }) => {
|
|
75
|
+
clack.intro('Analytics — Dashboard');
|
|
76
|
+
const config = getAnalyticsConfig();
|
|
77
|
+
const store = await loadStore(config);
|
|
78
|
+
const range = makeRange(parseInt(opts.days));
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const [summary, volume, agents] = await Promise.all([
|
|
82
|
+
store.getSummary(range),
|
|
83
|
+
store.getMessageVolume(range, 'day'),
|
|
84
|
+
store.getAgentStats(range),
|
|
85
|
+
]);
|
|
86
|
+
|
|
87
|
+
if (summary.totalMessages === 0) {
|
|
88
|
+
clack.log.info('No messages recorded in this period.');
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Messages
|
|
93
|
+
const trend = sparkline(volume.map(v => v.count));
|
|
94
|
+
clack.log.info(` Messages`);
|
|
95
|
+
clack.log.info(` Total: ${summary.totalMessages}`);
|
|
96
|
+
clack.log.info(` Avg/day: ${summary.avgPerDay.toFixed(1)}`);
|
|
97
|
+
clack.log.info(` Peak day: ${summary.peakDay || '—'}`);
|
|
98
|
+
clack.log.info(` Trend: ${trend}`);
|
|
99
|
+
|
|
100
|
+
// Response quality
|
|
101
|
+
clack.log.info('');
|
|
102
|
+
clack.log.info(` Response Quality`);
|
|
103
|
+
clack.log.info(` KB answered: ${fmtPct(summary.kbAnsweredPct)}`);
|
|
104
|
+
clack.log.info(` LLM fallback: ${fmtPct(summary.llmFallbackPct)}`);
|
|
105
|
+
clack.log.info(` No answer: ${fmtPct(summary.noAnswerPct)}`);
|
|
106
|
+
clack.log.info(` Avg response: ${fmtMs(summary.avgResponseTime)}`);
|
|
107
|
+
clack.log.info(` FAQ hit rate: ${fmtPct(summary.faqHitRate)}`);
|
|
108
|
+
|
|
109
|
+
// Customers
|
|
110
|
+
clack.log.info('');
|
|
111
|
+
clack.log.info(` Customers`);
|
|
112
|
+
clack.log.info(` Unique: ${summary.uniqueCustomers}`);
|
|
113
|
+
clack.log.info(` New: ${summary.newCustomers}`);
|
|
114
|
+
clack.log.info(` Returning: ${summary.returningCustomers}`);
|
|
115
|
+
if (summary.uniqueCustomers > 0) {
|
|
116
|
+
clack.log.info(` Avg msgs: ${(summary.totalMessages / summary.uniqueCustomers).toFixed(1)}/customer`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Agents
|
|
120
|
+
if (agents.length > 0) {
|
|
121
|
+
clack.log.info('');
|
|
122
|
+
clack.log.info(` Agents`);
|
|
123
|
+
for (const a of agents) {
|
|
124
|
+
const pct = summary.totalMessages > 0
|
|
125
|
+
? ((a.messageCount / summary.totalMessages) * 100).toFixed(1)
|
|
126
|
+
: '0.0';
|
|
127
|
+
clack.log.info(` ${a.agentName.padEnd(20)} ${String(a.messageCount).padStart(5)} msgs (${pct}%) avg ${fmtMs(a.avgResponseTimeMs)}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Copilot
|
|
132
|
+
if (summary.pendingReviews > 0) {
|
|
133
|
+
clack.log.info('');
|
|
134
|
+
clack.log.info(` Copilot`);
|
|
135
|
+
clack.log.info(` Pending reviews: ${summary.pendingReviews}`);
|
|
136
|
+
}
|
|
137
|
+
} catch (error: any) {
|
|
138
|
+
clack.log.error(error.message);
|
|
139
|
+
process.exit(1);
|
|
140
|
+
} finally {
|
|
141
|
+
await store.close();
|
|
142
|
+
}
|
|
143
|
+
clack.outro('');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Messages subcommand
|
|
147
|
+
analytics
|
|
148
|
+
.command('messages')
|
|
149
|
+
.description('Message volume breakdown')
|
|
150
|
+
.option('--days <n>', 'Number of days to include', '7')
|
|
151
|
+
.action(async (opts: { days: string }) => {
|
|
152
|
+
clack.intro('Analytics — Messages');
|
|
153
|
+
const config = getAnalyticsConfig();
|
|
154
|
+
const store = await loadStore(config);
|
|
155
|
+
const days = parseInt(opts.days);
|
|
156
|
+
const range = makeRange(days);
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
const [daily, hourly] = await Promise.all([
|
|
160
|
+
store.getMessageVolume(range, 'day'),
|
|
161
|
+
store.getMessageVolume(makeRange(1), 'hour'),
|
|
162
|
+
]);
|
|
163
|
+
|
|
164
|
+
if (daily.length === 0) {
|
|
165
|
+
clack.log.info('No messages recorded in this period.');
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Daily breakdown
|
|
170
|
+
const maxDaily = Math.max(...daily.map(d => d.count));
|
|
171
|
+
clack.log.info(' Daily Volume');
|
|
172
|
+
for (const d of daily) {
|
|
173
|
+
clack.log.info(` ${d.date} ${bar(d.count, maxDaily, 25)} ${d.count}`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Hourly distribution (today)
|
|
177
|
+
if (hourly.length > 0) {
|
|
178
|
+
const maxHourly = Math.max(...hourly.map(h => h.count));
|
|
179
|
+
clack.log.info('');
|
|
180
|
+
clack.log.info(' Today — Hourly');
|
|
181
|
+
for (const h of hourly) {
|
|
182
|
+
const hour = h.date.split(' ')[1] || h.date;
|
|
183
|
+
clack.log.info(` ${hour} ${bar(h.count, maxHourly, 20)} ${h.count}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
} catch (error: any) {
|
|
187
|
+
clack.log.error(error.message);
|
|
188
|
+
process.exit(1);
|
|
189
|
+
} finally {
|
|
190
|
+
await store.close();
|
|
191
|
+
}
|
|
192
|
+
clack.outro('');
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// Agents subcommand
|
|
196
|
+
analytics
|
|
197
|
+
.command('agents')
|
|
198
|
+
.description('Per-agent performance breakdown')
|
|
199
|
+
.option('--days <n>', 'Number of days to include', '7')
|
|
200
|
+
.action(async (opts: { days: string }) => {
|
|
201
|
+
clack.intro('Analytics — Agents');
|
|
202
|
+
const config = getAnalyticsConfig();
|
|
203
|
+
const store = await loadStore(config);
|
|
204
|
+
const range = makeRange(parseInt(opts.days));
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
const [agents, summary] = await Promise.all([
|
|
208
|
+
store.getAgentStats(range),
|
|
209
|
+
store.getSummary(range),
|
|
210
|
+
]);
|
|
211
|
+
|
|
212
|
+
if (agents.length === 0) {
|
|
213
|
+
clack.log.info('No agent activity in this period.');
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const header = `${'Agent'.padEnd(22)} ${'Msgs'.padStart(6)} ${'%'.padStart(6)} ${'Avg Time'.padStart(10)} ${'Confidence'.padStart(11)} ${'Escalations'.padStart(12)} ${'Tools'.padStart(6)}`;
|
|
218
|
+
clack.log.info(header);
|
|
219
|
+
clack.log.info('─'.repeat(header.length));
|
|
220
|
+
|
|
221
|
+
for (const a of agents) {
|
|
222
|
+
const pct = summary.totalMessages > 0
|
|
223
|
+
? ((a.messageCount / summary.totalMessages) * 100).toFixed(1)
|
|
224
|
+
: '0.0';
|
|
225
|
+
const line = `${a.agentName.padEnd(22)} ${String(a.messageCount).padStart(6)} ${(pct + '%').padStart(6)} ${fmtMs(a.avgResponseTimeMs).padStart(10)} ${fmtPct(a.avgConfidence * 100).padStart(11)} ${String(a.escalationCount).padStart(12)} ${String(a.toolCallCount).padStart(6)}`;
|
|
226
|
+
clack.log.info(line);
|
|
227
|
+
}
|
|
228
|
+
} catch (error: any) {
|
|
229
|
+
clack.log.error(error.message);
|
|
230
|
+
process.exit(1);
|
|
231
|
+
} finally {
|
|
232
|
+
await store.close();
|
|
233
|
+
}
|
|
234
|
+
clack.outro('');
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// Customers subcommand
|
|
238
|
+
analytics
|
|
239
|
+
.command('customers')
|
|
240
|
+
.description('Customer engagement metrics')
|
|
241
|
+
.option('--days <n>', 'Number of days to include', '7')
|
|
242
|
+
.option('-n, --limit <n>', 'Top customers to show', '10')
|
|
243
|
+
.action(async (opts: { days: string; limit: string }) => {
|
|
244
|
+
clack.intro('Analytics — Customers');
|
|
245
|
+
const config = getAnalyticsConfig();
|
|
246
|
+
const store = await loadStore(config);
|
|
247
|
+
const range = makeRange(parseInt(opts.days));
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
const [customers, summary] = await Promise.all([
|
|
251
|
+
store.getCustomerStats(range, parseInt(opts.limit)),
|
|
252
|
+
store.getSummary(range),
|
|
253
|
+
]);
|
|
254
|
+
|
|
255
|
+
if (customers.length === 0) {
|
|
256
|
+
clack.log.info('No customer activity in this period.');
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
clack.log.info(` Unique: ${summary.uniqueCustomers}`);
|
|
261
|
+
clack.log.info(` New: ${summary.newCustomers}`);
|
|
262
|
+
clack.log.info(` Returning: ${summary.returningCustomers}`);
|
|
263
|
+
|
|
264
|
+
// Engagement distribution
|
|
265
|
+
const buckets = { '1 msg': 0, '2-5': 0, '6-10': 0, '10+': 0 };
|
|
266
|
+
for (const c of customers) {
|
|
267
|
+
if (c.messageCount === 1) buckets['1 msg']++;
|
|
268
|
+
else if (c.messageCount <= 5) buckets['2-5']++;
|
|
269
|
+
else if (c.messageCount <= 10) buckets['6-10']++;
|
|
270
|
+
else buckets['10+']++;
|
|
271
|
+
}
|
|
272
|
+
clack.log.info('');
|
|
273
|
+
clack.log.info(' Engagement Distribution');
|
|
274
|
+
const maxBucket = Math.max(...Object.values(buckets));
|
|
275
|
+
for (const [label, count] of Object.entries(buckets)) {
|
|
276
|
+
clack.log.info(` ${label.padEnd(8)} ${bar(count, maxBucket, 20)} ${count}`);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Top customers
|
|
280
|
+
clack.log.info('');
|
|
281
|
+
clack.log.info(' Top Customers');
|
|
282
|
+
const header = `${'Phone'.padEnd(16)} ${'Msgs'.padStart(6)} ${'Status'.padStart(8)} ${'Last Seen'.padStart(12)}`;
|
|
283
|
+
clack.log.info(` ${header}`);
|
|
284
|
+
clack.log.info(` ${'─'.repeat(header.length)}`);
|
|
285
|
+
for (const c of customers) {
|
|
286
|
+
const lastSeen = new Date(c.lastSeen).toLocaleDateString();
|
|
287
|
+
const status = c.isNew ? 'new' : 'return';
|
|
288
|
+
clack.log.info(` ${maskPhone(c.customerPhone).padEnd(16)} ${String(c.messageCount).padStart(6)} ${status.padStart(8)} ${lastSeen.padStart(12)}`);
|
|
289
|
+
}
|
|
290
|
+
} catch (error: any) {
|
|
291
|
+
clack.log.error(error.message);
|
|
292
|
+
process.exit(1);
|
|
293
|
+
} finally {
|
|
294
|
+
await store.close();
|
|
295
|
+
}
|
|
296
|
+
clack.outro('');
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// KB subcommand
|
|
300
|
+
analytics
|
|
301
|
+
.command('kb')
|
|
302
|
+
.description('Knowledge Base performance metrics')
|
|
303
|
+
.option('--days <n>', 'Number of days to include', '7')
|
|
304
|
+
.action(async (opts: { days: string }) => {
|
|
305
|
+
clack.intro('Analytics — Knowledge Base');
|
|
306
|
+
const config = getAnalyticsConfig();
|
|
307
|
+
const store = await loadStore(config);
|
|
308
|
+
const range = makeRange(parseInt(opts.days));
|
|
309
|
+
|
|
310
|
+
try {
|
|
311
|
+
const kbStats = await store.getKbStats(range);
|
|
312
|
+
|
|
313
|
+
if (kbStats.totalQueries === 0) {
|
|
314
|
+
clack.log.info('No KB queries in this period.');
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
clack.log.info(` Total queries: ${kbStats.totalQueries}`);
|
|
319
|
+
clack.log.info(` FAQ hits: ${kbStats.faqHits} (${fmtPct(kbStats.faqHitRate)})`);
|
|
320
|
+
clack.log.info(` Avg top score: ${kbStats.avgTopScore.toFixed(4)}`);
|
|
321
|
+
|
|
322
|
+
// Pipeline breakdown
|
|
323
|
+
const hybridCount = kbStats.totalQueries - kbStats.faqHits;
|
|
324
|
+
clack.log.info('');
|
|
325
|
+
clack.log.info(' Pipeline Breakdown');
|
|
326
|
+
clack.log.info(` FAQ fast-path: ${kbStats.faqHits}`);
|
|
327
|
+
clack.log.info(` Hybrid/LLM: ${hybridCount}`);
|
|
328
|
+
|
|
329
|
+
// Top FAQ hits
|
|
330
|
+
if (kbStats.topFaqHits.length > 0) {
|
|
331
|
+
clack.log.info('');
|
|
332
|
+
clack.log.info(' Top FAQ Hits');
|
|
333
|
+
for (const h of kbStats.topFaqHits.slice(0, 10)) {
|
|
334
|
+
clack.log.info(` ${String(h.count).padStart(4)}x ${h.intent}`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Top misses
|
|
339
|
+
if (kbStats.topMisses.length > 0) {
|
|
340
|
+
clack.log.info('');
|
|
341
|
+
clack.log.info(' Top Misses (consider teaching these)');
|
|
342
|
+
for (const m of kbStats.topMisses.slice(0, 10)) {
|
|
343
|
+
clack.log.info(` ${String(m.count).padStart(4)}x ${m.intent} (avg score: ${m.avgScore.toFixed(3)})`);
|
|
344
|
+
}
|
|
345
|
+
clack.log.info('');
|
|
346
|
+
clack.log.info(' Tip: Use "operor kb add-faq <question> <answer>" to teach missed queries.');
|
|
347
|
+
}
|
|
348
|
+
} catch (error: any) {
|
|
349
|
+
clack.log.error(error.message);
|
|
350
|
+
process.exit(1);
|
|
351
|
+
} finally {
|
|
352
|
+
await store.close();
|
|
353
|
+
}
|
|
354
|
+
clack.outro('');
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// Export subcommand
|
|
358
|
+
analytics
|
|
359
|
+
.command('export')
|
|
360
|
+
.description('Export analytics data as CSV or JSON')
|
|
361
|
+
.option('--days <n>', 'Number of days to include', '7')
|
|
362
|
+
.option('--json', 'Export as JSON instead of CSV')
|
|
363
|
+
.option('-o, --output <file>', 'Output file path')
|
|
364
|
+
.action(async (opts: { days: string; json?: boolean; output?: string }) => {
|
|
365
|
+
clack.intro('Analytics — Export');
|
|
366
|
+
const config = getAnalyticsConfig();
|
|
367
|
+
const store = await loadStore(config);
|
|
368
|
+
const range = makeRange(parseInt(opts.days));
|
|
369
|
+
|
|
370
|
+
try {
|
|
371
|
+
const [summary, volume, agents, customers, kbStats] = await Promise.all([
|
|
372
|
+
store.getSummary(range),
|
|
373
|
+
store.getMessageVolume(range, 'day'),
|
|
374
|
+
store.getAgentStats(range),
|
|
375
|
+
store.getCustomerStats(range, 100),
|
|
376
|
+
store.getKbStats(range),
|
|
377
|
+
]);
|
|
378
|
+
|
|
379
|
+
const data = { summary, volume, agents, customers, kbStats };
|
|
380
|
+
|
|
381
|
+
let output: string;
|
|
382
|
+
if (opts.json) {
|
|
383
|
+
output = JSON.stringify(data, null, 2);
|
|
384
|
+
} else {
|
|
385
|
+
// CSV: export daily volume as the primary table
|
|
386
|
+
const lines = ['date,messages'];
|
|
387
|
+
for (const v of volume) {
|
|
388
|
+
lines.push(`${v.date},${v.count}`);
|
|
389
|
+
}
|
|
390
|
+
output = lines.join('\n');
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (opts.output) {
|
|
394
|
+
const { writeFileSync } = await import('node:fs');
|
|
395
|
+
writeFileSync(opts.output, output);
|
|
396
|
+
clack.log.success(`Exported to ${opts.output}`);
|
|
397
|
+
} else {
|
|
398
|
+
console.log(output);
|
|
399
|
+
}
|
|
400
|
+
} catch (error: any) {
|
|
401
|
+
clack.log.error(error.message);
|
|
402
|
+
process.exit(1);
|
|
403
|
+
} finally {
|
|
404
|
+
await store.close();
|
|
405
|
+
}
|
|
406
|
+
clack.outro('');
|
|
407
|
+
});
|
|
408
|
+
}
|