@mintlify/cli 4.0.1098 → 4.0.1100
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 +28 -20
- package/__test__/analytics/format.test.ts +9 -14
- package/__test__/authenticatedFetch.test.ts +182 -0
- package/__test__/keyring.test.ts +7 -0
- package/__test__/telemetry.test.ts +2 -2
- package/bin/analytics/client.js +3 -20
- package/bin/analytics/index.js +24 -39
- package/bin/analytics/output.js +3 -2
- package/bin/authenticatedFetch.js +46 -0
- package/bin/cli.js +5 -5
- package/bin/keyring.js +6 -0
- package/bin/login.js +42 -2
- package/bin/middlewares/subdomainMiddleware.js +28 -0
- package/bin/{telemetry/middleware.js → middlewares/telemetryMiddleware.js} +1 -1
- package/bin/status.js +10 -12
- package/bin/tokenRefresh.js +50 -0
- package/bin/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/analytics/client.ts +3 -20
- package/src/analytics/index.tsx +28 -43
- package/src/analytics/output.ts +4 -2
- package/src/authenticatedFetch.ts +42 -0
- package/src/cli.tsx +3 -3
- package/src/keyring.ts +5 -0
- package/src/login.tsx +48 -2
- package/src/middlewares/subdomainMiddleware.ts +16 -0
- package/src/{telemetry/middleware.ts → middlewares/telemetryMiddleware.ts} +1 -1
- package/src/status.tsx +9 -10
- package/src/tokenRefresh.ts +48 -0
|
@@ -12,6 +12,8 @@ import {
|
|
|
12
12
|
|
|
13
13
|
vi.mock('../../src/keyring.js', () => ({
|
|
14
14
|
getAccessToken: vi.fn().mockResolvedValue(null),
|
|
15
|
+
getRefreshToken: vi.fn().mockResolvedValue(null),
|
|
16
|
+
storeCredentials: vi.fn().mockResolvedValue(undefined),
|
|
15
17
|
}));
|
|
16
18
|
|
|
17
19
|
const mockFetch = vi.fn();
|
|
@@ -30,6 +32,7 @@ afterEach(() => {
|
|
|
30
32
|
function mockOk(data: unknown) {
|
|
31
33
|
mockFetch.mockResolvedValueOnce({
|
|
32
34
|
ok: true,
|
|
35
|
+
status: 200,
|
|
33
36
|
json: () => Promise.resolve(data),
|
|
34
37
|
});
|
|
35
38
|
}
|
|
@@ -43,8 +46,13 @@ function mockError(status: number, body: string) {
|
|
|
43
46
|
});
|
|
44
47
|
}
|
|
45
48
|
|
|
46
|
-
function calledUrl():
|
|
47
|
-
|
|
49
|
+
function calledUrl(): string {
|
|
50
|
+
const arg = mockFetch.mock.calls[0]![0];
|
|
51
|
+
return typeof arg === 'string' ? arg : String(arg);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function calledUrlObj(): URL {
|
|
55
|
+
return new URL(calledUrl());
|
|
48
56
|
}
|
|
49
57
|
|
|
50
58
|
describe('client auth', () => {
|
|
@@ -59,7 +67,7 @@ describe('client auth', () => {
|
|
|
59
67
|
mockOk({ feedback: [], nextCursor: null, hasMore: false });
|
|
60
68
|
await getFeedback({ dateFrom: '2024-01-01', dateTo: '2024-01-31' }, 'test');
|
|
61
69
|
expect(mockFetch).toHaveBeenCalledWith(
|
|
62
|
-
expect.any(
|
|
70
|
+
expect.any(String),
|
|
63
71
|
expect.objectContaining({
|
|
64
72
|
headers: expect.objectContaining({
|
|
65
73
|
Authorization: 'Bearer test-token',
|
|
@@ -80,14 +88,14 @@ describe('client request handling', () => {
|
|
|
80
88
|
it('passes subdomain as query param when provided', async () => {
|
|
81
89
|
mockOk({ feedback: [], nextCursor: null, hasMore: false });
|
|
82
90
|
await getFeedback({ dateFrom: '2024-01-01', dateTo: '2024-01-31' }, 'my-docs');
|
|
83
|
-
expect(
|
|
84
|
-
expect(
|
|
91
|
+
expect(calledUrlObj().searchParams.get('subdomain')).toBe('my-docs');
|
|
92
|
+
expect(calledUrlObj().pathname).toBe('/api/cli/analytics/feedback');
|
|
85
93
|
});
|
|
86
94
|
|
|
87
95
|
it('omits subdomain param when not provided', async () => {
|
|
88
96
|
mockOk({ feedback: [], nextCursor: null, hasMore: false });
|
|
89
97
|
await getFeedback({ dateFrom: '2024-01-01', dateTo: '2024-01-31' });
|
|
90
|
-
expect(
|
|
98
|
+
expect(calledUrlObj().searchParams.has('subdomain')).toBe(false);
|
|
91
99
|
});
|
|
92
100
|
|
|
93
101
|
it('sets query params and omits undefined values', async () => {
|
|
@@ -98,9 +106,9 @@ describe('client request handling', () => {
|
|
|
98
106
|
limit: 10,
|
|
99
107
|
cursor: undefined,
|
|
100
108
|
});
|
|
101
|
-
expect(
|
|
102
|
-
expect(
|
|
103
|
-
expect(
|
|
109
|
+
expect(calledUrlObj().searchParams.get('dateFrom')).toBe('2024-01-01');
|
|
110
|
+
expect(calledUrlObj().searchParams.get('limit')).toBe('10');
|
|
111
|
+
expect(calledUrlObj().searchParams.has('cursor')).toBe(false);
|
|
104
112
|
});
|
|
105
113
|
});
|
|
106
114
|
|
|
@@ -108,51 +116,51 @@ describe('endpoint paths', () => {
|
|
|
108
116
|
it('getKpi', async () => {
|
|
109
117
|
mockOk({ humanVisitors: 0 });
|
|
110
118
|
await getKpi({ dateFrom: '2024-01-01', dateTo: '2024-01-31' }, 'docs');
|
|
111
|
-
expect(
|
|
112
|
-
expect(
|
|
119
|
+
expect(calledUrlObj().pathname).toBe('/api/cli/analytics/kpi');
|
|
120
|
+
expect(calledUrlObj().searchParams.get('subdomain')).toBe('docs');
|
|
113
121
|
});
|
|
114
122
|
|
|
115
123
|
it('getFeedbackByPage', async () => {
|
|
116
124
|
mockOk({ feedback: [], hasMore: false });
|
|
117
125
|
await getFeedbackByPage({ dateFrom: '2024-01-01', dateTo: '2024-01-31' }, 'docs');
|
|
118
|
-
expect(
|
|
126
|
+
expect(calledUrlObj().pathname).toBe('/api/cli/analytics/feedback/by-page');
|
|
119
127
|
});
|
|
120
128
|
|
|
121
129
|
it('getConversations', async () => {
|
|
122
130
|
mockOk({ conversations: [], nextCursor: null, hasMore: false });
|
|
123
131
|
await getConversations({ dateFrom: '2024-01-01', dateTo: '2024-01-31' }, 'docs');
|
|
124
|
-
expect(
|
|
132
|
+
expect(calledUrlObj().pathname).toBe('/api/cli/analytics/assistant');
|
|
125
133
|
});
|
|
126
134
|
|
|
127
135
|
it('getSearches', async () => {
|
|
128
136
|
mockOk({ searches: [], totalSearches: 0, nextCursor: null });
|
|
129
137
|
await getSearches({ dateFrom: '2024-01-01', dateTo: '2024-01-31' }, 'docs');
|
|
130
|
-
expect(
|
|
138
|
+
expect(calledUrlObj().pathname).toBe('/api/cli/analytics/searches');
|
|
131
139
|
});
|
|
132
140
|
|
|
133
141
|
it('getViews', async () => {
|
|
134
142
|
mockOk({ totals: {}, views: [], hasMore: false });
|
|
135
143
|
await getViews({ dateFrom: '2024-01-01', dateTo: '2024-01-31' }, 'docs');
|
|
136
|
-
expect(
|
|
144
|
+
expect(calledUrlObj().pathname).toBe('/api/cli/analytics/views');
|
|
137
145
|
});
|
|
138
146
|
|
|
139
147
|
it('getVisitors', async () => {
|
|
140
148
|
mockOk({ totals: {}, visitors: [], hasMore: false });
|
|
141
149
|
await getVisitors({ dateFrom: '2024-01-01', dateTo: '2024-01-31' }, 'docs');
|
|
142
|
-
expect(
|
|
150
|
+
expect(calledUrlObj().pathname).toBe('/api/cli/analytics/visitors');
|
|
143
151
|
});
|
|
144
152
|
|
|
145
153
|
it('getBuckets', async () => {
|
|
146
154
|
mockOk({ data: [], pagination: { total: 0 } });
|
|
147
155
|
await getBuckets({ dateFrom: '2024-01-01', dateTo: '2024-01-31' }, 'docs');
|
|
148
|
-
expect(
|
|
149
|
-
expect(
|
|
156
|
+
expect(calledUrlObj().pathname).toBe('/api/cli/analytics/conversations/buckets');
|
|
157
|
+
expect(calledUrlObj().searchParams.get('subdomain')).toBe('docs');
|
|
150
158
|
});
|
|
151
159
|
|
|
152
160
|
it('getBucketThreads', async () => {
|
|
153
161
|
mockOk({ data: [], pagination: { total: 0, hasMore: false, nextCursor: null } });
|
|
154
162
|
await getBucketThreads('bucket-123', { dateFrom: '2024-01-01' }, 'docs');
|
|
155
|
-
expect(
|
|
156
|
-
expect(
|
|
163
|
+
expect(calledUrlObj().pathname).toBe('/api/cli/analytics/conversations/buckets/bucket-123');
|
|
164
|
+
expect(calledUrlObj().searchParams.get('subdomain')).toBe('docs');
|
|
157
165
|
});
|
|
158
166
|
});
|
|
@@ -5,6 +5,11 @@ import {
|
|
|
5
5
|
formatPrettyTable,
|
|
6
6
|
resolveFormat,
|
|
7
7
|
} from '../../src/analytics/output.js';
|
|
8
|
+
import * as helpers from '../../src/helpers.js';
|
|
9
|
+
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
vi.restoreAllMocks();
|
|
12
|
+
});
|
|
8
13
|
|
|
9
14
|
describe('num', () => {
|
|
10
15
|
it('formats numbers with locale separators', () => {
|
|
@@ -81,31 +86,21 @@ describe('formatPlainTable', () => {
|
|
|
81
86
|
});
|
|
82
87
|
|
|
83
88
|
describe('resolveFormat', () => {
|
|
84
|
-
it('returns json when
|
|
85
|
-
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it('returns json when CLAUDECODE env is set', () => {
|
|
89
|
-
const prev = process.env.CLAUDECODE;
|
|
90
|
-
process.env.CLAUDECODE = '1';
|
|
89
|
+
it('returns json when AI mode is active', () => {
|
|
90
|
+
vi.spyOn(helpers, 'isAI').mockReturnValue(true);
|
|
91
91
|
expect(resolveFormat({})).toBe('json');
|
|
92
|
-
process.env.CLAUDECODE = prev;
|
|
93
92
|
});
|
|
94
93
|
|
|
95
94
|
it('returns specified format', () => {
|
|
96
|
-
|
|
97
|
-
delete process.env.CLAUDECODE;
|
|
95
|
+
vi.spyOn(helpers, 'isAI').mockReturnValue(true);
|
|
98
96
|
expect(resolveFormat({ format: 'plain' })).toBe('plain');
|
|
99
97
|
expect(resolveFormat({ format: 'json' })).toBe('json');
|
|
100
98
|
expect(resolveFormat({ format: 'graph' })).toBe('graph');
|
|
101
|
-
process.env.CLAUDECODE = prev;
|
|
102
99
|
});
|
|
103
100
|
|
|
104
101
|
it('defaults to plain', () => {
|
|
105
|
-
|
|
106
|
-
delete process.env.CLAUDECODE;
|
|
102
|
+
vi.spyOn(helpers, 'isAI').mockReturnValue(false);
|
|
107
103
|
expect(resolveFormat({})).toBe('plain');
|
|
108
|
-
process.env.CLAUDECODE = prev;
|
|
109
104
|
});
|
|
110
105
|
});
|
|
111
106
|
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { authenticatedFetch } from '../src/authenticatedFetch.js';
|
|
4
|
+
|
|
5
|
+
const mockGetAccessToken = vi.fn();
|
|
6
|
+
const mockGetRefreshToken = vi.fn();
|
|
7
|
+
const mockStoreCredentials = vi.fn();
|
|
8
|
+
|
|
9
|
+
vi.mock('../src/keyring.js', () => ({
|
|
10
|
+
getAccessToken: (...args: unknown[]) => mockGetAccessToken(...args),
|
|
11
|
+
getRefreshToken: (...args: unknown[]) => mockGetRefreshToken(...args),
|
|
12
|
+
storeCredentials: (...args: unknown[]) => mockStoreCredentials(...args),
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
const mockFetch = vi.fn();
|
|
16
|
+
global.fetch = mockFetch;
|
|
17
|
+
|
|
18
|
+
function makeResponse(status: number, body: unknown = {}) {
|
|
19
|
+
return {
|
|
20
|
+
ok: status >= 200 && status < 300,
|
|
21
|
+
status,
|
|
22
|
+
statusText: status === 401 ? 'Unauthorized' : 'OK',
|
|
23
|
+
json: () => Promise.resolve(body),
|
|
24
|
+
text: () => Promise.resolve(JSON.stringify(body)),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('authenticatedFetch', () => {
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
vi.clearAllMocks();
|
|
31
|
+
mockFetch.mockReset();
|
|
32
|
+
vi.stubEnv('MINTLIFY_SESSION_TOKEN', '');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
vi.unstubAllEnvs();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('sends access token from keyring', async () => {
|
|
40
|
+
mockGetAccessToken.mockResolvedValue('access-123');
|
|
41
|
+
mockFetch.mockResolvedValueOnce(makeResponse(200, { ok: true }));
|
|
42
|
+
|
|
43
|
+
await authenticatedFetch('http://test/api');
|
|
44
|
+
|
|
45
|
+
expect(mockFetch).toHaveBeenCalledWith('http://test/api', {
|
|
46
|
+
headers: { Authorization: 'Bearer access-123' },
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('falls back to MINTLIFY_SESSION_TOKEN env var', async () => {
|
|
51
|
+
mockGetAccessToken.mockResolvedValue(null);
|
|
52
|
+
vi.stubEnv('MINTLIFY_SESSION_TOKEN', 'env-token');
|
|
53
|
+
mockFetch.mockResolvedValueOnce(makeResponse(200));
|
|
54
|
+
|
|
55
|
+
await authenticatedFetch('http://test/api');
|
|
56
|
+
|
|
57
|
+
expect(mockFetch).toHaveBeenCalledWith('http://test/api', {
|
|
58
|
+
headers: { Authorization: 'Bearer env-token' },
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('throws when no token is available', async () => {
|
|
63
|
+
mockGetAccessToken.mockResolvedValue(null);
|
|
64
|
+
|
|
65
|
+
await expect(authenticatedFetch('http://test/api')).rejects.toThrow('Not authenticated');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('refreshes token on 401 and retries', async () => {
|
|
69
|
+
mockGetAccessToken.mockResolvedValue('expired-token');
|
|
70
|
+
mockGetRefreshToken.mockResolvedValue('refresh-123');
|
|
71
|
+
mockStoreCredentials.mockResolvedValue(undefined);
|
|
72
|
+
|
|
73
|
+
mockFetch
|
|
74
|
+
.mockResolvedValueOnce(makeResponse(401))
|
|
75
|
+
.mockResolvedValueOnce(
|
|
76
|
+
makeResponse(200, {
|
|
77
|
+
access_token: 'new-access',
|
|
78
|
+
refresh_token: 'new-refresh',
|
|
79
|
+
token_type: 'bearer',
|
|
80
|
+
expires_in: 3600,
|
|
81
|
+
})
|
|
82
|
+
)
|
|
83
|
+
.mockResolvedValueOnce(makeResponse(200, { ok: true }));
|
|
84
|
+
|
|
85
|
+
const res = await authenticatedFetch('http://test/api');
|
|
86
|
+
|
|
87
|
+
expect(res.status).toBe(200);
|
|
88
|
+
expect(mockFetch).toHaveBeenCalledTimes(3);
|
|
89
|
+
expect(mockFetch).toHaveBeenLastCalledWith('http://test/api', {
|
|
90
|
+
headers: { Authorization: 'Bearer new-access' },
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('falls back to env token when refresh fails', async () => {
|
|
95
|
+
mockGetAccessToken.mockResolvedValue('expired-token');
|
|
96
|
+
mockGetRefreshToken.mockResolvedValue('bad-refresh');
|
|
97
|
+
vi.stubEnv('MINTLIFY_SESSION_TOKEN', 'env-token');
|
|
98
|
+
|
|
99
|
+
mockFetch
|
|
100
|
+
.mockResolvedValueOnce(makeResponse(401))
|
|
101
|
+
.mockResolvedValueOnce(makeResponse(400, { error: 'invalid_grant' }))
|
|
102
|
+
.mockResolvedValueOnce(makeResponse(200, { ok: true }));
|
|
103
|
+
|
|
104
|
+
const res = await authenticatedFetch('http://test/api');
|
|
105
|
+
|
|
106
|
+
expect(res.status).toBe(200);
|
|
107
|
+
expect(mockFetch).toHaveBeenCalledTimes(3);
|
|
108
|
+
expect(mockFetch).toHaveBeenLastCalledWith('http://test/api', {
|
|
109
|
+
headers: { Authorization: 'Bearer env-token' },
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('returns original 401 when refresh fails and no env token', async () => {
|
|
114
|
+
mockGetAccessToken.mockResolvedValue('expired-token');
|
|
115
|
+
mockGetRefreshToken.mockResolvedValue('bad-refresh');
|
|
116
|
+
|
|
117
|
+
mockFetch
|
|
118
|
+
.mockResolvedValueOnce(makeResponse(401))
|
|
119
|
+
.mockResolvedValueOnce(makeResponse(400, { error: 'invalid_grant' }));
|
|
120
|
+
|
|
121
|
+
const res = await authenticatedFetch('http://test/api');
|
|
122
|
+
|
|
123
|
+
expect(res.status).toBe(401);
|
|
124
|
+
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('falls back to env token when no refresh token exists', async () => {
|
|
128
|
+
mockGetAccessToken.mockResolvedValue('expired-token');
|
|
129
|
+
mockGetRefreshToken.mockResolvedValue(null);
|
|
130
|
+
vi.stubEnv('MINTLIFY_SESSION_TOKEN', 'env-token');
|
|
131
|
+
|
|
132
|
+
mockFetch
|
|
133
|
+
.mockResolvedValueOnce(makeResponse(401))
|
|
134
|
+
.mockResolvedValueOnce(makeResponse(200, { ok: true }));
|
|
135
|
+
|
|
136
|
+
const res = await authenticatedFetch('http://test/api');
|
|
137
|
+
|
|
138
|
+
expect(res.status).toBe(200);
|
|
139
|
+
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
140
|
+
expect(mockFetch).toHaveBeenLastCalledWith('http://test/api', {
|
|
141
|
+
headers: { Authorization: 'Bearer env-token' },
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('returns original 401 when no refresh token and no env token', async () => {
|
|
146
|
+
mockGetAccessToken.mockResolvedValue('expired-token');
|
|
147
|
+
mockGetRefreshToken.mockResolvedValue(null);
|
|
148
|
+
|
|
149
|
+
mockFetch.mockResolvedValueOnce(makeResponse(401));
|
|
150
|
+
|
|
151
|
+
const res = await authenticatedFetch('http://test/api');
|
|
152
|
+
|
|
153
|
+
expect(res.status).toBe(401);
|
|
154
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('does not attempt refresh when only env token is available', async () => {
|
|
158
|
+
mockGetAccessToken.mockResolvedValue(null);
|
|
159
|
+
vi.stubEnv('MINTLIFY_SESSION_TOKEN', 'env-token');
|
|
160
|
+
|
|
161
|
+
mockFetch.mockResolvedValueOnce(makeResponse(401));
|
|
162
|
+
|
|
163
|
+
const res = await authenticatedFetch('http://test/api');
|
|
164
|
+
|
|
165
|
+
expect(res.status).toBe(401);
|
|
166
|
+
expect(mockGetRefreshToken).not.toHaveBeenCalled();
|
|
167
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('merges custom headers with auth header', async () => {
|
|
171
|
+
mockGetAccessToken.mockResolvedValue('token-123');
|
|
172
|
+
mockFetch.mockResolvedValueOnce(makeResponse(200));
|
|
173
|
+
|
|
174
|
+
await authenticatedFetch('http://test/api', {
|
|
175
|
+
headers: { Accept: 'application/json' },
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
expect(mockFetch).toHaveBeenCalledWith('http://test/api', {
|
|
179
|
+
headers: { Accept: 'application/json', Authorization: 'Bearer token-123' },
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
});
|
package/__test__/keyring.test.ts
CHANGED
|
@@ -33,6 +33,13 @@ describe('keyring', () => {
|
|
|
33
33
|
expect(mockKeytar.getPassword).toHaveBeenCalledWith('mintlify', 'access_token');
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
+
it('retrieves the refresh token via keytar', async () => {
|
|
37
|
+
const { getRefreshToken } = await import('../src/keyring.js');
|
|
38
|
+
const token = await getRefreshToken();
|
|
39
|
+
expect(token).toBe('test-token');
|
|
40
|
+
expect(mockKeytar.getPassword).toHaveBeenCalledWith('mintlify', 'refresh_token');
|
|
41
|
+
});
|
|
42
|
+
|
|
36
43
|
it('deletes both tokens via keytar', async () => {
|
|
37
44
|
const { clearCredentials } = await import('../src/keyring.js');
|
|
38
45
|
await clearCredentials();
|
|
@@ -4,11 +4,11 @@ import os from 'os';
|
|
|
4
4
|
|
|
5
5
|
import { isTelemetryEnabled, setTelemetryEnabled } from '../src/config.js';
|
|
6
6
|
import { TELEMETRY_ASYNC_TIMEOUT_MS } from '../src/constants.js';
|
|
7
|
-
import { getDistinctId } from '../src/telemetry/distinctId.js';
|
|
8
7
|
import {
|
|
9
8
|
createTelemetryMiddleware,
|
|
10
9
|
getSanitizedCommandForTelemetry,
|
|
11
|
-
} from '../src/
|
|
10
|
+
} from '../src/middlewares/telemetryMiddleware.js';
|
|
11
|
+
import { getDistinctId } from '../src/telemetry/distinctId.js';
|
|
12
12
|
import * as trackModule from '../src/telemetry/track.js';
|
|
13
13
|
import { trackCommand, trackTelemetryPreferenceChange } from '../src/telemetry/track.js';
|
|
14
14
|
|
package/bin/analytics/client.js
CHANGED
|
@@ -7,24 +7,8 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
7
7
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
8
|
});
|
|
9
9
|
};
|
|
10
|
+
import { authenticatedFetch } from '../authenticatedFetch.js';
|
|
10
11
|
import { API_URL } from '../constants.js';
|
|
11
|
-
function getAuthHeaders() {
|
|
12
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
13
|
-
try {
|
|
14
|
-
const { getAccessToken } = yield import('../keyring.js');
|
|
15
|
-
const token = yield getAccessToken();
|
|
16
|
-
if (token) {
|
|
17
|
-
return { Authorization: `Bearer ${token}` };
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
catch (_a) { }
|
|
21
|
-
const envToken = process.env.MINTLIFY_SESSION_TOKEN;
|
|
22
|
-
if (envToken) {
|
|
23
|
-
return { Authorization: `Bearer ${envToken}` };
|
|
24
|
-
}
|
|
25
|
-
throw new Error('Not authenticated. Run `mint login` to authenticate.');
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
12
|
function request(path_1) {
|
|
29
13
|
return __awaiter(this, arguments, void 0, function* (path, params = {}) {
|
|
30
14
|
const url = new URL(`${API_URL}/api/cli/analytics${path}`);
|
|
@@ -32,9 +16,8 @@ function request(path_1) {
|
|
|
32
16
|
if (value !== undefined)
|
|
33
17
|
url.searchParams.set(key, String(value));
|
|
34
18
|
}
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
headers: Object.assign(Object.assign({}, authHeaders), { Accept: 'application/json' }),
|
|
19
|
+
const res = yield authenticatedFetch(url.toString(), {
|
|
20
|
+
headers: { Accept: 'application/json' },
|
|
38
21
|
});
|
|
39
22
|
if (!res.ok) {
|
|
40
23
|
const body = yield res.text().catch(() => '');
|
package/bin/analytics/index.js
CHANGED
|
@@ -13,12 +13,12 @@ import chalk from 'chalk';
|
|
|
13
13
|
import { Text } from 'ink';
|
|
14
14
|
import { getConfigValue } from '../config.js';
|
|
15
15
|
import { terminate } from '../helpers.js';
|
|
16
|
+
import { subdomainMiddleware } from '../middlewares/subdomainMiddleware.js';
|
|
16
17
|
import { getBucketThreads, getBuckets, getConversations, getFeedback, getFeedbackByPage, getKpi, getSearches, } from './client.js';
|
|
17
18
|
import { num, truncate } from './format.js';
|
|
18
19
|
import { formatBarChart, formatOutput, resolveFormat } from './output.js';
|
|
19
20
|
const withSubdomain = (yargs) => yargs.option('subdomain', {
|
|
20
21
|
type: 'string',
|
|
21
|
-
default: getConfigValue('subdomain'),
|
|
22
22
|
description: 'Documentation subdomain (default: mint config set subdomain)',
|
|
23
23
|
});
|
|
24
24
|
function defaultFrom() {
|
|
@@ -44,15 +44,10 @@ const withDates = (yargs) => yargs
|
|
|
44
44
|
default: defaultTo(),
|
|
45
45
|
description: 'End date (YYYY-MM-DD)',
|
|
46
46
|
});
|
|
47
|
-
const withFormat = (yargs) => yargs
|
|
48
|
-
.option('format', {
|
|
47
|
+
const withFormat = (yargs) => yargs.option('format', {
|
|
49
48
|
type: 'string',
|
|
50
49
|
choices: ['table', 'plain', 'json', 'graph'],
|
|
51
50
|
description: 'Output format (table=pretty, plain=pipeable, json=raw)',
|
|
52
|
-
})
|
|
53
|
-
.option('agent', {
|
|
54
|
-
type: 'boolean',
|
|
55
|
-
description: 'Agent-friendly output (equivalent to --format json)',
|
|
56
51
|
});
|
|
57
52
|
const withAll = (yargs) => withFormat(withDates(withSubdomain(yargs)));
|
|
58
53
|
function output(format, text) {
|
|
@@ -63,11 +58,12 @@ function output(format, text) {
|
|
|
63
58
|
process.stdout.write(text + '\n');
|
|
64
59
|
}
|
|
65
60
|
}
|
|
66
|
-
export const analyticsBuilder = (yargs) => yargs
|
|
67
|
-
.
|
|
68
|
-
.
|
|
69
|
-
|
|
70
|
-
|
|
61
|
+
export const analyticsBuilder = (yargs) => withAll(yargs)
|
|
62
|
+
.middleware(subdomainMiddleware)
|
|
63
|
+
.command('stats', 'display KPI numbers (views, visitors, searches)', (yargs) => withAll(yargs).option('page', {
|
|
64
|
+
type: 'string',
|
|
65
|
+
description: 'Filter to a specific page path',
|
|
66
|
+
}), (argv) => __awaiter(void 0, void 0, void 0, function* () {
|
|
71
67
|
var _a, _b;
|
|
72
68
|
const format = resolveFormat(argv);
|
|
73
69
|
try {
|
|
@@ -105,7 +101,7 @@ export const analyticsBuilder = (yargs) => yargs
|
|
|
105
101
|
return;
|
|
106
102
|
}
|
|
107
103
|
if (format === 'graph') {
|
|
108
|
-
const label = (_a = argv.subdomain) !== null && _a !== void 0 ? _a : '
|
|
104
|
+
const label = (_a = argv.subdomain) !== null && _a !== void 0 ? _a : '';
|
|
109
105
|
const lines = [];
|
|
110
106
|
lines.push(chalk.bold(`\nAnalytics \u2014 ${label} (${argv.from} to ${argv.to})\n`));
|
|
111
107
|
lines.push(chalk.bold(' Human vs Agent\n'));
|
|
@@ -126,31 +122,20 @@ export const analyticsBuilder = (yargs) => yargs
|
|
|
126
122
|
yield terminate(0);
|
|
127
123
|
return;
|
|
128
124
|
}
|
|
129
|
-
const agentOnly = argv.agents && !argv.humans;
|
|
130
|
-
const humanOnly = argv.humans && !argv.agents;
|
|
131
|
-
const showHuman = !agentOnly;
|
|
132
|
-
const showAgent = !humanOnly;
|
|
133
|
-
const showTotal = showHuman && showAgent;
|
|
134
125
|
const lines = [];
|
|
135
|
-
const label = (_b = argv.subdomain) !== null && _b !== void 0 ? _b : '
|
|
126
|
+
const label = (_b = argv.subdomain) !== null && _b !== void 0 ? _b : '';
|
|
136
127
|
lines.push(chalk.bold(`\nAnalytics \u2014 ${label} (${argv.from} to ${argv.to})\n`));
|
|
137
128
|
if (argv.page) {
|
|
138
129
|
lines.push(` Page: ${argv.page}\n`);
|
|
139
130
|
}
|
|
140
131
|
lines.push(chalk.bold(' Views'));
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
lines.push(` Agent: ${chalk.magenta(num(kpi.agentViews).padStart(8))}`);
|
|
145
|
-
if (showTotal)
|
|
146
|
-
lines.push(` Total: ${num(kpi.humanViews + kpi.agentViews).padStart(8)}`);
|
|
132
|
+
lines.push(` Human: ${chalk.cyan(num(kpi.humanViews).padStart(8))}`);
|
|
133
|
+
lines.push(` Agent: ${chalk.magenta(num(kpi.agentViews).padStart(8))}`);
|
|
134
|
+
lines.push(` Total: ${num(kpi.humanViews + kpi.agentViews).padStart(8)}`);
|
|
147
135
|
lines.push(chalk.bold('\n Visitors'));
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
lines.push(` Agent: ${chalk.magenta(num(kpi.agentVisitors).padStart(8))}`);
|
|
152
|
-
if (showTotal)
|
|
153
|
-
lines.push(` Total: ${num(kpi.humanVisitors + kpi.agentVisitors).padStart(8)}`);
|
|
136
|
+
lines.push(` Human: ${chalk.cyan(num(kpi.humanVisitors).padStart(8))}`);
|
|
137
|
+
lines.push(` Agent: ${chalk.magenta(num(kpi.agentVisitors).padStart(8))}`);
|
|
138
|
+
lines.push(` Total: ${num(kpi.humanVisitors + kpi.agentVisitors).padStart(8)}`);
|
|
154
139
|
lines.push(`\n Searches: ${chalk.bold(num(kpi.humanSearches))}`);
|
|
155
140
|
lines.push(` Feedback: ${chalk.bold(num(kpi.humanFeedback))}`);
|
|
156
141
|
lines.push(` Assistant: ${chalk.bold(num(kpi.humanAssistant))} web, ${chalk.bold(num(kpi.agentMcpSearches))} API`);
|
|
@@ -195,7 +180,7 @@ export const analyticsBuilder = (yargs) => yargs
|
|
|
195
180
|
output(format, JSON.stringify(data, null, 2));
|
|
196
181
|
}
|
|
197
182
|
else if (format === 'graph') {
|
|
198
|
-
const label = (_a = argv.subdomain) !== null && _a !== void 0 ? _a : '
|
|
183
|
+
const label = (_a = argv.subdomain) !== null && _a !== void 0 ? _a : '';
|
|
199
184
|
const lines = [];
|
|
200
185
|
lines.push(chalk.bold(`\nSearch Queries \u2014 ${label} (${argv.from} to ${argv.to})\n`));
|
|
201
186
|
lines.push(formatBarChart(rows.slice(0, 20).map((r) => ({
|
|
@@ -209,7 +194,7 @@ export const analyticsBuilder = (yargs) => yargs
|
|
|
209
194
|
output(format, formatOutput(format, headers, tableRows, data));
|
|
210
195
|
}
|
|
211
196
|
else {
|
|
212
|
-
const label = (_b = argv.subdomain) !== null && _b !== void 0 ? _b : '
|
|
197
|
+
const label = (_b = argv.subdomain) !== null && _b !== void 0 ? _b : '';
|
|
213
198
|
const lines = [];
|
|
214
199
|
lines.push(chalk.bold(`\nSearch Analytics \u2014 ${label} (${argv.from} to ${argv.to})`));
|
|
215
200
|
lines.push(`Total Searches: ${chalk.bold(num(data.totalSearches))}\n`);
|
|
@@ -256,7 +241,7 @@ export const analyticsBuilder = (yargs) => yargs
|
|
|
256
241
|
output(format, JSON.stringify(data, null, 2));
|
|
257
242
|
}
|
|
258
243
|
else if (format === 'graph') {
|
|
259
|
-
const label = (_a = argv.subdomain) !== null && _a !== void 0 ? _a : '
|
|
244
|
+
const label = (_a = argv.subdomain) !== null && _a !== void 0 ? _a : '';
|
|
260
245
|
const lines = [];
|
|
261
246
|
lines.push(chalk.bold(`\nFeedback by Page \u2014 ${label} (${argv.from} to ${argv.to})\n`));
|
|
262
247
|
lines.push(formatBarChart(rows.slice(0, 20).map((r) => ({
|
|
@@ -267,7 +252,7 @@ export const analyticsBuilder = (yargs) => yargs
|
|
|
267
252
|
output('table', lines.join('\n'));
|
|
268
253
|
}
|
|
269
254
|
else {
|
|
270
|
-
const label = (_b = argv.subdomain) !== null && _b !== void 0 ? _b : '
|
|
255
|
+
const label = (_b = argv.subdomain) !== null && _b !== void 0 ? _b : '';
|
|
271
256
|
const lines = [];
|
|
272
257
|
if (format === 'table')
|
|
273
258
|
lines.push(chalk.bold(`\nFeedback \u2014 ${label} (${argv.from} to ${argv.to})\n`));
|
|
@@ -301,7 +286,7 @@ export const analyticsBuilder = (yargs) => yargs
|
|
|
301
286
|
output(format, JSON.stringify(data, null, 2));
|
|
302
287
|
}
|
|
303
288
|
else {
|
|
304
|
-
const label = (_c = argv.subdomain) !== null && _c !== void 0 ? _c : '
|
|
289
|
+
const label = (_c = argv.subdomain) !== null && _c !== void 0 ? _c : '';
|
|
305
290
|
const lines = [];
|
|
306
291
|
if (format === 'table')
|
|
307
292
|
lines.push(chalk.bold(`\nFeedback \u2014 ${label} (${argv.from} to ${argv.to})\n`));
|
|
@@ -348,7 +333,7 @@ export const analyticsBuilder = (yargs) => yargs
|
|
|
348
333
|
output(format, JSON.stringify(data, null, 2));
|
|
349
334
|
}
|
|
350
335
|
else {
|
|
351
|
-
const label = (_a = argv.subdomain) !== null && _a !== void 0 ? _a : '
|
|
336
|
+
const label = (_a = argv.subdomain) !== null && _a !== void 0 ? _a : '';
|
|
352
337
|
const lines = [];
|
|
353
338
|
if (format === 'table')
|
|
354
339
|
lines.push(chalk.bold(`\nConversations \u2014 ${label} (${argv.from} to ${argv.to})\n`));
|
|
@@ -449,7 +434,7 @@ export const analyticsBuilder = (yargs) => yargs
|
|
|
449
434
|
output(format, JSON.stringify(data, null, 2));
|
|
450
435
|
}
|
|
451
436
|
else if (format === 'graph') {
|
|
452
|
-
const label = (_a = argv.subdomain) !== null && _a !== void 0 ? _a : '
|
|
437
|
+
const label = (_a = argv.subdomain) !== null && _a !== void 0 ? _a : '';
|
|
453
438
|
const lines = [];
|
|
454
439
|
lines.push(chalk.bold(`\nConversation Buckets \u2014 ${label} (${argv.from} to ${argv.to})\n`));
|
|
455
440
|
lines.push(formatBarChart(data.data.slice(0, 20).map((b) => ({
|
|
@@ -460,7 +445,7 @@ export const analyticsBuilder = (yargs) => yargs
|
|
|
460
445
|
output('table', lines.join('\n'));
|
|
461
446
|
}
|
|
462
447
|
else {
|
|
463
|
-
const label = (_b = argv.subdomain) !== null && _b !== void 0 ? _b : '
|
|
448
|
+
const label = (_b = argv.subdomain) !== null && _b !== void 0 ? _b : '';
|
|
464
449
|
const lines = [];
|
|
465
450
|
if (format === 'table')
|
|
466
451
|
lines.push(chalk.bold(`\nConversation Buckets \u2014 ${label} (${argv.from} to ${argv.to})\n`));
|
package/bin/analytics/output.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
+
import { isAI } from '../helpers.js';
|
|
2
3
|
export function resolveFormat(argv) {
|
|
3
|
-
if (argv.agent || process.env.CLAUDECODE === '1')
|
|
4
|
-
return 'json';
|
|
5
4
|
if (argv.format === 'table' ||
|
|
6
5
|
argv.format === 'plain' ||
|
|
7
6
|
argv.format === 'json' ||
|
|
8
7
|
argv.format === 'graph')
|
|
9
8
|
return argv.format;
|
|
9
|
+
if (isAI())
|
|
10
|
+
return 'json';
|
|
10
11
|
return 'plain';
|
|
11
12
|
}
|
|
12
13
|
export function formatPlainTable(headers, rows) {
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import { getAccessToken } from './keyring.js';
|
|
11
|
+
import { refreshAccessToken } from './tokenRefresh.js';
|
|
12
|
+
function getKeyringToken() {
|
|
13
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
14
|
+
try {
|
|
15
|
+
return yield getAccessToken();
|
|
16
|
+
}
|
|
17
|
+
catch (_a) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
export function authenticatedFetch(url, init) {
|
|
23
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
24
|
+
const keyringToken = yield getKeyringToken();
|
|
25
|
+
const envToken = process.env.MINTLIFY_SESSION_TOKEN;
|
|
26
|
+
const token = keyringToken !== null && keyringToken !== void 0 ? keyringToken : envToken;
|
|
27
|
+
if (!token) {
|
|
28
|
+
throw new Error('Not authenticated. Run `mint login` to authenticate.');
|
|
29
|
+
}
|
|
30
|
+
const makeRequest = (t) => fetch(url, Object.assign(Object.assign({}, init), { headers: Object.assign(Object.assign({}, init === null || init === void 0 ? void 0 : init.headers), { Authorization: `Bearer ${t}` }) }));
|
|
31
|
+
const res = yield makeRequest(token);
|
|
32
|
+
if (res.status !== 401)
|
|
33
|
+
return res;
|
|
34
|
+
// If the keyring token expired, try refreshing it
|
|
35
|
+
if (keyringToken) {
|
|
36
|
+
const refreshed = yield refreshAccessToken();
|
|
37
|
+
if (refreshed)
|
|
38
|
+
return makeRequest(refreshed);
|
|
39
|
+
}
|
|
40
|
+
// Fall back to the env session token if it wasn't already used
|
|
41
|
+
if (envToken && token !== envToken) {
|
|
42
|
+
return makeRequest(envToken);
|
|
43
|
+
}
|
|
44
|
+
return res;
|
|
45
|
+
});
|
|
46
|
+
}
|