@kylewadegrove/cutline-mcp-cli 0.3.2 → 0.4.1
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/dist/commands/login.js +56 -1
- package/dist/commands/status.js +52 -12
- package/package.json +1 -1
- package/src/commands/login.ts +71 -1
- package/src/commands/status.ts +59 -13
package/dist/commands/login.js
CHANGED
|
@@ -11,6 +11,42 @@ const callback_js_1 = require("../auth/callback.js");
|
|
|
11
11
|
const keychain_js_1 = require("../auth/keychain.js");
|
|
12
12
|
const config_store_js_1 = require("../utils/config-store.js");
|
|
13
13
|
const config_js_1 = require("../utils/config.js");
|
|
14
|
+
async function getSubscriptionStatus(idToken, isStaging) {
|
|
15
|
+
try {
|
|
16
|
+
const baseUrl = isStaging
|
|
17
|
+
? 'https://us-central1-cutline-staging.cloudfunctions.net'
|
|
18
|
+
: 'https://us-central1-cutline-prod.cloudfunctions.net';
|
|
19
|
+
const response = await fetch(`${baseUrl}/mcpSubscriptionStatus`, {
|
|
20
|
+
method: 'GET',
|
|
21
|
+
headers: {
|
|
22
|
+
'Authorization': `Bearer ${idToken}`,
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
if (!response.ok) {
|
|
26
|
+
return { status: 'unknown' };
|
|
27
|
+
}
|
|
28
|
+
return await response.json();
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
// Silently fail - subscription check is optional during login
|
|
32
|
+
return { status: 'unknown' };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async function exchangeRefreshForIdToken(refreshToken, apiKey) {
|
|
36
|
+
const response = await fetch(`https://securetoken.googleapis.com/v1/token?key=${apiKey}`, {
|
|
37
|
+
method: 'POST',
|
|
38
|
+
headers: { 'Content-Type': 'application/json' },
|
|
39
|
+
body: JSON.stringify({
|
|
40
|
+
grant_type: 'refresh_token',
|
|
41
|
+
refresh_token: refreshToken,
|
|
42
|
+
}),
|
|
43
|
+
});
|
|
44
|
+
if (!response.ok) {
|
|
45
|
+
throw new Error('Failed to get ID token');
|
|
46
|
+
}
|
|
47
|
+
const data = await response.json();
|
|
48
|
+
return data.id_token;
|
|
49
|
+
}
|
|
14
50
|
async function exchangeCustomToken(customToken, apiKey) {
|
|
15
51
|
const response = await fetch(`https://identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken?key=${apiKey}`, {
|
|
16
52
|
method: 'POST',
|
|
@@ -104,7 +140,26 @@ async function loginCommand(options) {
|
|
|
104
140
|
if (email || result.email) {
|
|
105
141
|
console.log(chalk_1.default.gray(` Logged in as: ${email || result.email}`));
|
|
106
142
|
}
|
|
107
|
-
|
|
143
|
+
// Check subscription status
|
|
144
|
+
try {
|
|
145
|
+
spinner.start('Checking subscription...');
|
|
146
|
+
const idToken = await exchangeRefreshForIdToken(refreshToken, firebaseApiKey);
|
|
147
|
+
const subscription = await getSubscriptionStatus(idToken, !!options.staging);
|
|
148
|
+
spinner.stop();
|
|
149
|
+
if (subscription.status === 'active' || subscription.status === 'trialing') {
|
|
150
|
+
const statusLabel = subscription.status === 'trialing' ? ' (trial)' : '';
|
|
151
|
+
console.log(chalk_1.default.gray(' Plan:'), chalk_1.default.green(`✓ ${subscription.planName || 'Premium'}${statusLabel}`));
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
console.log(chalk_1.default.gray(' Plan:'), chalk_1.default.white('Free'));
|
|
155
|
+
console.log(chalk_1.default.dim(' Upgrade at'), chalk_1.default.cyan('https://thecutline.ai/pricing'));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
spinner.stop();
|
|
160
|
+
// Silently skip subscription check on error
|
|
161
|
+
}
|
|
162
|
+
console.log(chalk_1.default.gray('\n MCP servers can now access your account\n'));
|
|
108
163
|
console.log(chalk_1.default.dim(' Run'), chalk_1.default.cyan('cutline-mcp status'), chalk_1.default.dim('to verify\n'));
|
|
109
164
|
}
|
|
110
165
|
catch (error) {
|
package/dist/commands/status.js
CHANGED
|
@@ -6,9 +6,29 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.statusCommand = statusCommand;
|
|
7
7
|
const chalk_1 = __importDefault(require("chalk"));
|
|
8
8
|
const ora_1 = __importDefault(require("ora"));
|
|
9
|
-
const firebase_admin_1 = __importDefault(require("firebase-admin"));
|
|
10
9
|
const keychain_js_1 = require("../auth/keychain.js");
|
|
11
10
|
const config_js_1 = require("../utils/config.js");
|
|
11
|
+
async function getSubscriptionStatus(idToken, isStaging) {
|
|
12
|
+
try {
|
|
13
|
+
const baseUrl = isStaging
|
|
14
|
+
? 'https://us-central1-cutline-staging.cloudfunctions.net'
|
|
15
|
+
: 'https://us-central1-cutline-prod.cloudfunctions.net';
|
|
16
|
+
const response = await fetch(`${baseUrl}/mcpSubscriptionStatus`, {
|
|
17
|
+
method: 'GET',
|
|
18
|
+
headers: {
|
|
19
|
+
'Authorization': `Bearer ${idToken}`,
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
if (!response.ok) {
|
|
23
|
+
return { status: 'unknown' };
|
|
24
|
+
}
|
|
25
|
+
return await response.json();
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
console.error('Error fetching subscription:', error);
|
|
29
|
+
return { status: 'unknown' };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
12
32
|
async function exchangeRefreshToken(refreshToken, apiKey) {
|
|
13
33
|
const response = await fetch(`https://securetoken.googleapis.com/v1/token?key=${apiKey}`, {
|
|
14
34
|
method: 'POST',
|
|
@@ -58,16 +78,12 @@ async function statusCommand(options) {
|
|
|
58
78
|
// Exchange refresh token for ID token
|
|
59
79
|
spinner.text = 'Verifying credentials...';
|
|
60
80
|
const idToken = await exchangeRefreshToken(refreshToken, firebaseApiKey);
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
firebase_admin_1.default.initializeApp({ projectId });
|
|
65
|
-
}
|
|
66
|
-
// Verify the ID token
|
|
67
|
-
const decoded = await firebase_admin_1.default.auth().verifyIdToken(idToken);
|
|
81
|
+
// Decode JWT payload (base64) to get user info - no verification needed, just display
|
|
82
|
+
const payloadBase64 = idToken.split('.')[1];
|
|
83
|
+
const decoded = JSON.parse(Buffer.from(payloadBase64, 'base64').toString());
|
|
68
84
|
spinner.succeed(chalk_1.default.green('Authenticated'));
|
|
69
|
-
console.log(chalk_1.default.gray(' User:'), chalk_1.default.white(decoded.email || decoded.
|
|
70
|
-
console.log(chalk_1.default.gray(' UID:'), chalk_1.default.dim(decoded.
|
|
85
|
+
console.log(chalk_1.default.gray(' User:'), chalk_1.default.white(decoded.email || decoded.user_id || decoded.sub));
|
|
86
|
+
console.log(chalk_1.default.gray(' UID:'), chalk_1.default.dim(decoded.user_id || decoded.sub));
|
|
71
87
|
// Calculate token expiry
|
|
72
88
|
const expiresIn = Math.floor((decoded.exp * 1000 - Date.now()) / 1000 / 60);
|
|
73
89
|
console.log(chalk_1.default.gray(' Token expires in:'), chalk_1.default.white(`${expiresIn} minutes`));
|
|
@@ -78,8 +94,32 @@ async function statusCommand(options) {
|
|
|
78
94
|
if (decoded.deviceId) {
|
|
79
95
|
console.log(chalk_1.default.gray(' Device ID:'), chalk_1.default.dim(decoded.deviceId));
|
|
80
96
|
}
|
|
81
|
-
//
|
|
82
|
-
|
|
97
|
+
// Check subscription status via Cloud Function
|
|
98
|
+
spinner.start('Checking subscription...');
|
|
99
|
+
const subscription = await getSubscriptionStatus(idToken, !!options.staging);
|
|
100
|
+
spinner.stop();
|
|
101
|
+
if (subscription.status === 'active' || subscription.status === 'trialing') {
|
|
102
|
+
const statusLabel = subscription.status === 'trialing' ? ' (trial)' : '';
|
|
103
|
+
console.log(chalk_1.default.gray(' Plan:'), chalk_1.default.green(`✓ ${subscription.planName || 'Premium'}${statusLabel}`));
|
|
104
|
+
if (subscription.periodEnd) {
|
|
105
|
+
const periodEndDate = new Date(subscription.periodEnd);
|
|
106
|
+
const daysLeft = Math.ceil((periodEndDate.getTime() - Date.now()) / (1000 * 60 * 60 * 24));
|
|
107
|
+
console.log(chalk_1.default.gray(' Renews:'), chalk_1.default.white(`${periodEndDate.toLocaleDateString()} (${daysLeft} days)`));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
else if (subscription.status === 'past_due') {
|
|
111
|
+
console.log(chalk_1.default.gray(' Plan:'), chalk_1.default.yellow('⚠ Premium (payment past due)'));
|
|
112
|
+
}
|
|
113
|
+
else if (subscription.status === 'canceled') {
|
|
114
|
+
console.log(chalk_1.default.gray(' Plan:'), chalk_1.default.yellow('Premium (canceled)'));
|
|
115
|
+
if (subscription.periodEnd) {
|
|
116
|
+
console.log(chalk_1.default.gray(' Access until:'), chalk_1.default.white(new Date(subscription.periodEnd).toLocaleDateString()));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
console.log(chalk_1.default.gray(' Plan:'), chalk_1.default.white('Free'));
|
|
121
|
+
console.log(chalk_1.default.dim(' Upgrade at'), chalk_1.default.cyan('https://thecutline.ai/pricing'));
|
|
122
|
+
}
|
|
83
123
|
console.log();
|
|
84
124
|
}
|
|
85
125
|
catch (error) {
|
package/package.json
CHANGED
package/src/commands/login.ts
CHANGED
|
@@ -6,6 +6,57 @@ import { storeRefreshToken } from '../auth/keychain.js';
|
|
|
6
6
|
import { saveConfig } from '../utils/config-store.js';
|
|
7
7
|
import { getConfig, fetchFirebaseApiKey } from '../utils/config.js';
|
|
8
8
|
|
|
9
|
+
interface SubscriptionInfo {
|
|
10
|
+
status: 'free' | 'active' | 'trialing' | 'past_due' | 'canceled' | 'unknown';
|
|
11
|
+
planName?: string;
|
|
12
|
+
periodEnd?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function getSubscriptionStatus(idToken: string, isStaging: boolean): Promise<SubscriptionInfo> {
|
|
16
|
+
try {
|
|
17
|
+
const baseUrl = isStaging
|
|
18
|
+
? 'https://us-central1-cutline-staging.cloudfunctions.net'
|
|
19
|
+
: 'https://us-central1-cutline-prod.cloudfunctions.net';
|
|
20
|
+
|
|
21
|
+
const response = await fetch(`${baseUrl}/mcpSubscriptionStatus`, {
|
|
22
|
+
method: 'GET',
|
|
23
|
+
headers: {
|
|
24
|
+
'Authorization': `Bearer ${idToken}`,
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
if (!response.ok) {
|
|
29
|
+
return { status: 'unknown' };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return await response.json();
|
|
33
|
+
} catch (error) {
|
|
34
|
+
// Silently fail - subscription check is optional during login
|
|
35
|
+
return { status: 'unknown' };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function exchangeRefreshForIdToken(refreshToken: string, apiKey: string): Promise<string> {
|
|
40
|
+
const response = await fetch(
|
|
41
|
+
`https://securetoken.googleapis.com/v1/token?key=${apiKey}`,
|
|
42
|
+
{
|
|
43
|
+
method: 'POST',
|
|
44
|
+
headers: { 'Content-Type': 'application/json' },
|
|
45
|
+
body: JSON.stringify({
|
|
46
|
+
grant_type: 'refresh_token',
|
|
47
|
+
refresh_token: refreshToken,
|
|
48
|
+
}),
|
|
49
|
+
}
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
if (!response.ok) {
|
|
53
|
+
throw new Error('Failed to get ID token');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const data = await response.json();
|
|
57
|
+
return data.id_token;
|
|
58
|
+
}
|
|
59
|
+
|
|
9
60
|
async function exchangeCustomToken(customToken: string, apiKey: string): Promise<{ refreshToken: string; email?: string }> {
|
|
10
61
|
const response = await fetch(
|
|
11
62
|
`https://identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken?key=${apiKey}`,
|
|
@@ -117,7 +168,26 @@ export async function loginCommand(options: { staging?: boolean; signup?: boolea
|
|
|
117
168
|
console.log(chalk.gray(` Logged in as: ${email || result.email}`));
|
|
118
169
|
}
|
|
119
170
|
|
|
120
|
-
|
|
171
|
+
// Check subscription status
|
|
172
|
+
try {
|
|
173
|
+
spinner.start('Checking subscription...');
|
|
174
|
+
const idToken = await exchangeRefreshForIdToken(refreshToken, firebaseApiKey);
|
|
175
|
+
const subscription = await getSubscriptionStatus(idToken, !!options.staging);
|
|
176
|
+
spinner.stop();
|
|
177
|
+
|
|
178
|
+
if (subscription.status === 'active' || subscription.status === 'trialing') {
|
|
179
|
+
const statusLabel = subscription.status === 'trialing' ? ' (trial)' : '';
|
|
180
|
+
console.log(chalk.gray(' Plan:'), chalk.green(`✓ ${subscription.planName || 'Premium'}${statusLabel}`));
|
|
181
|
+
} else {
|
|
182
|
+
console.log(chalk.gray(' Plan:'), chalk.white('Free'));
|
|
183
|
+
console.log(chalk.dim(' Upgrade at'), chalk.cyan('https://thecutline.ai/pricing'));
|
|
184
|
+
}
|
|
185
|
+
} catch {
|
|
186
|
+
spinner.stop();
|
|
187
|
+
// Silently skip subscription check on error
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
console.log(chalk.gray('\n MCP servers can now access your account\n'));
|
|
121
191
|
console.log(chalk.dim(' Run'), chalk.cyan('cutline-mcp status'), chalk.dim('to verify\n'));
|
|
122
192
|
|
|
123
193
|
} catch (error) {
|
package/src/commands/status.ts
CHANGED
|
@@ -1,9 +1,38 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import ora from 'ora';
|
|
3
|
-
import admin from 'firebase-admin';
|
|
4
3
|
import { getRefreshToken } from '../auth/keychain.js';
|
|
5
4
|
import { fetchFirebaseApiKey } from '../utils/config.js';
|
|
6
5
|
|
|
6
|
+
interface SubscriptionInfo {
|
|
7
|
+
status: 'free' | 'active' | 'trialing' | 'past_due' | 'canceled' | 'unknown';
|
|
8
|
+
planName?: string;
|
|
9
|
+
periodEnd?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function getSubscriptionStatus(idToken: string, isStaging: boolean): Promise<SubscriptionInfo> {
|
|
13
|
+
try {
|
|
14
|
+
const baseUrl = isStaging
|
|
15
|
+
? 'https://us-central1-cutline-staging.cloudfunctions.net'
|
|
16
|
+
: 'https://us-central1-cutline-prod.cloudfunctions.net';
|
|
17
|
+
|
|
18
|
+
const response = await fetch(`${baseUrl}/mcpSubscriptionStatus`, {
|
|
19
|
+
method: 'GET',
|
|
20
|
+
headers: {
|
|
21
|
+
'Authorization': `Bearer ${idToken}`,
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
if (!response.ok) {
|
|
26
|
+
return { status: 'unknown' };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return await response.json();
|
|
30
|
+
} catch (error) {
|
|
31
|
+
console.error('Error fetching subscription:', error);
|
|
32
|
+
return { status: 'unknown' };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
7
36
|
async function exchangeRefreshToken(refreshToken: string, apiKey: string): Promise<string> {
|
|
8
37
|
const response = await fetch(
|
|
9
38
|
`https://securetoken.googleapis.com/v1/token?key=${apiKey}`,
|
|
@@ -63,19 +92,14 @@ export async function statusCommand(options: { staging?: boolean }) {
|
|
|
63
92
|
spinner.text = 'Verifying credentials...';
|
|
64
93
|
const idToken = await exchangeRefreshToken(refreshToken, firebaseApiKey);
|
|
65
94
|
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
admin.initializeApp({ projectId });
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Verify the ID token
|
|
73
|
-
const decoded = await admin.auth().verifyIdToken(idToken);
|
|
95
|
+
// Decode JWT payload (base64) to get user info - no verification needed, just display
|
|
96
|
+
const payloadBase64 = idToken.split('.')[1];
|
|
97
|
+
const decoded = JSON.parse(Buffer.from(payloadBase64, 'base64').toString());
|
|
74
98
|
|
|
75
99
|
spinner.succeed(chalk.green('Authenticated'));
|
|
76
100
|
|
|
77
|
-
console.log(chalk.gray(' User:'), chalk.white(decoded.email || decoded.
|
|
78
|
-
console.log(chalk.gray(' UID:'), chalk.dim(decoded.
|
|
101
|
+
console.log(chalk.gray(' User:'), chalk.white(decoded.email || decoded.user_id || decoded.sub));
|
|
102
|
+
console.log(chalk.gray(' UID:'), chalk.dim(decoded.user_id || decoded.sub));
|
|
79
103
|
|
|
80
104
|
// Calculate token expiry
|
|
81
105
|
const expiresIn = Math.floor((decoded.exp * 1000 - Date.now()) / 1000 / 60);
|
|
@@ -89,8 +113,30 @@ export async function statusCommand(options: { staging?: boolean }) {
|
|
|
89
113
|
console.log(chalk.gray(' Device ID:'), chalk.dim(decoded.deviceId));
|
|
90
114
|
}
|
|
91
115
|
|
|
92
|
-
//
|
|
93
|
-
|
|
116
|
+
// Check subscription status via Cloud Function
|
|
117
|
+
spinner.start('Checking subscription...');
|
|
118
|
+
const subscription = await getSubscriptionStatus(idToken, !!options.staging);
|
|
119
|
+
spinner.stop();
|
|
120
|
+
|
|
121
|
+
if (subscription.status === 'active' || subscription.status === 'trialing') {
|
|
122
|
+
const statusLabel = subscription.status === 'trialing' ? ' (trial)' : '';
|
|
123
|
+
console.log(chalk.gray(' Plan:'), chalk.green(`✓ ${subscription.planName || 'Premium'}${statusLabel}`));
|
|
124
|
+
if (subscription.periodEnd) {
|
|
125
|
+
const periodEndDate = new Date(subscription.periodEnd);
|
|
126
|
+
const daysLeft = Math.ceil((periodEndDate.getTime() - Date.now()) / (1000 * 60 * 60 * 24));
|
|
127
|
+
console.log(chalk.gray(' Renews:'), chalk.white(`${periodEndDate.toLocaleDateString()} (${daysLeft} days)`));
|
|
128
|
+
}
|
|
129
|
+
} else if (subscription.status === 'past_due') {
|
|
130
|
+
console.log(chalk.gray(' Plan:'), chalk.yellow('⚠ Premium (payment past due)'));
|
|
131
|
+
} else if (subscription.status === 'canceled') {
|
|
132
|
+
console.log(chalk.gray(' Plan:'), chalk.yellow('Premium (canceled)'));
|
|
133
|
+
if (subscription.periodEnd) {
|
|
134
|
+
console.log(chalk.gray(' Access until:'), chalk.white(new Date(subscription.periodEnd).toLocaleDateString()));
|
|
135
|
+
}
|
|
136
|
+
} else {
|
|
137
|
+
console.log(chalk.gray(' Plan:'), chalk.white('Free'));
|
|
138
|
+
console.log(chalk.dim(' Upgrade at'), chalk.cyan('https://thecutline.ai/pricing'));
|
|
139
|
+
}
|
|
94
140
|
|
|
95
141
|
console.log();
|
|
96
142
|
|