@kylewadegrove/cutline-mcp-cli 0.1.0 → 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.
@@ -1,3 +1,4 @@
1
1
  export declare function loginCommand(options: {
2
2
  staging?: boolean;
3
+ signup?: boolean;
3
4
  }): Promise<void>;
@@ -9,6 +9,7 @@ const chalk_1 = __importDefault(require("chalk"));
9
9
  const ora_1 = __importDefault(require("ora"));
10
10
  const callback_js_1 = require("../auth/callback.js");
11
11
  const keychain_js_1 = require("../auth/keychain.js");
12
+ const config_store_js_1 = require("../utils/config-store.js");
12
13
  const config_js_1 = require("../utils/config.js");
13
14
  async function exchangeCustomToken(customToken, apiKey) {
14
15
  const response = await fetch(`https://identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken?key=${apiKey}`, {
@@ -31,15 +32,20 @@ async function exchangeCustomToken(customToken, apiKey) {
31
32
  }
32
33
  async function loginCommand(options) {
33
34
  const config = (0, config_js_1.getConfig)(options);
34
- console.log(chalk_1.default.bold('\nšŸ” Cutline MCP Authentication\n'));
35
+ if (options.signup) {
36
+ console.log(chalk_1.default.bold('\nšŸš€ Cutline MCP - Create Account\n'));
37
+ }
38
+ else {
39
+ console.log(chalk_1.default.bold('\nšŸ” Cutline MCP Authentication\n'));
40
+ }
35
41
  if (options.staging) {
36
42
  console.log(chalk_1.default.yellow(' āš ļø Using STAGING environment\n'));
37
43
  }
38
44
  if (!config.FIREBASE_API_KEY) {
39
- const varName = options.staging ? 'FIREBASE_API_KEY_STAGING' : 'FIREBASE_API_KEY';
40
- console.error(chalk_1.default.red(`Error: ${varName} environment variable is required.`));
45
+ console.error(chalk_1.default.red(`Error: FIREBASE_API_KEY or NEXT_PUBLIC_FIREBASE_API_KEY environment variable is required.`));
41
46
  console.error(chalk_1.default.gray('Please set it before running this command:'));
42
- console.error(chalk_1.default.cyan(` export ${varName}=AIzaSy...`));
47
+ console.error(chalk_1.default.cyan(` export FIREBASE_API_KEY=AIzaSy...`));
48
+ console.error(chalk_1.default.gray(' (or use NEXT_PUBLIC_FIREBASE_API_KEY for consistency with web app config)'));
43
49
  process.exit(1);
44
50
  }
45
51
  const spinner = (0, ora_1.default)('Starting authentication flow...').start();
@@ -48,17 +54,41 @@ async function loginCommand(options) {
48
54
  spinner.text = 'Waiting for authentication...';
49
55
  const serverPromise = (0, callback_js_1.startCallbackServer)();
50
56
  // Open browser
51
- const authUrl = `${config.AUTH_URL}?callback=${encodeURIComponent(config.CALLBACK_URL)}`;
57
+ let authUrl = `${config.AUTH_URL}?callback=${encodeURIComponent(config.CALLBACK_URL)}`;
58
+ if (options.signup) {
59
+ authUrl += '&mode=signup';
60
+ }
52
61
  await (0, open_1.default)(authUrl);
53
- spinner.text = 'Browser opened - please complete authentication';
62
+ spinner.text = options.signup
63
+ ? 'Browser opened - please create your account'
64
+ : 'Browser opened - please sign in or create an account';
54
65
  // Wait for callback with custom token
55
66
  const result = await serverPromise;
56
67
  // Exchange custom token for refresh token
57
68
  spinner.text = 'Exchanging token...';
58
69
  const { refreshToken, email } = await exchangeCustomToken(result.token, config.FIREBASE_API_KEY);
59
70
  // Store refresh token
60
- await (0, keychain_js_1.storeRefreshToken)(refreshToken);
71
+ try {
72
+ await (0, keychain_js_1.storeRefreshToken)(refreshToken);
73
+ }
74
+ catch (error) {
75
+ console.warn(chalk_1.default.yellow(' āš ļø Could not save to Keychain (skipping)'));
76
+ }
77
+ // Always save to file config (cross-platform)
78
+ try {
79
+ (0, config_store_js_1.saveConfig)({
80
+ refreshToken,
81
+ environment: options.staging ? 'staging' : 'production',
82
+ firebaseApiKey: config.FIREBASE_API_KEY
83
+ });
84
+ }
85
+ catch (error) {
86
+ console.error(chalk_1.default.red(' āœ— Failed to save config file:'), error);
87
+ }
61
88
  spinner.succeed(chalk_1.default.green('Successfully authenticated!'));
89
+ // Show environment indicator
90
+ const envLabel = options.staging ? chalk_1.default.yellow('STAGING') : chalk_1.default.green('PRODUCTION');
91
+ console.log(chalk_1.default.gray(` Environment: ${envLabel}`));
62
92
  if (email || result.email) {
63
93
  console.log(chalk_1.default.gray(` Logged in as: ${email || result.email}`));
64
94
  }
@@ -47,9 +47,8 @@ async function statusCommand(options) {
47
47
  // Get config for API key
48
48
  const config = (0, config_js_1.getConfig)(options);
49
49
  if (!config.FIREBASE_API_KEY) {
50
- const varName = options.staging ? 'FIREBASE_API_KEY_STAGING' : 'FIREBASE_API_KEY';
51
50
  spinner.fail(chalk_1.default.red('Configuration error'));
52
- console.error(chalk_1.default.red(` ${varName} environment variable is required.`));
51
+ console.error(chalk_1.default.red(` FIREBASE_API_KEY or NEXT_PUBLIC_FIREBASE_API_KEY environment variable is required.`));
53
52
  process.exit(1);
54
53
  }
55
54
  // Exchange refresh token for ID token
@@ -57,7 +56,7 @@ async function statusCommand(options) {
57
56
  const idToken = await exchangeRefreshToken(refreshToken, config.FIREBASE_API_KEY);
58
57
  // Initialize Firebase Admin with correct project ID
59
58
  if (firebase_admin_1.default.apps.length === 0) {
60
- const projectId = options.staging ? 'demo-makerkit' : 'makerkit-todo-app';
59
+ const projectId = options.staging ? 'cutline-staging' : 'cutline-prod';
61
60
  firebase_admin_1.default.initializeApp({ projectId });
62
61
  }
63
62
  // Verify the ID token
@@ -0,0 +1,3 @@
1
+ export declare function upgradeCommand(options: {
2
+ staging?: boolean;
3
+ }): Promise<void>;
@@ -0,0 +1,99 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.upgradeCommand = upgradeCommand;
7
+ const open_1 = __importDefault(require("open"));
8
+ const chalk_1 = __importDefault(require("chalk"));
9
+ const ora_1 = __importDefault(require("ora"));
10
+ const callback_js_1 = require("../auth/callback.js");
11
+ const keychain_js_1 = require("../auth/keychain.js");
12
+ const config_store_js_1 = require("../utils/config-store.js");
13
+ const config_js_1 = require("../utils/config.js");
14
+ async function exchangeCustomToken(customToken, apiKey) {
15
+ const response = await fetch(`https://identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken?key=${apiKey}`, {
16
+ method: 'POST',
17
+ headers: { 'Content-Type': 'application/json' },
18
+ body: JSON.stringify({
19
+ token: customToken,
20
+ returnSecureToken: true,
21
+ }),
22
+ });
23
+ if (!response.ok) {
24
+ const error = await response.text();
25
+ throw new Error(`Failed to exchange custom token: ${error}`);
26
+ }
27
+ const data = await response.json();
28
+ return {
29
+ refreshToken: data.refreshToken,
30
+ email: data.email,
31
+ };
32
+ }
33
+ async function upgradeCommand(options) {
34
+ const config = (0, config_js_1.getConfig)(options);
35
+ console.log(chalk_1.default.bold('\nā¬†ļø Cutline MCP - Upgrade to Premium\n'));
36
+ if (options.staging) {
37
+ console.log(chalk_1.default.yellow(' āš ļø Using STAGING environment\n'));
38
+ }
39
+ if (!config.FIREBASE_API_KEY) {
40
+ console.error(chalk_1.default.red(`Error: FIREBASE_API_KEY environment variable is required.`));
41
+ process.exit(1);
42
+ }
43
+ // Determine upgrade URL based on environment
44
+ const baseUrl = options.staging
45
+ ? 'https://cutline-staging.web.app'
46
+ : 'https://thecutline.ai';
47
+ console.log(chalk_1.default.gray(' Opening upgrade page in your browser...\n'));
48
+ console.log(chalk_1.default.dim(' After upgrading, your MCP session will be refreshed automatically.\n'));
49
+ const spinner = (0, ora_1.default)('Waiting for upgrade and re-authentication...').start();
50
+ try {
51
+ // Start callback server for re-auth after upgrade
52
+ const serverPromise = (0, callback_js_1.startCallbackServer)();
53
+ // Open upgrade page with callback for re-auth
54
+ // The upgrade page will redirect to mcp-auth after successful upgrade
55
+ const upgradeUrl = `${baseUrl}/upgrade?mcp_callback=${encodeURIComponent(config.CALLBACK_URL)}`;
56
+ await (0, open_1.default)(upgradeUrl);
57
+ spinner.text = 'Browser opened - complete your upgrade, then re-authenticate';
58
+ // Wait for callback with new token (after upgrade + re-auth)
59
+ const result = await serverPromise;
60
+ // Exchange custom token for refresh token
61
+ spinner.text = 'Refreshing your session...';
62
+ const { refreshToken, email } = await exchangeCustomToken(result.token, config.FIREBASE_API_KEY);
63
+ // Store refresh token
64
+ try {
65
+ await (0, keychain_js_1.storeRefreshToken)(refreshToken);
66
+ }
67
+ catch (error) {
68
+ console.warn(chalk_1.default.yellow(' āš ļø Could not save to Keychain (skipping)'));
69
+ }
70
+ // Save to file config
71
+ try {
72
+ (0, config_store_js_1.saveConfig)({
73
+ refreshToken,
74
+ environment: options.staging ? 'staging' : 'production',
75
+ firebaseApiKey: config.FIREBASE_API_KEY
76
+ });
77
+ }
78
+ catch (error) {
79
+ console.error(chalk_1.default.red(' āœ— Failed to save config file:'), error);
80
+ }
81
+ spinner.succeed(chalk_1.default.green('Upgrade complete! Session refreshed.'));
82
+ const envLabel = options.staging ? chalk_1.default.yellow('STAGING') : chalk_1.default.green('PRODUCTION');
83
+ console.log(chalk_1.default.gray(` Environment: ${envLabel}`));
84
+ if (email || result.email) {
85
+ console.log(chalk_1.default.gray(` Account: ${email || result.email}`));
86
+ }
87
+ console.log(chalk_1.default.green('\n ✨ Premium features are now available!\n'));
88
+ console.log(chalk_1.default.dim(' Try:'), chalk_1.default.cyan('exploration_graduate'), chalk_1.default.dim('to run a full Deep Dive\n'));
89
+ }
90
+ catch (error) {
91
+ spinner.fail(chalk_1.default.red('Upgrade flow failed'));
92
+ if (error instanceof Error) {
93
+ console.error(chalk_1.default.red(` ${error.message}`));
94
+ }
95
+ console.log(chalk_1.default.gray('\n You can also upgrade at:'), chalk_1.default.cyan(`${baseUrl}/upgrade`));
96
+ console.log(chalk_1.default.gray(' Then run:'), chalk_1.default.cyan('cutline-mcp login'), chalk_1.default.gray('to refresh your session\n'));
97
+ process.exit(1);
98
+ }
99
+ }
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ const commander_1 = require("commander");
5
5
  const login_js_1 = require("./commands/login.js");
6
6
  const logout_js_1 = require("./commands/logout.js");
7
7
  const status_js_1 = require("./commands/status.js");
8
+ const upgrade_js_1 = require("./commands/upgrade.js");
8
9
  const program = new commander_1.Command();
9
10
  program
10
11
  .name('cutline-mcp')
@@ -14,6 +15,7 @@ program
14
15
  .command('login')
15
16
  .description('Authenticate with Cutline and store credentials')
16
17
  .option('--staging', 'Use staging environment')
18
+ .option('--signup', 'Open sign-up page instead of sign-in')
17
19
  .action(login_js_1.loginCommand);
18
20
  program
19
21
  .command('logout')
@@ -24,4 +26,9 @@ program
24
26
  .description('Show current authentication status')
25
27
  .option('--staging', 'Use staging environment')
26
28
  .action(status_js_1.statusCommand);
29
+ program
30
+ .command('upgrade')
31
+ .description('Upgrade to Premium and refresh your session')
32
+ .option('--staging', 'Use staging environment')
33
+ .action(upgrade_js_1.upgradeCommand);
27
34
  program.parse();
@@ -0,0 +1,8 @@
1
+ export interface McpConfig {
2
+ refreshToken?: string;
3
+ environment?: 'production' | 'staging';
4
+ firebaseApiKey?: string;
5
+ }
6
+ export declare function saveConfig(config: McpConfig): void;
7
+ export declare function loadConfig(): McpConfig;
8
+ export declare function getRefreshTokenFromFile(): string | null;
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.saveConfig = saveConfig;
7
+ exports.loadConfig = loadConfig;
8
+ exports.getRefreshTokenFromFile = getRefreshTokenFromFile;
9
+ const fs_1 = __importDefault(require("fs"));
10
+ const path_1 = __importDefault(require("path"));
11
+ const os_1 = __importDefault(require("os"));
12
+ const CONFIG_DIR = path_1.default.join(os_1.default.homedir(), '.cutline-mcp');
13
+ const CONFIG_FILE = path_1.default.join(CONFIG_DIR, 'config.json');
14
+ function ensureConfigDir() {
15
+ if (!fs_1.default.existsSync(CONFIG_DIR)) {
16
+ fs_1.default.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
17
+ }
18
+ }
19
+ function saveConfig(config) {
20
+ ensureConfigDir();
21
+ const current = loadConfig();
22
+ const newConfig = { ...current, ...config };
23
+ fs_1.default.writeFileSync(CONFIG_FILE, JSON.stringify(newConfig, null, 2), { mode: 0o600 });
24
+ }
25
+ function loadConfig() {
26
+ try {
27
+ if (fs_1.default.existsSync(CONFIG_FILE)) {
28
+ const content = fs_1.default.readFileSync(CONFIG_FILE, 'utf-8');
29
+ return JSON.parse(content);
30
+ }
31
+ }
32
+ catch (e) {
33
+ // Ignore errors, return empty config
34
+ }
35
+ return {};
36
+ }
37
+ function getRefreshTokenFromFile() {
38
+ const config = loadConfig();
39
+ return config.refreshToken || null;
40
+ }
@@ -6,12 +6,16 @@ function getConfig(options = {}) {
6
6
  return {
7
7
  AUTH_URL: process.env.CUTLINE_AUTH_URL || 'https://cutline-staging.web.app/mcp-auth',
8
8
  CALLBACK_URL: 'http://localhost:8765',
9
- FIREBASE_API_KEY: process.env.FIREBASE_API_KEY_STAGING || process.env.FIREBASE_API_KEY || 'AIzaSyAAqU_euGAMtJoXp0sECblAIndifCp0pmE',
9
+ // For environment-scoped secrets, use FIREBASE_API_KEY (GitHub Actions will scope it to staging environment)
10
+ // Also support NEXT_PUBLIC_FIREBASE_API_KEY for consistency with web app config
11
+ FIREBASE_API_KEY: process.env.FIREBASE_API_KEY || process.env.NEXT_PUBLIC_FIREBASE_API_KEY || 'AIzaSyAAqU_euGAMtJoXp0sECblAIndifCp0pmE',
10
12
  };
11
13
  }
12
14
  return {
13
15
  AUTH_URL: process.env.CUTLINE_AUTH_URL || 'https://thecutline.ai/mcp-auth',
14
16
  CALLBACK_URL: 'http://localhost:8765',
15
- FIREBASE_API_KEY: process.env.FIREBASE_API_KEY || 'AIzaSyDW7846oQfvFU3Vnc1DELj4XdlvvYFjaIU',
17
+ // For environment-scoped secrets, use FIREBASE_API_KEY (GitHub Actions will scope it to production environment)
18
+ // Also support NEXT_PUBLIC_FIREBASE_API_KEY for consistency with web app config
19
+ FIREBASE_API_KEY: process.env.FIREBASE_API_KEY || process.env.NEXT_PUBLIC_FIREBASE_API_KEY || 'AIzaSyAYBQt7A-a_mxn5SuXPHUpyIlm0eCn42N8',
16
20
  };
17
21
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kylewadegrove/cutline-mcp-cli",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "CLI tool for authenticating with Cutline MCP servers",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -39,4 +39,4 @@
39
39
  "publishConfig": {
40
40
  "access": "public"
41
41
  }
42
- }
42
+ }
@@ -3,6 +3,7 @@ import chalk from 'chalk';
3
3
  import ora from 'ora';
4
4
  import { startCallbackServer } from '../auth/callback.js';
5
5
  import { storeRefreshToken } from '../auth/keychain.js';
6
+ import { saveConfig } from '../utils/config-store.js';
6
7
  import { getConfig } from '../utils/config.js';
7
8
 
8
9
  async function exchangeCustomToken(customToken: string, apiKey: string): Promise<{ refreshToken: string; email?: string }> {
@@ -30,19 +31,24 @@ async function exchangeCustomToken(customToken: string, apiKey: string): Promise
30
31
  };
31
32
  }
32
33
 
33
- export async function loginCommand(options: { staging?: boolean }) {
34
+ export async function loginCommand(options: { staging?: boolean; signup?: boolean }) {
34
35
  const config = getConfig(options);
35
- console.log(chalk.bold('\nšŸ” Cutline MCP Authentication\n'));
36
+
37
+ if (options.signup) {
38
+ console.log(chalk.bold('\nšŸš€ Cutline MCP - Create Account\n'));
39
+ } else {
40
+ console.log(chalk.bold('\nšŸ” Cutline MCP Authentication\n'));
41
+ }
36
42
 
37
43
  if (options.staging) {
38
44
  console.log(chalk.yellow(' āš ļø Using STAGING environment\n'));
39
45
  }
40
46
 
41
47
  if (!config.FIREBASE_API_KEY) {
42
- const varName = options.staging ? 'FIREBASE_API_KEY_STAGING' : 'FIREBASE_API_KEY';
43
- console.error(chalk.red(`Error: ${varName} environment variable is required.`));
48
+ console.error(chalk.red(`Error: FIREBASE_API_KEY or NEXT_PUBLIC_FIREBASE_API_KEY environment variable is required.`));
44
49
  console.error(chalk.gray('Please set it before running this command:'));
45
- console.error(chalk.cyan(` export ${varName}=AIzaSy...`));
50
+ console.error(chalk.cyan(` export FIREBASE_API_KEY=AIzaSy...`));
51
+ console.error(chalk.gray(' (or use NEXT_PUBLIC_FIREBASE_API_KEY for consistency with web app config)'));
46
52
  process.exit(1);
47
53
  }
48
54
 
@@ -54,9 +60,14 @@ export async function loginCommand(options: { staging?: boolean }) {
54
60
  const serverPromise = startCallbackServer();
55
61
 
56
62
  // Open browser
57
- const authUrl = `${config.AUTH_URL}?callback=${encodeURIComponent(config.CALLBACK_URL)}`;
63
+ let authUrl = `${config.AUTH_URL}?callback=${encodeURIComponent(config.CALLBACK_URL)}`;
64
+ if (options.signup) {
65
+ authUrl += '&mode=signup';
66
+ }
58
67
  await open(authUrl);
59
- spinner.text = 'Browser opened - please complete authentication';
68
+ spinner.text = options.signup
69
+ ? 'Browser opened - please create your account'
70
+ : 'Browser opened - please sign in or create an account';
60
71
 
61
72
  // Wait for callback with custom token
62
73
  const result = await serverPromise;
@@ -66,10 +77,29 @@ export async function loginCommand(options: { staging?: boolean }) {
66
77
  const { refreshToken, email } = await exchangeCustomToken(result.token, config.FIREBASE_API_KEY);
67
78
 
68
79
  // Store refresh token
69
- await storeRefreshToken(refreshToken);
80
+ try {
81
+ await storeRefreshToken(refreshToken);
82
+ } catch (error) {
83
+ console.warn(chalk.yellow(' āš ļø Could not save to Keychain (skipping)'));
84
+ }
85
+
86
+ // Always save to file config (cross-platform)
87
+ try {
88
+ saveConfig({
89
+ refreshToken,
90
+ environment: options.staging ? 'staging' : 'production',
91
+ firebaseApiKey: config.FIREBASE_API_KEY
92
+ });
93
+ } catch (error) {
94
+ console.error(chalk.red(' āœ— Failed to save config file:'), error);
95
+ }
70
96
 
71
97
  spinner.succeed(chalk.green('Successfully authenticated!'));
72
98
 
99
+ // Show environment indicator
100
+ const envLabel = options.staging ? chalk.yellow('STAGING') : chalk.green('PRODUCTION');
101
+ console.log(chalk.gray(` Environment: ${envLabel}`));
102
+
73
103
  if (email || result.email) {
74
104
  console.log(chalk.gray(` Logged in as: ${email || result.email}`));
75
105
  }
@@ -52,9 +52,8 @@ export async function statusCommand(options: { staging?: boolean }) {
52
52
  const config = getConfig(options);
53
53
 
54
54
  if (!config.FIREBASE_API_KEY) {
55
- const varName = options.staging ? 'FIREBASE_API_KEY_STAGING' : 'FIREBASE_API_KEY';
56
55
  spinner.fail(chalk.red('Configuration error'));
57
- console.error(chalk.red(` ${varName} environment variable is required.`));
56
+ console.error(chalk.red(` FIREBASE_API_KEY or NEXT_PUBLIC_FIREBASE_API_KEY environment variable is required.`));
58
57
  process.exit(1);
59
58
  }
60
59
 
@@ -64,7 +63,7 @@ export async function statusCommand(options: { staging?: boolean }) {
64
63
 
65
64
  // Initialize Firebase Admin with correct project ID
66
65
  if (admin.apps.length === 0) {
67
- const projectId = options.staging ? 'demo-makerkit' : 'makerkit-todo-app';
66
+ const projectId = options.staging ? 'cutline-staging' : 'cutline-prod';
68
67
  admin.initializeApp({ projectId });
69
68
  }
70
69
 
@@ -0,0 +1,118 @@
1
+ import open from 'open';
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+ import { startCallbackServer } from '../auth/callback.js';
5
+ import { storeRefreshToken } from '../auth/keychain.js';
6
+ import { saveConfig } from '../utils/config-store.js';
7
+ import { getConfig } from '../utils/config.js';
8
+
9
+ async function exchangeCustomToken(customToken: string, apiKey: string): Promise<{ refreshToken: string; email?: string }> {
10
+ const response = await fetch(
11
+ `https://identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken?key=${apiKey}`,
12
+ {
13
+ method: 'POST',
14
+ headers: { 'Content-Type': 'application/json' },
15
+ body: JSON.stringify({
16
+ token: customToken,
17
+ returnSecureToken: true,
18
+ }),
19
+ }
20
+ );
21
+
22
+ if (!response.ok) {
23
+ const error = await response.text();
24
+ throw new Error(`Failed to exchange custom token: ${error}`);
25
+ }
26
+
27
+ const data = await response.json();
28
+ return {
29
+ refreshToken: data.refreshToken,
30
+ email: data.email,
31
+ };
32
+ }
33
+
34
+ export async function upgradeCommand(options: { staging?: boolean }) {
35
+ const config = getConfig(options);
36
+
37
+ console.log(chalk.bold('\nā¬†ļø Cutline MCP - Upgrade to Premium\n'));
38
+
39
+ if (options.staging) {
40
+ console.log(chalk.yellow(' āš ļø Using STAGING environment\n'));
41
+ }
42
+
43
+ if (!config.FIREBASE_API_KEY) {
44
+ console.error(chalk.red(`Error: FIREBASE_API_KEY environment variable is required.`));
45
+ process.exit(1);
46
+ }
47
+
48
+ // Determine upgrade URL based on environment
49
+ const baseUrl = options.staging
50
+ ? 'https://cutline-staging.web.app'
51
+ : 'https://thecutline.ai';
52
+
53
+ console.log(chalk.gray(' Opening upgrade page in your browser...\n'));
54
+ console.log(chalk.dim(' After upgrading, your MCP session will be refreshed automatically.\n'));
55
+
56
+ const spinner = ora('Waiting for upgrade and re-authentication...').start();
57
+
58
+ try {
59
+ // Start callback server for re-auth after upgrade
60
+ const serverPromise = startCallbackServer();
61
+
62
+ // Open upgrade page with callback for re-auth
63
+ // The upgrade page will redirect to mcp-auth after successful upgrade
64
+ const upgradeUrl = `${baseUrl}/upgrade?mcp_callback=${encodeURIComponent(config.CALLBACK_URL)}`;
65
+ await open(upgradeUrl);
66
+
67
+ spinner.text = 'Browser opened - complete your upgrade, then re-authenticate';
68
+
69
+ // Wait for callback with new token (after upgrade + re-auth)
70
+ const result = await serverPromise;
71
+
72
+ // Exchange custom token for refresh token
73
+ spinner.text = 'Refreshing your session...';
74
+ const { refreshToken, email } = await exchangeCustomToken(result.token, config.FIREBASE_API_KEY);
75
+
76
+ // Store refresh token
77
+ try {
78
+ await storeRefreshToken(refreshToken);
79
+ } catch (error) {
80
+ console.warn(chalk.yellow(' āš ļø Could not save to Keychain (skipping)'));
81
+ }
82
+
83
+ // Save to file config
84
+ try {
85
+ saveConfig({
86
+ refreshToken,
87
+ environment: options.staging ? 'staging' : 'production',
88
+ firebaseApiKey: config.FIREBASE_API_KEY
89
+ });
90
+ } catch (error) {
91
+ console.error(chalk.red(' āœ— Failed to save config file:'), error);
92
+ }
93
+
94
+ spinner.succeed(chalk.green('Upgrade complete! Session refreshed.'));
95
+
96
+ const envLabel = options.staging ? chalk.yellow('STAGING') : chalk.green('PRODUCTION');
97
+ console.log(chalk.gray(` Environment: ${envLabel}`));
98
+
99
+ if (email || result.email) {
100
+ console.log(chalk.gray(` Account: ${email || result.email}`));
101
+ }
102
+
103
+ console.log(chalk.green('\n ✨ Premium features are now available!\n'));
104
+ console.log(chalk.dim(' Try:'), chalk.cyan('exploration_graduate'), chalk.dim('to run a full Deep Dive\n'));
105
+
106
+ } catch (error) {
107
+ spinner.fail(chalk.red('Upgrade flow failed'));
108
+
109
+ if (error instanceof Error) {
110
+ console.error(chalk.red(` ${error.message}`));
111
+ }
112
+
113
+ console.log(chalk.gray('\n You can also upgrade at:'), chalk.cyan(`${baseUrl}/upgrade`));
114
+ console.log(chalk.gray(' Then run:'), chalk.cyan('cutline-mcp login'), chalk.gray('to refresh your session\n'));
115
+
116
+ process.exit(1);
117
+ }
118
+ }
package/src/index.ts CHANGED
@@ -3,6 +3,7 @@ import { Command } from 'commander';
3
3
  import { loginCommand } from './commands/login.js';
4
4
  import { logoutCommand } from './commands/logout.js';
5
5
  import { statusCommand } from './commands/status.js';
6
+ import { upgradeCommand } from './commands/upgrade.js';
6
7
 
7
8
  const program = new Command();
8
9
 
@@ -15,6 +16,7 @@ program
15
16
  .command('login')
16
17
  .description('Authenticate with Cutline and store credentials')
17
18
  .option('--staging', 'Use staging environment')
19
+ .option('--signup', 'Open sign-up page instead of sign-in')
18
20
  .action(loginCommand);
19
21
 
20
22
  program
@@ -28,4 +30,10 @@ program
28
30
  .option('--staging', 'Use staging environment')
29
31
  .action(statusCommand);
30
32
 
33
+ program
34
+ .command('upgrade')
35
+ .description('Upgrade to Premium and refresh your session')
36
+ .option('--staging', 'Use staging environment')
37
+ .action(upgradeCommand);
38
+
31
39
  program.parse();
@@ -0,0 +1,43 @@
1
+
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import os from 'os';
5
+
6
+ const CONFIG_DIR = path.join(os.homedir(), '.cutline-mcp');
7
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
8
+
9
+ export interface McpConfig {
10
+ refreshToken?: string;
11
+ environment?: 'production' | 'staging';
12
+ firebaseApiKey?: string; // Store the API key used during login for reference
13
+ }
14
+
15
+ function ensureConfigDir() {
16
+ if (!fs.existsSync(CONFIG_DIR)) {
17
+ fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
18
+ }
19
+ }
20
+
21
+ export function saveConfig(config: McpConfig) {
22
+ ensureConfigDir();
23
+ const current = loadConfig();
24
+ const newConfig = { ...current, ...config };
25
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(newConfig, null, 2), { mode: 0o600 });
26
+ }
27
+
28
+ export function loadConfig(): McpConfig {
29
+ try {
30
+ if (fs.existsSync(CONFIG_FILE)) {
31
+ const content = fs.readFileSync(CONFIG_FILE, 'utf-8');
32
+ return JSON.parse(content);
33
+ }
34
+ } catch (e) {
35
+ // Ignore errors, return empty config
36
+ }
37
+ return {};
38
+ }
39
+
40
+ export function getRefreshTokenFromFile(): string | null {
41
+ const config = loadConfig();
42
+ return config.refreshToken || null;
43
+ }
@@ -9,13 +9,17 @@ export function getConfig(options: { staging?: boolean } = {}): Config {
9
9
  return {
10
10
  AUTH_URL: process.env.CUTLINE_AUTH_URL || 'https://cutline-staging.web.app/mcp-auth',
11
11
  CALLBACK_URL: 'http://localhost:8765',
12
- FIREBASE_API_KEY: process.env.FIREBASE_API_KEY_STAGING || process.env.FIREBASE_API_KEY || 'AIzaSyAAqU_euGAMtJoXp0sECblAIndifCp0pmE',
12
+ // For environment-scoped secrets, use FIREBASE_API_KEY (GitHub Actions will scope it to staging environment)
13
+ // Also support NEXT_PUBLIC_FIREBASE_API_KEY for consistency with web app config
14
+ FIREBASE_API_KEY: process.env.FIREBASE_API_KEY || process.env.NEXT_PUBLIC_FIREBASE_API_KEY || 'AIzaSyAAqU_euGAMtJoXp0sECblAIndifCp0pmE',
13
15
  };
14
16
  }
15
17
 
16
18
  return {
17
19
  AUTH_URL: process.env.CUTLINE_AUTH_URL || 'https://thecutline.ai/mcp-auth',
18
20
  CALLBACK_URL: 'http://localhost:8765',
19
- FIREBASE_API_KEY: process.env.FIREBASE_API_KEY || 'AIzaSyDW7846oQfvFU3Vnc1DELj4XdlvvYFjaIU',
21
+ // For environment-scoped secrets, use FIREBASE_API_KEY (GitHub Actions will scope it to production environment)
22
+ // Also support NEXT_PUBLIC_FIREBASE_API_KEY for consistency with web app config
23
+ FIREBASE_API_KEY: process.env.FIREBASE_API_KEY || process.env.NEXT_PUBLIC_FIREBASE_API_KEY || 'AIzaSyAYBQt7A-a_mxn5SuXPHUpyIlm0eCn42N8',
20
24
  };
21
25
  }