@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.
Files changed (62) hide show
  1. package/README.md +76 -0
  2. package/dist/config-Bn2pbORi.js +34 -0
  3. package/dist/config-Bn2pbORi.js.map +1 -0
  4. package/dist/converse-C_PB7-JH.js +142 -0
  5. package/dist/converse-C_PB7-JH.js.map +1 -0
  6. package/dist/doctor-98gPl743.js +122 -0
  7. package/dist/doctor-98gPl743.js.map +1 -0
  8. package/dist/index.d.ts +1 -0
  9. package/dist/index.js +2268 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/llm-override-BIQl0V6H.js +445 -0
  12. package/dist/llm-override-BIQl0V6H.js.map +1 -0
  13. package/dist/reset-DT8SBgFS.js +87 -0
  14. package/dist/reset-DT8SBgFS.js.map +1 -0
  15. package/dist/simulate-BKv62GJc.js +144 -0
  16. package/dist/simulate-BKv62GJc.js.map +1 -0
  17. package/dist/status-D6LIZvQa.js +82 -0
  18. package/dist/status-D6LIZvQa.js.map +1 -0
  19. package/dist/test-DYjkxbtK.js +177 -0
  20. package/dist/test-DYjkxbtK.js.map +1 -0
  21. package/dist/test-suite-D8H_5uKs.js +209 -0
  22. package/dist/test-suite-D8H_5uKs.js.map +1 -0
  23. package/dist/utils-BuV4q7f6.js +11 -0
  24. package/dist/utils-BuV4q7f6.js.map +1 -0
  25. package/dist/vibe-Bl_js3Jo.js +395 -0
  26. package/dist/vibe-Bl_js3Jo.js.map +1 -0
  27. package/package.json +43 -0
  28. package/src/commands/analytics.ts +408 -0
  29. package/src/commands/chat.ts +310 -0
  30. package/src/commands/config.ts +34 -0
  31. package/src/commands/converse.ts +182 -0
  32. package/src/commands/doctor.ts +154 -0
  33. package/src/commands/history.ts +60 -0
  34. package/src/commands/init.ts +163 -0
  35. package/src/commands/kb.ts +429 -0
  36. package/src/commands/llm-override.ts +480 -0
  37. package/src/commands/reset.ts +72 -0
  38. package/src/commands/simulate.ts +187 -0
  39. package/src/commands/status.ts +112 -0
  40. package/src/commands/test-suite.ts +247 -0
  41. package/src/commands/test.ts +177 -0
  42. package/src/commands/vibe.ts +478 -0
  43. package/src/config.ts +127 -0
  44. package/src/index.ts +190 -0
  45. package/src/log-timestamps.ts +26 -0
  46. package/src/setup.ts +712 -0
  47. package/src/start.ts +573 -0
  48. package/src/utils.ts +6 -0
  49. package/templates/agents/_defaults/SOUL.md +20 -0
  50. package/templates/agents/_defaults/USER.md +16 -0
  51. package/templates/agents/customer-support/IDENTITY.md +6 -0
  52. package/templates/agents/customer-support/INSTRUCTIONS.md +79 -0
  53. package/templates/agents/customer-support/SOUL.md +26 -0
  54. package/templates/agents/faq-bot/IDENTITY.md +6 -0
  55. package/templates/agents/faq-bot/INSTRUCTIONS.md +53 -0
  56. package/templates/agents/faq-bot/SOUL.md +19 -0
  57. package/templates/agents/sales/IDENTITY.md +6 -0
  58. package/templates/agents/sales/INSTRUCTIONS.md +67 -0
  59. package/templates/agents/sales/SOUL.md +20 -0
  60. package/tsconfig.json +9 -0
  61. package/tsdown.config.ts +13 -0
  62. 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
+ }