@kylewadegrove/cutline-mcp-cli 0.4.0 → 0.4.2

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.
@@ -7,37 +7,25 @@ exports.loginCommand = loginCommand;
7
7
  const open_1 = __importDefault(require("open"));
8
8
  const chalk_1 = __importDefault(require("chalk"));
9
9
  const ora_1 = __importDefault(require("ora"));
10
- const firebase_admin_1 = __importDefault(require("firebase-admin"));
11
10
  const callback_js_1 = require("../auth/callback.js");
12
11
  const keychain_js_1 = require("../auth/keychain.js");
13
12
  const config_store_js_1 = require("../utils/config-store.js");
14
13
  const config_js_1 = require("../utils/config.js");
15
- async function getSubscriptionStatus(uid, projectId) {
14
+ async function getSubscriptionStatus(idToken, isStaging) {
16
15
  try {
17
- // Initialize Firebase Admin if not already
18
- if (firebase_admin_1.default.apps.length === 0) {
19
- firebase_admin_1.default.initializeApp({ projectId });
20
- }
21
- const db = firebase_admin_1.default.firestore();
22
- const userDoc = await db.collection('users').doc(uid).get();
23
- if (!userDoc.exists) {
24
- return { status: 'free' };
25
- }
26
- const userData = userDoc.data();
27
- const subscription = userData?.subscription;
28
- if (!subscription) {
29
- return { status: 'free' };
30
- }
31
- const status = subscription.status || 'unknown';
32
- // Get plan name from price ID or default
33
- let planName = 'Premium';
34
- if (subscription.priceId?.includes('yearly') || subscription.interval === 'year') {
35
- planName = 'Premium (Yearly)';
36
- }
37
- else if (subscription.priceId?.includes('monthly') || subscription.interval === 'month') {
38
- planName = 'Premium (Monthly)';
39
- }
40
- return { status, planName };
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();
41
29
  }
42
30
  catch (error) {
43
31
  // Silently fail - subscription check is optional during login
@@ -135,7 +123,7 @@ async function loginCommand(options) {
135
123
  catch (error) {
136
124
  console.warn(chalk_1.default.yellow(' ⚠️ Could not save to Keychain (skipping)'));
137
125
  }
138
- // Save to file config (cross-platform) - don't store API key anymore
126
+ // Save to file config (cross-platform)
139
127
  try {
140
128
  (0, config_store_js_1.saveConfig)({
141
129
  refreshToken,
@@ -155,14 +143,8 @@ async function loginCommand(options) {
155
143
  // Check subscription status
156
144
  try {
157
145
  spinner.start('Checking subscription...');
158
- const projectId = options.staging ? 'cutline-staging' : 'cutline-prod';
159
146
  const idToken = await exchangeRefreshForIdToken(refreshToken, firebaseApiKey);
160
- // Initialize Firebase Admin if needed
161
- if (firebase_admin_1.default.apps.length === 0) {
162
- firebase_admin_1.default.initializeApp({ projectId });
163
- }
164
- const decoded = await firebase_admin_1.default.auth().verifyIdToken(idToken);
165
- const subscription = await getSubscriptionStatus(decoded.uid, projectId);
147
+ const subscription = await getSubscriptionStatus(idToken, !!options.staging);
166
148
  spinner.stop();
167
149
  if (subscription.status === 'active' || subscription.status === 'trialing') {
168
150
  const statusLabel = subscription.status === 'trialing' ? ' (trial)' : '';
@@ -6,34 +6,23 @@ 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");
12
- async function getSubscriptionStatus(uid) {
11
+ async function getSubscriptionStatus(idToken, isStaging) {
13
12
  try {
14
- const db = firebase_admin_1.default.firestore();
15
- const userDoc = await db.collection('users').doc(uid).get();
16
- if (!userDoc.exists) {
17
- return { status: 'free' };
18
- }
19
- const userData = userDoc.data();
20
- const subscription = userData?.subscription;
21
- if (!subscription) {
22
- return { status: 'free' };
23
- }
24
- const status = subscription.status || 'unknown';
25
- const periodEnd = subscription.periodEndsAt
26
- ? new Date(subscription.periodEndsAt)
27
- : undefined;
28
- // Get plan name from price ID or default
29
- let planName = 'Premium';
30
- if (subscription.priceId?.includes('yearly') || subscription.interval === 'year') {
31
- planName = 'Premium (Yearly)';
32
- }
33
- else if (subscription.priceId?.includes('monthly') || subscription.interval === 'month') {
34
- planName = 'Premium (Monthly)';
35
- }
36
- return { status, planName, periodEnd };
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();
37
26
  }
38
27
  catch (error) {
39
28
  console.error('Error fetching subscription:', error);
@@ -89,16 +78,12 @@ async function statusCommand(options) {
89
78
  // Exchange refresh token for ID token
90
79
  spinner.text = 'Verifying credentials...';
91
80
  const idToken = await exchangeRefreshToken(refreshToken, firebaseApiKey);
92
- // Initialize Firebase Admin with correct project ID
93
- if (firebase_admin_1.default.apps.length === 0) {
94
- const projectId = options.staging ? 'cutline-staging' : 'cutline-prod';
95
- firebase_admin_1.default.initializeApp({ projectId });
96
- }
97
- // Verify the ID token
98
- 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());
99
84
  spinner.succeed(chalk_1.default.green('Authenticated'));
100
- console.log(chalk_1.default.gray(' User:'), chalk_1.default.white(decoded.email || decoded.uid));
101
- console.log(chalk_1.default.gray(' UID:'), chalk_1.default.dim(decoded.uid));
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));
102
87
  // Calculate token expiry
103
88
  const expiresIn = Math.floor((decoded.exp * 1000 - Date.now()) / 1000 / 60);
104
89
  console.log(chalk_1.default.gray(' Token expires in:'), chalk_1.default.white(`${expiresIn} minutes`));
@@ -109,14 +94,17 @@ async function statusCommand(options) {
109
94
  if (decoded.deviceId) {
110
95
  console.log(chalk_1.default.gray(' Device ID:'), chalk_1.default.dim(decoded.deviceId));
111
96
  }
112
- // Check subscription status from Firestore
113
- const subscription = await getSubscriptionStatus(decoded.uid);
97
+ // Check subscription status via Cloud Function
98
+ spinner.start('Checking subscription...');
99
+ const subscription = await getSubscriptionStatus(idToken, !!options.staging);
100
+ spinner.stop();
114
101
  if (subscription.status === 'active' || subscription.status === 'trialing') {
115
102
  const statusLabel = subscription.status === 'trialing' ? ' (trial)' : '';
116
103
  console.log(chalk_1.default.gray(' Plan:'), chalk_1.default.green(`✓ ${subscription.planName || 'Premium'}${statusLabel}`));
117
104
  if (subscription.periodEnd) {
118
- const daysLeft = Math.ceil((subscription.periodEnd.getTime() - Date.now()) / (1000 * 60 * 60 * 24));
119
- console.log(chalk_1.default.gray(' Renews:'), chalk_1.default.white(`${subscription.periodEnd.toLocaleDateString()} (${daysLeft} days)`));
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)`));
120
108
  }
121
109
  }
122
110
  else if (subscription.status === 'past_due') {
@@ -125,7 +113,7 @@ async function statusCommand(options) {
125
113
  else if (subscription.status === 'canceled') {
126
114
  console.log(chalk_1.default.gray(' Plan:'), chalk_1.default.yellow('Premium (canceled)'));
127
115
  if (subscription.periodEnd) {
128
- console.log(chalk_1.default.gray(' Access until:'), chalk_1.default.white(subscription.periodEnd.toLocaleDateString()));
116
+ console.log(chalk_1.default.gray(' Access until:'), chalk_1.default.white(new Date(subscription.periodEnd).toLocaleDateString()));
129
117
  }
130
118
  }
131
119
  else {
@@ -20,6 +20,9 @@ function saveConfig(config) {
20
20
  ensureConfigDir();
21
21
  const current = loadConfig();
22
22
  const newConfig = { ...current, ...config };
23
+ // Remove legacy fields that are no longer stored (e.g. firebaseApiKey)
24
+ // to prevent stale values from causing cross-project auth mismatches
25
+ delete newConfig.firebaseApiKey;
23
26
  fs_1.default.writeFileSync(CONFIG_FILE, JSON.stringify(newConfig, null, 2), { mode: 0o600 });
24
27
  }
25
28
  function loadConfig() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kylewadegrove/cutline-mcp-cli",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "CLI tool for authenticating with Cutline MCP servers",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -1,7 +1,6 @@
1
1
  import open from 'open';
2
2
  import chalk from 'chalk';
3
3
  import ora from 'ora';
4
- import admin from 'firebase-admin';
5
4
  import { startCallbackServer } from '../auth/callback.js';
6
5
  import { storeRefreshToken } from '../auth/keychain.js';
7
6
  import { saveConfig } from '../utils/config-store.js';
@@ -10,40 +9,27 @@ import { getConfig, fetchFirebaseApiKey } from '../utils/config.js';
10
9
  interface SubscriptionInfo {
11
10
  status: 'free' | 'active' | 'trialing' | 'past_due' | 'canceled' | 'unknown';
12
11
  planName?: string;
12
+ periodEnd?: string;
13
13
  }
14
14
 
15
- async function getSubscriptionStatus(uid: string, projectId: string): Promise<SubscriptionInfo> {
15
+ async function getSubscriptionStatus(idToken: string, isStaging: boolean): Promise<SubscriptionInfo> {
16
16
  try {
17
- // Initialize Firebase Admin if not already
18
- if (admin.apps.length === 0) {
19
- admin.initializeApp({ projectId });
20
- }
21
-
22
- const db = admin.firestore();
23
- const userDoc = await db.collection('users').doc(uid).get();
17
+ const baseUrl = isStaging
18
+ ? 'https://us-central1-cutline-staging.cloudfunctions.net'
19
+ : 'https://us-central1-cutline-prod.cloudfunctions.net';
24
20
 
25
- if (!userDoc.exists) {
26
- return { status: 'free' };
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' };
27
30
  }
28
-
29
- const userData = userDoc.data();
30
- const subscription = userData?.subscription;
31
-
32
- if (!subscription) {
33
- return { status: 'free' };
34
- }
35
-
36
- const status = subscription.status || 'unknown';
37
-
38
- // Get plan name from price ID or default
39
- let planName = 'Premium';
40
- if (subscription.priceId?.includes('yearly') || subscription.interval === 'year') {
41
- planName = 'Premium (Yearly)';
42
- } else if (subscription.priceId?.includes('monthly') || subscription.interval === 'month') {
43
- planName = 'Premium (Monthly)';
44
- }
45
-
46
- return { status, planName };
31
+
32
+ return await response.json();
47
33
  } catch (error) {
48
34
  // Silently fail - subscription check is optional during login
49
35
  return { status: 'unknown' };
@@ -162,7 +148,7 @@ export async function loginCommand(options: { staging?: boolean; signup?: boolea
162
148
  console.warn(chalk.yellow(' ⚠️ Could not save to Keychain (skipping)'));
163
149
  }
164
150
 
165
- // Save to file config (cross-platform) - don't store API key anymore
151
+ // Save to file config (cross-platform)
166
152
  try {
167
153
  saveConfig({
168
154
  refreshToken,
@@ -185,16 +171,8 @@ export async function loginCommand(options: { staging?: boolean; signup?: boolea
185
171
  // Check subscription status
186
172
  try {
187
173
  spinner.start('Checking subscription...');
188
- const projectId = options.staging ? 'cutline-staging' : 'cutline-prod';
189
174
  const idToken = await exchangeRefreshForIdToken(refreshToken, firebaseApiKey);
190
-
191
- // Initialize Firebase Admin if needed
192
- if (admin.apps.length === 0) {
193
- admin.initializeApp({ projectId });
194
- }
195
-
196
- const decoded = await admin.auth().verifyIdToken(idToken);
197
- const subscription = await getSubscriptionStatus(decoded.uid, projectId);
175
+ const subscription = await getSubscriptionStatus(idToken, !!options.staging);
198
176
  spinner.stop();
199
177
 
200
178
  if (subscription.status === 'active' || subscription.status === 'trialing') {
@@ -1,45 +1,32 @@
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
 
7
6
  interface SubscriptionInfo {
8
7
  status: 'free' | 'active' | 'trialing' | 'past_due' | 'canceled' | 'unknown';
9
8
  planName?: string;
10
- periodEnd?: Date;
9
+ periodEnd?: string;
11
10
  }
12
11
 
13
- async function getSubscriptionStatus(uid: string): Promise<SubscriptionInfo> {
12
+ async function getSubscriptionStatus(idToken: string, isStaging: boolean): Promise<SubscriptionInfo> {
14
13
  try {
15
- const db = admin.firestore();
16
- const userDoc = await db.collection('users').doc(uid).get();
14
+ const baseUrl = isStaging
15
+ ? 'https://us-central1-cutline-staging.cloudfunctions.net'
16
+ : 'https://us-central1-cutline-prod.cloudfunctions.net';
17
17
 
18
- if (!userDoc.exists) {
19
- return { status: 'free' };
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' };
20
27
  }
21
-
22
- const userData = userDoc.data();
23
- const subscription = userData?.subscription;
24
-
25
- if (!subscription) {
26
- return { status: 'free' };
27
- }
28
-
29
- const status = subscription.status || 'unknown';
30
- const periodEnd = subscription.periodEndsAt
31
- ? new Date(subscription.periodEndsAt)
32
- : undefined;
33
-
34
- // Get plan name from price ID or default
35
- let planName = 'Premium';
36
- if (subscription.priceId?.includes('yearly') || subscription.interval === 'year') {
37
- planName = 'Premium (Yearly)';
38
- } else if (subscription.priceId?.includes('monthly') || subscription.interval === 'month') {
39
- planName = 'Premium (Monthly)';
40
- }
41
-
42
- return { status, planName, periodEnd };
28
+
29
+ return await response.json();
43
30
  } catch (error) {
44
31
  console.error('Error fetching subscription:', error);
45
32
  return { status: 'unknown' };
@@ -105,19 +92,14 @@ export async function statusCommand(options: { staging?: boolean }) {
105
92
  spinner.text = 'Verifying credentials...';
106
93
  const idToken = await exchangeRefreshToken(refreshToken, firebaseApiKey);
107
94
 
108
- // Initialize Firebase Admin with correct project ID
109
- if (admin.apps.length === 0) {
110
- const projectId = options.staging ? 'cutline-staging' : 'cutline-prod';
111
- admin.initializeApp({ projectId });
112
- }
113
-
114
- // Verify the ID token
115
- 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());
116
98
 
117
99
  spinner.succeed(chalk.green('Authenticated'));
118
100
 
119
- console.log(chalk.gray(' User:'), chalk.white(decoded.email || decoded.uid));
120
- console.log(chalk.gray(' UID:'), chalk.dim(decoded.uid));
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));
121
103
 
122
104
  // Calculate token expiry
123
105
  const expiresIn = Math.floor((decoded.exp * 1000 - Date.now()) / 1000 / 60);
@@ -131,22 +113,25 @@ export async function statusCommand(options: { staging?: boolean }) {
131
113
  console.log(chalk.gray(' Device ID:'), chalk.dim(decoded.deviceId));
132
114
  }
133
115
 
134
- // Check subscription status from Firestore
135
- const subscription = await getSubscriptionStatus(decoded.uid);
116
+ // Check subscription status via Cloud Function
117
+ spinner.start('Checking subscription...');
118
+ const subscription = await getSubscriptionStatus(idToken, !!options.staging);
119
+ spinner.stop();
136
120
 
137
121
  if (subscription.status === 'active' || subscription.status === 'trialing') {
138
122
  const statusLabel = subscription.status === 'trialing' ? ' (trial)' : '';
139
123
  console.log(chalk.gray(' Plan:'), chalk.green(`✓ ${subscription.planName || 'Premium'}${statusLabel}`));
140
124
  if (subscription.periodEnd) {
141
- const daysLeft = Math.ceil((subscription.periodEnd.getTime() - Date.now()) / (1000 * 60 * 60 * 24));
142
- console.log(chalk.gray(' Renews:'), chalk.white(`${subscription.periodEnd.toLocaleDateString()} (${daysLeft} days)`));
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)`));
143
128
  }
144
129
  } else if (subscription.status === 'past_due') {
145
130
  console.log(chalk.gray(' Plan:'), chalk.yellow('⚠ Premium (payment past due)'));
146
131
  } else if (subscription.status === 'canceled') {
147
132
  console.log(chalk.gray(' Plan:'), chalk.yellow('Premium (canceled)'));
148
133
  if (subscription.periodEnd) {
149
- console.log(chalk.gray(' Access until:'), chalk.white(subscription.periodEnd.toLocaleDateString()));
134
+ console.log(chalk.gray(' Access until:'), chalk.white(new Date(subscription.periodEnd).toLocaleDateString()));
150
135
  }
151
136
  } else {
152
137
  console.log(chalk.gray(' Plan:'), chalk.white('Free'));
@@ -22,6 +22,9 @@ export function saveConfig(config: McpConfig) {
22
22
  ensureConfigDir();
23
23
  const current = loadConfig();
24
24
  const newConfig = { ...current, ...config };
25
+ // Remove legacy fields that are no longer stored (e.g. firebaseApiKey)
26
+ // to prevent stale values from causing cross-project auth mismatches
27
+ delete (newConfig as any).firebaseApiKey;
25
28
  fs.writeFileSync(CONFIG_FILE, JSON.stringify(newConfig, null, 2), { mode: 0o600 });
26
29
  }
27
30