@optima-chat/bi-cli 0.2.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 (44) hide show
  1. package/dist/commands/analytics.d.ts +3 -0
  2. package/dist/commands/analytics.d.ts.map +1 -0
  3. package/dist/commands/analytics.js +228 -0
  4. package/dist/commands/analytics.js.map +1 -0
  5. package/dist/commands/auth.d.ts +3 -0
  6. package/dist/commands/auth.d.ts.map +1 -0
  7. package/dist/commands/auth.js +214 -0
  8. package/dist/commands/auth.js.map +1 -0
  9. package/dist/commands/product.d.ts +3 -0
  10. package/dist/commands/product.d.ts.map +1 -0
  11. package/dist/commands/product.js +199 -0
  12. package/dist/commands/product.js.map +1 -0
  13. package/dist/commands/sales.d.ts +3 -0
  14. package/dist/commands/sales.d.ts.map +1 -0
  15. package/dist/commands/sales.js +85 -0
  16. package/dist/commands/sales.js.map +1 -0
  17. package/dist/commands/trends.d.ts +3 -0
  18. package/dist/commands/trends.d.ts.map +1 -0
  19. package/dist/commands/trends.js +224 -0
  20. package/dist/commands/trends.js.map +1 -0
  21. package/dist/config/index.d.ts +21 -0
  22. package/dist/config/index.d.ts.map +1 -0
  23. package/dist/config/index.js +39 -0
  24. package/dist/config/index.js.map +1 -0
  25. package/dist/index.d.ts +3 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +52 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/utils/output.d.ts +11 -0
  30. package/dist/utils/output.d.ts.map +1 -0
  31. package/dist/utils/output.js +45 -0
  32. package/dist/utils/output.js.map +1 -0
  33. package/package.json +39 -0
  34. package/src/commands/analytics.ts +352 -0
  35. package/src/commands/auth.ts +277 -0
  36. package/src/commands/product.ts +327 -0
  37. package/src/commands/sales.ts +125 -0
  38. package/src/commands/trends.ts +355 -0
  39. package/src/config/index.ts +50 -0
  40. package/src/index.ts +64 -0
  41. package/src/utils/output.ts +52 -0
  42. package/test-auth.js +63 -0
  43. package/test-sales.js +45 -0
  44. package/tsconfig.json +13 -0
package/dist/index.js ADDED
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { createAuthCommand } from './commands/auth.js';
4
+ import { createSalesCommand } from './commands/sales.js';
5
+ import { createProductCommand } from './commands/product.js';
6
+ import { createTrendsCommand } from './commands/trends.js';
7
+ import { createAnalyticsCommand } from './commands/analytics.js';
8
+ const program = new Command();
9
+ program
10
+ .name('bi-cli')
11
+ .description('Optima BI CLI - AI-friendly business intelligence tool')
12
+ .version('0.2.0');
13
+ // Auth commands
14
+ program.addCommand(createAuthCommand());
15
+ // Sales commands
16
+ program.addCommand(createSalesCommand());
17
+ // Product commands
18
+ program.addCommand(createProductCommand());
19
+ // Trends commands
20
+ program.addCommand(createTrendsCommand());
21
+ // Analytics commands
22
+ program.addCommand(createAnalyticsCommand());
23
+ // Config commands (placeholder)
24
+ program
25
+ .command('config')
26
+ .description('Manage configuration')
27
+ .action(() => {
28
+ console.log('Config management coming soon...');
29
+ });
30
+ // Customer commands (placeholder)
31
+ program
32
+ .command('customer')
33
+ .description('Customer analytics')
34
+ .action(() => {
35
+ console.log('Customer analytics coming soon...');
36
+ });
37
+ // Inventory commands (placeholder)
38
+ program
39
+ .command('inventory')
40
+ .description('Inventory analytics')
41
+ .action(() => {
42
+ console.log('Inventory analytics coming soon...');
43
+ });
44
+ // Platform commands (admin only - placeholder)
45
+ program
46
+ .command('platform')
47
+ .description('Platform analytics (admin only)')
48
+ .action(() => {
49
+ console.log('Platform analytics coming soon...');
50
+ });
51
+ program.parse();
52
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AAEjE,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;AAE9B,OAAO;KACJ,IAAI,CAAC,QAAQ,CAAC;KACd,WAAW,CAAC,wDAAwD,CAAC;KACrE,OAAO,CAAC,OAAO,CAAC,CAAC;AAEpB,gBAAgB;AAChB,OAAO,CAAC,UAAU,CAAC,iBAAiB,EAAE,CAAC,CAAC;AAExC,iBAAiB;AACjB,OAAO,CAAC,UAAU,CAAC,kBAAkB,EAAE,CAAC,CAAC;AAEzC,mBAAmB;AACnB,OAAO,CAAC,UAAU,CAAC,oBAAoB,EAAE,CAAC,CAAC;AAE3C,kBAAkB;AAClB,OAAO,CAAC,UAAU,CAAC,mBAAmB,EAAE,CAAC,CAAC;AAE1C,qBAAqB;AACrB,OAAO,CAAC,UAAU,CAAC,sBAAsB,EAAE,CAAC,CAAC;AAE7C,gCAAgC;AAChC,OAAO;KACJ,OAAO,CAAC,QAAQ,CAAC;KACjB,WAAW,CAAC,sBAAsB,CAAC;KACnC,MAAM,CAAC,GAAG,EAAE;IACX,OAAO,CAAC,GAAG,CAAC,kCAAkC,CAAC,CAAC;AAClD,CAAC,CAAC,CAAC;AAEL,kCAAkC;AAClC,OAAO;KACJ,OAAO,CAAC,UAAU,CAAC;KACnB,WAAW,CAAC,oBAAoB,CAAC;KACjC,MAAM,CAAC,GAAG,EAAE;IACX,OAAO,CAAC,GAAG,CAAC,mCAAmC,CAAC,CAAC;AACnD,CAAC,CAAC,CAAC;AAEL,mCAAmC;AACnC,OAAO;KACJ,OAAO,CAAC,WAAW,CAAC;KACpB,WAAW,CAAC,qBAAqB,CAAC;KAClC,MAAM,CAAC,GAAG,EAAE;IACX,OAAO,CAAC,GAAG,CAAC,oCAAoC,CAAC,CAAC;AACpD,CAAC,CAAC,CAAC;AAEL,+CAA+C;AAC/C,OAAO;KACJ,OAAO,CAAC,UAAU,CAAC;KACnB,WAAW,CAAC,iCAAiC,CAAC;KAC9C,MAAM,CAAC,GAAG,EAAE;IACX,OAAO,CAAC,GAAG,CAAC,mCAAmC,CAAC,CAAC;AACnD,CAAC,CAAC,CAAC;AAEL,OAAO,CAAC,KAAK,EAAE,CAAC"}
@@ -0,0 +1,11 @@
1
+ export declare enum OutputFormat {
2
+ JSON = "json",
3
+ PRETTY = "pretty"
4
+ }
5
+ export declare function outputJson(data: any): void;
6
+ export declare function outputPretty(data: any, headers?: string[]): void;
7
+ export declare function success(message: string): void;
8
+ export declare function error(message: string): void;
9
+ export declare function info(message: string): void;
10
+ export declare function warn(message: string): void;
11
+ //# sourceMappingURL=output.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"output.d.ts","sourceRoot":"","sources":["../../src/utils/output.ts"],"names":[],"mappings":"AAGA,oBAAY,YAAY;IACtB,IAAI,SAAS;IACb,MAAM,WAAW;CAClB;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,GAAG,GAAG,IAAI,CAE1C;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,GAAG,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,CAuBhE;AAED,wBAAgB,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAE7C;AAED,wBAAgB,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAE3C;AAED,wBAAgB,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAE1C;AAED,wBAAgB,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAE1C"}
@@ -0,0 +1,45 @@
1
+ import chalk from 'chalk';
2
+ import { table } from 'table';
3
+ export var OutputFormat;
4
+ (function (OutputFormat) {
5
+ OutputFormat["JSON"] = "json";
6
+ OutputFormat["PRETTY"] = "pretty";
7
+ })(OutputFormat || (OutputFormat = {}));
8
+ export function outputJson(data) {
9
+ console.log(JSON.stringify(data, null, 2));
10
+ }
11
+ export function outputPretty(data, headers) {
12
+ if (Array.isArray(data)) {
13
+ if (data.length === 0) {
14
+ console.log(chalk.yellow('No data found.'));
15
+ return;
16
+ }
17
+ const keys = headers || Object.keys(data[0]);
18
+ const rows = [keys.map((k) => chalk.bold(k))];
19
+ data.forEach((item) => {
20
+ rows.push(keys.map((k) => String(item[k] || '')));
21
+ });
22
+ console.log(table(rows));
23
+ }
24
+ else {
25
+ // Single object
26
+ const rows = [];
27
+ Object.entries(data).forEach(([key, value]) => {
28
+ rows.push([chalk.bold(key), String(value)]);
29
+ });
30
+ console.log(table(rows));
31
+ }
32
+ }
33
+ export function success(message) {
34
+ console.log(chalk.green('✓ ' + message));
35
+ }
36
+ export function error(message) {
37
+ console.error(chalk.red('✗ ' + message));
38
+ }
39
+ export function info(message) {
40
+ console.log(chalk.blue('ℹ ' + message));
41
+ }
42
+ export function warn(message) {
43
+ console.log(chalk.yellow('⚠ ' + message));
44
+ }
45
+ //# sourceMappingURL=output.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"output.js","sourceRoot":"","sources":["../../src/utils/output.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC;AAE9B,MAAM,CAAN,IAAY,YAGX;AAHD,WAAY,YAAY;IACtB,6BAAa,CAAA;IACb,iCAAiB,CAAA;AACnB,CAAC,EAHW,YAAY,KAAZ,YAAY,QAGvB;AAED,MAAM,UAAU,UAAU,CAAC,IAAS;IAClC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;AAC7C,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,IAAS,EAAE,OAAkB;IACxD,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QACxB,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACtB,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,gBAAgB,CAAC,CAAC,CAAC;YAC5C,OAAO;QACT,CAAC;QAED,MAAM,IAAI,GAAG,OAAO,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;QAC7C,MAAM,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAE9C,IAAI,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE;YACpB,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;QACpD,CAAC,CAAC,CAAC;QAEH,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;IAC3B,CAAC;SAAM,CAAC;QACN,gBAAgB;QAChB,MAAM,IAAI,GAAe,EAAE,CAAC;QAC5B,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE;YAC5C,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QAC9C,CAAC,CAAC,CAAC;QACH,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;IAC3B,CAAC;AACH,CAAC;AAED,MAAM,UAAU,OAAO,CAAC,OAAe;IACrC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,GAAG,OAAO,CAAC,CAAC,CAAC;AAC3C,CAAC;AAED,MAAM,UAAU,KAAK,CAAC,OAAe;IACnC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,GAAG,OAAO,CAAC,CAAC,CAAC;AAC3C,CAAC;AAED,MAAM,UAAU,IAAI,CAAC,OAAe;IAClC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,GAAG,OAAO,CAAC,CAAC,CAAC;AAC1C,CAAC;AAED,MAAM,UAAU,IAAI,CAAC,OAAe;IAClC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,GAAG,OAAO,CAAC,CAAC,CAAC;AAC5C,CAAC"}
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@optima-chat/bi-cli",
3
+ "version": "0.2.0",
4
+ "type": "module",
5
+ "description": "Optima BI CLI - AI-friendly business intelligence tool",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "bi-cli": "./dist/index.js"
9
+ },
10
+ "scripts": {
11
+ "dev": "tsx src/index.ts",
12
+ "build": "tsc",
13
+ "start": "node dist/index.js",
14
+ "lint": "eslint src --ext .ts",
15
+ "lint:fix": "eslint src --ext .ts --fix",
16
+ "test": "echo \"Error: no test specified\" && exit 0"
17
+ },
18
+ "dependencies": {
19
+ "axios": "^1.6.5",
20
+ "chalk": "^4.1.2",
21
+ "commander": "^11.1.0",
22
+ "conf": "^12.0.0",
23
+ "open": "^8.4.2",
24
+ "ora": "^5.4.1",
25
+ "table": "^6.8.1"
26
+ },
27
+ "devDependencies": {
28
+ "@types/node": "^18.19.50",
29
+ "tsx": "^4.7.0",
30
+ "typescript": "^5.3.3"
31
+ },
32
+ "keywords": [
33
+ "bi",
34
+ "business-intelligence",
35
+ "analytics",
36
+ "cli",
37
+ "optima"
38
+ ]
39
+ }
@@ -0,0 +1,352 @@
1
+ import { Command } from 'commander';
2
+ import axios from 'axios';
3
+ import { getConfig } from '../config/index.js';
4
+ import { outputJson, outputPretty, error, OutputFormat, info } from '../utils/output.js';
5
+
6
+ interface CompareResponse {
7
+ success: boolean;
8
+ data: {
9
+ merchant_id: string;
10
+ period: { days: number; compare_to: string };
11
+ current: { revenue: number; orders: number; avg_order_value: number; customers: number };
12
+ previous: { revenue: number; orders: number; avg_order_value: number; customers: number };
13
+ changes: {
14
+ revenue: { absolute: number; percentage: number };
15
+ orders: { absolute: number; percentage: number };
16
+ avg_order_value: { absolute: number; percentage: number };
17
+ customers: { absolute: number; percentage: number };
18
+ };
19
+ };
20
+ }
21
+
22
+ interface GrowthResponse {
23
+ success: boolean;
24
+ data: {
25
+ merchant_id: string;
26
+ period_type: string;
27
+ trends: Array<{
28
+ period: string;
29
+ revenue: number;
30
+ orders: number;
31
+ avg_order_value: number;
32
+ customers: number;
33
+ revenue_growth: number | null;
34
+ }>;
35
+ };
36
+ }
37
+
38
+ interface CohortResponse {
39
+ success: boolean;
40
+ data: {
41
+ merchant_id: string;
42
+ cohorts: Array<{
43
+ month: string;
44
+ customers: number;
45
+ avg_lifetime_value: number;
46
+ avg_orders: number;
47
+ total_revenue: number;
48
+ }>;
49
+ };
50
+ }
51
+
52
+ interface FunnelResponse {
53
+ success: boolean;
54
+ data: {
55
+ merchant_id: string;
56
+ period: { days: number };
57
+ total_orders: number;
58
+ funnel: Array<{
59
+ stage: string;
60
+ count: number;
61
+ amount: number;
62
+ percentage: number;
63
+ }>;
64
+ dropoffs: Array<{
65
+ status: string;
66
+ count: number;
67
+ amount: number;
68
+ percentage: number;
69
+ }>;
70
+ conversion_rate: number;
71
+ };
72
+ }
73
+
74
+ export function createAnalyticsCommand(): Command {
75
+ const analytics = new Command('analytics').description('Advanced analytics');
76
+
77
+ // analytics compare
78
+ analytics
79
+ .command('compare')
80
+ .description('Compare current period with previous period')
81
+ .option('--days <number>', 'Number of days', '30')
82
+ .option(
83
+ '--compare-to <type>',
84
+ 'Compare to: previous_period or previous_year',
85
+ 'previous_period'
86
+ )
87
+ .option('--pretty', 'Output in pretty table format')
88
+ .action(async (options) => {
89
+ const cfg = getConfig();
90
+ if (!cfg.accessToken) {
91
+ error('Not logged in. Run: bi-cli auth login');
92
+ process.exit(1);
93
+ }
94
+
95
+ const format = options.pretty ? OutputFormat.PRETTY : OutputFormat.JSON;
96
+
97
+ try {
98
+ const response = await axios.get<CompareResponse>(
99
+ `${cfg.backendUrl}/api/v1/analytics/compare`,
100
+ {
101
+ params: { days: options.days, compare_to: options.compareTo },
102
+ headers: { Authorization: `Bearer ${cfg.accessToken}` },
103
+ }
104
+ );
105
+
106
+ if (!response.data.success) {
107
+ error('Failed to fetch comparison data');
108
+ process.exit(1);
109
+ }
110
+
111
+ const { current, previous, changes, period } = response.data.data;
112
+
113
+ if (format === OutputFormat.JSON) {
114
+ outputJson(response.data.data);
115
+ } else {
116
+ console.log(
117
+ `\n📊 Period Comparison (${period.days} days vs ${period.compare_to.replace('_', ' ')})\n`
118
+ );
119
+
120
+ const formatChange = (pct: number | null) => {
121
+ if (pct === null) return '-';
122
+ const icon = pct > 0 ? '↑' : pct < 0 ? '↓' : '→';
123
+ const color = pct > 0 ? '+' : '';
124
+ return `${icon} ${color}${pct.toFixed(1)}%`;
125
+ };
126
+
127
+ const formatCurrency = (val: number | null) => {
128
+ if (val === null) return '-';
129
+ return `¥${val.toFixed(2)}`;
130
+ };
131
+
132
+ outputPretty(
133
+ [
134
+ {
135
+ Metric: 'Revenue',
136
+ Current: formatCurrency(current.revenue),
137
+ Previous: formatCurrency(previous.revenue),
138
+ Change: formatChange(changes.revenue.percentage),
139
+ },
140
+ {
141
+ Metric: 'Orders',
142
+ Current: current.orders,
143
+ Previous: previous.orders,
144
+ Change: formatChange(changes.orders.percentage),
145
+ },
146
+ {
147
+ Metric: 'Avg Order Value',
148
+ Current: formatCurrency(current.avg_order_value),
149
+ Previous: formatCurrency(previous.avg_order_value),
150
+ Change: formatChange(changes.avg_order_value.percentage),
151
+ },
152
+ {
153
+ Metric: 'Customers',
154
+ Current: current.customers,
155
+ Previous: previous.customers,
156
+ Change: formatChange(changes.customers.percentage),
157
+ },
158
+ ],
159
+ ['Metric', 'Current', 'Previous', 'Change']
160
+ );
161
+ }
162
+ } catch (err: unknown) {
163
+ handleApiError(err);
164
+ }
165
+ });
166
+
167
+ // analytics growth
168
+ analytics
169
+ .command('growth')
170
+ .description('Get growth trends over time')
171
+ .option('--period <type>', 'Period type: daily, weekly, monthly', 'daily')
172
+ .option('--limit <number>', 'Number of periods', '30')
173
+ .option('--pretty', 'Output in pretty table format')
174
+ .action(async (options) => {
175
+ const cfg = getConfig();
176
+ if (!cfg.accessToken) {
177
+ error('Not logged in. Run: bi-cli auth login');
178
+ process.exit(1);
179
+ }
180
+
181
+ const format = options.pretty ? OutputFormat.PRETTY : OutputFormat.JSON;
182
+
183
+ try {
184
+ const response = await axios.get<GrowthResponse>(
185
+ `${cfg.backendUrl}/api/v1/analytics/growth`,
186
+ {
187
+ params: { period: options.period, limit: options.limit },
188
+ headers: { Authorization: `Bearer ${cfg.accessToken}` },
189
+ }
190
+ );
191
+
192
+ if (!response.data.success) {
193
+ error('Failed to fetch growth data');
194
+ process.exit(1);
195
+ }
196
+
197
+ const { trends, period_type } = response.data.data;
198
+
199
+ if (format === OutputFormat.JSON) {
200
+ outputJson(response.data.data);
201
+ } else {
202
+ console.log(`\n📈 Growth Trends (${period_type})\n`);
203
+
204
+ const recentTrends = trends.slice(-15);
205
+ outputPretty(
206
+ recentTrends.map((t) => ({
207
+ Period: t.period.split('T')[0],
208
+ Revenue: `¥${t.revenue.toFixed(2)}`,
209
+ Orders: t.orders,
210
+ Customers: t.customers,
211
+ Growth: t.revenue_growth !== null ? `${t.revenue_growth.toFixed(1)}%` : '-',
212
+ })),
213
+ ['Period', 'Revenue', 'Orders', 'Customers', 'Growth']
214
+ );
215
+ }
216
+ } catch (err: unknown) {
217
+ handleApiError(err);
218
+ }
219
+ });
220
+
221
+ // analytics cohort
222
+ analytics
223
+ .command('cohort')
224
+ .description('Customer cohort analysis')
225
+ .option('--pretty', 'Output in pretty table format')
226
+ .action(async (options) => {
227
+ const cfg = getConfig();
228
+ if (!cfg.accessToken) {
229
+ error('Not logged in. Run: bi-cli auth login');
230
+ process.exit(1);
231
+ }
232
+
233
+ const format = options.pretty ? OutputFormat.PRETTY : OutputFormat.JSON;
234
+
235
+ try {
236
+ const response = await axios.get<CohortResponse>(
237
+ `${cfg.backendUrl}/api/v1/analytics/cohort`,
238
+ {
239
+ headers: { Authorization: `Bearer ${cfg.accessToken}` },
240
+ }
241
+ );
242
+
243
+ if (!response.data.success) {
244
+ error('Failed to fetch cohort data');
245
+ process.exit(1);
246
+ }
247
+
248
+ const { cohorts } = response.data.data;
249
+
250
+ if (format === OutputFormat.JSON) {
251
+ outputJson(response.data.data);
252
+ } else {
253
+ console.log('\n👥 Customer Cohort Analysis\n');
254
+ outputPretty(
255
+ cohorts.map((c) => ({
256
+ Cohort: c.month.split('T')[0].slice(0, 7),
257
+ Customers: c.customers,
258
+ 'Avg LTV': `¥${c.avg_lifetime_value.toFixed(2)}`,
259
+ 'Avg Orders': c.avg_orders.toFixed(1),
260
+ 'Total Revenue': `¥${c.total_revenue.toFixed(2)}`,
261
+ })),
262
+ ['Cohort', 'Customers', 'Avg LTV', 'Avg Orders', 'Total Revenue']
263
+ );
264
+ }
265
+ } catch (err: unknown) {
266
+ handleApiError(err);
267
+ }
268
+ });
269
+
270
+ // analytics funnel
271
+ analytics
272
+ .command('funnel')
273
+ .description('Order status funnel analysis')
274
+ .option('--days <number>', 'Number of days', '30')
275
+ .option('--pretty', 'Output in pretty table format')
276
+ .action(async (options) => {
277
+ const cfg = getConfig();
278
+ if (!cfg.accessToken) {
279
+ error('Not logged in. Run: bi-cli auth login');
280
+ process.exit(1);
281
+ }
282
+
283
+ const format = options.pretty ? OutputFormat.PRETTY : OutputFormat.JSON;
284
+
285
+ try {
286
+ const response = await axios.get<FunnelResponse>(
287
+ `${cfg.backendUrl}/api/v1/analytics/order-funnel`,
288
+ {
289
+ params: { days: options.days },
290
+ headers: { Authorization: `Bearer ${cfg.accessToken}` },
291
+ }
292
+ );
293
+
294
+ if (!response.data.success) {
295
+ error('Failed to fetch funnel data');
296
+ process.exit(1);
297
+ }
298
+
299
+ const { funnel, dropoffs, total_orders, conversion_rate } = response.data.data;
300
+
301
+ if (format === OutputFormat.JSON) {
302
+ outputJson(response.data.data);
303
+ } else {
304
+ console.log('\n🔄 Order Funnel\n');
305
+ outputPretty(
306
+ funnel.map((f) => ({
307
+ Stage: f.stage,
308
+ Count: f.count,
309
+ Amount: `¥${f.amount.toFixed(2)}`,
310
+ Percentage: `${f.percentage.toFixed(1)}%`,
311
+ })),
312
+ ['Stage', 'Count', 'Amount', 'Percentage']
313
+ );
314
+
315
+ if (dropoffs.length > 0) {
316
+ console.log('\n❌ Dropoffs\n');
317
+ outputPretty(
318
+ dropoffs.map((d) => ({
319
+ Status: d.status,
320
+ Count: d.count,
321
+ Amount: `¥${d.amount.toFixed(2)}`,
322
+ Percentage: `${d.percentage.toFixed(1)}%`,
323
+ })),
324
+ ['Status', 'Count', 'Amount', 'Percentage']
325
+ );
326
+ }
327
+
328
+ info(`Total Orders: ${total_orders} | Conversion Rate: ${conversion_rate.toFixed(1)}%`);
329
+ }
330
+ } catch (err: unknown) {
331
+ handleApiError(err);
332
+ }
333
+ });
334
+
335
+ return analytics;
336
+ }
337
+
338
+ function handleApiError(err: unknown): never {
339
+ const axiosError = err as {
340
+ response?: { status?: number; data?: { error?: { message?: string } } };
341
+ };
342
+ const errorObj = err as Error;
343
+
344
+ if (axiosError.response?.status === 401) {
345
+ error('Authentication failed. Please login again: bi-cli auth login');
346
+ } else if (axiosError.response?.data?.error) {
347
+ error(`Error: ${axiosError.response.data.error.message}`);
348
+ } else {
349
+ error(`Request failed: ${errorObj.message || 'Unknown error'}`);
350
+ }
351
+ process.exit(1);
352
+ }